Compare commits

..

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

@ -0,0 +1,59 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
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")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
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.save()
return user
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

@ -1,5 +1,3 @@
#wty 模块功能:定义账户相关的表单类,包括登录表单、注册表单、忘记密码表单等,用于处理用户输入验证
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
@ -10,55 +8,40 @@ from . import utils
from .models import BlogUser
#wty LoginForm类继承自AuthenticationForm的登录表单自定义用户名和密码字段的显示样式
class LoginForm(AuthenticationForm):
#wty __init__方法初始化表单设置用户名和密码输入框的样式和占位符
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
#wty 设置用户名输入框的样式和提示信息
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
#wty 设置密码输入框的样式和提示信息
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
#wty RegisterForm类继承自UserCreationForm的用户注册表单自定义注册字段的显示样式和邮箱唯一性验证
class RegisterForm(UserCreationForm):
#wty __init__方法初始化注册表单设置各字段输入框的样式和占位符
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
#wty 设置用户名输入框样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
#wty 设置邮箱输入框样式
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
#wty 设置密码输入框样式
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
#wty 设置确认密码输入框样式
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
#wty clean_email方法验证邮箱唯一性确保邮箱未被注册
def clean_email(self):
email = self.cleaned_data['email']
#wty 检查邮箱是否已存在于数据库中
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
#wty Meta类定义表单关联的模型和字段
class Meta:
model = get_user_model()
fields = ("username", "email")
#wty ForgetPasswordForm类处理忘记密码功能的表单包含新密码、确认密码、邮箱和验证码字段
class ForgetPasswordForm(forms.Form):
#wty new_password1字段新密码输入字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -69,7 +52,6 @@ class ForgetPasswordForm(forms.Form):
),
)
#wty new_password2字段确认密码输入字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -80,7 +62,6 @@ class ForgetPasswordForm(forms.Form):
),
)
#wty email字段邮箱输入字段
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -91,7 +72,6 @@ class ForgetPasswordForm(forms.Form):
),
)
#wty code字段验证码输入字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -102,22 +82,17 @@ class ForgetPasswordForm(forms.Form):
),
)
#wty clean_new_password2方法验证两次输入的密码是否一致并符合密码强度要求
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
#wty 检查两次密码输入是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
#wty 验证密码强度
password_validation.validate_password(password2)
return password2
#wty clean_email方法验证邮箱是否存在确保是已注册的邮箱
def clean_email(self):
user_email = self.cleaned_data.get("email")
#wty 检查邮箱是否存在于用户数据库中
if not BlogUser.objects.filter(
email=user_email
).exists():
@ -125,10 +100,8 @@ class ForgetPasswordForm(forms.Form):
raise ValidationError(_("email does not exist"))
return user_email
#wty clean_code方法验证验证码是否正确有效
def clean_code(self):
code = self.cleaned_data.get("code")
#wty 调用utils模块验证验证码
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
@ -138,9 +111,7 @@ class ForgetPasswordForm(forms.Form):
return code
#wty ForgetPasswordCodeForm类用于发送忘记密码验证码的表单只包含邮箱字段
class ForgetPasswordCodeForm(forms.Form):
#wty email字段邮箱输入字段用于接收验证码的邮箱地址
email = forms.EmailField(
label=_('Email'),
)

@ -1,55 +1,43 @@
#wty 模块功能Django数据库迁移文件用于修改BlogUser模型的字段结构和选项配置
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
#wty Migration类继承自migrations.Migration的数据库迁移类定义了模型字段的变更操作
class Migration(migrations.Migration):
#wty dependencies属性定义迁移依赖关系指定该迁移依赖于accounts应用的0001_initial迁移
dependencies = [
('accounts', '0001_initial'),
]
#wty operations属性定义具体的数据库操作列表包含模型修改、字段删除、字段添加和字段修改等操作
operations = [
#wty AlterModelOptions操作修改BlogUser模型的Meta选项配置
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
#wty RemoveField操作删除BlogUser模型中的created_time字段
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
#wty RemoveField操作删除BlogUser模型中的last_mod_time字段
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
#wty AddField操作向BlogUser模型添加creation_time字段
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#wty AddField操作向BlogUser模型添加last_modify_time字段
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
#wty AlterField操作修改BlogUser模型中的nickname字段属性
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
#wty AlterField操作修改BlogUser模型中的source字段属性
migrations.AlterField(
model_name='bloguser',
name='source',

@ -0,0 +1,35 @@
from django.contrib.auth.models import AbstractUser
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 djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(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):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
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'

@ -1,4 +1,3 @@
#wty 模块功能:实现账户相关功能的单元测试,包括用户注册、登录、邮箱验证、密码重置等核心功能的测试用例
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
@ -10,9 +9,9 @@ from djangoblog.utils import *
from . import utils
#wty AccountTest类继承自TestCase的测试类包含账户相关功能的所有测试用例
# Create your tests here.
class AccountTest(TestCase):
#wty setUp方法测试初始化方法创建测试客户端、请求工厂和测试用户
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
@ -23,17 +22,14 @@ class AccountTest(TestCase):
)
self.new_test = "xxx123--="
#wty test_validate_account方法测试账户验证功能包括管理员登录和文章管理权限验证
def test_validate_account(self):
site = get_current_site().domain
#wty 创建超级用户用于测试
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
#wty 测试用户登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
@ -41,7 +37,6 @@ class AccountTest(TestCase):
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
#wty 创建测试分类和文章
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
@ -57,31 +52,25 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
#wty 测试文章管理页面访问权限
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#wty test_validate_register方法测试用户注册流程包括注册、邮箱验证、登录和权限验证
def test_validate_register(self):
#wty 验证初始状态下用户不存在
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
#wty 测试用户注册功能
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
#wty 验证用户已成功创建
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
#wty 生成并验证邮箱激活链接
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
@ -89,14 +78,12 @@ class AccountTest(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
#wty 测试用户登录和权限设置
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
#wty 创建测试内容用于权限验证
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
@ -113,13 +100,108 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
#wty 测试文章管理权限
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#wty 测试用户登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
#wty 测试登出后权限丢失
response = self.client.get(article.get_admin_url)
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
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")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
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(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)

@ -0,0 +1,28 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

@ -1,36 +1,26 @@
#wty 模块功能自定义Django认证后端支持使用用户名或邮箱进行用户登录验证
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
#wty EmailOrUsernameModelBackend类继承自ModelBackend的自定义认证后端允许用户使用用户名或邮箱登录系统
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
#wty authenticate方法重写认证方法支持用户名和邮箱两种登录方式
def authenticate(self, request, username=None, password=None, **kwargs):
#wty 根据输入中是否包含@符号判断是邮箱还是用户名登录
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
#wty 根据用户名或邮箱查找用户对象
user = get_user_model().objects.get(**kwargs)
#wty 验证密码是否正确
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
#wty 用户不存在时返回None
return None
#wty get_user方法根据用户ID获取用户对象
def get_user(self, username):
try:
#wty 根据主键获取用户对象
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
#wty 用户不存在时返回None
return None
return None

@ -1,4 +1,3 @@
#wty 模块功能:提供账户相关的工具函数,包括验证码的发送、验证、存储和获取功能
import typing
from datetime import timedelta
@ -8,11 +7,9 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
#wty _code_ttl变量定义验证码的默认有效期为5分钟
_code_ttl = timedelta(minutes=5)
#wty send_verify_email函数发送邮箱验证码邮件用于密码重置等场景
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
@ -20,14 +17,12 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
subject: 邮件主题
code: 验证码
"""
#wty 构造包含验证码的邮件内容
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
send_email([to_mail], subject, html_content)
#wty verify函数验证邮箱验证码是否正确有效
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
@ -39,21 +34,16 @@ def verify(email: str, code: str) -> typing.Optional[str]:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
#wty 从缓存中获取存储的验证码并进行比对
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
#wty set_code函数将验证码存储到缓存中设置有效期
def set_code(email: str, code: str):
"""设置code"""
#wty 使用Django缓存系统存储验证码设置过期时间
cache.set(email, code, _code_ttl.seconds)
#wty get_code函数从缓存中获取指定邮箱的验证码
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
#wty 从缓存中获取验证码如果不存在则返回None
return cache.get(email)
return cache.get(email)

@ -1,4 +1,3 @@
#wty 模块功能:实现用户账户相关的视图逻辑,包括用户注册、登录、登出、密码重置等功能
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
@ -25,37 +24,27 @@ from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser, UserProfile, UserBrowseHistory
from django.contrib.auth.decorators import login_required
from django.views.generic import TemplateView
from django.shortcuts import redirect
from blog.models import Article, UserFavorite,ArticleInteraction
from comments.models import Comment
from .models import BlogUser
logger = logging.getLogger(__name__)
#wty RegisterView类处理用户注册请求的视图类继承自FormView
# Create your views here.
class RegisterView(FormView):
#wty form_class属性指定注册表单使用的表单类
form_class = RegisterForm
#wty template_name属性指定注册页面使用的模板文件
template_name = 'account/registration_form.html'
#wty dispatch方法添加CSRF保护装饰器分发HTTP请求到相应处理方法
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
#wty form_valid方法处理表单验证成功的逻辑保存用户信息并发送验证邮件
def form_valid(self, form):
if form.is_valid():
#wty 创建未激活用户并保存基本信息
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
#wty 生成邮箱验证链接
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
@ -65,7 +54,6 @@ class RegisterView(FormView):
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
#wty 构造验证邮件内容并发送
content = """
<p>请点击下面链接验证您的邮箱</p>
@ -83,7 +71,6 @@ class RegisterView(FormView):
title='验证您的电子邮箱',
content=content)
#wty 跳转到注册结果页面
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
@ -93,37 +80,26 @@ class RegisterView(FormView):
})
#wty LogoutView类处理用户登出请求的视图类继承自RedirectView
class LogoutView(RedirectView):
#wty url属性指定登出后的重定向URL
url = '/login/'
#wty dispatch方法添加永不缓存装饰器分发HTTP请求到相应处理方法
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
#wty get方法执行用户登出操作并清除侧边栏缓存
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
#wty LoginView类处理用户登录请求的视图类继承自FormView
class LoginView(FormView):
#wty form_class属性指定登录表单使用的表单类
form_class = LoginForm
#wty template_name属性指定登录页面使用的模板文件
template_name = 'account/login.html'
#wty success_url属性指定登录成功的默认重定向URL
success_url = '/'
#wty redirect_field_name属性指定重定向字段名称
redirect_field_name = REDIRECT_FIELD_NAME
#wty login_ttl属性指定登录会话的过期时间一个月
login_ttl = 2626560 # 一个月的时间
#wty dispatch方法添加敏感参数保护、CSRF保护和永不缓存装饰器
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
@ -131,7 +107,6 @@ class LoginView(FormView):
return super(LoginView, self).dispatch(request, *args, **kwargs)
#wty get_context_data方法获取传递给模板的上下文数据包括重定向URL
def get_context_data(self, **kwargs):
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
@ -140,18 +115,14 @@ class LoginView(FormView):
return super(LoginView, self).get_context_data(**kwargs)
#wty form_valid方法处理登录表单验证成功的逻辑执行用户认证和登录
def form_valid(self, form):
#wty 使用Django内置认证表单进行用户验证
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
logger.info(self.redirect_field_name)
#wty 执行用户登录操作
auth.login(self.request, form.get_user())
#wty 处理"记住我"功能,设置会话过期时间
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
@ -161,11 +132,9 @@ class LoginView(FormView):
'form': form
})
#wty get_success_url方法获取登录成功后的重定向URL验证URL安全性
def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name)
#wty 验证重定向URL是否属于允许的主机
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
@ -173,19 +142,14 @@ class LoginView(FormView):
return redirect_to
#wty account_result函数处理账户操作结果页面包括注册成功和邮箱验证
def account_result(request):
#wty 获取请求参数中的操作类型和用户ID
type = request.GET.get('type')
id = request.GET.get('id')
#wty 获取对应用户对象
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
#wty 如果用户已激活则重定向到首页
if user.is_active:
return HttpResponseRedirect('/')
#wty 根据操作类型显示相应的结果页面
if type and type in ['register', 'validation']:
if type == 'register':
content = '''
@ -193,7 +157,6 @@ def account_result(request):
'''
title = '注册成功'
else:
#wty 验证邮箱链接签名,激活用户账户
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
@ -212,69 +175,30 @@ def account_result(request):
return HttpResponseRedirect('/')
#wty ForgetPasswordView类处理忘记密码请求的视图类继承自FormView
class ForgetPasswordView(FormView):
#wty form_class属性指定忘记密码表单使用的表单类
form_class = ForgetPasswordForm
#wty template_name属性指定忘记密码页面使用的模板文件
template_name = 'account/forget_password.html'
#wty form_valid方法处理忘记密码表单验证成功的逻辑重置用户密码
def form_valid(self, form):
if form.is_valid():
#wty 根据邮箱查找用户并更新密码
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
#wty 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
#wty ForgetPasswordEmailCode类处理忘记密码验证码发送请求的视图类继承自View
class ForgetPasswordEmailCode(View):
#wty post方法接收邮箱地址生成并发送验证码
def post(self, request: HttpRequest):
#wty 验证表单数据
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
#wty 生成验证码并发送邮件
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok")
# 新增:用户收藏管理视图
class UserProfileView(TemplateView):
template_name = 'account/user_profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(** kwargs)
# 修复:通过 URL 中的 username 参数查询用户(与 urls.py 中的 <str:username> 匹配)
username = self.kwargs.get('username')
# 修复:使用 username 而非 id 查询用户,避免 BigAutoField 冲突
user = get_object_or_404(get_user_model(), username=username)
# 修复:确保查询方法正确调用,避免字段冲突
context.update({
'profile_user': user,
'browse_history': list(user.get_browse_history()[:20]),
'favorite_articles': user.get_favorite_articles()[:10] if hasattr(user, 'get_favorite_articles') else Article.objects.none(),
'liked_articles': user.get_liked_articles()[:10] if hasattr(user, 'get_liked_articles') else Article.objects.none(),
'liked_comments': user.get_liked_comments()[:10] if hasattr(user, 'get_liked_comments') else Comment.objects.none(),
'articles_count': Article.objects.filter(author=user).count(),
'comments_count': Comment.objects.filter(author=user).count(),
})
return context
# 保留重定向视图:未传 username 时,重定向到当前登录用户的个人中心
@login_required
def user_profile_redirect(request):
return redirect('account:user_profile', username=request.user.username)

