diff --git a/src/README.md b/src/README.md
index 5be71bf..d0ebc37 100644
--- a/src/README.md
+++ b/src/README.md
@@ -146,7 +146,7 @@ python manage.py runserver
## 🙏 鸣谢
-特别感谢 **JetBrains** 为本项目提供的免费开源许可证。gs ls syj zyd164
+特别感谢 **JetBrains** 为本项目提供的免费开源许可证。gs130 ls syj zyd164
diff --git a/src/accounts/admin.py b/src/accounts/admin.py
index 32e483c..6babbe0 100644
--- a/src/accounts/admin.py
+++ b/src/accounts/admin.py
@@ -9,14 +9,21 @@ from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
+ """
+ 用于在管理员界面创建BlogUser的表单
+ """
+ # 密码输入字段
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
- fields = ('email',)
+ fields = ('email',) # 表单包含的字段
def clean_password2(self):
+ """
+ 验证两个密码字段是否匹配
+ """
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
@@ -25,28 +32,39 @@ class BlogUserCreationForm(forms.ModelForm):
return password2
def save(self, commit=True):
+ """
+ 以哈希格式保存密码
+ """
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
- user.source = 'adminsite'
+ user.source = 'adminsite' # 设置用户来源为管理站点
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
+ """
+ 用于在管理员界面修改BlogUser信息的表单
+ """
class Meta:
model = BlogUser
- fields = '__all__'
- field_classes = {'username': UsernameField}
+ fields = '__all__' # 包含所有字段
+ field_classes = {'username': UsernameField} # 指定username字段的类型
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
- form = BlogUserChangeForm
- add_form = BlogUserCreationForm
+ """
+ BlogUser模型在管理员界面的配置类
+ """
+ form = BlogUserChangeForm # 修改用户时使用的表单
+ add_form = BlogUserCreationForm # 创建用户时使用的表单
+
+ # 在列表中显示的字段
list_display = (
'id',
'nickname',
@@ -55,5 +73,10 @@ class BlogUserAdmin(UserAdmin):
'last_login',
'date_joined',
'source')
+
+ # 在列表中可以点击跳转到编辑页的字段
list_display_links = ('id', 'username')
+
+ # 默认排序方式
ordering = ('-id',)
+
diff --git a/src/accounts/apps.py b/src/accounts/apps.py
index 9b3fc5a..9b95005 100644
--- a/src/accounts/apps.py
+++ b/src/accounts/apps.py
@@ -2,4 +2,8 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
- name = 'accounts'
+ """
+ Django应用配置类,用于配置accounts应用的基本信息
+ """
+ name = 'accounts' # 定义应用的名称,与项目中的应用目录名一致
+
diff --git a/src/accounts/forms.py b/src/accounts/forms.py
index fce4137..bedfac8 100644
--- a/src/accounts/forms.py
+++ b/src/accounts/forms.py
@@ -9,28 +9,43 @@ from .models import BlogUser
class LoginForm(AuthenticationForm):
+ """
+ 用户登录表单,继承自Django内置的AuthenticationForm
+ """
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
+ # 自定义用户名输入框的样式和属性
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
+ # 自定义密码输入框的样式和属性
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
+ """
+ 用户注册表单,继承自Django内置的UserCreationForm
+ """
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
+ # 自定义用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
+ # 自定义邮箱输入框样式
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
+ # 自定义密码输入框样式
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
+ # 自定义确认密码输入框样式
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
+ """
+ 验证邮箱是否已经被注册
+ """
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
@@ -38,10 +53,14 @@ class RegisterForm(UserCreationForm):
class Meta:
model = get_user_model()
- fields = ("username", "email")
+ fields = ("username", "email") # 定义表单包含的字段
class ForgetPasswordForm(forms.Form):
+ """
+ 忘记密码表单,用于用户重置密码
+ """
+ # 新密码输入字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@@ -52,6 +71,7 @@ class ForgetPasswordForm(forms.Form):
),
)
+ # 确认新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@@ -62,6 +82,7 @@ class ForgetPasswordForm(forms.Form):
),
)
+ # 邮箱字段
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@@ -72,6 +93,7 @@ class ForgetPasswordForm(forms.Form):
),
)
+ # 验证码字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@@ -83,6 +105,9 @@ class ForgetPasswordForm(forms.Form):
)
def clean_new_password2(self):
+ """
+ 验证两次输入的密码是否一致,并验证密码强度
+ """
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
@@ -92,6 +117,9 @@ class ForgetPasswordForm(forms.Form):
return password2
def clean_email(self):
+ """
+ 验证邮箱是否存在
+ """
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
@@ -101,6 +129,9 @@ class ForgetPasswordForm(forms.Form):
return user_email
def clean_code(self):
+ """
+ 验证验证码是否正确
+ """
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
@@ -112,6 +143,10 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form):
+ """
+ 忘记密码时获取验证码的表单
+ """
email = forms.EmailField(
label=_('Email'),
)
+
diff --git a/src/accounts/migrations/0001_initial.py b/src/accounts/migrations/0001_initial.py
index d2fbcab..43f134c 100644
--- a/src/accounts/migrations/0001_initial.py
+++ b/src/accounts/migrations/0001_initial.py
@@ -7,43 +7,66 @@ import django.utils.timezone
class Migration(migrations.Migration):
-
+ # 标记这是一个初始迁移文件
initial = True
+ # 定义依赖关系,依赖于auth应用的0012_alter_user_first_name_max_length迁移
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
+ # 定义具体的操作
operations = [
+ # 创建BlogUser模型
migrations.CreateModel(
name='BlogUser',
fields=[
+ # 主键字段,自动创建的BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ # 密码字段,最大长度128字符
('password', models.CharField(max_length=128, verbose_name='password')),
+ # 最后登录时间,可为空
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ # 超级用户状态,用于标识是否具有所有权限
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ # 用户名字段,唯一且有验证器
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
+ # 名字字段,可为空
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
+ # 姓氏字段,可为空
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+ # 邮箱地址字段,可为空
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+ # 员工状态,标识用户是否可以登录管理站点
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ # 活跃状态,标识用户是否应该被视为活跃用户
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+ # 加入日期,默认为当前时间
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ # 昵称字段,可为空
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
+ # 创建时间,默认为当前时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
+ # 最后修改时间,默认为当前时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
+ # 创建来源字段,可为空
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
+ # 用户组关联字段,多对多关系
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
+ # 用户权限字段,多对多关系
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
+ # 模型选项配置
options={
- 'verbose_name': '用户',
- 'verbose_name_plural': '用户',
- 'ordering': ['-id'],
- 'get_latest_by': 'id',
+ 'verbose_name': '用户', # 单数名称
+ 'verbose_name_plural': '用户', # 复数名称
+ 'ordering': ['-id'], # 默认排序方式
+ 'get_latest_by': 'id', # 获取最新记录的字段
},
+ # 模型管理器
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
+
diff --git a/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
index 1a9f509..cc3ae13 100644
--- a/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
+++ b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -5,42 +5,56 @@ import django.utils.timezone
class Migration(migrations.Migration):
-
+ # 定义该迁移文件的依赖关系,依赖于accounts应用的0001_initial迁移
dependencies = [
('accounts', '0001_initial'),
]
+ # 定义具体的迁移操作
operations = [
+ # 修改BlogUser模型的选项配置
migrations.AlterModelOptions(
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' # 复数名称(英文)
+ },
),
+ # 移除BlogUser模型中的created_time字段
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
+ # 移除BlogUser模型中的last_mod_time字段
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
+ # 添加creation_time字段到BlogUser模型
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+ # 添加last_modify_time字段到BlogUser模型
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
+ # 修改BlogUser模型中的nickname字段
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
+ # 修改BlogUser模型中的source字段
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]
+
diff --git a/src/accounts/models.py b/src/accounts/models.py
index 3baddbb..decf6ff 100644
--- a/src/accounts/models.py
+++ b/src/accounts/models.py
@@ -9,27 +9,45 @@ from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
+ """
+ 博客用户模型,继承自Django的AbstractUser
+ 扩展了用户的基本信息
+ """
+ # 用户昵称,可为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
+ # 用户创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
+ # 用户最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+ # 用户创建来源,可为空
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
+ """
+ 获取用户详情页的绝对URL
+ """
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
+ """
+ 定义对象的字符串表示,返回用户的邮箱
+ """
return self.email
def get_full_url(self):
+ """
+ 获取用户的完整URL地址
+ """
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
- ordering = ['-id']
- verbose_name = _('user')
- verbose_name_plural = verbose_name
- get_latest_by = 'id'
+ ordering = ['-id'] # 默认按ID降序排列
+ verbose_name = _('user') # 单数名称
+ verbose_name_plural = verbose_name # 复数名称
+ get_latest_by = 'id' # 获取最新记录的字段
+
diff --git a/src/accounts/tests.py b/src/accounts/tests.py
index 6893411..fc2cae8 100644
--- a/src/accounts/tests.py
+++ b/src/accounts/tests.py
@@ -12,17 +12,26 @@ from . import utils
# Create your tests here.
class AccountTest(TestCase):
+ """
+ 账户相关功能的测试类
+ """
def setUp(self):
- self.client = Client()
- self.factory = RequestFactory()
+ """
+ 测试前的准备工作,创建测试客户端、请求工厂和测试用户
+ """
+ self.client = Client() # 创建测试客户端
+ self.factory = RequestFactory() # 创建请求工厂
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
- self.new_test = "xxx123--="
+ self.new_test = "xxx123--=" # 测试用的新密码
def test_validate_account(self):
+ """
+ 测试账户验证功能
+ """
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
@@ -30,6 +39,7 @@ class AccountTest(TestCase):
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
+ # 测试登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
@@ -37,12 +47,14 @@ class AccountTest(TestCase):
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
+ # 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
+ # 创建测试文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
@@ -52,14 +64,19 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
+ # 测试访问文章管理URL
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
+ """
+ 测试用户注册功能
+ """
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
+ # 测试用户注册
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
@@ -78,6 +95,7 @@ class AccountTest(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
+ # 测试登录后操作
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
@@ -103,12 +121,14 @@ class AccountTest(TestCase):
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
+ # 测试登出
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
+ # 测试错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
@@ -119,18 +139,26 @@ class AccountTest(TestCase):
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
+ """
+ 测试邮箱验证码验证功能
+ """
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
+ # 测试正确验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
+ # 测试错误邮箱
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
+ """
+ 测试忘记密码时成功获取验证码
+ """
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
@@ -140,12 +168,17 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
+ """
+ 测试忘记密码时获取验证码失败的情况
+ """
+ # 测试空邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
+ # 测试无效邮箱格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
@@ -153,6 +186,9 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
+ """
+ 测试通过邮箱成功重置密码
+ """
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
@@ -175,6 +211,9 @@ class AccountTest(TestCase):
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
+ """
+ 测试为不存在的用户重置密码
+ """
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
@@ -188,8 +227,10 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
-
def test_forget_password_email_code_error(self):
+ """
+ 测试使用错误验证码重置密码
+ """
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
diff --git a/src/accounts/urls.py b/src/accounts/urls.py
index 107a801..be1736b 100644
--- a/src/accounts/urls.py
+++ b/src/accounts/urls.py
@@ -4,25 +4,38 @@ from django.urls import re_path
from . import views
from .forms import LoginForm
-app_name = "accounts"
+app_name = "accounts" # 定义应用的命名空间
-urlpatterns = [re_path(r'^login/$',
+urlpatterns = [
+ # 登录URL,使用自定义的LoginForm表单
+ re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
- re_path(r'^register/$',
+
+ # 注册URL
+ re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
- re_path(r'^logout/$',
+
+ # 登出URL
+ re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
- path(r'account/result.html',
+
+ # 账户操作结果页面URL
+ path(r'account/result.html',
views.account_result,
name='result'),
- re_path(r'^forget_password/$',
+
+ # 忘记密码页面URL
+ re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
- re_path(r'^forget_password_code/$',
+
+ # 忘记密码验证码发送URL
+ re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
- ]
+]
+
diff --git a/src/accounts/user_login_backend.py b/src/accounts/user_login_backend.py
index 73cdca1..adde0d8 100644
--- a/src/accounts/user_login_backend.py
+++ b/src/accounts/user_login_backend.py
@@ -4,23 +4,52 @@ from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
- 允许使用用户名或邮箱登录
+ 自定义认证后端,允许用户使用用户名或邮箱进行登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
+ """
+ 重写authenticate方法,支持邮箱或用户名登录
+
+ Args:
+ request: HTTP请求对象
+ username: 用户名或邮箱
+ password: 密码
+ **kwargs: 其他参数
+
+ Returns:
+ 用户对象或None
+ """
+ # 判断输入的是邮箱还是用户名
if '@' in username:
- kwargs = {'email': username}
+ kwargs = {'email': username} # 使用邮箱查询
else:
- kwargs = {'username': username}
+ kwargs = {'username': username} # 使用用户名查询
+
try:
+ # 获取用户对象
user = get_user_model().objects.get(**kwargs)
+ # 验证密码是否正确
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
+ # 用户不存在时返回None
return None
def get_user(self, username):
+ """
+ 根据用户ID获取用户对象
+
+ Args:
+ username: 用户ID
+
+ Returns:
+ 用户对象或None
+ """
try:
+ # 根据主键获取用户
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
+ # 用户不存在时返回None
return None
+
diff --git a/src/accounts/utils.py b/src/accounts/utils.py
index 4b94bdf..05640be 100644
--- a/src/accounts/utils.py
+++ b/src/accounts/utils.py
@@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
-_code_ttl = timedelta(minutes=5)
+_code_ttl = timedelta(minutes=5) # 验证码有效期为5分钟
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
@@ -17,6 +17,7 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
subject: 邮件主题
code: 验证码
"""
+ # 构造邮件内容,包含验证码和有效期提示
html_content = _(
"You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
@@ -30,20 +31,32 @@ def verify(email: str, code: str) -> typing.Optional[str]:
code: 验证码
Return:
如果有错误就返回错误str
- Node:
+ Note:
这里的错误处理不太合理,应该采用raise抛出
否测调用方也需要对error进行处理
"""
- cache_code = get_code(email)
+ cache_code = get_code(email) # 从缓存中获取存储的验证码
if cache_code != code:
- return gettext("Verification code error")
+ return gettext("Verification code error") # 验证码不匹配时返回错误信息
def set_code(email: str, code: str):
- """设置code"""
- cache.set(email, code, _code_ttl.seconds)
+ """设置code
+ 将验证码存储到缓存中,设置过期时间
+ Args:
+ email: 邮箱地址作为缓存的键
+ code: 验证码作为缓存的值
+ """
+ cache.set(email, code, _code_ttl.seconds) # 使用邮箱作为键存储验证码
def get_code(email: str) -> typing.Optional[str]:
- """获取code"""
- return cache.get(email)
+ """获取code
+ 从缓存中获取指定邮箱的验证码
+ Args:
+ email: 邮箱地址
+ Returns:
+ 验证码字符串或None(如果不存在或已过期)
+ """
+ return cache.get(email) # 从缓存中获取验证码
+
diff --git a/src/accounts/views.py b/src/accounts/views.py
index ae67aec..cf3775d 100644
--- a/src/accounts/views.py
+++ b/src/accounts/views.py
@@ -26,34 +26,41 @@ from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__) # 创建日志记录器
# Create your views here.
class RegisterView(FormView):
- form_class = RegisterForm
- template_name = 'account/registration_form.html'
+ """
+ 用户注册视图类
+ """
+ form_class = RegisterForm # 使用的表单类
+ template_name = 'account/registration_form.html' # 模板文件
- @method_decorator(csrf_protect)
+ @method_decorator(csrf_protect) # 添加CSRF保护装饰器
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
+ """
+ 表单验证成功时的处理方法
+ """
if form.is_valid():
- user = form.save(False)
- user.is_active = False
- user.source = 'Register'
- user.save(True)
- site = get_current_site().domain
- sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+ user = form.save(False) # 保存表单但不提交到数据库
+ user.is_active = False # 设置用户为非活跃状态,需要邮箱验证
+ user.source = 'Register' # 设置用户来源
+ user.save(True) # 提交到数据库
+ site = get_current_site().domain # 获取当前站点域名
+ sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 生成验证签名
if settings.DEBUG:
- site = '127.0.0.1:8000'
- path = reverse('account:result')
+ site = '127.0.0.1:8000' # 调试模式下使用本地地址
+ path = reverse('account:result') # 获取验证结果页面URL
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
+ # 构造验证邮件内容
content = """
请点击下面链接验证您的邮箱
@@ -64,6 +71,7 @@ class RegisterView(FormView):
如果上面链接无法打开,请将此链接复制至浏览器。
{url}
""".format(url=url)
+ # 发送验证邮件
send_email(
emailto=[
user.email,
@@ -73,7 +81,7 @@ class RegisterView(FormView):
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
- return HttpResponseRedirect(url)
+ return HttpResponseRedirect(url) # 重定向到注册结果页面
else:
return self.render_to_response({
'form': form
@@ -81,33 +89,44 @@ class RegisterView(FormView):
class LogoutView(RedirectView):
- url = '/login/'
+ """
+ 用户登出视图类
+ """
+ url = '/login/' # 登出后重定向的URL
- @method_decorator(never_cache)
+ @method_decorator(never_cache) # 添加不缓存装饰器
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
- logout(request)
- delete_sidebar_cache()
- return super(LogoutView, self).get(request, *args, **kwargs)
+ """
+ 处理GET请求,执行登出操作
+ """
+ logout(request) # 执行登出
+ delete_sidebar_cache() # 删除侧边栏缓存
+ return super(LogoutView, self).get(request, *args, **kwargs) # 重定向到登录页
class LoginView(FormView):
- form_class = LoginForm
- template_name = 'account/login.html'
- success_url = '/'
- redirect_field_name = REDIRECT_FIELD_NAME
- login_ttl = 2626560 # 一个月的时间
-
- @method_decorator(sensitive_post_parameters('password'))
- @method_decorator(csrf_protect)
- @method_decorator(never_cache)
+ """
+ 用户登录视图类
+ """
+ form_class = LoginForm # 使用的表单类
+ template_name = 'account/login.html' # 模板文件
+ success_url = '/' # 登录成功后重定向的URL
+ redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名
+ login_ttl = 2626560 # 登录会话保持时间(一个月)
+
+ @method_decorator(sensitive_post_parameters('password')) # 敏感参数保护
+ @method_decorator(csrf_protect) # CSRF保护
+ @method_decorator(never_cache) # 不缓存
def dispatch(self, request, *args, **kwargs):
-
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
+ """
+ 获取上下文数据,添加重定向URL
+ """
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
@@ -116,25 +135,30 @@ class LoginView(FormView):
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
+ """
+ 表单验证成功时的处理方法
+ """
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
- delete_sidebar_cache()
+ delete_sidebar_cache() # 删除侧边栏缓存
logger.info(self.redirect_field_name)
- auth.login(self.request, form.get_user())
- if self.request.POST.get("remember"):
- self.request.session.set_expiry(self.login_ttl)
+ auth.login(self.request, form.get_user()) # 执行登录
+ if self.request.POST.get("remember"): # 如果选择了记住登录
+ self.request.session.set_expiry(self.login_ttl) # 设置会话过期时间
return super(LoginView, self).form_valid(form)
- # return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
-
+ """
+ 获取登录成功后的重定向URL
+ """
redirect_to = self.request.POST.get(self.redirect_field_name)
+ # 验证重定向URL是否安全
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
@@ -143,62 +167,82 @@ class LoginView(FormView):
def account_result(request):
- type = request.GET.get('type')
- id = request.GET.get('id')
+ """
+ 账户操作结果页面
+ """
+ type = request.GET.get('type') # 获取操作类型
+ id = request.GET.get('id') # 获取用户ID
- user = get_object_or_404(get_user_model(), id=id)
+ user = get_object_or_404(get_user_model(), id=id) # 获取用户对象
logger.info(type)
if user.is_active:
- return HttpResponseRedirect('/')
+ return HttpResponseRedirect('/') # 如果用户已激活,重定向到首页
if type and type in ['register', 'validation']:
if type == 'register':
+ # 注册成功提示
content = '''
恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。
'''
title = '注册成功'
else:
+ # 邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
- return HttpResponseForbidden()
- user.is_active = True
+ return HttpResponseForbidden() # 签名验证失败
+ user.is_active = True # 激活用户
user.save()
content = '''
恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。
'''
title = '验证成功'
+ # 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
- return HttpResponseRedirect('/')
+ return HttpResponseRedirect('/') # 重定向到首页
class ForgetPasswordView(FormView):
- form_class = ForgetPasswordForm
- template_name = 'account/forget_password.html'
+ """
+ 忘记密码视图类
+ """
+ form_class = ForgetPasswordForm # 使用的表单类
+ template_name = 'account/forget_password.html' # 模板文件
def form_valid(self, form):
+ """
+ 表单验证成功时的处理方法
+ """
if form.is_valid():
+ # 根据邮箱查找用户并更新密码
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
- blog_user.password = make_password(form.cleaned_data["new_password2"])
+ blog_user.password = make_password(form.cleaned_data["new_password2"]) # 加密新密码
blog_user.save()
- return HttpResponseRedirect('/login/')
+ return HttpResponseRedirect('/login/') # 重定向到登录页面
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
+ """
+ 忘记密码时发送验证码视图类
+ """
def post(self, request: HttpRequest):
- form = ForgetPasswordCodeForm(request.POST)
+ """
+ 处理POST请求,发送验证码邮件
+ """
+ form = ForgetPasswordCodeForm(request.POST) # 验证表单
if not form.is_valid():
- return HttpResponse("错误的邮箱")
- to_email = form.cleaned_data["email"]
+ return HttpResponse("错误的邮箱") # 表单验证失败返回错误信息
+ to_email = form.cleaned_data["email"] # 获取邮箱
+
+ code = generate_code() # 生成验证码
+ utils.send_verify_email(to_email, code) # 发送验证码邮件
+ utils.set_code(to_email, code) # 将验证码存储到缓存
- code = generate_code()
- utils.send_verify_email(to_email, code)
- utils.set_code(to_email, code)
+ return HttpResponse("ok") # 返回成功信息
- return HttpResponse("ok")
diff --git a/src/blog/admin.py b/src/blog/admin.py
index 46c3420..d27bb88 100644
--- a/src/blog/admin.py
+++ b/src/blog/admin.py
@@ -10,29 +10,45 @@ from .models import Article
class ArticleForm(forms.ModelForm):
+ """
+ 文章表单类,用于在管理界面编辑文章
+ """
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
- fields = '__all__'
+ fields = '__all__' # 包含所有字段
def makr_article_publish(modeladmin, request, queryset):
- queryset.update(status='p')
+ """
+ 批量发布文章操作
+ """
+ queryset.update(status='p') # 将选中的文章状态设置为已发布
def draft_article(modeladmin, request, queryset):
- queryset.update(status='d')
+ """
+ 批量将文章设为草稿操作
+ """
+ queryset.update(status='d') # 将选中的文章状态设置为草稿
def close_article_commentstatus(modeladmin, request, queryset):
- queryset.update(comment_status='c')
+ """
+ 批量关闭文章评论功能
+ """
+ queryset.update(comment_status='c') # 将选中的文章评论状态设置为关闭
def open_article_commentstatus(modeladmin, request, queryset):
- queryset.update(comment_status='o')
+ """
+ 批量开启文章评论功能
+ """
+ queryset.update(comment_status='o') # 将选中的文章评论状态设置为开启
+# 为批量操作设置显示名称
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
@@ -40,10 +56,13 @@ open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
- list_per_page = 20
- search_fields = ('body', 'title')
- form = ArticleForm
- list_display = (
+ """
+ 文章模型在管理界面的配置类
+ """
+ list_per_page = 20 # 每页显示20条记录
+ search_fields = ('body', 'title') # 设置搜索字段
+ form = ArticleForm # 使用自定义表单
+ list_display = ( # 列表页显示的字段
'id',
'title',
'author',
@@ -53,34 +72,46 @@ class ArticlelAdmin(admin.ModelAdmin):
'status',
'type',
'article_order')
- list_display_links = ('id', 'title')
- list_filter = ('status', 'type', 'category')
- filter_horizontal = ('tags',)
- exclude = ('creation_time', 'last_modify_time')
- view_on_site = True
- actions = [
+ list_display_links = ('id', 'title') # 列表页中可点击进入编辑页的字段
+ list_filter = ('status', 'type', 'category') # 设置过滤器
+ filter_horizontal = ('tags',) # 标签字段使用水平过滤器
+ exclude = ('creation_time', 'last_modify_time') # 在表单中排除这些字段
+ view_on_site = True # 显示"在站点上查看"链接
+ actions = [ # 注册批量操作
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj):
+ """
+ 在列表页显示分类的链接
+ """
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'%s' % (link, obj.category.name))
- link_to_category.short_description = _('category')
+ link_to_category.short_description = _('category') # 设置列名
def get_form(self, request, obj=None, **kwargs):
+ """
+ 获取表单,限制作者字段只能选择超级用户
+ """
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
+ """
+ 保存模型实例
+ """
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
+ """
+ 获取在站点上查看的URL
+ """
if obj:
url = obj.get_full_url()
return url
@@ -91,22 +122,38 @@ class ArticlelAdmin(admin.ModelAdmin):
class TagAdmin(admin.ModelAdmin):
- exclude = ('slug', 'last_mod_time', 'creation_time')
+ """
+ 标签模型在管理界面的配置类
+ """
+ exclude = ('slug', 'last_mod_time', 'creation_time') # 排除字段
class CategoryAdmin(admin.ModelAdmin):
- list_display = ('name', 'parent_category', 'index')
- exclude = ('slug', 'last_mod_time', 'creation_time')
+ """
+ 分类模型在管理界面的配置类
+ """
+ list_display = ('name', 'parent_category', 'index') # 列表页显示字段
+ exclude = ('slug', 'last_mod_time', 'creation_time') # 排除字段
class LinksAdmin(admin.ModelAdmin):
- exclude = ('last_mod_time', 'creation_time')
+ """
+ 友情链接模型在管理界面的配置类
+ """
+ exclude = ('last_mod_time', 'creation_time') # 排除字段
class SideBarAdmin(admin.ModelAdmin):
- list_display = ('name', 'content', 'is_enable', 'sequence')
- exclude = ('last_mod_time', 'creation_time')
+ """
+ 侧边栏模型在管理界面的配置类
+ """
+ list_display = ('name', 'content', 'is_enable', 'sequence') # 列表页显示字段
+ exclude = ('last_mod_time', 'creation_time') # 排除字段
class BlogSettingsAdmin(admin.ModelAdmin):
+ """
+ 博客设置模型在管理界面的配置类
+ """
pass
+
diff --git a/src/blog/apps.py b/src/blog/apps.py
index 7930587..cb0d6c6 100644
--- a/src/blog/apps.py
+++ b/src/blog/apps.py
@@ -2,4 +2,7 @@ from django.apps import AppConfig
class BlogConfig(AppConfig):
- name = 'blog'
+ """
+ Django应用配置类,用于配置blog应用的基本信息
+ """
+ name = 'blog' # 定义应用的名称,与项目中的应用目录名一致
diff --git a/src/blog/context_processors.py b/src/blog/context_processors.py
index 73e3088..a3d321c 100644
--- a/src/blog/context_processors.py
+++ b/src/blog/context_processors.py
@@ -5,39 +5,55 @@ from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__) # 创建日志记录器
def seo_processor(requests):
- key = 'seo_processor'
- value = cache.get(key)
+ """
+ SEO上下文处理器,为模板提供SEO相关数据
+
+ Args:
+ requests: HTTP请求对象
+
+ Returns:
+ dict: 包含SEO和网站配置信息的字典
+ """
+ key = 'seo_processor' # 缓存键名
+ value = cache.get(key) # 从缓存中获取数据
if value:
+ # 如果缓存中存在数据,直接返回
return value
else:
+ # 如果缓存中没有数据,记录日志并生成新数据
logger.info('set processor cache.')
- setting = get_blog_setting()
+ setting = get_blog_setting() # 获取博客设置
+
+ # 构造返回值字典,包含网站SEO和配置信息
value = {
- 'SITE_NAME': setting.site_name,
- 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
- 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
- 'SITE_SEO_DESCRIPTION': setting.site_seo_description,
- 'SITE_DESCRIPTION': setting.site_description,
- 'SITE_KEYWORDS': setting.site_keywords,
- 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
- 'ARTICLE_SUB_LENGTH': setting.article_sub_length,
- 'nav_category_list': Category.objects.all(),
- 'nav_pages': Article.objects.filter(
+ 'SITE_NAME': setting.site_name, # 网站名称
+ 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示谷歌广告
+ 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # 谷歌广告代码
+ 'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述
+ 'SITE_DESCRIPTION': setting.site_description, # 网站描述
+ 'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
+ 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # 网站基础URL
+ 'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度
+ 'nav_category_list': Category.objects.all(), # 导航分类列表
+ 'nav_pages': Article.objects.filter( # 导航页面列表
type='p',
status='p'),
- 'OPEN_SITE_COMMENT': setting.open_site_comment,
- 'BEIAN_CODE': setting.beian_code,
- 'ANALYTICS_CODE': setting.analytics_code,
- "BEIAN_CODE_GONGAN": setting.gongan_beiancode,
- "SHOW_GONGAN_CODE": setting.show_gongan_code,
- "CURRENT_YEAR": timezone.now().year,
- "GLOBAL_HEADER": setting.global_header,
- "GLOBAL_FOOTER": setting.global_footer,
- "COMMENT_NEED_REVIEW": setting.comment_need_review,
+ 'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论
+ 'BEIAN_CODE': setting.beian_code, # 备案号
+ 'ANALYTICS_CODE': setting.analytics_code, # 网站统计代码
+ "BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号
+ "SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案号
+ "CURRENT_YEAR": timezone.now().year, # 当前年份
+ "GLOBAL_HEADER": setting.global_header, # 公共头部内容
+ "GLOBAL_FOOTER": setting.global_footer, # 公共尾部内容
+ "COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核
}
+
+ # 将数据缓存10小时
cache.set(key, value, 60 * 60 * 10)
return value
+
diff --git a/src/blog/documents.py b/src/blog/documents.py
index 0f1db7b..a49a257 100644
--- a/src/blog/documents.py
+++ b/src/blog/documents.py
@@ -7,9 +7,11 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
+# 检查是否在Django设置中配置了Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
+ # 创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@@ -19,8 +21,10 @@ if ELASTICSEARCH_ENABLED:
c = IngestClient(es)
try:
+ # 尝试获取geoip管道
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
+ # 如果geoip管道不存在,则创建一个用于添加地理位置信息的管道
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@@ -34,57 +38,81 @@ if ELASTICSEARCH_ENABLED:
class GeoIp(InnerDoc):
- continent_name = Keyword()
- country_iso_code = Keyword()
- country_name = Keyword()
- location = GeoPoint()
+ """
+ 地理IP信息内部文档类
+ """
+ continent_name = Keyword() # 大洲名称
+ country_iso_code = Keyword() # 国家ISO代码
+ country_name = Keyword() # 国家名称
+ location = GeoPoint() # 地理位置坐标
class UserAgentBrowser(InnerDoc):
- Family = Keyword()
- Version = Keyword()
+ """
+ 用户代理浏览器信息内部文档类
+ """
+ Family = Keyword() # 浏览器家族
+ Version = Keyword() # 浏览器版本
class UserAgentOS(UserAgentBrowser):
+ """
+ 用户代理操作系统信息内部文档类
+ """
pass
class UserAgentDevice(InnerDoc):
- Family = Keyword()
- Brand = Keyword()
- Model = Keyword()
+ """
+ 用户代理设备信息内部文档类
+ """
+ Family = Keyword() # 设备家族
+ Brand = Keyword() # 设备品牌
+ Model = Keyword() # 设备型号
class UserAgent(InnerDoc):
- browser = Object(UserAgentBrowser, required=False)
- os = Object(UserAgentOS, required=False)
- device = Object(UserAgentDevice, required=False)
- string = Text()
- is_bot = Boolean()
+ """
+ 用户代理完整信息内部文档类
+ """
+ browser = Object(UserAgentBrowser, required=False) # 浏览器信息
+ os = Object(UserAgentOS, required=False) # 操作系统信息
+ device = Object(UserAgentDevice, required=False) # 设备信息
+ string = Text() # 完整的User-Agent字符串
+ is_bot = Boolean() # 是否为爬虫
class ElapsedTimeDocument(Document):
- url = Keyword()
- time_taken = Long()
- log_datetime = Date()
- ip = Keyword()
- geoip = Object(GeoIp, required=False)
- useragent = Object(UserAgent, required=False)
+ """
+ 页面响应时间文档类,用于记录网站性能数据
+ """
+ url = Keyword() # 请求URL
+ time_taken = Long() # 耗时(毫秒)
+ log_datetime = Date() # 记录时间
+ ip = Keyword() # 访问者IP
+ geoip = Object(GeoIp, required=False) # 地理位置信息
+ useragent = Object(UserAgent, required=False) # 用户代理信息
class Index:
- name = 'performance'
+ name = 'performance' # 索引名称
settings = {
- "number_of_shards": 1,
- "number_of_replicas": 0
+ "number_of_shards": 1, # 分片数量
+ "number_of_replicas": 0 # 副本数量
}
class Meta:
- doc_type = 'ElapsedTime'
+ doc_type = 'ElapsedTime' # 文档类型
class ElaspedTimeDocumentManager:
+ """
+ 页面响应时间文档管理器类
+ """
@staticmethod
def build_index():
+ """
+ 构建性能监控索引
+ """
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
@@ -93,13 +121,20 @@ class ElaspedTimeDocumentManager:
@staticmethod
def delete_index():
+ """
+ 删除性能监控索引
+ """
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
+ """
+ 创建并保存页面响应时间记录
+ """
ElaspedTimeDocumentManager.build_index()
+ # 构建用户代理信息对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@@ -116,98 +151,125 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
+ # 创建文档实例
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
- 1000))
+ 1000)) # 使用当前时间戳作为文档ID
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
- useragent=ua, ip=ip)
+ useragent=ua,
+ ip=ip)
+ # 保存文档,并使用geoip管道处理IP地理位置信息
doc.save(pipeline="geoip")
class ArticleDocument(Document):
- body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
- title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
- author = Object(properties={
+ """
+ 文章文档类,用于Elasticsearch全文搜索
+ """
+ body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 文章正文,使用IK分词器
+ title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 文章标题,使用IK分词器
+ author = Object(properties={ # 作者信息
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
- category = Object(properties={
+ category = Object(properties={ # 分类信息
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
- tags = Object(properties={
+ tags = Object(properties={ # 标签信息
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
- pub_time = Date()
- status = Text()
- comment_status = Text()
- type = Text()
- views = Integer()
- article_order = Integer()
+ pub_time = Date() # 发布时间
+ status = Text() # 文章状态
+ comment_status = Text() # 评论状态
+ type = Text() # 文章类型
+ views = Integer() # 浏览量
+ article_order = Integer() # 文章排序
class Index:
- name = 'blog'
+ name = 'blog' # 索引名称
settings = {
- "number_of_shards": 1,
- "number_of_replicas": 0
+ "number_of_shards": 1, # 分片数量
+ "number_of_replicas": 0 # 副本数量
}
class Meta:
- doc_type = 'Article'
+ doc_type = 'Article' # 文档类型
class ArticleDocumentManager():
+ """
+ 文章文档管理器类,用于管理文章在Elasticsearch中的索引
+ """
def __init__(self):
self.create_index()
def create_index(self):
+ """
+ 创建文章索引
+ """
ArticleDocument.init()
def delete_index(self):
+ """
+ 删除文章索引
+ """
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
+ """
+ 将Django文章对象转换为Elasticsearch文档对象
+ """
return [
ArticleDocument(
meta={
- 'id': article.id},
- body=article.body,
- title=article.title,
- author={
+ 'id': article.id}, # 文档ID
+ body=article.body, # 文章正文
+ title=article.title, # 文章标题
+ author={ # 作者信息
'nickname': article.author.username,
'id': article.author.id},
- category={
+ category={ # 分类信息
'name': article.category.name,
'id': article.category.id},
- tags=[
+ tags=[ # 标签信息
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
- pub_time=article.pub_time,
- status=article.status,
- comment_status=article.comment_status,
- type=article.type,
- views=article.views,
+ pub_time=article.pub_time, # 发布时间
+ status=article.status, # 文章状态
+ comment_status=article.comment_status, # 评论状态
+ type=article.type, # 文章类型
+ views=article.views, # 浏览量
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
+ """
+ 重新构建文章索引
+ """
ArticleDocument.init()
+ # 如果没有提供文章列表,则获取所有文章
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
+ # 逐个保存文档
for doc in docs:
doc.save()
def update_docs(self, docs):
+ """
+ 更新文档
+ """
for doc in docs:
doc.save()
+
diff --git a/src/blog/forms.py b/src/blog/forms.py
index 715be76..a1e2816 100644
--- a/src/blog/forms.py
+++ b/src/blog/forms.py
@@ -3,17 +3,33 @@ import logging
from django import forms
from haystack.forms import SearchForm
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__) # 创建日志记录器
class BlogSearchForm(SearchForm):
+ """
+ 博客搜索表单类,继承自Haystack的SearchForm
+ """
+ # 定义搜索查询字段,设置为必填
querydata = forms.CharField(required=True)
def search(self):
+ """
+ 执行搜索操作
+
+ Returns:
+ 搜索结果数据
+ """
+ # 调用父类的搜索方法
datas = super(BlogSearchForm, self).search()
+
+ # 验证表单数据
if not self.is_valid():
- return self.no_query_found()
+ return self.no_query_found() # 如果表单无效,返回无查询结果
+ # 如果查询数据存在,记录查询日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
- return datas
+
+ return datas # 返回搜索结果
+
diff --git a/src/blog/management/commands/build_index.py b/src/blog/management/commands/build_index.py
index 3c4acd7..60d68c6 100644
--- a/src/blog/management/commands/build_index.py
+++ b/src/blog/management/commands/build_index.py
@@ -6,13 +6,27 @@ from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedT
# TODO 参数化
class Command(BaseCommand):
- help = 'build search index'
+ """
+ Django管理命令,用于构建搜索引擎索引
+ """
+ help = 'build search index' # 命令帮助信息
def handle(self, *args, **options):
+ """
+ 命令处理函数,执行索引构建操作
+ """
+ # 检查是否启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
+ # 构建耗时文档索引
ElaspedTimeDocumentManager.build_index()
+ # 初始化耗时文档
manager = ElapsedTimeDocument()
manager.init()
+ # 重新构建文章文档索引
manager = ArticleDocumentManager()
- manager.delete_index()
- manager.rebuild()
+ manager.delete_index() # 删除现有索引
+ manager.rebuild() # 重新构建索引
+ else:
+ # 如果未启用Elasticsearch,可以添加提示信息或处理逻辑
+ pass
+
diff --git a/src/blog/management/commands/build_search_words.py b/src/blog/management/commands/build_search_words.py
index cfe7e0d..05c7720 100644
--- a/src/blog/management/commands/build_search_words.py
+++ b/src/blog/management/commands/build_search_words.py
@@ -5,9 +5,19 @@ from blog.models import Tag, Category
# TODO 参数化
class Command(BaseCommand):
- help = 'build search words'
+ """
+ Django管理命令,用于构建搜索词列表
+ """
+ help = 'build search words' # 命令帮助信息
def handle(self, *args, **options):
- datas = set([t.name for t in Tag.objects.all()] +
- [t.name for t in Category.objects.all()])
+ """
+ 命令处理函数,生成标签和分类名称的搜索词列表
+ """
+ # 收集所有标签和分类的名称,使用set去重
+ datas = set([t.name for t in Tag.objects.all()] + # 获取所有标签名称
+ [t.name for t in Category.objects.all()]) # 获取所有分类名称
+
+ # 将搜索词列表按行打印输出
print('\n'.join(datas))
+
diff --git a/src/blog/management/commands/clear_cache.py b/src/blog/management/commands/clear_cache.py
index 0d66172..fd33028 100644
--- a/src/blog/management/commands/clear_cache.py
+++ b/src/blog/management/commands/clear_cache.py
@@ -4,8 +4,17 @@ from djangoblog.utils import cache
class Command(BaseCommand):
- help = 'clear the whole cache'
+ """
+ Django管理命令,用于清除整个缓存
+ """
+ help = 'clear the whole cache' # 命令帮助信息
def handle(self, *args, **options):
- cache.clear()
+ """
+ 命令处理函数,执行缓存清除操作
+ """
+ cache.clear() # 清除所有缓存
+
+ # 输出成功信息到控制台
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
+
diff --git a/src/blog/management/commands/create_testdata.py b/src/blog/management/commands/create_testdata.py
index 675d2ba..01f8036 100644
--- a/src/blog/management/commands/create_testdata.py
+++ b/src/blog/management/commands/create_testdata.py
@@ -6,35 +6,61 @@ from blog.models import Article, Tag, Category
class Command(BaseCommand):
- help = 'create test datas'
+ """
+ Django管理命令,用于创建测试数据
+ """
+ help = 'create test datas' # 命令帮助信息
def handle(self, *args, **options):
+ """
+ 命令处理函数,创建测试用的用户、分类、标签和文章数据
+ """
+ # 创建或获取测试用户
user = get_user_model().objects.get_or_create(
- email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
+ email='test@test.com',
+ username='测试用户',
+ password=make_password('test!q@w#eTYU'))[0]
+ # 创建父分类
pcategory = Category.objects.get_or_create(
- name='我是父类目', parent_category=None)[0]
+ name='我是父类目',
+ parent_category=None)[0]
+ # 创建子分类
category = Category.objects.get_or_create(
- name='子类目', parent_category=pcategory)[0]
+ name='子类目',
+ parent_category=pcategory)[0]
category.save()
+
+ # 创建基础标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
+
+ # 创建20篇文章及其对应的标签
for i in range(1, 20):
+ # 创建文章
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
+
+ # 为每篇文章创建专属标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
+
+ # 将标签关联到文章
article.tags.add(tag)
article.tags.add(basetag)
article.save()
+ # 清除缓存
from djangoblog.utils import cache
cache.clear()
+
+ # 输出成功信息
self.stdout.write(self.style.SUCCESS('created test datas \n'))
+
diff --git a/src/blog/management/commands/ping_baidu.py b/src/blog/management/commands/ping_baidu.py
index 2c7fbdd..6f13c92 100644
--- a/src/blog/management/commands/ping_baidu.py
+++ b/src/blog/management/commands/ping_baidu.py
@@ -4,47 +4,73 @@ from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
-site = get_current_site().domain
+site = get_current_site().domain # 获取当前站点域名
class Command(BaseCommand):
- help = 'notify baidu url'
+ """
+ Django管理命令,用于向百度搜索引擎提交URL进行收录通知
+ """
+ help = 'notify baidu url' # 命令帮助信息
def add_arguments(self, parser):
+ """
+ 添加命令行参数
+ """
parser.add_argument(
- 'data_type',
- type=str,
- choices=[
+ 'data_type', # 参数名称
+ type=str, # 参数类型
+ choices=[ # 参数可选值
'all',
'article',
'tag',
'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):
+ """
+ 根据相对路径生成完整URL
+ Args:
+ path: 相对路径
+ Returns:
+ 完整的HTTPS URL
+ """
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
- type = options['data_type']
- self.stdout.write('start get %s' % type)
+ """
+ 命令处理函数,执行百度URL提交操作
+ """
+ type = options['data_type'] # 获取数据类型参数
+ self.stdout.write('start get %s' % type) # 输出开始信息
- urls = []
+ urls = [] # 存储需要提交的URL列表
+
+ # 根据数据类型收集相应的URL
if type == 'article' or type == 'all':
+ # 收集所有已发布文章的URL
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
+ # 收集所有标签页面的URL
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
+ # 收集所有分类页面的URL
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
+ # 输出准备提交的URL数量
self.stdout.write(
self.style.SUCCESS(
- 'start notify %d urls' %
- len(urls)))
+ 'start notify %d urls' % len(urls)))
+
+ # 向百度提交URL
SpiderNotify.baidu_notify(urls)
+
+ # 输出完成信息
self.stdout.write(self.style.SUCCESS('finish notify'))
+
diff --git a/src/blog/management/commands/sync_user_avatar.py b/src/blog/management/commands/sync_user_avatar.py
index d0f4612..dabf810 100644
--- a/src/blog/management/commands/sync_user_avatar.py
+++ b/src/blog/management/commands/sync_user_avatar.py
@@ -8,40 +8,68 @@ from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
- help = 'sync user avatar'
+ """
+ Django管理命令,用于同步用户头像
+ """
+ help = 'sync user avatar' # 命令帮助信息
def test_picture(self, url):
+ """
+ 测试图片URL是否有效
+ Args:
+ url: 图片URL
+ Returns:
+ bool: URL是否有效
+ """
try:
+ # 发送GET请求测试图片URL,超时时间为2秒
if requests.get(url, timeout=2).status_code == 200:
return True
except:
+ # 出现异常说明URL无效
pass
def handle(self, *args, **options):
- static_url = static("../")
- users = OAuthUser.objects.all()
- self.stdout.write(f'开始同步{len(users)}个用户头像')
+ """
+ 命令处理函数,执行用户头像同步操作
+ """
+ static_url = static("../") # 获取静态文件基础URL
+ users = OAuthUser.objects.all() # 获取所有OAuth用户
+ self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出开始信息
+
+ # 遍历所有用户进行头像同步
for u in users:
- self.stdout.write(f'开始同步:{u.nickname}')
- url = u.picture
+ self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户
+ url = u.picture # 获取用户当前头像URL
+
if url:
+ # 如果URL以静态URL开头,说明是本地头像
if url.startswith(static_url):
+ # 测试图片是否有效
if self.test_picture(url):
- continue
+ continue # 有效则跳过
else:
+ # 无效则尝试从元数据重新获取
if u.metadata:
- manage = get_manager_by_type(u.type)
- url = manage.get_picture(u.metadata)
- url = save_user_avatar(url)
+ manage = get_manager_by_type(u.type) # 获取对应的OAuth管理器
+ url = manage.get_picture(u.metadata) # 从元数据获取新图片URL
+ url = save_user_avatar(url) # 保存用户头像
else:
+ # 没有元数据则使用默认头像
url = static('blog/img/avatar.png')
else:
+ # 非本地头像则保存新头像
url = save_user_avatar(url)
else:
+ # 没有头像URL则使用默认头像
url = static('blog/img/avatar.png')
+
+ # 如果获取到有效的头像URL,则更新用户头像
if url:
self.stdout.write(
- f'结束同步:{u.nickname}.url:{url}')
- u.picture = url
- u.save()
- self.stdout.write('结束同步')
+ f'结束同步:{u.nickname}.url:{url}') # 输出同步结果
+ u.picture = url # 更新用户头像URL
+ u.save() # 保存到数据库
+
+ self.stdout.write('结束同步') # 输出结束信息
+
diff --git a/src/blog/middleware.py b/src/blog/middleware.py
index 94dd70c..a46b8e8 100644
--- a/src/blog/middleware.py
+++ b/src/blog/middleware.py
@@ -6,37 +6,66 @@ from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__) # 创建日志记录器
class OnlineMiddleware(object):
+ """
+ 在线中间件类,用于记录页面加载时间和用户访问信息
+ """
def __init__(self, get_response=None):
+ """
+ 初始化中间件
+ Args:
+ get_response: Django的响应获取函数
+ """
self.get_response = get_response
super().__init__()
def __call__(self, request):
+ """
+ 中间件调用方法,处理请求和响应
+ Args:
+ request: HTTP请求对象
+ Returns:
+ HTTP响应对象
+ """
''' page render time '''
- start_time = time.time()
- response = self.get_response(request)
- http_user_agent = request.META.get('HTTP_USER_AGENT', '')
+ start_time = time.time() # 记录请求开始时间
+ response = self.get_response(request) # 获取响应
+
+ # 获取用户IP地址
ip, _ = get_client_ip(request)
+
+ # 解析用户代理信息
+ http_user_agent = request.META.get('HTTP_USER_AGENT', '')
user_agent = parse(http_user_agent)
+
+ # 如果响应不是流式响应,则处理性能数据
if not response.streaming:
try:
+ # 计算页面渲染耗时
cast_time = time.time() - start_time
+
+ # 如果启用了Elasticsearch,则记录性能数据
if ELASTICSEARCH_ENABLED:
- time_taken = round((cast_time) * 1000, 2)
- url = request.path
+ time_taken = round((cast_time) * 1000, 2) # 转换为毫秒
+ url = request.path # 获取请求路径
from django.utils import timezone
+ # 创建并保存性能文档
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
+
+ # 在响应内容中替换加载时间占位符
response.content = response.content.replace(
b'', str.encode(str(cast_time)[:5]))
except Exception as e:
+ # 记录错误日志
logger.error("Error OnlineMiddleware: %s" % e)
- return response
+ return response # 返回响应
+
diff --git a/src/blog/migrations/0001_initial.py b/src/blog/migrations/0001_initial.py
index 3d391b6..46ae7a3 100644
--- a/src/blog/migrations/0001_initial.py
+++ b/src/blog/migrations/0001_initial.py
@@ -8,14 +8,17 @@ import mdeditor.fields
class Migration(migrations.Migration):
-
+ # 标记这是一个初始迁移文件
initial = True
+ # 定义依赖关系
dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型
]
+ # 定义具体的操作
operations = [
+ # 创建BlogSettings模型(网站配置)
migrations.CreateModel(
name='BlogSettings',
fields=[
@@ -41,6 +44,8 @@ class Migration(migrations.Migration):
'verbose_name_plural': '网站配置',
},
),
+
+ # 创建Links模型(友情链接)
migrations.CreateModel(
name='Links',
fields=[
@@ -59,6 +64,8 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
+
+ # 创建SideBar模型(侧边栏)
migrations.CreateModel(
name='SideBar',
fields=[
@@ -76,6 +83,8 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
+
+ # 创建Tag模型(标签)
migrations.CreateModel(
name='Tag',
fields=[
@@ -91,6 +100,8 @@ class Migration(migrations.Migration):
'ordering': ['name'],
},
),
+
+ # 创建Category模型(分类)
migrations.CreateModel(
name='Category',
fields=[
@@ -108,6 +119,8 @@ class Migration(migrations.Migration):
'ordering': ['-index'],
},
),
+
+ # 创建Article模型(文章)
migrations.CreateModel(
name='Article',
fields=[
@@ -115,7 +128,7 @@ class Migration(migrations.Migration):
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
- ('body', mdeditor.fields.MDTextField(verbose_name='正文')),
+ ('body', mdeditor.fields.MDTextField(verbose_name='正文')), # 使用Markdown编辑器字段
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
@@ -130,8 +143,9 @@ class Migration(migrations.Migration):
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
- 'ordering': ['-article_order', '-pub_time'],
+ 'ordering': ['-article_order', '-pub_time'], # 按排序和发布时间降序排列
'get_latest_by': 'id',
},
),
]
+
diff --git a/src/blog/migrations/0002_blogsettings_global_footer_and_more.py b/src/blog/migrations/0002_blogsettings_global_footer_and_more.py
index adbaa36..e20a6f5 100644
--- a/src/blog/migrations/0002_blogsettings_global_footer_and_more.py
+++ b/src/blog/migrations/0002_blogsettings_global_footer_and_more.py
@@ -4,20 +4,24 @@ from django.db import migrations, models
class Migration(migrations.Migration):
-
+ # 定义该迁移文件的依赖关系,依赖于blog应用的0001_initial迁移
dependencies = [
('blog', '0001_initial'),
]
+ # 定义具体的迁移操作
operations = [
+ # 为BlogSettings模型添加global_footer字段
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
+ # 为BlogSettings模型添加global_header字段
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]
+
diff --git a/src/blog/migrations/0003_blogsettings_comment_need_review.py b/src/blog/migrations/0003_blogsettings_comment_need_review.py
index e9f5502..4c283e9 100644
--- a/src/blog/migrations/0003_blogsettings_comment_need_review.py
+++ b/src/blog/migrations/0003_blogsettings_comment_need_review.py
@@ -4,14 +4,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
+ # 定义该迁移文件的依赖关系,依赖于blog应用的0002_blogsettings_global_footer_and_more迁移
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
+ # 定义具体的迁移操作
operations = [
+ # 为BlogSettings模型添加comment_need_review字段
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]
+
diff --git a/src/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/src/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
index ceb1398..0b0d51a 100644
--- a/src/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
+++ b/src/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
@@ -4,24 +4,30 @@ from django.db import migrations
class Migration(migrations.Migration):
+ # 定义该迁移文件的依赖关系,依赖于blog应用的0003_blogsettings_comment_need_review迁移
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
+ # 定义具体的迁移操作
operations = [
+ # 将BlogSettings模型中的analyticscode字段重命名为analytics_code
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
+ # 将BlogSettings模型中的beiancode字段重命名为beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
+ # 将BlogSettings模型中的sitename字段重命名为site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]
+
diff --git a/src/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/src/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
index d08e853..a0f253c 100644
--- a/src/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
+++ b/src/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
@@ -8,293 +8,357 @@ import mdeditor.fields
class Migration(migrations.Migration):
-
+ # 定义该迁移文件的依赖关系
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
+ # 定义具体的迁移操作
operations = [
+ # 修改Article模型的选项配置,将显示名称改为英文
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
+ # 修改Category模型的选项配置,将显示名称改为英文
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
+ # 修改Links模型的选项配置,将显示名称改为英文
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
+ # 修改SideBar模型的选项配置,将显示名称改为英文
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
+ # 修改Tag模型的选项配置,将显示名称改为英文
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
+
+ # 移除Article模型中的created_time字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
+ # 移除Article模型中的last_mod_time字段
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
+ # 移除Category模型中的created_time字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
+ # 移除Category模型中的last_mod_time字段
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
+ # 移除Links模型中的created_time字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
+ # 移除SideBar模型中的created_time字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
+ # 移除Tag模型中的created_time字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
+ # 移除Tag模型中的last_mod_time字段
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
+
+ # 添加Article模型中的creation_time字段
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+ # 添加Article模型中的last_modify_time字段
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
+ # 添加Category模型中的creation_time字段
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+ # 添加Category模型中的last_modify_time字段
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
+ # 添加Links模型中的creation_time字段
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+ # 添加SideBar模型中的creation_time字段
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+ # 添加Tag模型中的creation_time字段
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
+ # 添加Tag模型中的last_modify_time字段
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
+
+ # 修改Article模型中的article_order字段显示名称
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
+ # 修改Article模型中的author字段显示名称
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
+ # 修改Article模型中的body字段显示名称
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
+ # 修改Article模型中的category字段显示名称
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
+ # 修改Article模型中的comment_status字段显示名称和选项
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
+ # 修改Article模型中的pub_time字段显示名称
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
+ # 修改Article模型中的show_toc字段显示名称
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
+ # 修改Article模型中的status字段显示名称和选项
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
+ # 修改Article模型中的tags字段显示名称
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
+ # 修改Article模型中的title字段显示名称
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
+ # 修改Article模型中的type字段显示名称和选项
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
+ # 修改Article模型中的views字段显示名称
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
+ # 修改BlogSettings模型中的article_comment_count字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
+ # 修改BlogSettings模型中的article_sub_length字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
+ # 修改BlogSettings模型中的google_adsense_codes字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
+ # 修改BlogSettings模型中的open_site_comment字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
+ # 修改BlogSettings模型中的show_google_adsense字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
+ # 修改BlogSettings模型中的sidebar_article_count字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
+ # 修改BlogSettings模型中的sidebar_comment_count字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
+ # 修改BlogSettings模型中的site_description字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
+ # 修改BlogSettings模型中的site_keywords字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
+ # 修改BlogSettings模型中的site_name字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
+ # 修改BlogSettings模型中的site_seo_description字段显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
+ # 修改Category模型中的index字段显示名称
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
+ # 修改Category模型中的name字段显示名称
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
+ # 修改Category模型中的parent_category字段显示名称
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
+ # 修改Links模型中的is_enable字段显示名称
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
+ # 修改Links模型中的last_mod_time字段显示名称
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
+ # 修改Links模型中的link字段显示名称
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
+ # 修改Links模型中的name字段显示名称
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
+ # 修改Links模型中的sequence字段显示名称
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
+ # 修改Links模型中的show_type字段显示名称和选项
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
+ # 修改SideBar模型中的content字段显示名称
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
+ # 修改SideBar模型中的is_enable字段显示名称
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
+ # 修改SideBar模型中的last_mod_time字段显示名称
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
+ # 修改SideBar模型中的name字段显示名称
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
+ # 修改SideBar模型中的sequence字段显示名称
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
+ # 修改Tag模型中的name字段显示名称
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]
+
diff --git a/src/blog/migrations/0006_alter_blogsettings_options.py b/src/blog/migrations/0006_alter_blogsettings_options.py
index e36feb4..7df04ed 100644
--- a/src/blog/migrations/0006_alter_blogsettings_options.py
+++ b/src/blog/migrations/0006_alter_blogsettings_options.py
@@ -4,14 +4,17 @@ from django.db import migrations
class Migration(migrations.Migration):
-
+ # 定义该迁移文件的依赖关系,依赖于blog应用的0005_alter_article_options_alter_category_options_and_more迁移
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
+ # 定义具体的迁移操作
operations = [
+ # 修改BlogSettings模型的选项配置,将显示名称改为英文
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]
+
diff --git a/src/blog/models.py b/src/blog/models.py
index 083788b..af073bc 100644
--- a/src/blog/models.py
+++ b/src/blog/models.py
@@ -14,6 +14,444 @@ from uuslug import slugify
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
+logger = logging.getLogger(__name__) # 创建日志记录器
+
+
+class LinkShowType(models.TextChoices):
+ """
+ 链接显示类型枚举类
+ """
+ I = ('i', _('index')) # 首页
+ L = ('l', _('list')) # 列表页
+ P = ('p', _('post')) # 文章页面
+ A = ('a', _('all')) # 全站
+ S = ('s', _('slide')) # 幻灯片
+
+
+class BaseModel(models.Model):
+ """
+ 基础模型类,提供通用字段和方法
+ """
+ id = models.AutoField(primary_key=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_modify_time = models.DateTimeField(_('modify time'), default=now)
+
+ def save(self, *args, **kwargs):
+ """
+ 重写保存方法,处理slug生成和视图计数更新
+ """
+ # 判断是否是仅更新浏览量的操作
+ is_update_views = isinstance(
+ self,
+ Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
+ if is_update_views:
+ # 如果是更新浏览量,直接更新数据库避免触发其他逻辑
+ Article.objects.filter(pk=self.pk).update(views=self.views)
+ else:
+ # 如果有slug字段,自动生成slug
+ if 'slug' in self.__dict__:
+ slug = getattr(
+ self, 'title') if 'title' in self.__dict__ else getattr(
+ self, 'name')
+ setattr(self, 'slug', slugify(slug))
+ super().save(*args, **kwargs)
+
+ def get_full_url(self):
+ """
+ 获取完整的URL地址
+ """
+ site = get_current_site().domain
+ url = "https://{site}{path}".format(site=site,
+ path=self.get_absolute_url())
+ return url
+
+ class Meta:
+ abstract = True # 声明为抽象类
+
+ @abstractmethod
+ def get_absolute_url(self):
+ """
+ 抽象方法,子类必须实现获取绝对URL的方法
+ """
+ pass
+
+
+class Article(BaseModel):
+ """文章模型"""
+ STATUS_CHOICES = (
+ ('d', _('Draft')), # 草稿
+ ('p', _('Published')), # 已发布
+ )
+ COMMENT_STATUS = (
+ ('o', _('Open')), # 开启评论
+ ('c', _('Close')), # 关闭评论
+ )
+ TYPE = (
+ ('a', _('Article')), # 文章
+ ('p', _('Page')), # 页面
+ )
+
+ title = models.CharField(_('title'), max_length=200, unique=True)
+ body = MDTextField(_('body')) # 使用Markdown编辑器字段
+ pub_time = models.DateTimeField(
+ _('publish time'), blank=False, null=False, default=now)
+ status = models.CharField(
+ _('status'),
+ max_length=1,
+ choices=STATUS_CHOICES,
+ default='p')
+ comment_status = models.CharField(
+ _('comment status'),
+ max_length=1,
+ choices=COMMENT_STATUS,
+ default='o')
+ type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
+ views = models.PositiveIntegerField(_('views'), default=0)
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ blank=False,
+ null=False,
+ on_delete=models.CASCADE)
+ article_order = models.IntegerField(
+ _('order'), blank=False, null=False, default=0)
+ show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
+ category = models.ForeignKey(
+ 'Category',
+ verbose_name=_('category'),
+ on_delete=models.CASCADE,
+ blank=False,
+ null=False)
+ tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
+
+ def body_to_string(self):
+ """
+ 将文章正文转换为字符串
+ """
+ return self.body
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ ordering = ['-article_order', '-pub_time'] # 按排序和发布时间降序排列
+ verbose_name = _('article')
+ verbose_name_plural = verbose_name
+ get_latest_by = 'id'
+
+ def get_absolute_url(self):
+ """
+ 获取文章绝对URL
+ """
+ return reverse('blog:detailbyid', kwargs={
+ 'article_id': self.id,
+ 'year': self.creation_time.year,
+ 'month': self.creation_time.month,
+ 'day': self.creation_time.day
+ })
+
+ @cache_decorator(60 * 60 * 10)
+ def get_category_tree(self):
+ """
+ 获取分类树结构
+ """
+ tree = self.category.get_category_tree()
+ names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
+
+ return names
+
+ def save(self, *args, **kwargs):
+ """
+ 保存文章
+ """
+ super().save(*args, **kwargs)
+
+ def viewed(self):
+ """
+ 增加文章浏览量
+ """
+ self.views += 1
+ self.save(update_fields=['views'])
+
+ def comment_list(self):
+ """
+ 获取文章评论列表(带缓存)
+ """
+ cache_key = 'article_comments_{id}'.format(id=self.id)
+ value = cache.get(cache_key)
+ if value:
+ logger.info('get article comments:{id}'.format(id=self.id))
+ return value
+ else:
+ comments = self.comment_set.filter(is_enable=True).order_by('-id')
+ cache.set(cache_key, comments, 60 * 100)
+ logger.info('set article comments:{id}'.format(id=self.id))
+ return comments
+
+ def get_admin_url(self):
+ """
+ 获取管理后台的文章编辑URL
+ """
+ info = (self._meta.app_label, self._meta.model_name)
+ return reverse('admin:%s_%s_change' % info, args=(self.pk,))
+
+ @cache_decorator(expiration=60 * 100)
+ def next_article(self):
+ """
+ 获取下一篇已发布的文章
+ """
+ # 下一篇
+ return Article.objects.filter(
+ id__gt=self.id, status='p').order_by('id').first()
+
+ @cache_decorator(expiration=60 * 100)
+ def prev_article(self):
+ """
+ 获取上一篇已发布的文章
+ """
+ # 前一篇
+ return Article.objects.filter(id__lt=self.id, status='p').first()
+
+ def get_first_image_url(self):
+ """
+ 从文章正文中获取第一张图片的URL
+ :return: 图片URL或空字符串
+ """
+ match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
+ if match:
+ return match.group(1)
+ return ""
+
+
+class Category(BaseModel):
+ """文章分类模型"""
+ name = models.CharField(_('category name'), max_length=30, unique=True)
+ parent_category = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent category'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+ index = models.IntegerField(default=0, verbose_name=_('index'))
+
+ class Meta:
+ ordering = ['-index'] # 按索引降序排列
+ verbose_name = _('category')
+ verbose_name_plural = verbose_name
+
+ def get_absolute_url(self):
+ """
+ 获取分类绝对URL
+ """
+ return reverse(
+ 'blog:category_detail', kwargs={
+ 'category_name': self.slug})
+
+ def __str__(self):
+ return self.name
+
+ @cache_decorator(60 * 60 * 10)
+ def get_category_tree(self):
+ """
+ 递归获得分类目录的父级分类树
+ :return: 分类列表
+ """
+ categorys = []
+
+ def parse(category):
+ categorys.append(category)
+ if category.parent_category:
+ parse(category.parent_category)
+
+ parse(self)
+ return categorys
+
+ @cache_decorator(60 * 60 * 10)
+ def get_sub_categorys(self):
+ """
+ 获得当前分类目录所有子分类
+ :return: 子分类列表
+ """
+ categorys = []
+ all_categorys = Category.objects.all()
+
+ def parse(category):
+ if category not in categorys:
+ categorys.append(category)
+ childs = all_categorys.filter(parent_category=category)
+ for child in childs:
+ if category not in categorys:
+ categorys.append(child)
+ parse(child)
+
+ parse(self)
+ return categorys
+
+
+class Tag(BaseModel):
+ """文章标签模型"""
+ name = models.CharField(_('tag name'), max_length=30, unique=True)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ """
+ 获取标签绝对URL
+ """
+ return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
+
+ @cache_decorator(60 * 60 * 10)
+ def get_article_count(self):
+ """
+ 获取该标签下的文章数量
+ """
+ return Article.objects.filter(tags__name=self.name).distinct().count()
+
+ class Meta:
+ ordering = ['name'] # 按名称升序排列
+ verbose_name = _('tag')
+ verbose_name_plural = verbose_name
+
+
+class Links(models.Model):
+ """友情链接模型"""
+
+ name = models.CharField(_('link name'), max_length=30, unique=True)
+ link = models.URLField(_('link'))
+ sequence = models.IntegerField(_('order'), unique=True)
+ is_enable = models.BooleanField(
+ _('is show'), default=True, blank=False, null=False)
+ show_type = models.CharField(
+ _('show type'),
+ max_length=1,
+ choices=LinkShowType.choices,
+ default=LinkShowType.I)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ class Meta:
+ ordering = ['sequence'] # 按顺序排列
+ verbose_name = _('link')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.name
+
+
+class SideBar(models.Model):
+ """侧边栏模型,可以展示一些html内容"""
+ name = models.CharField(_('title'), max_length=100)
+ content = models.TextField(_('content'))
+ sequence = models.IntegerField(_('order'), unique=True)
+ is_enable = models.BooleanField(_('is enable'), default=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ class Meta:
+ ordering = ['sequence'] # 按顺序排列
+ verbose_name = _('sidebar')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.name
+
+
+class BlogSettings(models.Model):
+ """博客配置模型"""
+ site_name = models.CharField(
+ _('site name'),
+ max_length=200,
+ null=False,
+ blank=False,
+ default='')
+ site_description = models.TextField(
+ _('site description'),
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ site_seo_description = models.TextField(
+ _('site seo description'), max_length=1000, null=False, blank=False, default='')
+ site_keywords = models.TextField(
+ _('site keywords'),
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ article_sub_length = models.IntegerField(_('article sub length'), default=300)
+ sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
+ sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
+ article_comment_count = models.IntegerField(_('article comment count'), default=5)
+ show_google_adsense = models.BooleanField(_('show adsense'), default=False)
+ google_adsense_codes = models.TextField(
+ _('adsense code'), max_length=2000, null=True, blank=True, default='')
+ open_site_comment = models.BooleanField(_('open site comment'), default=True)
+ global_header = models.TextField("公共头部", null=True, blank=True, default='')
+ global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
+ beian_code = models.CharField(
+ '备案号',
+ max_length=2000,
+ null=True,
+ blank=True,
+ default='')
+ analytics_code = models.TextField(
+ "网站统计代码",
+ max_length=1000,
+ null=False,
+ blank=False,
+ default='')
+ show_gongan_code = models.BooleanField(
+ '是否显示公安备案号', default=False, null=False)
+ gongan_beiancode = models.TextField(
+ '公安备案号',
+ max_length=2000,
+ null=True,
+ blank=True,
+ default='')
+ comment_need_review = models.BooleanField(
+ '评论是否需要审核', default=False, null=False)
+
+ class Meta:
+ verbose_name = _('Website configuration')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.site_name
+
+ def clean(self):
+ """
+ 验证模型数据,确保只存在一个配置实例
+ """
+ if BlogSettings.objects.exclude(id=self.id).count():
+ raise ValidationError(_('There can only be one configuration'))
+
+ def save(self, *args, **kwargs):
+ """
+ 保存配置并清除缓存
+ """
+ super().save(*args, **kwargs)
+ from djangoblog.utils import cache
+ cache.clear()
+
+import logging
+import re
+from abc import abstractmethod
+
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.urls import reverse
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+from mdeditor.fields import MDTextField
+from uuslug import slugify
+
+from djangoblog.utils import cache_decorator, cache
+from djangoblog.utils import get_current_site
+
logger = logging.getLogger(__name__)
diff --git a/src/blog/search_indexes.py b/src/blog/search_indexes.py
index 7f1dfac..3074b74 100644
--- a/src/blog/search_indexes.py
+++ b/src/blog/search_indexes.py
@@ -4,10 +4,25 @@ from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
+ """
+ 文章搜索索引类,用于Haystack全文搜索
+ """
+ # 定义文档字段,use_template=True表示使用模板来构建索引内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
+ """
+ 返回索引对应的模型类
+ """
return Article
def index_queryset(self, using=None):
- return self.get_model().objects.filter(status='p')
+ """
+ 返回需要建立索引的查询集
+ Args:
+ using: 数据库别名
+ Returns:
+ 已发布文章的查询集
+ """
+ return self.get_model().objects.filter(status='p') # 只对已发布的文章建立索引
+
diff --git a/src/blog/templatetags/blog_tags.py b/src/blog/templatetags/blog_tags.py
index d6cd5d5..2d5980d 100644
--- a/src/blog/templatetags/blog_tags.py
+++ b/src/blog/templatetags/blog_tags.py
@@ -20,18 +20,26 @@ from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
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)
def head_meta(context):
+ """
+ 生成页面头部meta信息
+ """
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
+ """
+ 格式化时间显示
+ :param data: 时间数据
+ :return: 格式化后的时间字符串
+ """
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
@@ -41,6 +49,11 @@ def timeformat(data):
@register.simple_tag
def datetimeformat(data):
+ """
+ 格式化日期时间显示
+ :param data: 日期时间数据
+ :return: 格式化后的日期时间字符串
+ """
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
@@ -51,11 +64,21 @@ def datetimeformat(data):
@register.filter()
@stringfilter
def custom_markdown(content):
+ """
+ 将内容转换为markdown格式
+ :param content: 原始内容
+ :return: markdown格式的内容
+ """
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
def get_markdown_toc(content):
+ """
+ 获取markdown内容的目录
+ :param content: markdown内容
+ :return: 目录HTML
+ """
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@@ -64,6 +87,11 @@ def get_markdown_toc(content):
@register.filter()
@stringfilter
def comment_markdown(content):
+ """
+ 将评论内容转换为markdown格式并清理HTML
+ :param content: 评论内容
+ :return: 清理后的markdown格式内容
+ """
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@@ -73,8 +101,8 @@ def comment_markdown(content):
def truncatechars_content(content):
"""
获得文章内容的摘要
- :param content:
- :return:
+ :param content: 文章内容
+ :return: 截取后的摘要内容
"""
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
@@ -85,17 +113,21 @@ def truncatechars_content(content):
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
+ """
+ 截取内容前150个字符(去除HTML标签)
+ :param content: 原始内容
+ :return: 截取后的纯文本内容
+ """
from django.utils.html import strip_tags
-
return strip_tags(content)[:150]
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
- 获得文章面包屑
- :param article:
- :return:
+ 获得文章面包屑导航
+ :param article: 文章对象
+ :return: 面包屑导航数据
"""
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
@@ -114,9 +146,9 @@ def load_breadcrumb(article):
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
- 文章标签
- :param article:
- :return:
+ 加载文章标签列表
+ :param article: 文章对象
+ :return: 标签列表数据
"""
tags = article.tags.all()
tags_list = []
@@ -135,7 +167,9 @@ def load_articletags(article):
def load_sidebar(user, linktype):
"""
加载侧边栏
- :return:
+ :param user: 当前用户
+ :param linktype: 链接显示类型
+ :return: 侧边栏数据
"""
value = cache.get("sidebar" + linktype)
if value:
@@ -145,28 +179,38 @@ def load_sidebar(user, linktype):
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
+ # 获取最新文章
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
+ # 获取所有分类
sidebar_categorys = Category.objects.all()
+ # 获取启用的额外侧边栏
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
+ # 获取阅读量最高的文章
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
+ # 获取文章发布日期
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
+ # 获取友情链接
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
+ # 获取最新评论
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
+
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
if tags and len(tags) > 0:
+ # 获取每个标签的文章数量
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
+ # 计算标签字体大小
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags)
@@ -185,6 +229,7 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
+ # 缓存侧边栏数据3小时
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
@@ -195,8 +240,9 @@ def load_sidebar(user, linktype):
def load_article_metas(article, user):
"""
获得文章meta信息
- :param article:
- :return:
+ :param article: 文章对象
+ :param user: 当前用户
+ :return: 文章meta信息数据
"""
return {
'article': article,
@@ -206,8 +252,17 @@ def load_article_metas(article, user):
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
+ """
+ 加载分页信息
+ :param page_obj: 分页对象
+ :param page_type: 页面类型
+ :param tag_name: 标签名/分类名/作者名
+ :return: 分页链接数据
+ """
previous_url = ''
next_url = ''
+
+ # 首页分页
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
@@ -217,6 +272,8 @@ def load_pagination_info(page_obj, page_type, tag_name):
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
+
+ # 标签页面分页
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
@@ -233,6 +290,8 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={
'page': previous_number,
'tag_name': tag.slug})
+
+ # 作者文章页面分页
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
@@ -249,6 +308,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
'page': previous_number,
'author_name': tag_name})
+ # 分类目录页面分页
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
@@ -277,9 +337,10 @@ def load_pagination_info(page_obj, page_type, tag_name):
def load_article_detail(article, isindex, user):
"""
加载文章详情
- :param article:
- :param isindex:是否列表页,若是列表页只显示摘要
- :return:
+ :param article: 文章对象
+ :param isindex: 是否为列表页
+ :param user: 当前用户
+ :return: 文章详情数据
"""
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
@@ -296,12 +357,18 @@ def load_article_detail(article, isindex, user):
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
- """获得gravatar头像"""
+ """
+ 获得gravatar头像URL
+ :param email: 邮箱地址
+ :param size: 头像大小
+ :return: gravatar头像URL
+ """
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
else:
+ # 查找OAuth用户是否有自定义头像
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
@@ -311,8 +378,10 @@ def gravatar_url(email, size=40):
default = static('blog/img/avatar.png')
+ # 生成gravatar URL
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
+ # 缓存头像URL 10小时
cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@@ -320,25 +389,39 @@ def gravatar_url(email, size=40):
@register.filter
def gravatar(email, size=40):
- """获得gravatar头像"""
+ """
+ 获得gravatar头像HTML标签
+ :param email: 邮箱地址
+ :param size: 头像大小
+ :return: 头像img标签
+ """
url = gravatar_url(email, size)
return mark_safe(
- '
' %
- (url, size, size))
+ '
' % (url, size, size))
@register.simple_tag
def query(qs, **kwargs):
- """ template tag which allows queryset filtering. Usage:
- {% query books author=author as mybooks %}
- {% for book in mybooks %}
- ...
- {% endfor %}
+ """
+ 模板标签,允许查询集过滤。用法:
+ {% query books author=author as mybooks %}
+ {% for book in mybooks %}
+ ...
+ {% endfor %}
+ :param qs: 查询集
+ :param kwargs: 过滤条件
+ :return: 过滤后的查询集
"""
return qs.filter(**kwargs)
@register.filter
def addstr(arg1, arg2):
- """concatenate arg1 & arg2"""
+ """
+ 连接两个字符串
+ :param arg1: 第一个字符串
+ :param arg2: 第二个字符串
+ :return: 连接后的字符串
+ """
return str(arg1) + str(arg2)
+
diff --git a/src/blog/tests.py b/src/blog/tests.py
index ee13505..f658e24 100644
--- a/src/blog/tests.py
+++ b/src/blog/tests.py
@@ -20,12 +20,22 @@ from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
class ArticleTest(TestCase):
+ """
+ 文章相关功能测试类
+ """
def setUp(self):
- self.client = Client()
- self.factory = RequestFactory()
+ """
+ 测试前的准备工作
+ """
+ self.client = Client() # 创建测试客户端
+ self.factory = RequestFactory() # 创建请求工厂
def test_validate_article(self):
+ """
+ 测试文章相关功能验证
+ """
site = get_current_site().domain
+ # 创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@@ -33,10 +43,16 @@ class ArticleTest(TestCase):
user.is_staff = True
user.is_superuser = True
user.save()
+
+ # 测试用户详情页访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
+
+ # 测试管理页面访问
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
+
+ # 创建侧边栏
s = SideBar()
s.sequence = 1
s.name = 'test'
@@ -44,16 +60,19 @@ class ArticleTest(TestCase):
s.is_enable = True
s.save()
+ # 创建分类
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
+ # 创建标签
tag = Tag()
tag.name = "nicetag"
tag.save()
+ # 创建文章
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
@@ -68,6 +87,7 @@ class ArticleTest(TestCase):
article.save()
self.assertEqual(1, article.tags.count())
+ # 创建更多测试文章
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@@ -79,32 +99,46 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
+
+ # 如果启用了Elasticsearch,测试搜索功能
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
+ # 测试文章详情页访问
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
+
+ # 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
+
+ # 测试标签页访问
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
+ # 测试分类页访问
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
+ # 测试搜索功能
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
+
+ # 测试加载文章标签功能
s = load_articletags(article)
self.assertIsNotNone(s)
+ # 用户登录测试
self.client.login(username='liangliangyy', password='liangliangyy')
+ # 测试归档页面访问
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
+ # 测试分页功能
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
@@ -119,16 +153,20 @@ class ArticleTest(TestCase):
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
+ # 测试搜索表单
f = BlogSearchForm()
f.search()
- # self.client.login(username='liangliangyy', password='liangliangyy')
+
+ # 测试百度通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
+ # 测试头像功能
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
+ # 测试友情链接
link = Links(
sequence=1,
name="lylinux",
@@ -137,37 +175,53 @@ class ArticleTest(TestCase):
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
+ # 测试RSS订阅
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
+ # 测试站点地图
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
+ # 测试管理后台功能
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
+ """
+ 检查分页功能
+ """
for page in range(1, p.num_pages + 1):
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
+ # 测试上一页链接
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
+ # 测试下一页链接
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
+ """
+ 测试图片上传功能
+ """
import requests
+ # 下载测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
+
+ # 测试未授权上传
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
+
+ # 测试授权上传
sign = get_sha256(get_sha256(settings.SECRET_KEY))
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
@@ -176,17 +230,28 @@ class ArticleTest(TestCase):
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
+
+ # 清理测试文件
os.remove(imagepath)
+
+ # 测试用户头像保存和邮件发送功能
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
+ """
+ 测试错误页面
+ """
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
+ """
+ 测试管理命令
+ """
+ # 创建测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@@ -195,12 +260,14 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
+ # 创建OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
+ # 创建OAuth用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@@ -222,6 +289,7 @@ class ArticleTest(TestCase):
}'''
u.save()
+ # 测试各种管理命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
@@ -230,3 +298,4 @@ class ArticleTest(TestCase):
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
+
diff --git a/src/blog/urls.py b/src/blog/urls.py
index adf2703..9334a24 100644
--- a/src/blog/urls.py
+++ b/src/blog/urls.py
@@ -3,60 +3,74 @@ from django.views.decorators.cache import cache_page
from . import views
-app_name = "blog"
+app_name = "blog" # 定义应用的命名空间
urlpatterns = [
+ # 首页路由
path(
r'',
views.IndexView.as_view(),
name='index'),
+ # 首页分页路由
path(
r'page//',
views.IndexView.as_view(),
name='index_page'),
+ # 文章详情页路由,根据年月日和文章ID访问
path(
r'article////.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
+ # 分类详情页路由
path(
r'category/.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
+ # 分类详情页分页路由
path(
r'category//.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
+ # 作者详情页路由
path(
r'author/.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
+ # 作者详情页分页路由
path(
r'author//.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
+ # 标签详情页路由
path(
r'tag/.html',
views.TagDetailView.as_view(),
name='tag_detail'),
+ # 标签详情页分页路由
path(
r'tag//.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
+ # 归档页面路由,缓存1小时
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
+ # 友情链接页面路由
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
+ # 文件上传路由
path(
r'upload',
views.fileupload,
name='upload'),
+ # 清理缓存路由
path(
r'clean',
views.clean_cache_view,
name='clean'),
]
+
diff --git a/src/blog/views.py b/src/blog/views.py
index d5dc7ec..9b32ac3 100644
--- a/src/blog/views.py
+++ b/src/blog/views.py
@@ -21,10 +21,13 @@ from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__) # 创建日志记录器
class ArticleListView(ListView):
+ """
+ 文章列表视图基类,提供分页和缓存功能
+ """
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
@@ -33,15 +36,21 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等
page_type = ''
- paginate_by = settings.PAGINATE_BY
- page_kwarg = 'page'
- link_type = LinkShowType.L
+ paginate_by = settings.PAGINATE_BY # 每页文章数量
+ page_kwarg = 'page' # 分页参数名
+ link_type = LinkShowType.L # 链接显示类型
def get_view_cache_key(self):
+ """
+ 获取视图缓存key
+ """
return self.request.get['pages']
@property
def page_number(self):
+ """
+ 获取当前页码
+ """
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
@@ -63,7 +72,7 @@ class ArticleListView(ListView):
'''
缓存页面数据
:param cache_key: 缓存key
- :return:
+ :return: 查询结果集
'''
value = cache.get(cache_key)
if value:
@@ -78,36 +87,45 @@ class ArticleListView(ListView):
def get_queryset(self):
'''
重写默认,从缓存获取数据
- :return:
+ :return: 查询结果集
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
+ """
+ 添加额外的上下文数据
+ """
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
'''
- 首页
+ 首页视图
'''
# 友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
+ """
+ 获取首页文章数据
+ """
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
+ """
+ 获取首页缓存key
+ """
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
- 文章详情页面
+ 文章详情页面视图
'''
template_name = 'blog/article_detail.html'
model = Article
@@ -115,11 +133,15 @@ class ArticleDetailView(DetailView):
context_object_name = "article"
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)
- blog_setting = get_blog_setting()
+ article_comments = self.object.comment_list() # 获取文章评论列表
+ parent_comments = article_comments.filter(parent_comment=None) # 获取父级评论
+ blog_setting = get_blog_setting() # 获取博客设置
+ # 对父级评论进行分页
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
@@ -132,6 +154,7 @@ class ArticleDetailView(DetailView):
page = paginator.num_pages
p_comments = paginator.page(page)
+ # 计算评论分页的前后页链接
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
@@ -141,12 +164,15 @@ class ArticleDetailView(DetailView):
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
+
+ # 添加评论相关数据到上下文
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
+ # 添加上下篇文章信息
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
@@ -154,7 +180,7 @@ class ArticleDetailView(DetailView):
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
- # # Filter Hook, 允许插件修改文章正文
+ # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
@@ -163,11 +189,14 @@ class ArticleDetailView(DetailView):
class CategoryDetailView(ArticleListView):
'''
- 分类目录列表
+ 分类目录列表视图
'''
page_type = "分类目录归档"
def get_queryset_data(self):
+ """
+ 获取分类文章数据
+ """
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
@@ -180,6 +209,9 @@ class CategoryDetailView(ArticleListView):
return article_list
def get_queryset_cache_key(self):
+ """
+ 获取分类列表缓存key
+ """
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
@@ -189,7 +221,9 @@ class CategoryDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
-
+ """
+ 添加分类相关上下文数据
+ """
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
@@ -202,11 +236,14 @@ class CategoryDetailView(ArticleListView):
class AuthorDetailView(ArticleListView):
'''
- 作者详情页
+ 作者详情页视图
'''
page_type = '作者文章归档'
def get_queryset_cache_key(self):
+ """
+ 获取作者文章列表缓存key
+ """
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
@@ -214,12 +251,18 @@ class AuthorDetailView(ArticleListView):
return cache_key
def get_queryset_data(self):
+ """
+ 获取作者文章数据
+ """
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
+ """
+ 添加作者相关上下文数据
+ """
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
@@ -228,11 +271,14 @@ class AuthorDetailView(ArticleListView):
class TagDetailView(ArticleListView):
'''
- 标签列表页面
+ 标签列表页面视图
'''
page_type = '分类标签归档'
def get_queryset_data(self):
+ """
+ 获取标签文章数据
+ """
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@@ -242,6 +288,9 @@ class TagDetailView(ArticleListView):
return article_list
def get_queryset_cache_key(self):
+ """
+ 获取标签列表缓存key
+ """
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@@ -251,6 +300,9 @@ class TagDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
+ """
+ 添加标签相关上下文数据
+ """
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
@@ -260,31 +312,49 @@ class TagDetailView(ArticleListView):
class ArchivesView(ArticleListView):
'''
- 文章归档页面
+ 文章归档页面视图
'''
page_type = '文章归档'
- paginate_by = None
- page_kwarg = None
+ paginate_by = None # 不分页
+ page_kwarg = None # 无分页参数
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
+ """
+ 获取所有已发布文章
+ """
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
+ """
+ 获取归档页面缓存key
+ """
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
+ """
+ 友情链接列表视图
+ """
model = Links
template_name = 'blog/links_list.html'
def get_queryset(self):
+ """
+ 获取启用的友情链接
+ """
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
+ """
+ Elasticsearch搜索视图
+ """
def get_context(self):
+ """
+ 获取搜索上下文数据
+ """
paginator, page = self.build_page()
context = {
"query": self.query,
@@ -303,31 +373,37 @@ class EsSearchView(SearchView):
@csrf_exempt
def fileupload(request):
"""
- 该方法需自己写调用端来上传图片,该方法仅提供图床功能
- :param request:
- :return:
+ 文件上传视图,该方法需自己写调用端来上传图片,该方法仅提供图床功能
+ :param request: HTTP请求对象
+ :return: 上传文件的URL列表
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
+ # 验证签名,确保是授权上传
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
+ # 处理上传的每个文件
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
+ # 根据文件类型确定保存目录
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
+ # 生成安全的保存路径
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
+ # 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
+ # 如果是图片,进行压缩优化
if isimage:
from PIL import Image
image = Image.open(savepath)
@@ -344,6 +420,9 @@ def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
+ """
+ 404页面不存在视图
+ """
if exception:
logger.error(exception)
url = request.get_full_path()
@@ -355,6 +434,9 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'):
+ """
+ 500服务器错误视图
+ """
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@@ -366,6 +448,9 @@ def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
+ """
+ 403权限拒绝视图
+ """
if exception:
logger.error(exception)
return render(
@@ -375,5 +460,9 @@ def permission_denied_view(
def clean_cache_view(request):
+ """
+ 清理缓存视图
+ """
cache.clear()
return HttpResponse('ok')
+