@ -0,0 +1,112 @@
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20
search_fields = ('body', 'title')
form = ArticleForm
list_display = (
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'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 = [
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'<a href="%s">%s</a>' % (link, obj.category.name))
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):
if obj:
url = obj.get_full_url()
return url
else:
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
pass

@ -0,0 +1,5 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = 'blog'

@ -0,0 +1,43 @@
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
if value:
return value
else:
logger.info('set processor cache.')
setting = get_blog_setting()
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'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,
}
cache.set(key, value, 60 * 60 * 10)
return value

@ -1,17 +1,15 @@
#sxc 该模块是 Django 博客系统的 Elasticsearch 集成配置模块,核心功能是定义 Elasticsearch 文档模型(性能日志、文章)、实现文档的创建 / 更新 / 删除等管理操作,同时配置 IP 地理信息解析管道,为博客提供高性能的全文搜索和访问性能监控能力
#sxc 依赖 elasticsearch-dsl 库,支持条件启用(通过 settings.ELASTICSEARCH_DSL 配置),包含文档模型定义、索引管理、数据同步三大核心模块
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
#sxc 判断项目是否配置 Elasticsearch控制后续功能是否启用
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
#sxc 建立与 Elasticsearch 的连接,使用项目配置的地址
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@ -21,10 +19,8 @@ if ELASTICSEARCH_ENABLED:
c = IngestClient(es)
try:
#sxc 检查是否存在 "geoip" 管道(用于解析 IP 地理信息)
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
#sxc 若不存在则创建 "geoip" 管道,定义 IP 解析逻辑(从 ip 字段提取地理信息)
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -38,33 +34,28 @@ if ELASTICSEARCH_ENABLED:
class GeoIp(InnerDoc):
#sxc 内部文档类:定义 IP 地理信息的字段结构,用于嵌套在性能日志文档中
continent_name = Keyword()#sxc 洲名
country_iso_code = Keyword()#sxc 国家 ISO 代码(如 CN、US
country_name = Keyword()#sxc 国家名称
location = GeoPoint()#sxc 地理位置坐标(经纬度)
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
class UserAgentBrowser(InnerDoc):
#sxc 内部文档类:定义用户代理中的浏览器信息结构
Family = Keyword()#sxc 浏览器家族(如 Chrome、Safari
Version = Keyword() #sxc 浏览器版本
Family = Keyword()
Version = Keyword()
class UserAgentOS(UserAgentBrowser):
#sxc 内部文档类:定义用户代理中的操作系统信息结构(继承浏览器类字段)
pass
class UserAgentDevice(InnerDoc):
#sxc 内部文档类:定义用户代理中的设备信息结构
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc):
#sxc 内部文档类定义完整的用户代理信息结构嵌套浏览器、OS、设备信息
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
@ -73,7 +64,6 @@ class UserAgent(InnerDoc):
class ElapsedTimeDocument(Document):
#sxc Elasticsearch 文档类:定义访问性能日志模型,用于记录页面加载耗时等信息
url = Keyword()
time_taken = Long()
log_datetime = Date()
@ -82,7 +72,6 @@ class ElapsedTimeDocument(Document):
useragent = Object(UserAgent, required=False)
class Index:
#sxc 定义索引配置:索引名为 "performance",分片 1 个、副本 0 个(单节点环境优化)
name = 'performance'
settings = {
"number_of_shards": 1,
@ -94,10 +83,8 @@ class ElapsedTimeDocument(Document):
class ElaspedTimeDocumentManager:
#sxc 性能日志文档管理器:提供索引创建、删除、日志写入等静态方法
@staticmethod
def build_index():
#sxc 静态方法:创建 "performance" 索引(若不存在)
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
@ -106,16 +93,13 @@ class ElaspedTimeDocumentManager:
@staticmethod
def delete_index():
#sxc 静态方法:删除 "performance" 索引(忽略 400/404 错误:索引不存在或删除失败)
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):
#sxc 静态方法:创建一条性能日志文档,并应用 geoip 管道解析 IP 地理信息
ElaspedTimeDocumentManager.build_index()
#sxc 组装用户代理信息(从传入的 useragent 对象提取字段)
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@ -131,7 +115,7 @@ class ElaspedTimeDocumentManager:
ua.device.Model = useragent.device.model
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
#sxc 创建性能日志文档ID 用时间戳(毫秒级)确保唯一,关联 geoip 管道
doc = ElapsedTimeDocument(
meta={
'id': int(
@ -147,26 +131,21 @@ class ElaspedTimeDocumentManager:
class ArticleDocument(Document):
#sxc Elasticsearch 文档类:定义博客文章模型,用于全文搜索
#sxc 正文和标题使用 IK 分词器ik_max_word细粒度分词ik_smart粗粒度搜索
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
#sxc 作者信息(嵌套 Object包含昵称和 ID昵称支持分词
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#sxc 分类信息(嵌套 Object包含名称和 ID名称支持分词
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#sxc 标签信息(嵌套 Object 列表:包含名称和 ID名称支持分词
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#sxc 文章其他字段:发布时间、状态、评论状态、类型、浏览量、排序
pub_time = Date()
status = Text()
comment_status = Text()
@ -175,7 +154,6 @@ class ArticleDocument(Document):
article_order = Integer()
class Index:
#sxc 定义索引配置:索引名为 "blog",分片 1 个、副本 0 个(单节点环境优化)
name = 'blog'
settings = {
"number_of_shards": 1,
@ -185,24 +163,21 @@ class ArticleDocument(Document):
class Meta:
doc_type = 'Article'
#sxc 文章文档管理器:提供索引创建、删除、数据同步(重建 / 更新)等实例方法
class ArticleDocumentManager():
def __init__(self):
self.create_index()
def create_index(self):
#sxc 实例方法:初始化 "blog" 索引(根据 ArticleDocument 定义创建)
ArticleDocument.init()
def delete_index(self):
#sxc 实例方法:删除 "blog" 索引(忽略 400/404 错误)
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):
#sxc 实例方法:将 Django Article 模型对象列表转换为 Elasticsearch ArticleDocument 列表
return [
ArticleDocument(
meta={
@ -227,9 +202,7 @@ class ArticleDocumentManager():
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
#sxc 实例方法:重建文章索引(全量同步或指定文章同步)
ArticleDocument.init()
#sxc 若未指定文章列表,则同步所有 Article 模型数据
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
for doc in docs:

@ -0,0 +1,19 @@
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True)
def search(self):
datas = super(BlogSearchForm, self).search()
if not self.is_valid():
return self.no_query_found()
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas

@ -0,0 +1,18 @@
from django.core.management.base import BaseCommand
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
class Command(BaseCommand):
help = 'build search index'
def handle(self, *args, **options):
if ELASTICSEARCH_ENABLED:
ElaspedTimeDocumentManager.build_index()
manager = ElapsedTimeDocument()
manager.init()
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# TODO 参数化
class Command(BaseCommand):
help = 'build search words'
def handle(self, *args, **options):
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
print('\n'.join(datas))

@ -0,0 +1,11 @@
from django.core.management.base import BaseCommand
from djangoblog.utils import cache
class Command(BaseCommand):
help = 'clear the whole cache'
def handle(self, *args, **options):
cache.clear()
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -0,0 +1,40 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
class Command(BaseCommand):
help = 'create test datas'
def handle(self, *args, **options):
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save()
basetag = Tag()
basetag.name = "标签"
basetag.save()
for i in range(1, 20):
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
article.tags.add(tag)
article.tags.add(basetag)
article.save()
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -0,0 +1,50 @@
from django.core.management.base import BaseCommand
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
class Command(BaseCommand):
help = 'notify baidu url'
def add_arguments(self, parser):
parser.add_argument(
'data_type',
type=str,
choices=[
'all',
'article',
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path):
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)
urls = []
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -0,0 +1,47 @@
import requests
from django.core.management.base import BaseCommand
from django.templatetags.static import static
from djangoblog.utils import save_user_avatar
from oauth.models import OAuthUser
from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
help = 'sync user avatar'
def test_picture(self, url):
try:
if requests.get(url, timeout=2).status_code == 200:
return True
except:
pass
def handle(self, *args, **options):
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
for u in users:
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
if url.startswith(static_url):
if self.test_picture(url):
continue
else:
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
else:
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
if url:
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
self.stdout.write('结束同步')

@ -0,0 +1,42 @@
import logging
import time
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
''' page render time '''
start_time = time.time()
response = self.get_response(request)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request)
user_agent = parse(http_user_agent)
if not response.streaming:
try:
cast_time = time.time() - start_time
if ELASTICSEARCH_ENABLED:
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'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response

@ -1,4 +1,3 @@
#sxc 该模块是 Django 的数据库迁移文件,用于定义博客系统的数据模型结构及迁移操作
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
@ -9,48 +8,32 @@ import mdeditor.fields
class Migration(migrations.Migration):
#sxc 标记这是初始迁移
initial = True
#sxc 依赖项,依赖于 Django 的用户模型
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#sxc 迁移操作列表,定义了所有模型的创建操作
operations = [
migrations.CreateModel(
name='BlogSettings',
fields=[
#sxc 自增主键 ID
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#sxc 网站名称字段,默认空字符串,最大长度 200
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
#sxc 网站描述字段,默认空字符串,最大长度 1000
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
#sxc 网站 SEO 描述字段,用于搜索引擎优化
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
#sxc 网站关键字字段,用于搜索引擎优化
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
#sxc 文章摘要长度设置,默认 300
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
#sxc 侧边栏文章数目设置,默认 10
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
#sxc 侧边栏评论数目设置,默认 5
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
#sxc 文章页面默认显示评论数目,默认 5
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
#sxc 是否显示谷歌广告的布尔值,默认不显示
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
#sxc 谷歌广告代码内容,可为空
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
#sxc 是否打开网站评论功能,默认打开
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
#sxc 网站备案号,可为空
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
#sxc 网站统计代码
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
#sxc 是否显示公安备案号,默认不显示
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
#sxc 公安备案号内容,可为空
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
@ -58,24 +41,16 @@ class Migration(migrations.Migration):
'verbose_name_plural': '网站配置',
},
),
#sxc 创建友情链接模型
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#sxc 链接名称,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
#sxc 链接地址URL 类型
('link', models.URLField(verbose_name='链接地址')),
#sxc 排序序号,唯一
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
#sxc 是否显示,默认显示
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
#sxc 显示类型,可选首页、列表页、文章页面、全站、友情链接页面
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
#sxc 创建时间,默认当前时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#sxc 最后修改时间,默认当前时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
@ -84,20 +59,15 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
#sxc 创建侧边栏模型
migrations.CreateModel(
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#sxc 侧边栏标题
('name', models.CharField(max_length=100, verbose_name='标题')),
#sxc 侧边栏内容
('content', models.TextField(verbose_name='内容')),
#sxc 排序序号,唯一
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
#sxc 是否启用,默认启用
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('is_enable', models.BooleanField(default=True, 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='修改时间')),
],
options={
@ -106,16 +76,13 @@ class Migration(migrations.Migration):
'ordering': ['sequence'],
},
),
#sxc 创建标签模型
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#sxc 标签名称,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
#sxc URL 友好的标识符,默认 'no-slug'
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
@ -124,19 +91,15 @@ class Migration(migrations.Migration):
'ordering': ['name'],
},
),
#sxc 创建分类模型
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#sxc 分类名称,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
#sxc 权重排序,数字越大越靠前,默认 0
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
#sxc 父级分类,自引用外键,可为空
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
],
options={
@ -145,42 +108,28 @@ class Migration(migrations.Migration):
'ordering': ['-index'],
},
),
#sxc 创建文章模型
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#sxc 文章标题,唯一
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
#sxc 文章正文,使用 markdown 编辑器字段
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
#sxc 发布时间,默认当前时间
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
#sxc 文章状态,可选草稿或发表,默认发表
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
#sxc 评论状态,可选打开或关闭,默认打开
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
#sxc 类型,可选文章或页面,默认文章
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
#sxc 浏览量,正整数,默认 0
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
#sxc 排序,数字越大越靠前,默认 0
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
#sxc 是否显示 toc 目录,默认不显示
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
#sxc 外键关联到用户模型,作者
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#sxc 外键关联到分类模型
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
#sxc 多对多关联到标签模型
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
#sxc 先按排序降序,再按发布时间降序
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
},

@ -0,0 +1,23 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -0,0 +1,27 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
operations = [
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -1,4 +1,3 @@
#sxc 该模块是 Django 的数据库迁移文件,用于对博客系统的多个模型进行批量修改,包括更新模型选项、字段重命名、字段属性调整以及统一将 verbose_name 从中文改为英文
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
@ -9,349 +8,290 @@ import mdeditor.fields
class Migration(migrations.Migration):
#sxc 迁移依赖项,依赖于用户模型和 blog 应用的上一次迁移 (0004_...)
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
#sxc 迁移操作列表,包含模型选项修改、字段移除、新增和属性修改等操作
operations = [
#sxc 修改 Article 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
#sxc 修改 Category 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
#sxc 修改 Links 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
#sxc 修改 SideBar 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
#sxc 修改 Tag 模型的元选项,将 verbose_name 改为英文
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
#sxc 移除 Article 模型中的 created_time 字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
#sxc 移除 Article 模型中的 last_mod_time 字段
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
#sxc 移除 Category 模型中的 created_time 字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
#sxc 移除 Category 模型中的 last_mod_time 字段
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
#sxc 移除 Links 模型中的 created_time 字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
#sxc 移除 SideBar 模型中的 created_time 字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
#sxc 移除 Tag 模型中的 created_time 字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
#sxc 移除 Tag 模型中的 last_mod_time 字段
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
#sxc 为 Article 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 Article 模型添加 last_modify_time 字段 (替代原 last_mod_time)
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 为 Category 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 Category 模型添加 last_modify_time 字段 (替代原 last_mod_time)
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 为 Links 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 SideBar 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 Tag 模型添加 creation_time 字段 (替代原 created_time)
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#sxc 为 Tag 模型添加 last_modify_time 字段 (替代原 last_mod_time)
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 修改 Article 模型的 article_order 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
#sxc 修改 Article 模型的 author 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
#sxc 修改 Article 模型的 body 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
#sxc 修改 Article 模型的 category 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
#sxc 修改 Article 模型的 comment_status 字段,选项和 verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
#sxc 修改 Article 模型的 pub_time 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
#sxc 修改 Article 模型的 show_toc 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
#sxc 修改 Article 模型的 status 字段,选项和 verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
#sxc 修改 Article 模型的 tags 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
#sxc 修改 Article 模型的 title 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
#sxc 修改 Article 模型的 type 字段,选项和 verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
#sxc 修改 Article 模型的 views 字段verbose_name 改为英文
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
#sxc 修改 BlogSettings 模型的 article_comment_count 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
#sxc 修改 BlogSettings 模型的 article_sub_length 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
#sxc 修改 BlogSettings 模型的 google_adsense_codes 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
#sxc 修改 BlogSettings 模型的 open_site_comment 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
#sxc 修改 BlogSettings 模型的 show_google_adsense 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
#sxc 修改 BlogSettings 模型的 sidebar_article_count 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
#sxc 修改 BlogSettings 模型的 sidebar_comment_count 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
#sxc 修改 BlogSettings 模型的 site_description 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
#sxc 修改 BlogSettings 模型的 site_keywords 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
#sxc 修改 BlogSettings 模型的 site_name 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
#sxc 修改 BlogSettings 模型的 site_seo_description 字段verbose_name 改为英文
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
#sxc 修改 Category 模型的 index 字段verbose_name 改为英文
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
#sxc 修改 Category 模型的 name 字段verbose_name 改为英文
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
#sxc 修改 Category 模型的 parent_category 字段verbose_name 改为英文
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
#sxc 修改 Links 模型的 is_enable 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
#sxc 修改 Links 模型的 last_mod_time 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 修改 Links 模型的 link 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
#sxc 修改 Links 模型的 name 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
#sxc 修改 Links 模型的 sequence 字段verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
#sxc 修改 Links 模型的 show_type 字段,选项和 verbose_name 改为英文
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
#sxc 修改 SideBar 模型的 content 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
#sxc 修改 SideBar 模型的 is_enable 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
#sxc 修改 SideBar 模型的 last_mod_time 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
#sxc 修改 SideBar 模型的 name 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
#sxc 修改 SideBar 模型的 sequence 字段verbose_name 改为英文
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
#sxc 修改 Tag 模型的 name 字段verbose_name 改为英文
migrations.AlterField(
model_name='tag',
name='name',

@ -1,15 +1,14 @@
#sxc 该模块是 Django 的数据库迁移文件,用于修改博客系统中 BlogSettings 模型的元选项,将其 verbose_name 从中文改为英文
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
class Migration(migrations.Migration):
#sxc 迁移依赖项,依赖于 blog 应用的上一次迁移 (0005_...)
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
#sxc 迁移操作列表,仅包含修改 BlogSettings 模型选项的操作
operations = [
migrations.AlterModelOptions(
name='blogsettings',

@ -1,4 +1,3 @@
#sxc 该模块是 Django 博客系统的核心数据模型定义模块,包含文章、分类、标签、友情链接、侧边栏、网站配置 6 大核心模型以及基础抽象模型和链接显示类型枚举同时集成缓存、URL 生成、数据验证等功能,支撑博客的内容管理和系统配置
import logging
import re
from abc import abstractmethod
@ -9,42 +8,35 @@ 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 #sxc 导入 Markdown 编辑器字段,用于文章正文
from uuslug import slugify #sxc 导入 slug 生成工具,用于生成 URL 友好的标识
from mdeditor.fields import MDTextField
from uuslug import slugify
from djangoblog.utils import cache_decorator, cache #sxc 导入缓存工具,优化数据查询性能
from djangoblog.utils import get_current_site #sxc 导入获取当前站点信息的工具
from django.core.validators import MinValueValidator, MaxValueValidator
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__) #sxc 初始化日志记录器,用于记录模型操作相关日志
logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
#sxc 枚举类:定义友情链接的显示位置选项,支持国际化
I = ('i', _('index'))#sxc 首页显示
L = ('l', _('list'))#sxc 列表页显示
P = ('p', _('post'))#sxc 文章页显示
A = ('a', _('all'))#sxc 全站显示
S = ('s', _('slide'))#sxc 幻灯片区域显示
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
class BaseModel(models.Model):
#sxc 抽象基础模型:为所有业务模型提供通用字段和方法,避免代码
id = models.AutoField(primary_key=True) #sxc 自增主键重复
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):
#sxc 重写保存方法:优化文章浏览量更新逻辑,自动生成 slug 字段
#sxc 判断是否为文章模型的浏览量更新操作(仅更新 views 字段)
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
#sxc 浏览量更新:直接通过 QuerySet 更新,避免触发完整 save 流程
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
#sxc 非浏览量更新:若模型有 slug 字段,自动生成 URL 友好的 slug基于 title 或 name 字段)
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
@ -63,46 +55,19 @@ class BaseModel(models.Model):
@abstractmethod
def get_absolute_url(self):
#sxc 抽象方法:要求子类实现获取自身相对 URL 的逻辑,用于路由跳转
pass
# 新增:用户收藏模型
class UserFavorite(models.Model):
"""用户收藏文章关系模型"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name='用户',
related_name='user_favorites' # #修改:显式定义反向关联名,供 BlogUser 调用
)
article = models.ForeignKey('Article', on_delete=models.CASCADE, verbose_name='文章')
creation_time = models.DateTimeField('收藏时间', default=now)
class Meta:
verbose_name = '用户收藏'
verbose_name_plural = verbose_name
unique_together = ('user', 'article') # 防止重复收藏
indexes = [models.Index(fields=['user', 'article'])] # 新增索引,优化查询速度
def __str__(self):
return f"{self.user.username}收藏了{self.article.title}"
class Article(BaseModel):
"""文章"""
#sxc 文章状态选项草稿d、已发布p
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
#sxc 评论状态选项打开o、关闭c
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
#sxc 文章类型选项文章a、独立页面p如关于页、联系页
TYPE = (
('a', _('Article')),
('p', _('Page')),
@ -140,100 +105,10 @@ class Article(BaseModel):
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
# 新增电影相关字段
director = models.CharField(max_length=100, verbose_name='导演', blank=True)
actors = models.TextField(verbose_name='演员', blank=True, help_text='多个演员用逗号分隔')
poster = models.ImageField(upload_to='movie_posters/', verbose_name='海报', blank=True, null=True)
movie_year = models.IntegerField(verbose_name='电影年份', blank=True, null=True)
rating = models.FloatField(verbose_name='评分', default=0.0)
movie_type = models.CharField(max_length=50, verbose_name='电影类型', blank=True)
# 新增收藏相关字段
favorites_count = models.IntegerField(default=0, verbose_name='收藏数')
# 新增SEO相关字段
summary = models.TextField(_('summary'), max_length=500, blank=True, default='')
keywords = models.CharField(_('keywords'), max_length=200, blank=True, default='')
meta_description = models.TextField(_('meta description'), max_length=1000, blank=True, default='')
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# 新增评分相关字段
average_rating = models.FloatField(default=0.0, verbose_name='平均评分')
rating_count = models.IntegerField(default=0, verbose_name='评分次数')
likes = models.PositiveIntegerField(_('likes'), default=0) # 点赞数
dislikes = models.PositiveIntegerField(_('dislikes'), default=0) # 点踩数
# 新增:新增点赞/点踩相关方法
def get_user_interaction(self, user):
"""获取用户对当前文章的互动类型like/dislike/none"""
if user.is_authenticated:
try:
return self.articleinteraction_set.get(user=user).interaction_type
except ArticleInteraction.DoesNotExist:
return None
return None
def toggle_interaction(self, user, interaction_type):
"""处理用户点赞/点踩:返回更新后的互动类型和计数"""
# 先删除用户之前的互动(点赞/点踩互斥)
self.articleinteraction_set.filter(user=user).delete()
# 处理当前互动
if interaction_type in ['like', 'dislike']:
# 新增互动记录
ArticleInteraction.objects.create(user=user, article=self, interaction_type=interaction_type)
# 更新对应计数
if interaction_type == 'like':
self.likes += 1
self.dislikes = max(0, self.dislikes - 1) # 取消之前的点踩
else:
self.dislikes += 1
self.likes = max(0, self.likes - 1) # 取消之前的点赞
self.save(update_fields=['likes', 'dislikes'])
return interaction_type
return None
# 新增:收藏方法
def add_to_favorites(self, user):
"""用户收藏文章"""
favorite, created = UserFavorite.objects.get_or_create(user=user, article=self)
if created:
self.favorites_count += 1
self.save(update_fields=['favorites_count'])
return created
def remove_from_favorites(self, user):
"""用户取消收藏文章"""
try:
favorite = UserFavorite.objects.get(user=user, article=self)
favorite.delete()
self.favorites_count =max(0,self.favorites_count - 1)
self.save(update_fields=['favorites_count'])
return True
except UserFavorite.DoesNotExist:
return False
def is_favorited_by_user(self, user):
"""检查用户是否收藏了该文章"""
if user.is_authenticated:
return UserFavorite.objects.filter(user=user, article=self).exists()
return False
#新增:将演员字符串转为列表
def get_actors_list(self):
if self.actors:
return [actor.strip() for actor in self.actors.split(',')]
return []
#新增:将演员列表转换为字符串
def set_actors_list(self, actors_list):
self.actors = ','.join(actors_list)
def body_to_string(self):
return self.body
def __str__(self):
#sxc 模型实例的字符串表示,返回文章标题
return self.title
class Meta:
@ -243,7 +118,6 @@ class Article(BaseModel):
get_latest_by = 'id'
def get_absolute_url(self):
#sxc 生成文章的相对 URL用于路由跳转包含发布日期和文章 ID
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -255,45 +129,41 @@ class Article(BaseModel):
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):
#sxc 重写 save 方法:可扩展文章保存时的自定义逻辑(当前仅调用父类方法)
super().save(*args, **kwargs)
def viewed(self):
#sxc 文章被浏览时调用,增加浏览量并保存(仅更新 views 字段)
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
#sxc 获取文章的有效评论列表,结果缓存 100 分钟,减少数据库查询
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:
#sxc 查询已启用的评论,按 ID 降序(最新评论在前)
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):
#sxc 生成文章在 Django Admin 后台的编辑 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):
#sxc 获取当前文章的下一篇ID 更大、已发布),结果缓存 100 分钟
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
#sxc 获取当前文章的前一篇ID 更小、已发布),结果缓存 100 分钟
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
@ -308,31 +178,28 @@ class Article(BaseModel):
class Category(BaseModel):
#sxc 文章分类模型,支持层级结构(父子分类),用于组织文章归属
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)#sxc 分类名称,唯一不可重复
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)#sxc 父分类,自关联,支持层级;父分类删除时子分类也删除
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index')) #sxc 排序权重,值越大越靠前
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
ordering = ['-index'] #sxc 默认按权重降序排列
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
#sxc 生成分类详情页的 URL基于 slug 参数
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
#sxc 模型实例的字符串表示,返回分类名称
return self.name
@cache_decorator(60 * 60 * 10)
@ -344,7 +211,6 @@ class Category(BaseModel):
categorys = []
def parse(category):
#sxc 递归函数:将当前分类加入列表,若有父分类则继续解析
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
@ -359,10 +225,9 @@ class Category(BaseModel):
:return:
"""
categorys = []
all_categorys = Category.objects.all() #sxc 预加载所有分类,减少数据库查询
all_categorys = Category.objects.all()
def parse(category):
#sxc 递归函数:将当前分类加入列表,再解析其所有子分类
if category not in categorys:
categorys.append(category)
childs = all_categorys.filter(parent_category=category)
@ -376,22 +241,18 @@ class Category(BaseModel):
class Tag(BaseModel):
#sxc 文章标签模型,用于文章的多维度标记和快速检索
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self):
#sxc 模型实例的字符串表示,返回标签名称
return self.name
def get_absolute_url(self):
#sxc 生成标签详情页的 URL基于 slug 参数
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
#sxc 获取关联当前标签的文章数量(去重),结果缓存 10 小时
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
@ -401,7 +262,6 @@ class Tag(BaseModel):
class Links(models.Model):
#sxc 友情链接模型,用于管理博客外部链接的展示
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
@ -418,17 +278,15 @@ class Links(models.Model):
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence']#sxc 默认按排序序号升序排列
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
#sxc 模型实例的字符串表示,返回链接名称
return self.name
class SideBar(models.Model):
#sxc 侧边栏模型,用于展示自定义 HTML 内容,增强页面灵活性
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
@ -509,32 +367,10 @@ class BlogSettings(models.Model):
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):
# 调用父类的save方法保存对象
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
# 新增:文章互动模型,记录用户对文章的点赞点踩行为
class ArticleInteraction(models.Model):
"""用户与文章的互动(点赞/点踩)模型"""
INTERACTION_CHOICES = (
('like', '点赞'),
('dislike', '点踩'),
)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,related_name='article_interactions', verbose_name='用户')
article = models.ForeignKey('Article', on_delete=models.CASCADE, verbose_name='文章')
interaction_type = models.CharField('互动类型', max_length=10, choices=INTERACTION_CHOICES)
created_time = models.DateTimeField('互动时间', default=now)
class Meta:
verbose_name = '文章互动'
verbose_name_plural = verbose_name
unique_together = ('user', 'article') # 同一用户对同一文章只能有一个互动(点赞/点踩互斥)
indexes = [models.Index(fields=['user', 'article'])] # 优化查询

@ -0,0 +1,13 @@
from haystack import indexes
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
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')

@ -1,5 +1,3 @@
#sxc 该模块是 Django 博客系统的自定义模板标签库,提供了格式化显示、内容处理、侧边栏加载、面包屑生成等 16 个实用模板标签 / 过滤器,用于在前端模板中复用核心业务逻辑
#sxc 依赖 Django 模板系统、博客数据模型、缓存工具及第三方头像服务,同时集成插件钩子机制扩展功能
import hashlib
import logging
import random
@ -21,24 +19,19 @@ from djangoblog.utils import cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
from djangoblog.plugin_manage import hooks
from collections.abc import Iterable
logger = logging.getLogger(__name__)
#sxc 注册模板标签库,使 Django 模板系统可识别该模块中的标签
register = template.Library()
@register.simple_tag(takes_context=True)
def head_meta(context):
#sxc 执行头部元信息的插件过滤钩子,返回处理后的安全 HTML 内容
#sxc takes_context=True 表示需要接收模板上下文参数
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
#sxc 简单标签:按项目配置的 TIME_FORMAT 格式化为时间字符串
#sxc 异常捕获避免时间格式错误导致页面崩溃,错误时返回空字符串
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
@ -48,8 +41,6 @@ def timeformat(data):
@register.simple_tag
def datetimeformat(data):
#sxc 简单标签:按项目配置的 DATE_TIME_FORMAT 格式化为日期时间字符串
#sxc 异常捕获处理,错误时返回空字符串
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
@ -60,14 +51,11 @@ def datetimeformat(data):
@register.filter()
@stringfilter
def custom_markdown(content):
#sxc 过滤器:将 markdown 格式的内容转换为 HTML并标记为安全内容避免 XSS 过滤)
#sxc @stringfilter 表示仅接收字符串类型输入
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
def get_markdown_toc(content):
#sxc 简单标签:获取 markdown 内容的目录TOC返回安全 HTML 格式
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@ -76,8 +64,6 @@ def get_markdown_toc(content):
@register.filter()
@stringfilter
def comment_markdown(content):
#sxc 过滤器:处理评论内容的 markdown 转换,先转 HTML 再 sanitize 过滤危险标签
#sxc 确保评论内容安全,防止恶意 HTML 注入
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@ -90,8 +76,6 @@ def truncatechars_content(content):
:param content:
:return:
"""
#sxc 过滤器:按博客配置的 “文章摘要长度” 截取内容,保留 HTML 标签结构
#sxc is_safe=True 表示返回的 HTML 内容安全,无需额外转义
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
@ -101,7 +85,6 @@ def truncatechars_content(content):
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
#sxc 过滤器:移除 HTML 标签后截取前 150 个字符,用于纯文本摘要场景
from django.utils.html import strip_tags
return strip_tags(content)[:150]
@ -114,8 +97,6 @@ def load_breadcrumb(article):
:param article:
:return:
"""
#sxc 包含标签:加载文章面包屑导航,渲染指定模板并传递数据
#sxc 从文章获取分类层级树,反转顺序后拼接网站名称,生成面包屑路径
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
@ -124,9 +105,9 @@ def load_breadcrumb(article):
names = names[::-1]
return {
'names': names,#sxc 面包屑层级列表,每个元素为 (名称,链接)
'title': article.title,#sxc 文章标题
'count': len(names) + 1#sxc 面包屑节点数量
'names': names,
'title': article.title,
'count': len(names) + 1
}
@ -137,8 +118,6 @@ def load_articletags(article):
:param article:
:return:
"""
#sxc 包含标签:加载文章的标签列表,渲染标签展示模板
#sxc 为每个标签生成链接、文章数量,并随机分配 Bootstrap 颜色
tags = article.tags.all()
tags_list = []
for tag in tags:
@ -148,7 +127,7 @@ def load_articletags(article):
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
))
return {
'article_tags_list': tags_list#sxc 传递标签数据列表到模板
'article_tags_list': tags_list
}
@ -158,38 +137,28 @@ def load_sidebar(user, linktype):
加载侧边栏
:return:
"""
#sxc 包含标签:加载博客侧边栏内容,优先从缓存获取,缓存未命中则查询数据库
#sxc 缓存键包含 linktype链接显示类型确保不同页面侧边栏内容正确
value = cache.get("sidebar" + linktype)
if value:
value['user'] = user#sxc 补充当前用户信息
value['user'] = user
return value
else:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
#sxc 查询发布状态的最新文章,数量由博客配置决定
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
#sxc 查询所有分类
sidebar_categorys = Category.objects.all()
#sxc 查询启用状态的额外侧边栏,按排序号排列
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
#sxc 查询浏览量最高的文章
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
#sxc 按月份分组的文章发布日期
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
#sxc 查询启用状态的链接,筛选当前页面类型或全站显示的链接
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
#sxc 查询启用状态的最新评论,数量由博客配置决定
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
#sxc 生成标签云:计算标签字体大小(按文章数量比例),随机打乱顺序
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
@ -216,7 +185,6 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
#sxc 设置缓存,有效期 3 小时60603 秒)
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
@ -238,7 +206,6 @@ def load_article_metas(article, user):
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
#sxc 包含标签:加载分页信息,根据不同页面类型(首页 / 标签 / 作者 / 分类)生成上下页链接
previous_url = ''
next_url = ''
if page_type == '':
@ -250,7 +217,6 @@ def load_pagination_info(page_obj, page_type, tag_name):
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
#sxc 标签归档分页逻辑:根据标签 slug 生成链接
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
@ -267,7 +233,6 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={
'page': previous_number,
'tag_name': tag.slug})
#sxc 作者归档分页逻辑:根据作者名生成链接
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
@ -283,7 +248,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={
'page': previous_number,
'author_name': tag_name})
#sxc 分类归档分页逻辑:根据分类 slug 生成链接
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
@ -316,7 +281,6 @@ def load_article_detail(article, isindex, user):
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
#sxc 包含标签:加载文章详情内容,根据 isindex 判断是否显示摘要(列表页)或完整内容(详情页)
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
@ -333,26 +297,22 @@ def load_article_detail(article, isindex, user):
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""
#sxc 过滤器:获取用户 Gravatar 头像 URL优先使用 OAuth 用户的自定义头像,其次调用 Gravatar 接口
#sxc 缓存头像 URL减少重复查询
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
else:
#sxc 查询 OAuth 用户表,获取用户自定义头像
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
email = email.encode('utf-8')
#sxc 头像默认图(项目静态文件)
default = static('blog/img/avatar.png')
#sxc 拼接 Gravatar URL包含 MD5 加密邮箱、默认图、尺寸参数
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
#sxc 缓存头像 URL有效期 10 小时
cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@ -361,7 +321,6 @@ def gravatar_url(email, size=40):
@register.filter
def gravatar(email, size=40):
"""获得gravatar头像"""
#sxc 过滤器:生成 Gravatar 头像的 img 标签,直接返回可渲染的 HTML
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' %
@ -383,8 +342,3 @@ def query(qs, **kwargs):
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
@register.filter(name='is_iterable') # 明确指定过滤器名称
def is_iterable(value):
"""判断变量是否为可迭代类型(排除字符串和字节)"""
return isinstance(value, Iterable) and not isinstance(value, (str, bytes))

@ -1,5 +1,5 @@
#sxc 该模块基于 Django 框架,通过 TestCase 类对博客系统核心功能进行自动化测试,覆盖文章管理、用户交互、支付流程、第三方集成等场景,确保系统各模块功能正常、接口响应符合预期。
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
@ -20,13 +20,11 @@ from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
class ArticleTest(TestCase):
#sxc 初始化测试环境,创建客户端(模拟用户请求)和请求工厂(构造自定义请求),为后续测试方法提供基础对象。
def setUp(self):
self.client = Client() #sxc 模拟用户浏览器,用于发送 HTTP 请求
self.factory = RequestFactory() #sxc 用于构造自定义请求对象,支持更灵活的请求参数配置
#sxc 测试文章相关核心功能,包括用户创建、分类 / 标签 / 侧边栏数据初始化、文章增删查、搜索、分页、第三方通知等,覆盖文章生命周期关键流程。
self.client = Client()
self.factory = RequestFactory()
def test_validate_article(self):
#sxc 获取当前站点域名,用于后续 URL 相关操作
site = get_current_site().domain
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
@ -35,31 +33,27 @@ class ArticleTest(TestCase):
user.is_staff = True
user.is_superuser = True
user.save()
#sxc 测试用户个人主页访问,验证响应状态码为 200正常访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
#sxc 测试管理员后台邮件发送日志页面访问(无实际业务校验,仅验证页面可达性)
response = self.client.get('/admin/servermanager/emailsendlog/')
#sxc 测试管理员后台操作日志页面访问(无实际业务校验,仅验证页面可达性)
response = self.client.get('admin/admin/logentry/')
#sxc 创建测试侧边栏数据,设置排序、名称、内容并启用,用于验证侧边栏功能
s = SideBar()
s.sequence = 1 #sxc 侧边栏排序序号,决定展示顺序
s.name = 'test' #sxc 侧边栏名称
s.content = 'test content' #sxc 侧边栏内容
s.is_enable = True #sxc 启用侧边栏
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.save()
#sxc 创建测试分类数据,设置名称和时间戳,用于文章分类关联
category = Category()
category.name = "category"
category.creation_time = timezone.now() #sxc 分类创建时间
category.last_mod_time = timezone.now() #sxc 分类最后修改时间
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
#sxc 创建测试标签数据,用于文章标签关联
tag = Tag()
tag.name = "nicetag"
tag.save()
#sxc 创建测试文章设置标题、内容、作者、分类、类型a普通文章、状态p已发布
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
@ -69,18 +63,15 @@ class ArticleTest(TestCase):
article.status = 'p'
article.save()
#sxc 验证文章初始状态下标签数量为 0未关联标签
self.assertEqual(0, article.tags.count())
#sxc 为文章关联标签并重新保存
article.tags.add(tag)
article.save()
#sxc 验证文章关联标签后数量为 1标签关联成功
self.assertEqual(1, article.tags.count())
#sxc 循环创建 20 篇测试文章,用于后续分页功能测试(模拟多数据场景)
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i) #sxc 动态生成文章标题(带序号)
article.body = "nicetitle" + str(i) #sxc 动态生成文章内容(带序号)
article.title = "nicetitle" + str(i)
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
@ -88,59 +79,56 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
#sxc 检查 Elasticsearch 是否启用,若启用则执行索引构建命令,确保搜索功能可用
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index") #sxc 执行 Django 自定义命令,构建文章搜索索引
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
#sxc 测试搜索功能,搜索关键词 "nicetitle",验证响应状态码为 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)
#sxc 测试文章详情页访问,验证响应状态码为 200
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
#sxc 测试搜索不存在的关键词 “django”验证搜索页面仍能正常响应状态码 200
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
#sxc 测试文章标签模板标签load_articletags验证返回结果非空
s = load_articletags(article)
self.assertIsNotNone(s)
#sxc 模拟用户登录(使用测试账号),用于后续需登录权限的操作
self.client.login(username='liangliangyy', password='liangliangyy')
#sxc 测试文章归档页面访问,验证响应状态码为 200
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
#sxc 测试所有文章的分页功能调用check_pagination方法验证分页链接有效性
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
#sxc 测试指定标签tag下文章的分页功能
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
#sxc 测试指定作者liangliangyy下文章的分页功能
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
#sxc 测试指定分类category下文章的分页功能
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
#sxc 测试博客搜索表单BlogSearchForm的search方法验证表单功能可执行
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
#sxc 调用百度爬虫通知工具,通知单篇文章的完整 URL
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
#sxc 测试头像相关模板标签gravatar_url和gravatar验证头像 URL 和 HTML 标签生成功能
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
#sxc 创建测试友情链接数据,验证友情链接页面功能
link = Links(
sequence=1,
name="lylinux",
@ -148,47 +136,39 @@ class ArticleTest(TestCase):
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
#sxc 测试 RSS 订阅页面访问,验证响应状态码为 200
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
#sxc 测试站点地图页面访问,验证响应状态码为 200
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
#sxc 测试管理员删除文章操作,验证页面可达性
self.client.get("/admin/blog/article/1/delete/")
#sxc 测试管理员后台邮件日志和操作日志页面访问,验证后台功能稳定性
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
#sxc 验证分页功能,遍历所有分页页面,确保上一页、下一页链接响应正常
def check_pagination(self, p, type, value):
for page in range(1, p.num_pages + 1):
#sxc 调用分页模板标签,获取分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
#sxc 验证上一页链接响应状态
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
#sxc 验证下一页链接响应状态
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
#sxc 测试图片上传功能,包括未授权拦截、授权上传、工具函数调用
def test_image(self):
import requests
#sxc 下载测试图片Python logo用于上传测试
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)
#sxc 测试未授权图片上传,验证返回 403
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
#sxc 生成上传授权签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
#sxc 构造授权上传请求,验证上传成功(状态码 200
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
@ -205,9 +185,8 @@ class ArticleTest(TestCase):
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
#sxc 测试 Django 自定义管理命令,覆盖索引构建、爬虫通知、数据生成等运维功能
def test_commands(self):
#sxc 创建测试超级用户,用于命令执行权限验证
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -215,13 +194,13 @@ class ArticleTest(TestCase):
user.is_staff = True
user.is_superuser = True
user.save()
#sxc 创建 QQ 第三方登录配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
#sxc 创建第一个 QQ 第三方用户(关联本地用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@ -232,7 +211,7 @@ class ArticleTest(TestCase):
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
#sxc 创建第二个 QQ 第三方用户(不关联本地用
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -242,11 +221,10 @@ class ArticleTest(TestCase):
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
#sxc 若 Elasticsearch 启用,执行索引构建命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
#sxc 执行百度爬虫通知、测试数据生成、缓存清理、头像同步、搜索词构建等命令
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")

@ -0,0 +1,62 @@
from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
app_name = "blog"
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
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'),
]

@ -0,0 +1,379 @@
import logging
import os
import uuid
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
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__)
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
def get_view_cache_key(self):
return self.request.get['pages']
@property
def page_number(self):
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
'''
重写默认从缓存获取数据
: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):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get_context_data(self, **kwargs):
comment_form = CommentForm()
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
else:
page = int(page)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
p_comments = paginator.page(page)
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
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
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
def get_queryset_data(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
def get_queryset_cache_key(self):
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
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
return super(AuthorDetailView, self).get_context_data(**kwargs)
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
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
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):
def get_context(self):
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context
@csrf_exempt
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
if not savepath.startswith(base_dir):
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)
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')

@ -0,0 +1,47 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
list_display_links = ('id', 'body', 'is_enable')
list_filter = ('is_enable',)
exclude = ('creation_time', 'last_modify_time')
actions = [disable_commentstatus, enable_commentstatus]
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
def link_to_article(self, obj):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -0,0 +1,5 @@
from django.apps import AppConfig
class CommentsConfig(AppConfig):
name = 'comments'

@ -3,13 +3,11 @@ from django.forms import ModelForm
from .models import Comment
#syy 评论表单类,继承自 ModelForm用于生成和处理评论提交表单
class CommentForm(ModelForm):
#syy 定义父评论 ID 字段,使用隐藏输入框,非必填(用于处理回复功能)
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
#syy 元数据类,用于配置表单与模型的关联及字段映射
class Meta:
model = Comment
fields = ['body']

@ -5,16 +5,16 @@ from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
#syy 迁移类,定义了数据库结构的变更操作
class Migration(migrations.Migration):
initial = True
#syy 依赖的其他迁移文件,需先执行这些迁移才能运行当前迁移
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#syy 迁移操作列表,包含要执行的数据库变更
operations = [
migrations.CreateModel(
name='Comment',
@ -28,7 +28,6 @@ class Migration(migrations.Migration):
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
#syy 模型的额外配置
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',

@ -2,18 +2,17 @@
from django.db import migrations, models
#syy 迁移类,定义对数据库结构的修改操作
class Migration(migrations.Migration):
#syy 依赖的前置迁移文件,需先执行 comments 应用的 0001_initial 迁移
dependencies = [
('comments', '0001_initial'),
]
#syy 迁移操作列表,包含具体的数据库变更
operations = [
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否显示'), #syy 将 is_enable 字段的默认值从 True 改为 False保持字段类型和 verbose_name 不变
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -5,61 +5,53 @@ from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
#syy 迁移类,定义对 Comment 模型的一系列修改操作
class Migration(migrations.Migration):
#syy 依赖的前置迁移文件,需先执行这些迁移才能运行当前迁移
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
#syy 迁移操作列表,包含具体的数据库变更
operations = [
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
#syy 移除 Comment 模型中的 created_time 字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
#syy 移除 Comment 模型中的 last_mod_time 字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
#syy 向 Comment 模型添加 creation_time 字段
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#syy 向 Comment 模型添加 last_modify_time 字段
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
#syy 修改 Comment 模型中 article 字段的配置
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
#syy 修改 Comment 模型中 author 字段的配置
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
#syy 修改 Comment 模型中 is_enable 字段的配置
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
#syy 修改 Comment 模型中 parent_comment 字段的配置
migrations.AlterField(
model_name='comment',
name='parent_comment',

@ -0,0 +1,39 @@
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
# Create your models here.
class Comment(models.Model):
body = models.TextField('正文', max_length=300)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
ordering = ['-id']
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def __str__(self):
return self.body

@ -1,16 +1,15 @@
#syy 该模块定义了评论相关的模板标签,用于在模板中处理评论树解析和评论项展示功能
from django import template
#syy 注册模板标签库,用于在模板中加载和使用自定义标签
register = template.Library()
#syy 用法示例:
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %}
"""
datas = []
#syy 递归函数:获取指定评论的直接子评论,并递归处理子评论的子评论
def parse(c):
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
@ -20,7 +19,7 @@ def parse_commenttree(commentlist, comment):
parse(comment)
return datas
#syy 会将数据传递到 comments/tags/comment_item.html 模板进行渲染
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""

@ -9,9 +9,8 @@ from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
#syy 评论功能测试类,继承自 TransactionTestCase用于测试评论相关的各种功能
class CommentsTest(TransactionTestCase):
#syy 测试前的初始化方法,设置测试客户端、创建测试用户和必要的配置
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
@ -19,26 +18,25 @@ class CommentsTest(TransactionTestCase):
value = BlogSettings()
value.comment_need_review = True
value.save()
#syy 创建超级用户用于测试登录
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
#syy 辅助方法:更新文章下所有评论的状态为启用
def update_article_comment_status(self, article):
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
#syy 测试评论验证功能,包括发布评论、回复评论等流程
def test_validate_comment(self):
self.client.login(username='liangliangyy1', password='liangliangyy1')
#syy 创建测试分类
category = Category()
category.name = "categoryccc"
category.save()
#syy 创建测试文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
@ -47,36 +45,36 @@ class CommentsTest(TransactionTestCase):
article.type = 'a'
article.status = 'p'
article.save()
#syy 获取评论提交的 URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
#syy 提交第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
#syy 验证评论提交后是否重定向(状态码 302
self.assertEqual(response.status_code, 302)
#syy 验证未审核的评论不在评论列表中
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
self.update_article_comment_status(article)
#syy 验证启用后评论列表长度为 1
self.assertEqual(len(article.comment_list()), 1)
#syy 提交第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
#syy 验证第二条评论启用后列表长度为 2
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
parent_comment_id = article.comment_list()[0].id
#syy 提交带格式的回复评论
response = self.client.post(comment_url,
{
'body': '''
@ -94,12 +92,10 @@ class CommentsTest(TransactionTestCase):
''',
'parent_comment_id': parent_comment_id
})
#syy 验证回复提交成功
self.assertEqual(response.status_code, 302)
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
#syy 验证回复后评论总数为 3
self.assertEqual(len(article.comment_list()), 3)
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
@ -108,6 +104,6 @@ class CommentsTest(TransactionTestCase):
self.assertIsNotNone(data)
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
#syy 验证评论邮件发送函数正常工作
from comments.utils import send_comment_email
send_comment_email(comment)

@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = "comments"
urlpatterns = [
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]

@ -4,10 +4,10 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
#syy 获取当前模块的日志记录器,用于记录发送邮件过程中的异常信息
logger = logging.getLogger(__name__)
#syy 发送评论相关邮件的函数,包括评论感谢邮件和回复通知邮件
def send_comment_email(comment):
site = get_current_site().domain
subject = _('Thanks for your comment')
@ -22,7 +22,6 @@ def send_comment_email(comment):
tomail = comment.author.email
send_email([tomail], subject, html_content)
try:
#syy 发送评论相关邮件的函数,包括评论感谢邮件和回复通知邮件
if comment.parent_comment:
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
@ -33,9 +32,7 @@ def send_comment_email(comment):
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
#syy 获取被回复者(父评论作者)的邮箱地址
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
#syy 记录发送回复通知邮件时的异常信息
logger.error(e)

@ -0,0 +1,63 @@
# Create your views here.
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView
from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm
from .models import Comment
class CommentPostView(FormView):
form_class = CommentForm
template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
return self.render_to_response({
'form': form,
'article': article
})
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
comment = form.save(False)
comment.article = article
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
comment.save(True)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

@ -0,0 +1 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -1,10 +1,8 @@
#psr导入Django管理站点相关模块和模型
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
#psr导入项目中各个应用的admin和models模块
from accounts.admin import *
from blog.admin import *
from blog.models import *
@ -18,16 +16,14 @@ from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
#psr自定义Django管理站点类用于统一管理博客系统所有模型
class DjangoBlogAdminSite(AdminSite):
#psr设置管理站点的头部标题
site_header = 'djangoblog administration'
#psr设置管理站点的标题
site_title = 'djangoblog site admin'
#psr初始化管理站点设置名称为'admin'
def __init__(self, name='admin'):
super().__init__(name)
#psr重写权限检查方法只允许超级用户访问管理站点
def has_permission(self, request):
return request.user.is_superuser
@ -41,10 +37,9 @@ class DjangoBlogAdminSite(AdminSite):
# ]
# return urls + my_urls
#psr创建DjangoBlog管理站点实例
admin_site = DjangoBlogAdminSite(name='admin')
#psr注册博客相关的模型到管理站点
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
@ -52,25 +47,18 @@ admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
#psr注册服务器管理相关的模型到管理站点
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
#psr注册用户管理相关的模型到管理站点
admin_site.register(BlogUser, BlogUserAdmin)
#psr注册评论相关的模型到管理站点
admin_site.register(Comment, CommentAdmin)
#psr注册OAuth认证相关的模型到管理站点
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
#psr注册OwnTracks位置追踪相关的模型到管理站点
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
#psr注册Django自带的Site模型到管理站点
admin_site.register(Site, SiteAdmin)
#psr注册Django自带的LogEntry模型到管理站点
admin_site.register(LogEntry, LogEntryAdmin)

@ -0,0 +1,11 @@
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
def ready(self):
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()

@ -1,4 +1,3 @@
#psr 导入线程、日志、Django信号、邮件、模型信号等相关模块
import _thread
import logging
@ -10,7 +9,6 @@ from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
#psr 导入项目相关模块和工具函数
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
@ -18,24 +16,19 @@ from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, del
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
#psr 获取日志记录器实例
logger = logging.getLogger(__name__)
#psr 定义OAuth用户登录信号和发送邮件信号
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
#psr 发送邮件信号处理函数用于发送HTML格式邮件
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
#psr 从信号参数中获取邮件信息
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
#psr 创建邮件消息对象并设置HTML内容类型
msg = EmailMultiAlternatives(
title,
content,
@ -43,7 +36,6 @@ def send_email_signal_handler(sender, **kwargs):
to=emailto)
msg.content_subtype = "html"
#psr 创建邮件发送日志记录
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
@ -51,35 +43,27 @@ def send_email_signal_handler(sender, **kwargs):
log.emailto = ','.join(emailto)
try:
#psr 发送邮件并记录发送结果
result = msg.send()
log.send_result = result > 0
except Exception as e:
#psr 处理邮件发送异常
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
#psr 保存邮件发送日志
log.save()
#psr OAuth用户登录信号处理函数用于处理用户头像等登录后操作
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
#psr 获取OAuth用户信息
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
#psr 如果用户头像不在当前站点,则保存用户头像到本地
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
#psr 删除侧边栏缓存
delete_sidebar_cache()
#psr 模型保存后回调函数,用于处理模型保存后的缓存清理和通知操作
@receiver(post_save)
def model_post_save_callback(
sender,
@ -90,24 +74,19 @@ def model_post_save_callback(
update_fields,
**kwargs):
clearcache = False
#psr 如果是日志条目则直接返回
if isinstance(instance, LogEntry):
return
#psr 如果实例有获取完整URL的方法
if 'get_full_url' in dir(instance):
is_update_views = update_fields == {'views'}
#psr 如果不是测试环境且不是仅更新浏览量,则通知搜索引擎
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
#psr 如果不是仅更新浏览量则需要清理缓存
if not is_update_views:
clearcache = True
#psr 如果是评论实例且已启用
if isinstance(instance, Comment):
if instance.is_enable:
path = instance.article.get_absolute_url()
@ -115,38 +94,29 @@ def model_post_save_callback(
if site.find(':') > 0:
site = site[0:site.find(':')]
#psr 过期文章详情页面缓存
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
#psr 删除SEO处理器缓存
if cache.get('seo_processor'):
cache.delete('seo_processor')
#psr 删除文章评论缓存
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
#psr 删除侧边栏和视图缓存
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
#psr 异步发送评论邮件通知
_thread.start_new_thread(send_comment_email, (instance,))
#psr 如果需要清理缓存则清空所有缓存
if clearcache:
cache.clear()
#psr 用户登录和登出回调函数,用于处理用户认证相关缓存清理
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
#psr 如果用户存在且有用户名
if user and user.username:
logger.info(user)
#psr 删除侧边栏缓存
delete_sidebar_cache()
# cache.clear()

@ -1,4 +1,3 @@
#psr导入所需的编码处理、Elasticsearch查询、Haystack搜索框架等相关模块
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
@ -6,144 +5,103 @@ from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
#psr导入博客应用的Elasticsearch文档和模型
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
#psr获取日志记录器实例
logger = logging.getLogger(__name__)
#psr定义Elasticsearch搜索后端类继承自BaseSearchBackend
class ElasticSearchBackend(BaseSearchBackend):
#psr初始化Elasticsearch后端设置连接别名和选项
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
#psr创建文章文档管理器实例
self.manager = ArticleDocumentManager()
#psr启用拼写检查功能
self.include_spelling = True
#psr获取模型数据并转换为文档格式
def _get_models(self, iterable):
#psr如果传入迭代器为空则获取所有文章对象
models = iterable if iterable and iterable[0] else Article.objects.all()
#psr将模型转换为Elasticsearch文档
docs = self.manager.convert_to_doc(models)
return docs
#psr创建索引包括创建索引结构和重建文档数据
def _create(self, models):
#psr创建Elasticsearch索引
self.manager.create_index()
#psr获取模型文档
docs = self._get_models(models)
#psr重建索引数据
self.manager.rebuild(docs)
#psr删除指定模型数据
def _delete(self, models):
#psr遍历模型列表并删除每个模型
for m in models:
m.delete()
return True
#psr重建索引数据
def _rebuild(self, models):
#psr如果模型为空则获取所有文章对象
models = models if models else Article.objects.all()
#psr转换模型为文档格式
docs = self.manager.convert_to_doc(models)
#psr更新文档数据
self.manager.update_docs(docs)
#psr更新索引中的文档数据
def update(self, index, iterable, commit=True):
#psr获取模型文档
models = self._get_models(iterable)
#psr更新文档到Elasticsearch
self.manager.update_docs(models)
#psr从索引中移除指定对象
def remove(self, obj_or_string):
#psr获取模型文档并删除
models = self._get_models([obj_or_string])
self._delete(models)
#psr清空索引数据
def clear(self, models=None, commit=True):
#psr调用remove方法清空数据
self.remove(None)
#psr静态方法获取搜索建议词
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
#psr构建Elasticsearch搜索查询使用term建议器
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
#psr处理建议结果提取建议词
keywords = []
for suggest in search.suggest.suggest_search:
if suggest["options"]:
#psr如果有建议选项则使用建议词
keywords.append(suggest["options"][0]["text"])
else:
#psr如果没有建议选项则使用原词
keywords.append(suggest["text"])
#psr将关键词组合成字符串返回
return ' '.join(keywords)
#psr执行搜索查询的核心方法
@log_query
def search(self, query_string, **kwargs):
#psr记录搜索查询日志
logger.info('search query_string:' + query_string)
#psr获取分页偏移量参数
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
#psr根据是否启用建议功能获取搜索建议词
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
#psr构建Elasticsearch布尔查询条件
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
#psr构建完整的Elasticsearch搜索查询
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
#psr执行搜索并获取结果
results = search.execute()
#psr获取搜索结果总数
hits = results['hits'].total
#psr处理原始搜索结果
raw_results = []
for raw_result in results['hits']['hits']:
#psr设置应用标签和模型名称
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
#psr创建搜索结果对象
result_class = SearchResult
result = result_class(
@ -153,11 +111,9 @@ class ElasticSearchBackend(BaseSearchBackend):
raw_result['_score'],
**additional_fields)
raw_results.append(result)
#psr初始化facet和拼写建议
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
#psr返回搜索结果字典
return {
'results': raw_results,
'hits': hits,
@ -166,17 +122,13 @@ class ElasticSearchBackend(BaseSearchBackend):
}
#psr定义Elasticsearch搜索查询类继承自BaseSearchQuery
class ElasticSearchQuery(BaseSearchQuery):
#psr转换日期时间格式
def _convert_datetime(self, date):
#psr判断日期是否包含小时信息并格式化
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
#psr清理用户输入的查询字符串
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
@ -186,17 +138,13 @@ class ElasticSearchQuery(BaseSearchQuery):
to escape reserved characters. Instead, the whole word should be
quoted.
"""
#psr将查询字符串分割为单词
words = query_fragment.split()
cleaned_words = []
#psr遍历每个单词进行清理处理
for word in words:
#psr处理保留字
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
#psr处理保留字符如果包含则用引号包围
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
@ -204,46 +152,32 @@ class ElasticSearchQuery(BaseSearchQuery):
cleaned_words.append(word)
#psr重新组合清理后的单词
return ' '.join(cleaned_words)
#psr构建查询片段
def build_query_fragment(self, field, filter_type, value):
#psr返回查询字符串
return value.query_string
#psr获取搜索结果数量
def get_count(self):
#psr获取搜索结果并返回长度
results = self.get_results()
return len(results) if results else 0
#psr获取拼写建议
def get_spelling_suggestion(self, preferred_query=None):
#psr返回拼写建议
return self._spelling_suggestion
#psr构建查询参数
def build_params(self, spelling_query=None):
#psr调用父类方法构建参数
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
#psr定义Elasticsearch模型搜索表单类继承自ModelSearchForm
class ElasticSearchModelSearchForm(ModelSearchForm):
#psr执行搜索操作
def search(self):
#psr根据表单数据设置是否启用建议功能
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
#psr调用父类搜索方法
sqs = super().search()
return sqs
#psr定义Elasticsearch搜索引擎类继承自BaseEngine
class ElasticSearchEngine(BaseEngine):
#psr指定后端和查询类
backend = ElasticSearchBackend
query = ElasticSearchQuery
query = ElasticSearchQuery

@ -1,4 +1,3 @@
#psr 导入Django管理后台、内容类型、URL处理等相关模块
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
@ -9,25 +8,20 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
#psr 日志条目管理类用于在Django管理后台中显示和管理管理员操作日志
class LogEntryAdmin(admin.ModelAdmin):
#psr 设置列表过滤器,按内容类型过滤日志
list_filter = [
'content_type'
]
#psr 设置搜索字段,支持按对象表示和变更消息搜索
search_fields = [
'object_repr',
'change_message'
]
#psr 设置列表显示链接字段
list_display_links = [
'action_time',
'get_change_message',
]
#psr 设置列表显示字段
list_display = [
'action_time',
'user_link',
@ -36,32 +30,25 @@ class LogEntryAdmin(admin.ModelAdmin):
'get_change_message',
]
#psr 禁止添加权限,日志条目只能由系统自动生成
def has_add_permission(self, request):
return False
#psr 设置变更权限只有超级用户或具有特定权限的用户才能查看且不能通过POST请求修改
def has_change_permission(self, request, obj=None):
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
#psr 禁止删除权限,日志条目不能被删除
def has_delete_permission(self, request, obj=None):
return False
#psr 生成对象链接,将日志中的对象表示转换为可点击的管理页面链接
def object_link(self, obj):
#psr 转义对象表示字符串防止XSS攻击
object_link = escape(obj.object_repr)
content_type = obj.content_type
#psr 如果不是删除操作且内容类型存在,则尝试生成链接
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
#psr 构造管理页面URL并生成HTML链接
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
@ -70,22 +57,16 @@ class LogEntryAdmin(admin.ModelAdmin):
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
pass
#psr 返回安全的HTML链接
return mark_safe(object_link)
#psr 设置对象链接字段的排序字段和显示名称
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
#psr 生成用户链接,将日志中的用户转换为可点击的管理页面链接
def user_link(self, obj):
#psr 获取用户对象的内容类型
content_type = ContentType.objects.get_for_model(type(obj.user))
#psr 转义用户字符串防止XSS攻击
user_link = escape(force_str(obj.user))
try:
# try returning an actual link instead of object repr string
#psr 构造用户管理页面URL并生成HTML链接
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
@ -94,19 +75,15 @@ class LogEntryAdmin(admin.ModelAdmin):
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
pass
#psr 返回安全的HTML链接
return mark_safe(user_link)
#psr 设置用户链接字段的排序字段和显示名称
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
#psr 重写查询集,预加载内容类型以提高查询性能
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
#psr 重写获取操作方法,移除删除选中项操作
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:

@ -8,52 +8,40 @@ https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
#psr Django博客项目的配置文件包含数据库、缓存、安全、国际化等所有配置项
#psr 该文件定义了Django应用的核心设置包括应用列表、中间件、模板、数据库连接等
#psr 更多信息请参考: https://docs.djangoproject.com/en/1.10/topics/settings/
#psr 完整配置项列表请参考: https://docs.djangoproject.com/en/1.10/ref/settings/
"""
#psr 导入操作系统、系统路径、路径处理等相关模块
import os
import sys
from pathlib import Path
#psr 导入Django国际化翻译模块
from django.utils.translation import gettext_lazy as _
#psr 环境变量转换为布尔值的辅助函数
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
#psr 构建项目内部路径BASE_DIR指向项目根目录
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
#psr Django安全密钥用于加密签名生产环境应从环境变量获取
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
#psr 调试模式开关,生产环境应关闭
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
#psr 测试模式标识,用于判断是否在运行测试
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = []
#psr 允许访问的主机列表,*表示允许所有主机
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置
#psr CSRF信任的源列表用于跨站请求伪造保护
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
#psr 已安装的应用列表包含Django核心应用和自定义应用
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
@ -76,7 +64,6 @@ INSTALLED_APPS = [
'djangoblog'
]
#psr 中间件配置列表,用于处理请求和响应
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
@ -94,10 +81,8 @@ MIDDLEWARE = [
'blog.middleware.OnlineMiddleware'
]
#psr 根URL配置文件
ROOT_URLCONF = 'djangoblog.urls'
#psr 模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -115,17 +100,16 @@ TEMPLATES = [
},
]
#psr WSGI应用配置
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
#psr 数据库配置使用MySQL数据库
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'DjangoBlog_TV',
'NAME': 'DjangoBlog',
'USER':'root',
'PASSWORD':'shuranpu',
'HOST':'localhost',
@ -138,7 +122,6 @@ DATABASES = {
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
#psr 密码验证器配置
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@ -154,18 +137,15 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
#psr 支持的语言列表
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
#psr 本地化文件路径
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
#psr 语言代码和时区设置
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
@ -179,7 +159,7 @@ USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
#psr Haystack搜索配置使用Whoosh搜索引擎
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
@ -187,41 +167,32 @@ HAYSTACK_CONNECTIONS = {
},
}
# Automatically update searching index
#psr 实时更新搜索索引的信号处理器
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
#psr 自定义认证后端,支持邮箱或用户名登录
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
#psr 静态文件配置
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
#psr 自定义用户模型和登录URL
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
#psr 时间格式配置
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles
#psr Bootstrap颜色样式列表
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# paginate
#psr 分页配置每页显示10条记录
PAGINATE_BY = 10
# http cache timeout
#psr HTTP缓存超时时间
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
#psr 缓存配置,使用本地内存缓存
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
@ -230,7 +201,6 @@ CACHES = {
}
}
# 使用redis作为缓存
#psr 如果配置了Redis URL则使用Redis作为缓存后端
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
@ -239,14 +209,11 @@ if os.environ.get("DJANGO_REDIS_URL"):
}
}
#psr 站点ID配置
SITE_ID = 1
#psr 百度搜索引擎通知URL
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# Email:
#psr 邮件配置使用SMTP后端
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
@ -257,19 +224,15 @@ EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
# Setting debug=false did NOT handle except email notifications
#psr 管理员邮箱配置,用于接收错误通知
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5)
#psr 微信管理员密码两次MD5加密
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
#psr 日志路径配置
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
#psr 日志配置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@ -331,18 +294,16 @@ LOGGING = {
}
}
#psr 静态文件查找器配置
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
)
#psr 启用资源压缩
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
#psr CSS和JS压缩过滤器配置
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
@ -353,16 +314,12 @@ COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
]
#psr 媒体文件配置
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
#psr X-Frame-Options安全头配置
X_FRAME_OPTIONS = 'SAMEORIGIN'
#psr 默认自动字段配置
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#psr 如果配置了Elasticsearch主机则使用Elasticsearch作为搜索引擎
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
@ -376,7 +333,6 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
}
# Plugin System
#psr 插件系统配置,定义插件目录和激活的插件列表
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',

@ -0,0 +1,59 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
def items(self):
return ['blog:index', ]
def location(self, item):
return reverse(item)
class ArticleSiteMap(Sitemap):
changefreq = "monthly"
priority = "0.6"
def items(self):
return Article.objects.filter(status='p')
def lastmod(self, obj):
return obj.last_modify_time
class CategorySiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.6"
def items(self):
return Category.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class TagSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return Tag.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class UserSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
return obj.date_joined

@ -0,0 +1,21 @@
import logging
import requests
from django.conf import settings
logger = logging.getLogger(__name__)
class SpiderNotify():
@staticmethod
def baidu_notify(urls):
try:
data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
logger.info(result.text)
except Exception as e:
logger.error(e)
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)

@ -1,21 +1,15 @@
#psr 导入Django测试基类和项目工具函数模块
from django.test import TestCase
from djangoblog.utils import *
#psr Django博客应用的测试类用于测试项目中的工具函数
class DjangoBlogTest(TestCase):
#psr 测试初始化方法,在每个测试方法执行前运行
def setUp(self):
pass
#psr 测试工具函数功能的测试方法
def test_utils(self):
#psr 测试SHA256哈希函数验证字符串'test'的哈希值不为空
md5 = get_sha256('test')
self.assertIsNotNone(md5)
#psr 测试Markdown转换功能验证包含标题、代码块和链接的Markdown文本能正确转换
c = CommonMarkdown.get_markdown('''
# Title1
@ -30,7 +24,6 @@ class DjangoBlogTest(TestCase):
''')
self.assertIsNotNone(c)
#psr 测试字典转URL参数功能验证字典能正确转换为URL查询字符串
d = {
'd': 'key1',
'd2': 'key2'

@ -1,7 +1,6 @@
#psr Django博客项目的URL配置文件定义了整个应用的路由映射关系
"""djangoblog URL Configuration
The [urlpatterns](file://d:\\PSR_projects\\src\\PSR_pro01\\urls.py#L19-L21) list routes URLs to views. For more information please see:
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
@ -14,7 +13,6 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
#psr 导入Django配置、国际化、静态文件、站点地图等相关模块
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
@ -23,14 +21,12 @@ from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
#psr 导入自定义视图、管理站点、搜索表单和Feed类
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
#psr 定义站点地图配置包含文章、分类、标签、用户和静态页面的sitemap
sitemaps = {
'blog': ArticleSiteMap,
@ -40,43 +36,29 @@ sitemaps = {
'static': StaticViewSitemap
}
#psr 定义自定义错误处理视图
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
#psr 定义基础URL模式包含国际化URL
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
]
#psr 使用国际化模式定义主要URL路由
urlpatterns += i18n_patterns(
#psr 管理后台路由
re_path(r'^admin/', admin_site.urls),
#psr 博客应用路由
re_path(r'', include('blog.urls', namespace='blog')),
#psr Markdown编辑器路由
re_path(r'mdeditor/', include('mdeditor.urls')),
#psr 评论应用路由
re_path(r'', include('comments.urls', namespace='comment')),
#psr 账户应用路由
re_path(r'', include('accounts.urls', namespace='account')),
#psr OAuth认证路由
re_path(r'', include('oauth.urls', namespace='oauth')),
#psr 站点地图路由
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
#psr RSS Feed路由
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
#psr 搜索路由使用Elasticsearch搜索视图
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
#psr 服务器管理路由
re_path(r'', include('servermanager.urls', namespace='servermanager')),
#psr OwnTracks位置追踪路由
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
#psr 在调试模式下添加媒体文件路由
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -1,7 +1,7 @@
#!/usr/bin/env python
# encoding: utf-8
#psr导入日志、操作系统、随机数、字符串、UUID、哈希等相关标准库模块
import logging
import os
import random
@ -9,7 +9,6 @@ import string
import uuid
from hashlib import sha256
#psr导入第三方库和Django相关模块
import bleach
import markdown
import requests
@ -18,59 +17,45 @@ from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
#psr获取日志记录器实例
logger = logging.getLogger(__name__)
#psr获取最新的文章ID和评论ID
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
#psr返回最新文章和评论的主键值
return (Article.objects.latest().pk, Comment.objects.latest().pk)
#psr计算字符串的SHA256哈希值
def get_sha256(str):
#psr创建SHA256哈希对象并计算摘要
m = sha256(str.encode('utf-8'))
return m.hexdigest()
#psr缓存装饰器用于缓存函数执行结果
def cache_decorator(expiration=3 * 60):
#psr装饰器包装函数
def wrapper(func):
#psr新的函数实现缓存逻辑
def news(*args, **kwargs):
try:
#psr尝试从视图获取缓存键
view = args[0]
key = view.get_cache_key()
except:
key = None
#psr如果无法获取缓存键则生成唯一键
if not key:
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
#psr从缓存中获取值
value = cache.get(key)
if value is not None:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
#psr处理默认缓存值
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
#psr缓存未命中执行函数并设置缓存
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
#psr根据函数返回值设置缓存
if value is None:
cache.set(key, '__default_cache_value__', expiration)
else:
@ -82,7 +67,6 @@ def cache_decorator(expiration=3 * 60):
return wrapper
#psr刷新视图缓存
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
@ -92,16 +76,13 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
:param key_prefix:前缀
:return:是否成功
'''
#psr导入Django HTTP请求和缓存相关模块
from django.http import HttpRequest
from django.utils.cache import get_cache_key
#psr创建HTTP请求对象并设置相关属性
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
#psr获取缓存键并删除对应缓存
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
@ -111,20 +92,15 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
return False
#psr使用缓存装饰器缓存当前站点信息
@cache_decorator()
def get_current_site():
#psr获取当前站点对象
site = Site.objects.get_current()
return site
#psrMarkdown处理工具类
class CommonMarkdown:
#psr静态方法转换Markdown文本为HTML并生成目录
@staticmethod
def _convert_markdown(value):
#psr创建Markdown处理器并配置扩展
md = markdown.Markdown(
extensions=[
'extra',
@ -133,30 +109,23 @@ class CommonMarkdown:
'tables',
]
)
#psr转换Markdown并获取正文和目录
body = md.convert(value)
toc = md.toc
return body, toc
#psr静态方法获取带目录的Markdown HTML
@staticmethod
def get_markdown_with_toc(value):
#psr调用转换方法获取正文和目录
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
#psr静态方法获取不带目录的Markdown HTML
@staticmethod
def get_markdown(value):
#psr调用转换方法只返回正文
body, toc = CommonMarkdown._convert_markdown(value)
return body
#psr发送邮件函数
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
#psr发送邮件信号
send_email_signal.send(
send_email.__class__,
emailto=emailto,
@ -164,33 +133,25 @@ def send_email(emailto, title, content):
content=content)
#psr生成6位随机数字验证码
def generate_code() -> str:
"""生成随机数验证码"""
#psr从数字中随机采样6个字符组成验证码
return ''.join(random.sample(string.digits, 6))
#psr将字典转换为URL查询字符串
def parse_dict_to_url(dict):
from urllib.parse import quote
#psr对字典键值对进行URL编码并拼接
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
#psr获取博客设置信息
def get_blog_setting():
#psr先从缓存中获取设置
value = cache.get('get_blog_setting')
if value:
return value
else:
#psr缓存未命中则从数据库获取或创建默认设置
from blog.models import BlogSettings
if not BlogSettings.objects.count():
#psr创建默认博客设置
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
@ -206,14 +167,12 @@ def get_blog_setting():
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
#psr获取第一个设置对象并缓存
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
return value
#psr保存用户头像到本地
def save_user_avatar(url):
'''
保存用户头像
@ -223,54 +182,40 @@ def save_user_avatar(url):
logger.info(url)
try:
#psr设置头像保存基础目录
basedir = os.path.join(settings.STATICFILES, 'avatar')
#psr下载头像图片
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
#psr创建目录如果不存在
if not os.path.exists(basedir):
os.makedirs(basedir)
#psr判断是否为图片文件并获取扩展名
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
#psr生成唯一文件名并保存文件
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
#psr返回静态文件路径
return static('avatar/' + save_filename)
except Exception as e:
#psr处理异常并返回默认头像
logger.error(e)
return static('blog/img/avatar.png')
#psr删除侧边栏缓存
def delete_sidebar_cache():
from blog.models import LinkShowType
#psr生成侧边栏缓存键列表
keys = ["sidebar" + x for x in LinkShowType.values]
#psr遍历删除每个缓存键
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
#psr删除模板片段缓存
def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key
#psr生成模板片段缓存键并删除
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
#psr获取资源URL路径
def get_resource_url():
#psr优先使用配置的静态URL否则根据当前站点生成
if settings.STATIC_URL:
return settings.STATIC_URL
else:
@ -278,14 +223,10 @@ def get_resource_url():
return 'http://' + site.domain + '/static/'
#psr定义允许的HTML标签
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
#psr定义允许的HTML属性
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
#psr清理HTML内容只保留允许的标签和属性
def sanitize_html(html):
#psr使用bleach库清理HTML
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -1,9 +1,7 @@
# encoding: utf-8
# Handle minimum requirement.
from __future__ import absolute_import, division, print_function, unicode_literals
# Handle minimum requirement.
import json
import os
import re
@ -11,7 +9,6 @@ import shutil
import threading
import warnings
# Handle minimum requirement.
import six
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
@ -37,7 +34,6 @@ from whoosh.qparser import QueryParser
from whoosh.searching import ResultsPage
from whoosh.writing import AsyncWriter
#psr:尝试导入whoosh库如果失败则抛出依赖缺失异常
try:
import whoosh
except ImportError:
@ -45,33 +41,29 @@ except ImportError:
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
# Handle minimum requirement.
#psr检查whoosh版本要求必须大于等于2.5.0
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
raise MissingDependency(
"The 'whoosh' backend requires version 2.5.0 or greater.")
# Bubble up the correct error.
#psr定义日期时间正则表达式模式
DATETIME_REGEX = re.compile(
'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$')
#psr创建线程本地存储对象
LOCALS = threading.local()
LOCALS.RAM_STORE = None
#psr自定义HTML格式化器类用于高亮显示搜索结果
class WhooshHtmlFormatter(HtmlFormatter):
"""
This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
We use it to have consistent results across backends. Specifically,
Solr, Xapian and Elasticsearch are using this formatting.
"""
#psr定义HTML模板格式
template = '<%(tag)s>%(t)s</%(tag)s>'
#psr定义Whoosh搜索后端类继承自BaseSearchBackend
class WhooshSearchBackend(BaseSearchBackend):
# Word reserved by Whoosh for special use.
#psr定义Whoosh保留关键字
RESERVED_WORDS = (
'AND',
'NOT',
@ -82,45 +74,35 @@ class WhooshSearchBackend(BaseSearchBackend):
# Characters reserved by Whoosh for special use.
# The '\\' must come first, so as not to overwrite the other slash
# replacements.
#psr定义Whoosh保留字符
RESERVED_CHARACTERS = (
'\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
'[', ']', '^', '"', '~', '*', '?', ':', '.',
)
#psr初始化Whoosh搜索后端
def __init__(self, connection_alias, **connection_options):
super(
WhooshSearchBackend,
self).__init__(
connection_alias,
**connection_options)
#psr标记设置是否完成
self.setup_complete = False
#psr是否使用文件存储
self.use_file_storage = True
#psrPOST请求限制大小
self.post_limit = getattr(
connection_options,
'POST_LIMIT',
128 * 1024 * 1024)
#psr索引文件路径
self.path = connection_options.get('PATH')
#psr根据配置决定存储类型
if connection_options.get('STORAGE', 'file') != 'file':
self.use_file_storage = False
#psr验证文件存储配置
if self.use_file_storage and not self.path:
raise ImproperlyConfigured(
"You must specify a 'PATH' in your settings for connection '%s'." %
connection_alias)
#psr获取日志记录器
self.log = logging.getLogger('haystack')
#psr设置Whoosh索引环境
def setup(self):
"""
Defers loading until needed.
@ -129,18 +111,15 @@ class WhooshSearchBackend(BaseSearchBackend):
new_index = False
# Make sure the index is there.
#psr创建索引目录如果不存在
if self.use_file_storage and not os.path.exists(self.path):
os.makedirs(self.path)
new_index = True
#psr检查索引目录写入权限
if self.use_file_storage and not os.access(self.path, os.W_OK):
raise IOError(
"The path to your Whoosh index '%s' is not writable for the current user/group." %
self.path)
#psr根据配置选择存储方式
if self.use_file_storage:
self.storage = FileStorage(self.path)
else:
@ -151,12 +130,10 @@ class WhooshSearchBackend(BaseSearchBackend):
self.storage = LOCALS.RAM_STORE
#psr构建索引模式和解析器
self.content_field_name, self.schema = self.build_schema(
connections[self.connection_alias].get_unified_index().all_searchfields())
self.parser = QueryParser(self.content_field_name, schema=self.schema)
#psr创建或打开索引
if new_index is True:
self.index = self.storage.create_index(self.schema)
else:
@ -165,12 +142,9 @@ class WhooshSearchBackend(BaseSearchBackend):
except index.EmptyIndexError:
self.index = self.storage.create_index(self.schema)
#psr标记设置完成
self.setup_complete = True
#psr构建Whoosh索引模式
def build_schema(self, fields):
#psr定义基本模式字段
schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True),
DJANGO_CT: WHOOSH_ID(stored=True),
@ -178,14 +152,11 @@ class WhooshSearchBackend(BaseSearchBackend):
}
# Grab the number of keys that are hard-coded into Haystack.
# We'll use this to (possibly) fail slightly more gracefully later.
#psr获取初始键数量
initial_key_count = len(schema_fields)
content_field_name = ''
#psr根据字段类型构建相应的模式字段
for field_name, field_class in fields.items():
if field_class.is_multivalued:
#psr处理多值字段
if field_class.indexed is False:
schema_fields[field_class.index_fieldname] = IDLIST(
stored=True, field_boost=field_class.boost)
@ -193,62 +164,48 @@ class WhooshSearchBackend(BaseSearchBackend):
schema_fields[field_class.index_fieldname] = KEYWORD(
stored=True, commas=True, scorable=True, field_boost=field_class.boost)
elif field_class.field_type in ['date', 'datetime']:
#psr处理日期时间字段
schema_fields[field_class.index_fieldname] = DATETIME(
stored=field_class.stored, sortable=True)
elif field_class.field_type == 'integer':
#psr处理整数字段
schema_fields[field_class.index_fieldname] = NUMERIC(
stored=field_class.stored, numtype=int, field_boost=field_class.boost)
elif field_class.field_type == 'float':
#psr处理浮点数字段
schema_fields[field_class.index_fieldname] = NUMERIC(
stored=field_class.stored, numtype=float, field_boost=field_class.boost)
elif field_class.field_type == 'boolean':
#psr处理布尔字段
# Field boost isn't supported on BOOLEAN as of 1.8.2.
schema_fields[field_class.index_fieldname] = BOOLEAN(
stored=field_class.stored)
elif field_class.field_type == 'ngram':
#psr处理ngram字段
schema_fields[field_class.index_fieldname] = NGRAM(
minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
elif field_class.field_type == 'edge_ngram':
#psr处理边缘ngram字段
schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start',
stored=field_class.stored,
field_boost=field_class.boost)
else:
#psr处理文本字段使用中文分析器
# schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True)
schema_fields[field_class.index_fieldname] = TEXT(
stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
#psr处理文档字段
if field_class.document is True:
content_field_name = field_class.index_fieldname
schema_fields[field_class.index_fieldname].spelling = True
# Fail more gracefully than relying on the backend to die if no fields
# are found.
#psr验证是否有字段被添加到模式中
if len(schema_fields) <= initial_key_count:
raise SearchBackendError(
"No fields were found in any search_indexes. Please correct this before attempting to search.")
#psr返回内容字段名和构建好的模式
return (content_field_name, Schema(**schema_fields))
#psr更新索引中的文档
def update(self, index, iterable, commit=True):
#psr如果设置未完成则先进行设置
if not self.setup_complete:
self.setup()
#psr刷新索引并创建异步写入器
self.index = self.index.refresh()
writer = AsyncWriter(self.index)
#psr遍历可迭代对象准备并更新文档
for obj in iterable:
try:
doc = index.full_prepare(obj)
@ -257,27 +214,22 @@ class WhooshSearchBackend(BaseSearchBackend):
else:
# Really make sure it's unicode, because Whoosh won't have it any
# other way.
#psr确保文档内容都是unicode格式
for key in doc:
doc[key] = self._from_python(doc[key])
# Document boosts aren't supported in Whoosh 2.5.0+.
#psr删除文档提升字段Whoosh 2.5.0+不支持)
if 'boost' in doc:
del doc['boost']
try:
#psr更新文档
writer.update_document(**doc)
except Exception as e:
#psr处理异常情况
if not self.silently_fail:
raise
# We'll log the object identifier but won't include the actual object
# to avoid the possibility of that generating encoding errors while
# processing the log message:
#psr记录错误日志
self.log.error(
u"%s while preparing object for update" %
e.__class__.__name__,
@ -287,54 +239,43 @@ class WhooshSearchBackend(BaseSearchBackend):
"index": index,
"object": get_identifier(obj)}})
#psr如果有数据则提交更改
if len(iterable) > 0:
# For now, commit no matter what, as we run into locking issues
# otherwise.
writer.commit()
#psr从索引中移除对象
def remove(self, obj_or_string, commit=True):
#psr如果设置未完成则先进行设置
if not self.setup_complete:
self.setup()
#psr刷新索引并获取对象标识符
self.index = self.index.refresh()
whoosh_id = get_identifier(obj_or_string)
try:
#psr通过查询删除文档
self.index.delete_by_query(
q=self.parser.parse(
u'%s:"%s"' % (ID, whoosh_id)))
u'%s:"%s"' %
(ID, whoosh_id)))
except Exception as e:
#psr处理异常情况
if not self.silently_fail:
raise
#psr记录错误日志
self.log.error(
"Failed to remove document '%s' from Whoosh: %s",
whoosh_id,
e,
exc_info=True)
#psr清空索引
def clear(self, models=None, commit=True):
#psr如果设置未完成则先进行设置
if not self.setup_complete:
self.setup()
#psr刷新索引
self.index = self.index.refresh()
#psr验证模型参数
if models is not None:
assert isinstance(models, (list, tuple))
try:
#psr根据参数决定是删除整个索引还是特定模型
if models is None:
self.delete_index()
else:
@ -342,17 +283,16 @@ class WhooshSearchBackend(BaseSearchBackend):
for model in models:
models_to_delete.append(
u"%s:%s" % (DJANGO_CT, get_model_ct(model)))
u"%s:%s" %
(DJANGO_CT, get_model_ct(model)))
self.index.delete_by_query(
q=self.parser.parse(
u" OR ".join(models_to_delete)))
except Exception as e:
#psr处理异常情况
if not self.silently_fail:
raise
#psr记录错误日志
if models is not None:
self.log.error(
"Failed to clear Whoosh index of models '%s': %s",
@ -363,34 +303,31 @@ class WhooshSearchBackend(BaseSearchBackend):
self.log.error(
"Failed to clear Whoosh index: %s", e, exc_info=True)
#psr删除整个索引
def delete_index(self):
#psr根据存储类型删除索引文件
# Per the Whoosh mailing list, if wiping out everything from the index,
# it's much more efficient to simply delete the index files.
if self.use_file_storage and os.path.exists(self.path):
shutil.rmtree(self.path)
elif not self.use_file_storage:
self.storage.clean()
#psr重新创建索引环境
# Recreate everything.
self.setup()
#psr优化索引
def optimize(self):
#psr如果设置未完成则先进行设置
if not self.setup_complete:
self.setup()
#psr刷新索引并执行优化
self.index = self.index.refresh()
self.index.optimize()
#psr计算分页参数
def calculate_page(self, start_offset=0, end_offset=None):
#psr防止Whoosh抛出错误确保结束偏移量大于0
# Prevent against Whoosh throwing an error. Requires an end_offset
# greater than 0.
if end_offset is not None and end_offset <= 0:
end_offset = 1
#psr确定页码
# Determine the page.
page_num = 0
if end_offset is None:
@ -404,11 +341,10 @@ class WhooshSearchBackend(BaseSearchBackend):
if page_length and page_length > 0:
page_num = int(start_offset / page_length)
#psr增加页码因为Whoosh使用1基页码
# Increment because Whoosh uses 1-based page numbers.
page_num += 1
return page_num, page_length
#psr执行搜索查询
@log_query
def search(
self,
@ -430,11 +366,10 @@ class WhooshSearchBackend(BaseSearchBackend):
limit_to_registered_models=None,
result_class=None,
**kwargs):
#psr如果设置未完成则先进行设置
if not self.setup_complete:
self.setup()
#psr处理零长度查询
# A zero length query should return no results.
if len(query_string) == 0:
return {
'results': [],
@ -443,7 +378,8 @@ class WhooshSearchBackend(BaseSearchBackend):
query_string = force_str(query_string)
#psr处理单字符查询
# A one-character query (non-wildcard) gets nabbed by a stopwords
# filter and should yield zero results.
if len(query_string) <= 1 and query_string != u'*':
return {
'results': [],
@ -452,7 +388,6 @@ class WhooshSearchBackend(BaseSearchBackend):
reverse = False
#psr处理排序参数
if sort_by is not None:
# Determine if we need to reverse the results and if Whoosh can
# handle what it's being asked to sort by. Reversing is an
@ -482,7 +417,6 @@ class WhooshSearchBackend(BaseSearchBackend):
sort_by = sort_by_list[0]
#psr处理分面搜索警告
if facets is not None:
warnings.warn(
"Whoosh does not handle faceting.",
@ -504,7 +438,6 @@ class WhooshSearchBackend(BaseSearchBackend):
narrowed_results = None
self.index = self.index.refresh()
#psr处理模型限制参数
if limit_to_registered_models is None:
limit_to_registered_models = getattr(
settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
@ -518,7 +451,6 @@ class WhooshSearchBackend(BaseSearchBackend):
else:
model_choices = []
#psr构建模型选择查询
if len(model_choices) > 0:
if narrow_queries is None:
narrow_queries = set()
@ -528,7 +460,6 @@ class WhooshSearchBackend(BaseSearchBackend):
narrow_searcher = None
#psr处理窄化查询
if narrow_queries is not None:
# Potentially expensive? I don't see another way to do it in
# Whoosh...
@ -551,7 +482,6 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index = self.index.refresh()
#psr执行实际搜索
if self.index.doc_count():
searcher = self.index.searcher()
parsed_query = self.parser.parse(query_string)
@ -566,7 +496,6 @@ class WhooshSearchBackend(BaseSearchBackend):
page_num, page_length = self.calculate_page(
start_offset, end_offset)
#psr设置搜索参数
search_kwargs = {
'pagelen': page_length,
'sortedby': sort_by,
@ -578,14 +507,12 @@ class WhooshSearchBackend(BaseSearchBackend):
search_kwargs['filter'] = narrowed_results
try:
#psr执行搜索页面查询
raw_page = searcher.search_page(
parsed_query,
page_num,
**search_kwargs
)
except ValueError:
#psr处理数值错误异常
if not self.silently_fail:
raise
@ -604,7 +531,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'spelling_suggestion': None,
}
#psr处理搜索结果
results = self._process_results(
raw_page,
highlight=highlight,
@ -618,7 +544,6 @@ class WhooshSearchBackend(BaseSearchBackend):
return results
else:
#psr处理空索引情况
if self.include_spelling:
if spelling_query:
spelling_suggestion = self.create_spelling_suggestion(
@ -635,7 +560,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
#psr相似文档搜索
def more_like_this(
self,
model_instance,
@ -646,7 +570,6 @@ class WhooshSearchBackend(BaseSearchBackend):
limit_to_registered_models=None,
result_class=None,
**kwargs):
#psr如果设置未完成则先进行设置
if not self.setup_complete:
self.setup()
@ -659,7 +582,6 @@ class WhooshSearchBackend(BaseSearchBackend):
narrowed_results = None
self.index = self.index.refresh()
#psr处理模型限制参数
if limit_to_registered_models is None:
limit_to_registered_models = getattr(
settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
@ -673,7 +595,6 @@ class WhooshSearchBackend(BaseSearchBackend):
else:
model_choices = []
#psr构建模型选择查询
if len(model_choices) > 0:
if narrow_queries is None:
narrow_queries = set()
@ -681,13 +602,11 @@ class WhooshSearchBackend(BaseSearchBackend):
narrow_queries.add(' OR '.join(
['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
#psr添加附加查询字符串
if additional_query_string and additional_query_string != '*':
narrow_queries.add(additional_query_string)
narrow_searcher = None
#psr处理窄化查询
if narrow_queries is not None:
# Potentially expensive? I don't see another way to do it in
# Whoosh...
@ -708,13 +627,11 @@ class WhooshSearchBackend(BaseSearchBackend):
else:
narrowed_results = recent_narrowed_results
#psr计算分页参数
page_num, page_length = self.calculate_page(start_offset, end_offset)
self.index = self.index.refresh()
raw_results = EmptyResults()
#psr执行相似文档搜索
if self.index.doc_count():
query = "%s:%s" % (ID, get_identifier(model_instance))
searcher = self.index.searcher()
@ -730,10 +647,8 @@ class WhooshSearchBackend(BaseSearchBackend):
raw_results.filter(narrowed_results)
try:
#psr创建结果页面
raw_page = ResultsPage(raw_results, page_num, page_length)
except ValueError:
#psr处理数值错误异常
if not self.silently_fail:
raise
@ -752,7 +667,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'spelling_suggestion': None,
}
#psr处理搜索结果
results = self._process_results(raw_page, result_class=result_class)
searcher.close()
@ -761,7 +675,6 @@ class WhooshSearchBackend(BaseSearchBackend):
return results
#psr处理搜索结果
def _process_results(
self,
raw_page,
@ -776,7 +689,6 @@ class WhooshSearchBackend(BaseSearchBackend):
# can cause pagination failures.
hits = len(raw_page)
#psr设置结果类
if result_class is None:
result_class = SearchResult
@ -785,7 +697,6 @@ class WhooshSearchBackend(BaseSearchBackend):
unified_index = connections[self.connection_alias].get_unified_index()
indexed_models = unified_index.get_indexed_models()
#psr遍历原始结果处理每条记录
for doc_offset, raw_result in enumerate(raw_page):
score = raw_page.score(doc_offset) or 0
app_label, model_name = raw_result[DJANGO_CT].split('.')
@ -793,7 +704,6 @@ class WhooshSearchBackend(BaseSearchBackend):
model = haystack_get_model(app_label, model_name)
if model and model in indexed_models:
#psr处理字段数据
for key, value in raw_result.items():
index = unified_index.get_index(model)
string_key = str(key)
@ -813,11 +723,9 @@ class WhooshSearchBackend(BaseSearchBackend):
else:
additional_fields[string_key] = self._to_python(value)
#psr删除不需要的字段
del (additional_fields[DJANGO_CT])
del (additional_fields[DJANGO_ID])
#psr处理高亮显示
if highlight:
sa = StemmingAnalyzer()
formatter = WhooshHtmlFormatter('em')
@ -834,7 +742,6 @@ class WhooshSearchBackend(BaseSearchBackend):
self.content_field_name: [whoosh_result],
}
#psr创建搜索结果对象
result = result_class(
app_label,
model_name,
@ -845,7 +752,6 @@ class WhooshSearchBackend(BaseSearchBackend):
else:
hits -= 1
#psr处理拼写建议
if self.include_spelling:
if spelling_query:
spelling_suggestion = self.create_spelling_suggestion(
@ -854,7 +760,6 @@ class WhooshSearchBackend(BaseSearchBackend):
spelling_suggestion = self.create_spelling_suggestion(
query_string)
#psr返回处理后的结果
return {
'results': results,
'hits': hits,
@ -862,7 +767,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
#psr创建拼写建议
def create_spelling_suggestion(self, query_string):
spelling_suggestion = None
reader = self.index.reader()
@ -883,7 +787,6 @@ class WhooshSearchBackend(BaseSearchBackend):
query_words = cleaned_query.split()
suggested_words = []
#psr为每个查询词生成建议
for word in query_words:
suggestions = corrector.suggest(word, limit=1)
@ -893,7 +796,6 @@ class WhooshSearchBackend(BaseSearchBackend):
spelling_suggestion = ' '.join(suggested_words)
return spelling_suggestion
#psr将Python值转换为Whoosh字符串
def _from_python(self, value):
"""
Converts Python values to a string for Whoosh.
@ -917,7 +819,6 @@ class WhooshSearchBackend(BaseSearchBackend):
value = force_str(value)
return value
#psr将Whoosh值转换为Python原生值
def _to_python(self, value):
"""
Converts values from Whoosh to native Python values.
@ -969,16 +870,13 @@ class WhooshSearchBackend(BaseSearchBackend):
return value
#psr定义Whoosh搜索查询类继承自BaseSearchQuery
class WhooshSearchQuery(BaseSearchQuery):
#psr转换日期时间格式
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
#psr清理查询片段
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
@ -1004,13 +902,11 @@ class WhooshSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
#psr构建查询片段
def build_query_fragment(self, field, filter_type, value):
from haystack import connections
query_frag = ''
is_datetime = False
#psr处理不同类型的值
if not hasattr(value, 'input_type_name'):
# Handle when we've got a ``ValuesListQuerySet``...
if hasattr(value, 'values_list'):
@ -1040,7 +936,6 @@ class WhooshSearchQuery(BaseSearchQuery):
index_fieldname = u'%s:' % connections[self._using].get_unified_index(
).get_index_fieldname(field)
#psr定义过滤类型映射
filter_types = {
'content': '%s',
'contains': '*%s*',
@ -1054,7 +949,6 @@ class WhooshSearchQuery(BaseSearchQuery):
'fuzzy': u'%s~',
}
#psr根据不同过滤类型构建查询片段
if value.post_process is False:
query_frag = prepared_value
else:
@ -1132,15 +1026,19 @@ class WhooshSearchQuery(BaseSearchQuery):
query_frag = filter_types[filter_type] % prepared_value
#psr格式化查询片段
if len(query_frag) and not isinstance(value, Raw):
if not query_frag.startswith('(') and not query_frag.endswith(')'):
query_frag = "(%s)" % query_frag
return u"%s%s" % (index_fieldname, query_frag)
#psr定义Whoosh搜索引擎类继承自BaseEngine
# if not filter_type in ('in', 'range'):
# # 'in' is a bit of a special case, as we don't want to
# # convert a valid list/tuple to string. Defer handling it
# # until later...
# value = self.backend._from_python(value)
class WhooshEngine(BaseEngine):
#psr指定后端和查询类
backend = WhooshSearchBackend
query = WhooshSearchQuery

@ -1,11 +1,8 @@
"""
#psrWSGI配置文件用于部署Django应用程序
WSGI config for djangoblog project.
#psr该文件将WSGI可调用对象暴露为模块级变量application
It exposes the WSGI callable as a module-level variable named ``application``.
#psr更多信息参考Django官方文档中的部署指南
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
@ -14,8 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
#psr设置默认的Django设置模块环境变量
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
#psr获取WSGI应用程序实例用于处理HTTP请求
application = get_wsgi_application()

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save