updata fifth

lsx 3 months ago
parent 09b3357510
commit 784b1424df

Binary file not shown.

@ -1,4 +1,4 @@
# 用户账户相关的表单定义
# flj用户账户相关的表单定义
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
@ -13,10 +13,10 @@ class LoginForm(AuthenticationForm):
"""用户登录表单"""
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
# 为用户名输入框添加样式和占位符
# flj为用户名输入框添加样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 为密码输入框添加样式和占位符
# flj为密码输入框添加样式和占位符
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
@ -26,7 +26,7 @@ class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
# 为所有输入框添加Bootstrap样式和占位符
# flj为所有输入框添加Bootstrap样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
@ -44,13 +44,13 @@ class RegisterForm(UserCreationForm):
return email
class Meta:
model = get_user_model() # 使用自定义用户模型
fields = ("username", "email") # 只包含用户名和邮箱字段
model = get_user_model() # flj使用自定义用户模型
fields = ("username", "email") # flj只包含用户名和邮箱字段
class ForgetPasswordForm(forms.Form):
"""忘记密码重置表单"""
# 新密码字段
# flj新密码字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -61,7 +61,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 确认新密码字段
# flj确认新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -72,7 +72,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 邮箱字段
# flj邮箱字段
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -83,7 +83,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 验证码字段
# flj验证码字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -128,6 +128,7 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form):
"""发送忘记密码验证码的表单"""
# flj邮箱字段用于发送验证码
email = forms.EmailField(
label=_('Email'), # 邮箱字段,用于发送验证码
label=_('Email'), # flj邮箱字段,用于发送验证码
)

@ -0,0 +1,76 @@
# Generated by Django 5.2.4 on 2025-11-21 17:04
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_alter_bloguser_options_remove_bloguser_created_time_and_more'),
]
operations = [
migrations.AddField(
model_name='bloguser',
name='avatar',
field=models.ImageField(blank=True, null=True, upload_to='avatars/', verbose_name='avatar'),
),
migrations.AddField(
model_name='bloguser',
name='bio',
field=models.TextField(blank=True, max_length=500, verbose_name='biography'),
),
migrations.AddField(
model_name='bloguser',
name='birth_date',
field=models.DateField(blank=True, null=True, verbose_name='birth date'),
),
migrations.AddField(
model_name='bloguser',
name='location',
field=models.CharField(blank=True, max_length=100, verbose_name='location'),
),
migrations.AddField(
model_name='bloguser',
name='website',
field=models.URLField(blank=True, verbose_name='website'),
),
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('notification_type', models.CharField(choices=[('comment', 'comment notification'), ('like', 'like notification'), ('follow', 'follow notification'), ('system', 'system notification')], max_length=20, verbose_name='notification type')),
('message', models.TextField(verbose_name='message')),
('target_url', models.URLField(blank=True, verbose_name='target url')),
('target_content', models.CharField(blank=True, max_length=200, verbose_name='target content')),
('is_read', models.BooleanField(default=False, verbose_name='is read')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='recipient')),
('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_notifications', to=settings.AUTH_USER_MODEL, verbose_name='sender')),
],
options={
'verbose_name': 'notification',
'verbose_name_plural': 'notifications',
'ordering': ['-created_time'],
},
),
migrations.CreateModel(
name='UserFollowing',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('is_confirmed', models.BooleanField(default=True, verbose_name='is confirmed')),
('following_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='followers', to=settings.AUTH_USER_MODEL, verbose_name='followed user')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following', to=settings.AUTH_USER_MODEL, verbose_name='follower user')),
],
options={
'verbose_name': 'user following',
'verbose_name_plural': 'user followings',
'ordering': ['-created_time'],
'unique_together': {('user', 'following_user')},
},
),
]

@ -19,6 +19,8 @@ class BlogUser(AbstractUser):
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#zxm 来源
source = models.CharField(_('create source'), max_length=100, blank=True)
#zxm 基础用户字段,移除了额外的个人资料字段
#zxm 获取用户详情页的url
def get_absolute_url(self):
@ -36,9 +38,17 @@ class BlogUser(AbstractUser):
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id'] #zxm 按ID倒序排列
verbose_name = _('user') #zxm 在管理后台显示的名称
verbose_name_plural = verbose_name #zxm 复数形式
get_latest_by = 'id' #zxm 获取最新记录的依据
# 已移除Notification模型不再需要通知功能

@ -13,83 +13,110 @@ from . import utils
class AccountTest(TestCase):
def setUp(self):
#flj 初始化测试客户端和请求工厂
self.client = Client()
self.factory = RequestFactory()
#flj 创建测试普通用户
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
#flj 测试用新密码
self.new_test = "xxx123--="
def test_validate_account(self):
#flj 获取当前站点域名
site = get_current_site().domain
#flj 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
#flj 查询创建的超级用户
testuser = BlogUser.objects.get(username='liangliangyy1')
#flj 测试超级用户登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
#flj 断言登录成功
self.assertEqual(loginresult, True)
#flj 访问管理员页面
response = self.client.get('/admin/')
#flj 断言管理员页面访问成功状态码200
self.assertEqual(response.status_code, 200)
#flj 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#flj 创建测试文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' #flj 文章类型(假设'a'代表普通文章)
article.status = 'p' #flj 文章状态(假设'p'代表已发布)
article.save()
#flj 访问文章的管理员编辑页面
response = self.client.get(article.get_admin_url())
#flj 断言管理员编辑页面访问成功
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
#flj 断言初始状态下该邮箱的用户不存在
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
#flj 发送用户注册请求
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',
})
#flj 断言注册后该邮箱的用户已存在
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
#flj 获取注册后的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
#flj 生成邮箱验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
#flj 构建邮箱验证URL
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
#flj 访问邮箱验证URL
response = self.client.get(url)
#flj 断言验证页面访问成功
self.assertEqual(response.status_code, 200)
#flj 使用注册账号登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
#flj 将该用户提升为超级用户和工作人员
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
#flj 删除侧边栏缓存
delete_sidebar_cache()
#flj 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#flj 创建测试文章
article = Article()
article.category = category
article.title = "nicetitle333"
@ -100,52 +127,67 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
#flj 访问文章管理员页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#flj 测试退出登录
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
#flj 退出后访问文章管理员页面(应重定向或拒绝访问)
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
#flj 使用错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
#flj 错误密码登录后访问文章管理员页面(应无法访问)
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
#flj 测试邮箱
to_email = "admin@admin.com"
#flj 生成邮箱验证码
code = generate_code()
#flj 存储验证码
utils.set_code(to_email, code)
#flj 发送验证邮件
utils.send_verify_email(to_email, code)
#flj 验证正确的邮箱和验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
#flj 验证错误的邮箱和正确的验证码
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
#flj 发送忘记密码的验证码请求(正确邮箱)
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
#flj 断言请求成功且返回"ok"
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
#flj 发送忘记密码的验证码请求(无邮箱参数)
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
#flj 发送忘记密码的验证码请求(无效邮箱格式)
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
@ -153,21 +195,25 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
#flj 生成验证码并存储
code = generate_code()
utils.set_code(self.blog_user.email, code)
#flj 构造忘记密码重置数据(正确验证码和匹配密码)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
#flj 发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#flj 断言重置成功重定向状态码302
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
#flj 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
@ -175,33 +221,38 @@ class AccountTest(TestCase):
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
#flj 构造忘记密码重置数据(不存在的邮箱)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
#flj 发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#flj 断言请求失败状态码200未重定向
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
#flj 生成验证码并存储
code = generate_code()
utils.set_code(self.blog_user.email, code)
#flj 构造忘记密码重置数据(错误验证码)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
)
#flj 发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
#flj 断言请求失败状态码200未重定向
self.assertEqual(resp.status_code, 200)

@ -1,4 +1,4 @@
# 用户账户应用的URL配置文件
# flj用户账户应用的URL配置文件
from django.urls import path
from django.urls import re_path
@ -8,34 +8,47 @@ from .forms import LoginForm
app_name = "accounts" # 应用命名空间
urlpatterns = [
# 用户登录
# flj用户登录
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}), # 登录页面
# 用户注册
# flj用户注册
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'), # 注册页面
# 用户登出
# flj用户登出
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'), # 登出页面
# 账户操作结果页面
# flj账户操作结果页面
path(r'account/result.html',
views.account_result,
name='result'), # 注册/验证结果页面
# 忘记密码
# flj忘记密码
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'), # 忘记密码页面
# 忘记密码验证码
# flj忘记密码验证码
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'), # 发送验证码接口
# flj用户收藏
path(
'favorites/',
views.FavoriteListView.as_view(),
name='favorites'), # 我的收藏
# flj用户点赞列表
re_path(r'^profile/(?P<username>\w+)/likes/$',
views.UserLikesView.as_view(),
name='user_likes'), # 用户点赞列表
]

@ -8,7 +8,8 @@ from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect, HttpResponseForbidden, JsonResponse
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
@ -20,12 +21,13 @@ from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from django.views.generic import FormView, RedirectView, ListView, DetailView
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
from blog.models import ArticleFavorite, Article
logger = logging.getLogger(__name__)
@ -124,18 +126,17 @@ class LoginView(FormView):
#zxm 表单验证成功后的处理
def form_valid(self, form):
form = AuthenticationForm(data=self.request.POST, request=self.request)
# 使用已验证的表单,不再重新创建
delete_sidebar_cache() #zxm 删除侧边栏缓存
logger.info(self.redirect_field_name)
if form.is_valid():
delete_sidebar_cache() #zxm 删除侧边栏缓存
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user()) #zxm 登录用户
if self.request.POST.get("remember"): #zxm 如果勾选记住我
self.request.session.set_expiry(self.login_ttl) #zxm 设置会话过期时间
return super(LoginView, self).form_valid(form)
auth.login(self.request, form.get_user()) #zxm 登录用户
if self.request.POST.get("remember"): #zxm 如果勾选记住我
self.request.session.set_expiry(self.login_ttl) #zxm 设置会话过期时间
return super(LoginView, self).form_valid(form)
else:
return self.render_to_response({
def form_invalid(self, form):
return self.render_to_response({
'form': form
})
@ -151,11 +152,11 @@ class LoginView(FormView):
#xy 账户结果处理函数
def account_result(request):
type = request.GET.get('type') #zxm 获取类型参数
id = request.GET.get('id') #zxm 获取用户ID
result_type = request.GET.get('type') #zxm 获取类型参数
user_id = request.GET.get('id') #zxm 获取用户ID
user = get_object_or_404(get_user_model(), id=id) #zxm 获取用户对象
logger.info(type)
user = get_object_or_404(get_user_model(), id=user_id) #zxm 获取用户对象
logger.info(result_type)
if user.is_active: #zxm 如果用户已激活
return HttpResponseRedirect('/') #zxm 重定向到首页
if type and type in ['register', 'validation']: #zxm 处理注册或验证类型
@ -214,3 +215,55 @@ class ForgetPasswordEmailCode(View):
utils.set_code(to_email, code) #zxm 保存验证码
return HttpResponse("ok") #zxm 返回成功信息
class FavoriteListView(LoginRequiredMixin, ListView):
"""展示当前用户收藏的文章"""
template_name = 'account/favorites.html'
context_object_name = 'favorites'
paginate_by = 10
def get_queryset(self):
return (
ArticleFavorite.objects.select_related('article')
.filter(user=self.request.user)
.order_by('-creation_time')
)
class UserFavoritesView(LoginRequiredMixin, ListView):
"""用户收藏文章列表"""
template_name = 'account/user_favorites.html'
context_object_name = 'favorites'
paginate_by = 10
def get_queryset(self):
username = self.kwargs.get('username')
user = get_object_or_404(BlogUser, username=username)
return ArticleFavorite.objects.filter(user=user).order_by('-creation_time')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['profile_user'] = get_object_or_404(BlogUser, username=self.kwargs.get('username'))
return context
class UserLikesView(LoginRequiredMixin, ListView):
"""用户点赞文章列表"""
template_name = 'account/user_likes.html'
context_object_name = 'liked_articles'
paginate_by = 10
def get_queryset(self):
username = self.kwargs.get('username')
user = get_object_or_404(BlogUser, username=username)
# 查询用户点赞的文章,并按点赞时间倒序排列
return Article.objects.filter(likes=user).order_by('-article_likes__created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['profile_user'] = get_object_or_404(BlogUser, username=self.kwargs.get('username'))
return context

@ -0,0 +1,63 @@
# Generated by Django 5.2.4 on 2025-11-20 14:58
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='article',
name='dislikes_count',
field=models.PositiveIntegerField(default=0, verbose_name='dislikes count'),
),
migrations.AddField(
model_name='article',
name='favorites_count',
field=models.PositiveIntegerField(default=0, verbose_name='favorites count'),
),
migrations.AddField(
model_name='article',
name='likes_count',
field=models.PositiveIntegerField(default=0, verbose_name='likes count'),
),
migrations.CreateModel(
name='ArticleFavorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_articles', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'article favorite',
'verbose_name_plural': 'article favorite',
'ordering': ['-creation_time'],
'unique_together': {('article', 'user')},
},
),
migrations.CreateModel(
name='ArticleReaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reaction', models.CharField(choices=[('like', 'Like'), ('dislike', 'Dislike')], max_length=7, verbose_name='reaction')),
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_reactions', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'article reaction',
'verbose_name_plural': 'article reaction',
'ordering': ['-creation_time'],
'unique_together': {('article', 'user')},
},
),
]

@ -1,5 +1,5 @@
#flj 这个文件里的是博客相关的数据模型,定义了博客系统中所有的数据表结构
#fkc这个文件里的是博客相关的数据模型,定义了博客系统中所有的数据表结构
import logging
import re
from abc import abstractmethod
@ -7,13 +7,14 @@ from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Count
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField # 用于支持Markdown编辑器的文本字段
from uuslug import slugify # 用于生成URL友好的slug
from mdeditor.fields import MDTextField #fkc 用于支持Markdown编辑器的文本字段
from uuslug import slugify #fkc 用于生成URL友好的slug
from djangoblog.utils import cache_decorator, cache # 缓存相关的工具函数
from djangoblog.utils import cache_decorator, cache # fkc缓存相关的工具函数
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__)
@ -22,11 +23,11 @@ logger = logging.getLogger(__name__)
#zxm 友情链接的展示类型选择,用于控制链接在哪些页面显示
class LinkShowType(models.TextChoices):
I = ('i', _('index')) # 只在首页显示
L = ('l', _('list')) # 只在列表页显示
P = ('p', _('post')) # 只在文章页显示
A = ('a', _('all')) # 在所有页面显示
S = ('s', _('slide')) # 以轮播形式显示
I = ('i', _('index')) # fkc只在首页显示
L = ('l', _('list')) # fkc只在列表页显示
P = ('p', _('post')) # fkc只在文章页显示
A = ('a', _('all')) # fkc在所有页面显示
S = ('s', _('slide')) # fkc以轮播形式显示
#fkc 所有模型的基类,包含通用字段,避免重复代码
@ -72,114 +73,153 @@ class BaseModel(models.Model):
class Article(BaseModel):
#cll 文章状态选择:草稿或已发布
STATUS_CHOICES = (
('d', _('Draft')), # 草稿
('p', _('Published')), # 已发布
('d', _('Draft')), # fkc草稿
('p', _('Published')), # fkc已发布
)
#cll 评论状态选择:开放或关闭
COMMENT_STATUS = (
('o', _('Open')), # 开放评论
('c', _('Close')), # 关闭评论
('o', _('Open')), # fkc开放评论
('c', _('Close')), # fkc关闭评论
)
#cll 内容类型选择:文章或页面
TYPE = (
('a', _('Article')), # 普通文章
('p', _('Page')), # 静态页面
('a', _('Article')), # fkc普通文章
('p', _('Page')), # fkc静态页面
)
title = models.CharField(_('title'), max_length=200, unique=True) #cll 文章标题
body = MDTextField(_('body')) #cll 文章正文支持Markdown格式
title = models.CharField(_('title'), max_length=200, unique=True) #fkc 文章标题
body = MDTextField(_('body')) #fkc 文章正文支持Markdown格式
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) #cll 发布时间
_('publish time'), blank=False, null=False, default=now) #fkc 发布时间
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p') #cll 文章状态
default='p') #fkc 文章状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o') #cll 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') #cll 内容类型
views = models.PositiveIntegerField(_('views'), default=0) #cll 浏览次数
default='o') #fkc 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') #fkc 内容类型
views = models.PositiveIntegerField(_('views'), default=0) #fkc 浏览次数
likes_count = models.PositiveIntegerField(_('likes count'), default=0)
dislikes_count = models.PositiveIntegerField(_('dislikes count'), default=0)
favorites_count = models.PositiveIntegerField(_('favorites count'), default=0)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE) #cll 作者,关联用户表
on_delete=models.CASCADE) #fkc 作者,关联用户表
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0) #cll 文章排序
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) #cll 是否显示目录
_('order'), blank=False, null=False, default=0) #fkc 文章排序
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) #fkc 是否显示目录
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False) #cll 分类,关联分类表
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) #cll 标签,多对多关系
null=False) #fkc 分类,关联分类表
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) #fkc 标签,多对多关系
#cll 将文章内容转换为字符串
#fkc 将文章内容转换为字符串
def body_to_string(self):
return self.body
#cll 返回文章标题作为对象的字符串表示
#fkc 返回文章标题作为对象的字符串表示
def __str__(self):
return self.title
class Meta:
ordering = ['-article_order', '-pub_time'] #cll 按排序字段和发布时间倒序排列
verbose_name = _('article') #cll 在管理后台显示的名称
verbose_name_plural = verbose_name #cll 复数形式
get_latest_by = 'id' #cll 获取最新记录的依据
ordering = ['-article_order', '-pub_time'] #fkc 按排序字段和发布时间倒序排列
verbose_name = _('article') #fkc 在管理后台显示的名称
verbose_name_plural = verbose_name #fkc 复数形式
get_latest_by = 'id' #fkc 获取最新记录的依据
#cll 获取文章的URL
#fkc 获取文章的URL
def get_absolute_url(self):
if self.type == 'a':
return reverse('blog:detail', kwargs={'article_id': self.id, 'slug': self.slug})
elif self.type == 'p':
return reverse('blog:page', kwargs={'article_id': self.id, 'slug': self.slug})
#cll 获取分类树缓存10小时
return reverse(
'blog:detailbyid',
kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
#fkc 获取分类树缓存10小时
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
category = self.category
names = [category.name]
while category.parent_category:
category = category.parent_category
names.append(category.name)
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
#cll 保存文章,更新修改时间
#fkc 保存文章,更新修改时间
def save(self, *args, **kwargs):
self.last_modify_time = now()
return super().save(*args, **kwargs)
#cll 增加文章浏览次数
#fkc 增加文章浏览次数
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
#cll 获取文章评论列表
def refresh_reaction_counters(self):
"""
重新统计点赞/点踩数量避免并发导致的计数不准
"""
reaction_totals = {
value['reaction']: value['total']
for value in self.reactions.values('reaction').annotate(total=Count('id'))
}
likes = reaction_totals.get(ArticleReaction.LIKE, 0)
dislikes = reaction_totals.get(ArticleReaction.DISLIKE, 0)
Article.objects.filter(pk=self.pk).update(
likes_count=likes,
dislikes_count=dislikes,
)
self.likes_count = likes
self.dislikes_count = dislikes
def refresh_favorites_count(self):
"""
重新统计收藏数量
"""
total = self.favorites.count()
Article.objects.filter(pk=self.pk).update(favorites_count=total)
self.favorites_count = total
def get_user_reaction(self, user):
if not user.is_authenticated:
return None
return self.reactions.filter(user=user).first()
def is_favorited_by(self, user):
if not user.is_authenticated:
return False
return self.favorites.filter(user=user).exists()
#fkc 获取文章评论列表
def comment_list(self):
comments = self.comment_set.filter(is_enable=True).order_by('-id')
return comments
#cll 获取文章在管理后台的URL
#fkc 获取文章在管理后台的URL
def get_admin_url(self):
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.id,))
#cll 获取下一篇文章缓存100分钟
#fkc 获取下一篇文章缓存100分钟
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first()
#cll 获取上一篇文章缓存100分钟
#fkc 获取上一篇文章缓存100分钟
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def prev_article(self):
return Article.objects.filter(id__lt=self.id, status='p').order_by('-id').first()
#cll 获取文章中的第一张图片URL
#fkc 获取文章中的第一张图片URL
def get_first_image_url(self):
pattern = re.compile(r'<img.*?src=["|\'](.*?)["|\']', re.S)
result = pattern.search(self.body)
@ -217,12 +257,15 @@ class Category(BaseModel):
#xy 获取分类树缓存10小时
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
names = [self.name]
category = self.parent_category
while category:
names.append(category.name)
category = category.parent_category
return names
categorys = []
def parse(category):
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
#xy 获取子分类列表缓存10小时
@cache_decorator(60 * 60 * 10) # 缓存10小时
@ -289,6 +332,63 @@ class Links(models.Model):
return self.name
class ArticleReaction(models.Model):
LIKE = 'like'
DISLIKE = 'dislike'
REACTION_CHOICES = (
(LIKE, _('Like')),
(DISLIKE, _('Dislike')),
)
article = models.ForeignKey(
Article,
related_name='reactions',
on_delete=models.CASCADE,
verbose_name=_('article'),
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name='article_reactions',
on_delete=models.CASCADE,
verbose_name=_('user'),
)
reaction = models.CharField(_('reaction'), max_length=7, choices=REACTION_CHOICES)
creation_time = models.DateTimeField(_('creation time'), default=now)
class Meta:
unique_together = ('article', 'user')
ordering = ['-creation_time']
verbose_name = _('article reaction')
verbose_name_plural = verbose_name
def __str__(self):
return f'{self.user} -> {self.article} ({self.reaction})'
class ArticleFavorite(models.Model):
article = models.ForeignKey(
Article,
related_name='favorites',
on_delete=models.CASCADE,
verbose_name=_('article'),
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name='favorite_articles',
on_delete=models.CASCADE,
verbose_name=_('user'),
)
creation_time = models.DateTimeField(_('creation time'), default=now)
class Meta:
unique_together = ('article', 'user')
ordering = ['-creation_time']
verbose_name = _('article favorite')
verbose_name_plural = verbose_name
def __str__(self):
return f'{self.user}{self.article}'
#zxm 侧边栏模型
class SideBar(models.Model):
name = models.CharField(_('title'), max_length=100) #zxm 侧边栏标题

@ -57,7 +57,7 @@ def custom_markdown(content):
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
_, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@ -100,7 +100,7 @@ def load_breadcrumb(article):
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
site = get_current_site().domain
_ = get_current_site().domain # 获取域名但暂未使用
names.append((blogsetting.site_name, '/'))
names = names[::-1]
@ -150,8 +150,9 @@ def load_sidebar(user, linktype):
sidebar_categorys = Category.objects.all()
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
# 热度榜单只显示前10篇文章
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
'-views')[:10]
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
@ -167,8 +168,8 @@ def load_sidebar(user, linktype):
count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
sidebar_tags = [
(x[0], x[1], (x[1] / dd) * increment + 10) for x in s]
random.shuffle(sidebar_tags)
value = {
@ -204,68 +205,85 @@ def load_article_metas(article, user):
}
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
def _get_pagination_urls_for_index(page_obj):
"""获取首页分页URLs"""
previous_url = ''
next_url = ''
if page_obj.has_next():
next_url = reverse('blog:index_page', kwargs={'page': page_obj.next_page_number()})
if page_obj.has_previous():
previous_url = reverse('blog:index_page', kwargs={'page': page_obj.previous_page_number()})
return previous_url, next_url
def _get_pagination_urls_for_tag(page_obj, tag_name):
"""获取标签分页URLs"""
previous_url = ''
next_url = ''
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_url = reverse(
'blog:tag_detail_page',
kwargs={'page': page_obj.next_page_number(), 'tag_name': tag.slug})
if page_obj.has_previous():
previous_url = reverse(
'blog:tag_detail_page',
kwargs={'page': page_obj.previous_page_number(), 'tag_name': tag.slug})
return previous_url, next_url
def _get_pagination_urls_for_author(page_obj, author_name):
"""获取作者分页URLs"""
previous_url = ''
next_url = ''
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': next_number,
'tag_name': tag.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': previous_number,
'tag_name': tag.slug})
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:author_detail_page',
kwargs={
'page': next_number,
'author_name': tag_name})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:author_detail_page',
kwargs={
'page': previous_number,
'author_name': tag_name})
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:category_detail_page',
kwargs={
'page': next_number,
'category_name': category.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:category_detail_page',
kwargs={
'page': previous_number,
'category_name': category.slug})
if page_obj.has_next():
next_url = reverse(
'blog:author_detail_page',
kwargs={'page': page_obj.next_page_number(), 'author_name': author_name})
if page_obj.has_previous():
previous_url = reverse(
'blog:author_detail_page',
kwargs={'page': page_obj.previous_page_number(), 'author_name': author_name})
return previous_url, next_url
def _get_pagination_urls_for_category(page_obj, category_name):
"""获取分类分页URLs"""
previous_url = ''
next_url = ''
category = get_object_or_404(Category, name=category_name)
if page_obj.has_next():
next_url = reverse(
'blog:category_detail_page',
kwargs={'page': page_obj.next_page_number(), 'category_name': category.slug})
if page_obj.has_previous():
previous_url = reverse(
'blog:category_detail_page',
kwargs={'page': page_obj.previous_page_number(), 'category_name': category.slug})
return previous_url, next_url
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
"""加载分页信息"""
# 使用字典映射页面类型到对应的处理函数
pagination_handlers = {
'': _get_pagination_urls_for_index,
'分类标签归档': lambda obj: _get_pagination_urls_for_tag(obj, tag_name),
'作者文章归档': lambda obj: _get_pagination_urls_for_author(obj, tag_name),
'分类目录归档': lambda obj: _get_pagination_urls_for_category(obj, tag_name)
}
# 获取对应的URL生成函数
handler = pagination_handlers.get(page_type, lambda obj: ('', ''))
# 调用处理函数生成URLs
previous_url, next_url = handler(page_obj)
return {
'previous_url': previous_url,
'next_url': next_url,

@ -21,22 +21,31 @@ from oauth.models import OAuthUser, OAuthConfig
class ArticleTest(TestCase):
def setUp(self):
#xy 初始化测试客户端模拟HTTP请求和请求工厂构造原始请求对象
self.client = Client()
self.factory = RequestFactory()
def test_validate_article(self):
#xy 获取当前站点域名
site = get_current_site().domain
#xy 获取或创建测试超级用户(用于发布文章和访问管理后台)
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.is_staff = True #xy 允许访问管理后台
user.is_superuser = True #xy 赋予超级用户权限
user.save()
#xy 测试访问用户个人主页
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
#xy 测试访问管理后台相关页面
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
#xy 创建测试侧边栏
s = SideBar()
s.sequence = 1
s.name = 'test'
@ -44,30 +53,35 @@ class ArticleTest(TestCase):
s.is_enable = True
s.save()
#xy 创建测试分类
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
#xy 创建测试标签
tag = Tag()
tag.name = "nicetag"
tag.save()
#xy 创建测试文章(已发布状态)
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' #xy 文章类型(假设'a'为普通文章)
article.status = 'p' #xy 文章状态(假设'p'为已发布)
article.save()
#xy 测试文章标签关联初始无标签添加后断言数量为1
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
article.save()
self.assertEqual(1, article.tags.count())
#xy 批量创建20篇测试文章用于测试分页功能
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@ -79,56 +93,77 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
#xy 若启用Elasticsearch构建搜索索引并测试搜索功能
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
call_command("build_index") #xy 执行索引构建命令
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
#xy 测试访问文章详情页
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
#xy 测试搜索引擎推送功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
#xy 测试访问标签归档页
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
#xy 测试访问分类归档页
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
#xy 测试搜索功能(搜索不存在的关键词)
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
#xy 测试文章标签模板标签
s = load_articletags(article)
self.assertIsNotNone(s)
#xy 超级用户登录(用于访问需权限的页面)
self.client.login(username='liangliangyy', password='liangliangyy')
#xy 测试访问归档页面
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
#xy 测试全量文章分页
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
#xy 测试标签筛选分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
#xy 测试作者筛选分页
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
#xy 测试分类筛选分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
#xy 测试搜索表单功能
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
#xy 测试百度搜索引擎推送
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
#xy 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
#xy 创建测试友情链接并访问链接页面
link = Links(
sequence=1,
name="lylinux",
@ -137,38 +172,52 @@ class ArticleTest(TestCase):
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
#xy 测试RSS订阅 feed 页面
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
#xy 测试站点地图页面
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
#xy 测试管理后台文章删除、日志查看等功能
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
#xy 遍历所有分页页面,验证分页链接有效性
for page in range(1, p.num_pages + 1):
#xy 加载分页信息(模板标签功能测试)
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
#xy 测试上一页链接存在则访问断言状态码200
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
#xy 测试下一页链接存在则访问断言状态码200
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
#xy 测试图片上传功能
import requests
#xy 下载测试图片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)
#xy 未带签名访问上传接口断言403禁止访问
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
#xy 生成上传接口签名基于SECRET_KEY
sign = get_sha256(get_sha256(settings.SECRET_KEY))
#xy 带签名上传图片(断言上传成功)
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
@ -176,17 +225,24 @@ class ArticleTest(TestCase):
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
#xy 删除本地测试图片
os.remove(imagepath)
#xy 测试用户头像保存和邮件发送工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
#xy 测试404错误页面访问不存在的路径
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
#xy 测试自定义管理命令功能
#xy 创建测试超级用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -195,12 +251,14 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
#xy 创建OAuth第三方登录配置QQ登录
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
#xy 创建OAuth关联用户测试头像同步
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@ -212,6 +270,7 @@ class ArticleTest(TestCase):
}'''
u.save()
#xy 创建另一个OAuth关联用户带远程头像地址
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -222,11 +281,14 @@ class ArticleTest(TestCase):
}'''
u.save()
#xy 若启用Elasticsearch执行索引构建命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
#xy 执行各类自定义管理命令(断言无报错)
call_command("ping_baidu", "all") #xy 百度链接推送
call_command("create_testdata") #xy 创建测试数据
call_command("clear_cache") #xy 清除缓存
call_command("sync_user_avatar") #xy 同步用户头像
call_command("build_search_words") #xy 构建搜索关键词

@ -1,76 +1,84 @@
# 博客应用的URL配置文件
# zxm博客应用的URL配置文件
from django.urls import path
from django.views.decorators.cache import cache_page # 页面缓存装饰器
from django.views.decorators.cache import cache_page #zxm 页面缓存装饰器
from . import views
app_name = "blog" # 应用命名空间
app_name = "blog" # zxm 应用命名空间
urlpatterns = [
# 首页相关
# zxm 首页相关
path(
r'',
views.IndexView.as_view(),
name='index'), # 首页
name='index'), # zxm 首页
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'), # 首页分页
name='index_page'), # zxm 首页分页
# 文章详情页
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'), # 文章详情页按ID
name='detailbyid'), # zxm 文章详情页按ID
# 分类相关页面
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'), # 分类页面
name='category_detail'), # zxm 分类页面
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'), # 分类页面分页
name='category_detail_page'), # zxm 分类页面分页
# 作者相关页面
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'), # 作者页面
name='author_detail'), # zxm 作者页面
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'), # 作者页面分页
name='author_detail_page'), # zxm 作者页面分页
# 标签相关页面
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'), # 标签页面
name='tag_detail'), # zxm 标签页面
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'), # 标签页面分页
name='tag_detail_page'), # zxm 标签页面分页
# 其他页面
path(
'archives.html',
cache_page(60 * 60)( # 缓存1小时
views.ArchivesView.as_view()),
name='archives'), # 文章归档页
name='archives'), # zxm 文章归档页
path(
'links.html',
views.LinkListView.as_view(),
name='links'), # 友情链接页
name='links'), # zxm 友情链接页
# 功能接口
path(
r'upload',
views.fileupload,
name='upload'), # 文件上传接口
name='upload'), # zxm 文件上传接口
path(
r'clean',
views.clean_cache_view,
name='clean'), # 清除缓存接口
name='clean'), # zxm 清除缓存接口
path(
r'article/<int:article_id>/react/',
views.react_article_view,
name='article_react'), # 文章点赞/点踩
path(
r'article/<int:article_id>/favorite/',
views.toggle_favorite_view,
name='article_favorite'), # 文章收藏
]

@ -1,304 +1,518 @@
#flj 博客视图文件,处理博客相关的页面请求
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.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
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 # 用于跳过CSRF验证
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 django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from blog.models import (
Article,
ArticleFavorite,
ArticleReaction,
Category,
LinkShowType,
Links,
Tag,
)
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks # 插件管理
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
#flj 文章列表视图基类,提供通用的文章列表功能,负责处理文章列表的展示逻辑,包括分页、缓存等功能
class ArticleListView(ListView):
template_name = 'blog/article_index.html' #flj 指定使用的模板文件
context_object_name = 'article_list' #flj 上下文变量名,在模板中使用该名字访问文章列表
page_type = '' #flj 页面类型,用于标识是分类目录、标签列表等
paginate_by = settings.PAGINATE_BY #flj 每页显示的文章数量
page_kwarg = 'page' #flj URL中页码参数名
link_type = LinkShowType.L #flj 友情链接显示类型
#flj 获取视图缓存键,注意:这个方法似乎有问题,应该返回字符串而不是字典
"""文章列表视图基类:提供分页、缓存等通用功能"""
# 指定渲染的模板
template_name = 'blog/article_index.html'
# 模板中使用的变量名
context_object_name = 'article_list'
# 页面类型(分类、标签等)
page_type = ''
# 每页数量
paginate_by = settings.PAGINATE_BY
# URL 中的页码参数名
page_kwarg = 'page'
# 友情链接展示类型
link_type = LinkShowType.L
def get_view_cache_key(self):
return self.request.get['pages']
"""
返回当前页面视图级缓存 key
原项目中未实际使用该方法这里保持兼容写法
"""
return f'view_cache_{self.__class__.__name__}_{self.page_number}'
@property
#flj 获取当前页码从URL参数或kwargs中获取页码默认为1
def page_number(self):
"""获取当前页码,默认 1"""
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
#flj 子类必须重写此方法,返回查询集的缓存键,不同的列表视图需要不同的缓存键来区分
def get_queryset_cache_key(self):
"""
子类重写获得 queryset 的缓存 key
"""
raise NotImplementedError()
#flj 子类必须重写此方法,返回查询集的数据,每个子类根据不同的需求过滤文章数据
def get_queryset_data(self):
"""
子类重写真正从数据库获取 queryset 数据
"""
raise NotImplementedError()
#flj 从缓存获取页面数据,提高性能,如果缓存不存在,则从数据库获取并存入缓存
def get_queryset_from_cache(self, cache_key):
"""
根据缓存 key 读取 / 写入文章列表
"""
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
#flj 重写默认方法,从缓存获取数据,优先使用缓存,提高页面响应速度
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):
"""
重写默认方法优先从缓存读取 queryset
"""
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
#flj 为模板添加上下文数据,添加友情链接类型等额外信息
def get_context_data(self, **kwargs):
"""
向模板上下文中注入友情链接展示类型
"""
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
#flj 首页视图,显示最新的已发布文章
class IndexView(ArticleListView):
'''
首页视图显示最新的已发布文章
'''
#flj 友情链接类型:只在首页显示
"""
首页展示最新已发布文章
"""
# 首页使用首页友链展示类型
link_type = LinkShowType.I
#flj 获取首页文章数据,过滤条件为类型为文章(a)且状态为已发布(p)
def get_queryset_data(self):
# 获取所有已发布的文章
# 只展示已发布的正式文章
article_list = Article.objects.filter(type='a', status='p')
return article_list
#flj 生成首页的缓存键,包含页码信息
def get_queryset_cache_key(self):
# 生成首页的缓存键
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
#flj 文章详情视图,负责显示单篇文章的详细内容和评论
class ArticleDetailView(DetailView):
'''
文章详情页视图
'''
template_name = 'blog/article_detail.html' #flj 使用的模板
model = Article #flj 关联的模型
pk_url_kwarg = 'article_id' #flj URL中的主键参数名
context_object_name = "article" #flj 模板中的对象变量名
#flj 获取文章详情页的上下文数据,包括评论表单、相关文章等
"""
文章详情页
"""
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get_context_data(self, **kwargs):
#flj 调用父类方法获取基础上下文数据
#flj 添加评论表单
#flj 获取文章评论列表
#flj 添加相关文章
#flj 调用插件处理文章内容
return super().get_context_data(**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
kwargs['likes_count'] = self.object.likes_count
kwargs['dislikes_count'] = self.object.dislikes_count
kwargs['favorites_count'] = self.object.favorites_count
if self.request.user.is_authenticated:
user = self.request.user
user_reaction = self.object.get_user_reaction(user)
kwargs['user_reaction'] = user_reaction.reaction if user_reaction else None
kwargs['user_has_favorited'] = self.object.is_favorited_by(user)
else:
kwargs['user_reaction'] = None
kwargs['user_has_favorited'] = False
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
#flj 分类详情视图,显示指定分类下的文章列表
class CategoryDetailView(ArticleListView):
'''
分类详情页视图
'''
page_type = "分类目录归档" #flj 页面类型标识
"""
分类目录列表页
"""
page_type = "分类目录归档"
#flj 获取分类下的文章数据根据URL参数中的分类ID过滤
def get_queryset_data(self):
#flj 获取分类ID
#flj 过滤该分类下的已发布文章
#flj 返回查询结果
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
#flj 生成分类页面的缓存键包含分类ID和页码
def get_queryset_cache_key(self):
#flj 生成缓存键
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
#flj 添加分类信息到上下文
def get_context_data(self, **kwargs):
#flj 获取分类对象
#flj 添加到上下文
return super().get_context_data(**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)
#flj 作者详情视图,显示指定作者的文章列表
class AuthorDetailView(ArticleListView):
'''
作者详情页视图
'''
page_type = '作者文章归档' #flj 页面类型标识
"""
作者详情页某个作者的所有文章
"""
page_type = '作者文章归档'
#flj 生成作者页面的缓存键包含作者ID和页码
def get_queryset_cache_key(self):
#flj 生成缓存键
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
#flj 获取作者的文章数据根据URL参数中的作者ID过滤
def get_queryset_data(self):
#flj 获取作者ID
#flj 过滤该作者的已发布文章
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p'
)
return article_list
#flj 添加作者信息到上下文
def get_context_data(self, **kwargs):
#flj 获取作者对象
#flj 添加到上下文
return super().get_context_data(**kwargs)
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
#flj 标签详情视图,显示指定标签下的文章列表
class TagDetailView(ArticleListView):
'''
标签详情页视图
'''
page_type = '分类标签归档' #flj 页面类型标识
"""
标签列表页
"""
page_type = '分类标签归档'
#flj 获取标签下的文章数据根据URL参数中的标签ID过滤
def get_queryset_data(self):
#flj 获取标签ID
#flj 过滤该标签下的已发布文章
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
#flj 生成标签页面的缓存键包含标签ID和页码
def get_queryset_cache_key(self):
#flj 生成缓存键
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
#flj 添加标签信息到上下文
def get_context_data(self, **kwargs):
#flj 获取标签对象
#flj 添加到上下文
return super().get_context_data(**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)
#flj 文章归档视图,显示所有文章按时间分组
class ArchivesView(ArticleListView):
'''
文章归档视图
'''
page_type = '文章归档' #flj 页面类型标识
paginate_by = None #flj 不分页,显示所有文章
"""
文章归档页不分页展示所有已发布文章
"""
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html' #flj 使用归档专用模板
template_name = 'blog/article_archives.html'
#flj 获取所有已发布文章,按时间排序
def get_queryset_data(self):
#flj 获取所有已发布文章
return article_list
return Article.objects.filter(status='p').all()
#flj 生成归档页面的缓存键
def get_queryset_cache_key(self):
#flj 生成缓存键
cache_key = 'archives'
return cache_key
#flj 友情链接列表视图
class LinkListView(ListView):
'''
友情链接列表视图
'''
model = Links #flj 关联的模型
template_name = 'blog/links_list.html' #flj 使用的模板
"""友情链接列表"""
model = Links
template_name = 'blog/links_list.html'
#flj 获取友情链接数据,按显示顺序排序
def get_queryset(self):
#flj 过滤显示状态的友情链接并排序
return links
return Links.objects.filter(is_enable=True)
#flj 搜索视图,处理文章搜索功能
class EsSearchView(SearchView):
'''
搜索视图
'''
#flj 获取搜索结果的上下文数据
"""ElasticSearch 搜索结果视图"""
def get_context(self):
#flj 获取基础上下文
#flj 添加额外的搜索相关信息
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
#flj 文件上传接口,允许上传图片等文件
@csrf_exempt #flj 跳过CSRF验证因为这是文件上传接口
@csrf_exempt
def fileupload(request):
'''
文件上传接口
'''
#flj 检查请求方法
#flj 验证权限
#flj 处理文件上传
#flj 保存文件到指定目录
#flj 返回文件URL
return HttpResponse(json.dumps(data), content_type="application/json")
#flj 404错误页面视图
#flj 处理页面未找到的情况
"""
图床上传接口需自行编写调用端
"""
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'):
'''
404错误页面
'''
#flj 渲染错误页面
return render(request, template_name, context, status=404)
"""404 页面"""
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,
)
#flj 500错误页面视图
#flj 处理服务器内部错误的情况
def server_error_view(request, template_name='blog/error_page.html'):
'''
500错误页面
'''
#flj 渲染错误页面
return render(request, template_name, context, status=500)
"""500 页面"""
return render(
request,
template_name,
{
'message': _(
'Sorry, the server is busy, please click the home page to see other?'
),
'statuscode': '500',
},
status=500,
)
#flj 403错误页面视图
#flj 处理权限拒绝的情况
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
'''
403错误页面
'''
#flj 渲染错误页面
return render(request, template_name, context, status=403)
"""403 页面"""
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,
)
#flj 清理缓存视图,用于手动清理站点缓存
#flj 提供管理功能,清除系统缓存
def clean_cache_view(request):
'''
清理缓存视图
'''
#flj 验证用户权限
#flj 清理缓存
#flj 返回成功信息
return HttpResponse(_('清理缓存成功'))
"""清空站点缓存"""
cache.clear()
return HttpResponse('ok')
@login_required
@require_POST
def react_article_view(request, article_id):
"""处理文章的点赞/点踩"""
article = get_object_or_404(Article, pk=article_id, status='p')
reaction_value = request.POST.get('reaction')
valid_reactions = dict(ArticleReaction.REACTION_CHOICES).keys()
if reaction_value not in valid_reactions:
return JsonResponse({'message': _('无效的操作')}, status=400)
reaction, created = ArticleReaction.objects.get_or_create(
article=article,
user=request.user,
defaults={'reaction': reaction_value},
)
current_reaction = reaction_value
if not created:
if reaction.reaction == reaction_value:
reaction.delete()
current_reaction = None
else:
reaction.reaction = reaction_value
reaction.creation_time = timezone.now()
reaction.save(update_fields=['reaction', 'creation_time'])
article.refresh_reaction_counters()
data = {
'likes': article.likes_count,
'dislikes': article.dislikes_count,
'current_reaction': current_reaction,
'message': _('操作成功'),
}
return JsonResponse(data)
@login_required
@require_POST
def toggle_favorite_view(request, article_id):
"""收藏/取消收藏文章"""
article = get_object_or_404(Article, pk=article_id, status='p')
favorite, created = ArticleFavorite.objects.get_or_create(
article=article,
user=request.user,
)
user_has_favorited = True
if not created:
favorite.delete()
user_has_favorited = False
article.refresh_favorites_count()
data = {
'favorites': article.favorites_count,
'favorited': user_has_favorited,
'message': _('操作成功'),
}
return JsonResponse(data)

@ -9,9 +9,13 @@ logger = logging.getLogger(__name__)
def send_comment_email(comment):
#zhj 获取当前站点域名(用于拼接文章访问链接)
site = get_current_site().domain
#zhj 邮件主题(支持国际化翻译)
subject = _('Thanks for your comment')
#zhj 拼接文章完整访问URLHTTPS协议
article_url = f"https://{site}{comment.article.get_absolute_url()}"
#zhj 构建邮件HTML内容包含文章链接和标题支持国际化
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
@ -19,10 +23,15 @@ def send_comment_email(comment):
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
#zhj 获取评论者邮箱(收件人)
tomail = comment.author.email
#zhj 发送评论感谢邮件
send_email([tomail], subject, html_content)
try:
#zhj 判断当前评论是否为回复(存在父评论)
if comment.parent_comment:
#zhj 构建父评论者的回复通知邮件内容
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
<br/>
@ -32,7 +41,10 @@ def send_comment_email(comment):
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
#zhj 获取父评论者邮箱(回复通知收件人)
tomail = comment.parent_comment.author.email
#zhj 发送回复通知邮件
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e)
#zhj 捕获发送邮件过程中的异常并记录日志
logger.error(e)

@ -101,6 +101,32 @@ def get_current_site():
class CommonMarkdown:
@staticmethod
def _convert_markdown(value):
# 定义常用emoji映射
emoji_mapping = {
':laughing:': '😂',
':smile:': '😊',
':heart:': '❤️',
':thumbsup:': '👍',
':thumbsdown:': '👎',
':star:': '',
':fire:': '🔥',
':rocket:': '🚀',
':clap:': '👏',
':thinking:': '🤔',
':cry:': '😢',
':angry:': '😠',
':wink:': '😉',
':love:': '😍',
':cool:': '😎',
':party:': '🎉',
':surprise:': '😮'
}
# 先替换emoji代码为实际emoji字符
for emoji_code, emoji_char in emoji_mapping.items():
value = value.replace(emoji_code, emoji_char)
# 然后进行markdown转换
md = markdown.Markdown(
extensions=[
'extra',

@ -33,3 +33,34 @@ class BlogUser(AbstractUser):
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'
class UserProfile(models.Model):
"""
用户扩展资料
"""
user = models.OneToOneField(BlogUser, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(_('bio'), blank=True, max_length=500)
avatar = models.ImageField(_('avatar'), upload_to='avatars/', blank=True, null=True)
location = models.CharField(_('location'), max_length=100, blank=True)
website = models.URLField(_('website'), blank=True)
birth_date = models.DateField(_('birth date'), blank=True, null=True)
def __str__(self):
return f"Profile of {self.user.username}"
class UserFollowing(models.Model):
"""
用户关注关系
"""
follower = models.ForeignKey(BlogUser, related_name='following', on_delete=models.CASCADE)
following = models.ForeignKey(BlogUser, related_name='followers', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('follower', 'following')
ordering = ['-created']
def __str__(self):
return f"{self.follower.username} follows {self.following.username}"

@ -25,4 +25,22 @@ urlpatterns = [re_path(r'^login/$',
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
# 用户个人信息中心相关URL
re_path(r'^profile/(?P<username>\w+)/$',
views.UserProfileView.as_view(),
name='profile'),
re_path(r'^profile/(?P<username>\w+)/articles/$',
views.UserArticlesView.as_view(),
name='user_articles'),
re_path(r'^profile/(?P<username>\w+)/favorites/$',
views.UserFavoritesView.as_view(),
name='user_favorites'),
# 关注功能相关路由已移除
re_path(r'^profile/edit/$',
views.ProfileEditView.as_view(),
name='edit_profile'),
re_path(r'^profile/$',
views.my_profile_view,
name='my_profile'),
]

@ -10,6 +10,7 @@ from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
@ -19,12 +20,13 @@ from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from django.views.generic import FormView, RedirectView, DetailView, ListView, UpdateView
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
from .models import BlogUser, UserProfile
from blog.models import Article
logger = logging.getLogger(__name__)
@ -202,3 +204,120 @@ class ForgetPasswordEmailCode(View):
utils.set_code(to_email, code)
return HttpResponse("ok")
class UserProfileView(DetailView):
"""
用户个人信息中心页面
"""
model = BlogUser
template_name = 'account/profile.html'
context_object_name = 'user_profile'
slug_field = 'username'
slug_url_kwarg = 'username'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.object
# 获取用户统计信息
context['article_count'] = Article.objects.filter(author=user, status='p').count()
# 如果是当前登录用户查看自己的资料,显示更多信息
if self.request.user == user:
context['is_own_profile'] = True
else:
context['is_own_profile'] = False
return context
class UserArticlesView(ListView):
"""
用户发布的文章列表
"""
model = Article
template_name = 'account/user_articles.html'
context_object_name = 'article_list'
paginate_by = 10
def get_queryset(self):
username = self.kwargs.get('username')
user = get_object_or_404(BlogUser, username=username)
self.user = user
return Article.objects.filter(author=user, status='p').order_by('-pub_time')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_profile'] = self.user
context['active_tab'] = 'articles'
return context
class UserFavoritesView(ListView):
"""
用户收藏的文章列表
"""
model = Article
template_name = 'account/user_favorites.html'
context_object_name = 'article_list'
paginate_by = 10
def get_queryset(self):
user = self.request.user
return Article.objects.filter(favorites__user=user, status='p').order_by('-favorites__creation_time')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_profile'] = self.request.user
context['active_tab'] = 'favorites'
return context
class ProfileEditView(UpdateView):
"""
编辑用户个人信息
"""
model = BlogUser
template_name = 'account/edit_profile.html'
fields = ['nickname', 'email', 'first_name', 'last_name']
success_url = '/account/profile/'
def get_object(self, queryset=None):
return self.request.user
def get_success_url(self):
return reverse('accounts:profile', kwargs={'username': self.request.user.username})
@login_required
def my_profile_view(request):
"""
跳转到当前登录用户的个人信息页面
"""
return HttpResponseRedirect(reverse('accounts:profile', kwargs={'username': request.user.username}))
# 关注相关功能已移除
class UserLikesView(ListView):
"""
用户点赞的文章列表
"""
model = Article
template_name = 'account/user_likes.html'
context_object_name = 'article_list'
paginate_by = 10
def get_queryset(self):
username = self.kwargs.get('username')
user = get_object_or_404(BlogUser, username=username)
self.user = user
return Article.objects.filter(reactions__user=user, reactions__reaction='like', status='p').order_by('-reactions__created')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_profile'] = self.user
context['active_tab'] = 'likes'
return context

@ -0,0 +1,47 @@
{% extends 'share_layout/base_account.html' %}
{% block title %}编辑个人资料 - DjangoBlog{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">个人信息</h3>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-4 text-center">
<img src="{% static 'img/default_avatar.png' %}"
alt="默认头像" class="img-thumbnail rounded-circle"
style="width: 150px; height: 150px; object-fit: cover;">
</div>
<div class="col-md-8">
<div class="form-group">
<label for="id_username">用户名</label>
<input type="text" id="id_username" class="form-control" value="{{ user.username }}" disabled>
</div>
<div class="form-group">
<label for="id_email">邮箱地址</label>
<input type="email" id="id_email" class="form-control"
value="{{ user.email }}" disabled>
</div>
</div>
</div>
<div class="alert alert-info">
<p>当前仅保留基本用户信息展示功能。</p>
</div>
<div class="d-flex justify-content-center mt-4">
<a href="/" class="btn btn-secondary">
<i class="fas fa-arrow-left mr-1"></i> 返回首页
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,44 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<article class="post">
<header class="entry-header">
<h1 class="entry-title">我的收藏</h1>
</header>
<div class="entry-content">
{% if favorites %}
<ul class="favorite-list">
{% for favorite in favorites %}
<li>
<a href="{{ favorite.article.get_absolute_url }}">
{{ favorite.article.title }}
</a>
<small>收藏时间:{{ favorite.creation_time|date:"Y-m-d H:i" }}</small>
</li>
{% endfor %}
</ul>
{% if is_paginated %}
<div class="navigation">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %}
<span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一页</a>
{% endif %}
</div>
{% endif %}
{% else %}
<p>您还没有收藏任何文章。</p>
{% endif %}
</div>
</article>
</div>
</div>
{% endblock %}
{% block sidebar %}
{% load_sidebar user "p" %}
{% endblock %}

@ -0,0 +1,173 @@
{% extends 'share_layout/base_account.html' %}
{% load static %}
{% block title %}{{ user_profile.username }} - 个人信息中心{% endblock %}
{% block content %}
<div class="container my-5">
<!-- 头部信息卡片 -->
<div class="card shadow-lg rounded-lg overflow-hidden mb-5">
<div class="card-header bg-primary text-white p-4">
<h3 class="mb-0">个人资料</h3>
</div>
<div class="card-body">
<div class="row">
<!-- 左侧信息区域 - 头像和操作按钮 -->
<div class="col-md-4 mb-4">
<div class="text-center p-3 bg-light rounded-lg">
<!-- 头像 -->
<div class="mb-3">
{% if user_profile.profile.avatar %}
<img src="{{ user_profile.profile.avatar.url }}" alt="{{ user_profile.username }} 的头像"
class="rounded-circle border-4 border-white shadow" style="width: 160px; height: 160px; object-fit: cover;">
{% else %}
<img src="{% static 'img/default_avatar.png' %}" alt="默认头像"
class="rounded-circle border-4 border-white shadow" style="width: 160px; height: 160px; object-fit: cover;">
{% endif %}
</div>
<!-- 用户名 -->
<h4 class="font-weight-bold">{{ user_profile.username }}</h4>
{% if user_profile.nickname %}
<p class="text-muted">{{ user_profile.nickname }}</p>
{% endif %}
<!-- 操作按钮 -->
<div class="mt-4">
{% if is_own_profile %}
<a href="{% url 'accounts:edit_profile' %}" class="btn btn-primary btn-block">
<i class="fas fa-edit mr-2"></i> 编辑资料
</a>
{% else %}
{% if user.is_authenticated %}
{% if user_profile.email %}
<a href="mailto:{{ user_profile.email }}" class="btn btn-outline-primary btn-block">
<i class="fas fa-envelope mr-2"></i> 发消息
</a>
{% endif %}
{% else %}
<a href="{% url 'accounts:login' %}" class="btn btn-primary btn-block">
<i class="fas fa-sign-in-alt mr-2"></i> 登录
</a>
{% endif %}
{% endif %}
</div>
</div>
</div>
<!-- 中间区域 - 详细信息 -->
<div class="col-md-4">
<div class="bg-white p-4 rounded-lg shadow-sm mb-4">
<h4 class="border-bottom pb-2 mb-3">关于我</h4>
<p class="text-muted">
{% if user_profile.profile.bio %}
{{ user_profile.profile.bio }}
{% else %}
这个人很懒,还没有填写个人介绍...
{% endif %}
</p>
</div>
<div class="bg-white p-4 rounded-lg shadow-sm">
<h4 class="border-bottom pb-2 mb-3">基本信息</h4>
<ul class="list-unstyled">
<li class="mb-3 d-flex items-center">
<i class="fas fa-envelope text-primary mr-3"></i>
<span>邮箱: {{ user_profile.email }}</span>
</li>
{% if user_profile.profile.location %}
<li class="mb-3 d-flex items-center">
<i class="fas fa-map-marker-alt text-primary mr-3"></i>
<span>所在地: {{ user_profile.profile.location }}</span>
</li>
{% endif %}
{% if user_profile.profile.website %}
<li class="mb-3 d-flex items-center">
<i class="fas fa-globe text-primary mr-3"></i>
<span>个人网站: <a href="{{ user_profile.profile.website }}" target="_blank" class="text-primary hover:underline">{{ user_profile.profile.website }}</a></span>
</li>
{% endif %}
<li class="mb-3 d-flex items-center">
<i class="fas fa-calendar-alt text-primary mr-3"></i>
<span>注册时间: {{ user_profile.date_joined|date:"Y年m月d日" }}</span>
</li>
</ul>
</div>
</div>
<!-- 右侧区域 - 统计数据 -->
<div class="col-md-4">
<div class="bg-white p-4 rounded-lg shadow-sm">
<h4 class="border-bottom pb-2 mb-4 text-center">统计数据</h4>
<div class="row text-center mb-4">
<div class="col-6 mb-4">
<a href="{% url 'accounts:user_articles' username=user_profile.username %}" class="text-decoration-none text-dark hover:text-primary">
<div class="stat-number font-bold text-3xl">{{ article_count|default:0 }}</div>
<div class="stat-label text-sm text-muted">文章</div>
</a>
</div>
<div class="col-6 mb-4">
<a href="{% url 'accounts:favorites' %}" class="text-decoration-none text-dark hover:text-primary">
<div class="stat-number font-bold text-3xl">{{ favorite_count }}</div>
<div class="stat-label text-sm text-muted">收藏</div>
</a>
</div>
</div>
<div class="row text-center">
<div class="col-12">
<a href="{% url 'accounts:user_likes' username=user_profile.username %}" class="text-decoration-none text-dark hover:text-primary">
<div class="stat-number font-bold text-3xl">{{ likes_given_count|default:0 }}</div>
<div class="stat-label text-sm text-muted">点赞</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.stat-item a {
color: inherit;
text-decoration: none;
transition: color 0.3s ease;
}
.stat-item a:hover {
color: #007bff;
}
.stat-number {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.875rem;
color: #6c757d;
}
.btn-block {
display: block;
width: 100%;
margin-bottom: 0.5rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.col-md-4 {
margin-bottom: 20px;
}
img {
width: 120px !important;
height: 120px !important;
}
}
</style>
{% endblock %}

@ -0,0 +1,155 @@
{% extends 'share_layout/base_account.html' %}
{% load static %}
{% block title %}{{ user_profile.username }} - 发布的文章{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card mb-4 shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">{{ user_profile.username }} 的文章 ({{ article_count }})</h3>
</div>
<div class="card-body">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {% if active_tab == 'articles' %}active{% endif %}"
href="{% url 'accounts:user_articles' username=user_profile.username %}">
<i class="fas fa-list-alt"></i> 文章 ({{ article_count }})
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_tab == 'favorites' %}active{% endif %}"
href="{% url 'accounts:user_favorites' username=user_profile.username %}">
<i class="fas fa-star"></i> 收藏 ({{ favorite_count }})
</a>
</li>
</ul>
<div class="tab-content mt-4">
<div class="tab-pane fade show active" id="articles">
{% if article_list %}
<div class="row">
{% for article in article_list %}
<div class="col-md-12 mb-4">
<div class="card h-100 border-l-4 {% if article.is_top %}border-l-danger{% else %}border-l-primary{% endif %}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<a href="{{ article.get_absolute_url }}" class="text-dark font-weight-bold text-decoration-none hover:text-primary">
<h4 class="card-title mb-1">
{% if article.is_top %}
<i class="fas fa-thumbtack text-danger mr-2"></i>
{% endif %}
{{ article.title }}
</h4>
</a>
{% if user == user_profile %}
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a href="{% url 'blog:article_update' article.id %}" class="dropdown-item">
<i class="fas fa-edit mr-2"></i>编辑
</a>
<a href="{% url 'blog:article_delete' article.id %}" class="dropdown-item text-danger">
<i class="fas fa-trash-alt mr-2"></i>删除
</a>
</div>
</div>
{% endif %}
</div>
<p class="card-text text-muted mb-3">
{{ article.body|striptags|truncatechars:150 }}
</p>
<div class="d-flex flex-wrap justify-content-between align-items-center text-sm text-muted">
<div class="d-flex items-center mb-2 mb-md-0">
<div class="text-muted">
{% for tag in article.tags.all %}
<span class="badge badge-secondary mr-1">{{ tag.name }}</span>
{% endfor %}
</div>
<small class="ml-3">{{ article.pub_time|date:"Y-m-d H:i" }}</small>
</div>
<div class="d-flex items-center">
<i class="far fa-eye mr-1"></i>
<span class="ml-1 mr-3">{{ article.views|default:0 }}</span>
<i class="far fa-comment mr-1"></i>
<span class="ml-1 mr-3">{{ article.comment_set.count }}</span>
<i class="far fa-thumbs-up mr-1"></i>
<span class="ml-1">{{ article.likes.count }}</span>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 分页控件 -->
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">上一页</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">上一页</a>
</li>
{% endif %}
{% for page_num in page_obj.paginator.page_range %}
{% if page_num == page_obj.number %}
<li class="page-item active">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% elif page_num > page_obj.number|add:'-3' and page_num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% elif page_num == 1 or page_num == page_obj.paginator.num_pages %}
<li class="page-item">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% elif page_num == page_obj.number|add:'-4' or page_num == page_obj.number|add:'4' %}
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">下一页</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% else %}
<div class="text-center py-8 text-muted">
<i class="fas fa-file-alt fa-5x mb-3"></i>
<h4 class="mb-2">暂无文章</h4>
{% if user == user_profile %}
<a href="{% url 'blog:article_create' %}" class="btn btn-primary mt-2">
<i class="fas fa-edit mr-1"></i>开始写文章
</a>
{% else %}
<p>等待精彩内容更新...</p>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

@ -0,0 +1,106 @@
{% extends 'share_layout/base_account.html' %}
{% block title %}{{ user_profile.username }} - 收藏的文章{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<!-- 内容区域 -->
<div class="col-md-12">
<!-- 用户资料头部卡片 -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">{{ user_profile.username }} 收藏的文章列表</h3>
</div>
<div class="card-body">
<!-- 导航标签页 -->
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {% if active_tab == 'articles' %}active{% endif %}"
href="{% url 'accounts:user_articles' username=user_profile.username %}">
<i class="fas fa-list-alt"></i> 文章
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_tab == 'favorites' %}active{% endif %}"
href="{% url 'accounts:user_favorites' username=user_profile.username %}">
<i class="fas fa-star"></i> 收藏
</a>
</li>
</ul>
</div>
</div>
<!-- 收藏文章列表 -->
<div class="card">
<div class="card-body">
{% if article_list %}
<div class="list-group">
{% for article in article_list %}
<a href="{{ article.get_absolute_url }}" class="list-group-item list-group-item-action flex-column align-items-start mb-3">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1 text-primary">{{ article.title }}</h5>
<small class="text-muted">收藏于 {{ article.favorites.first.creation_time|date:"Y-m-d H:i" }}</small>
</div>
<p class="mb-1 text-muted">{{ article.body|striptags|truncatechars:150 }}</p>
<div class="d-flex w-100 justify-content-between align-items-center">
<small class="text-muted">
作者: {{ article.author.username }} |
发布于: {{ article.pub_time|date:"Y-m-d" }}
</small>
<div class="text-muted">
<i class="far fa-eye"></i> {{ article.views }}
<i class="far fa-comment ml-2"></i> {{ article.comment_set.count }}
<i class="far fa-thumbs-up ml-2"></i> {{ article.likes.count }}
</div>
</div>
</a>
{% endfor %}
</div>
<!-- 分页控件 -->
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">上一页</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">上一页</a>
</li>
{% endif %}
{% for page_num in page_obj.paginator.page_range %}
{% if page_num == page_obj.number %}
<li class="page-item active">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">下一页</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% else %}
<div class="alert alert-info text-center">
<i class="fas fa-info-circle"></i> 暂无收藏的文章
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

@ -8,6 +8,28 @@
<div id="content" role="main">
{% load_article_detail article False user %}
<div class="article-engagement"
data-article="{{ article.pk }}"
data-user-auth="{{ user.is_authenticated|yesno:'true,false' }}">
<button type="button"
class="reaction-button {% if user_reaction == 'like' %}is-active{% endif %}"
data-reaction="like">
👍&nbsp;<span data-role="likes-count">{{ likes_count }}</span>
</button>
<button type="button"
class="reaction-button {% if user_reaction == 'dislike' %}is-active{% endif %}"
data-reaction="dislike">
👎&nbsp;<span data-role="dislikes-count">{{ dislikes_count }}</span>
</button>
<button type="button"
class="favorite-button {% if user_has_favorited %}is-active{% endif %}">
&nbsp;<span data-role="favorites-count">{{ favorites_count }}</span>
</button>
<small class="auth-hint" {% if user.is_authenticated %}style="display:none"{% endif %}>
请先<a href="{% url "account:login" %}?next={{ request.get_full_path }}">登录</a>再进行点赞或收藏
</small>
</div>
{% if article.type == 'a' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>

@ -1,7 +1,7 @@
{% load blog_tags %}
{% load i18n %}
<div id="secondary" class="widget-area" role="complementary">
<aside id="search-2" class="widget widget_search">
<aside id="secondary" class="widget-area" aria-label="侧边栏内容">
<aside id="search-2" class="widget widget_search" aria-label="搜索功能">
<form role="search" method="get" id="searchform" class="searchform" action="/search">
<div>
<label class="screen-reader-text" for="s">{% trans 'search' %}</label>
@ -13,7 +13,7 @@
{% if extra_sidebars %}
{% for sidebar in extra_sidebars %}
<aside class="widget_text widget widget_custom_html"><p class="widget-title">
<aside class="widget_text widget widget_custom_html" aria-label="额外侧边栏内容"><p class="widget-title">
{{ sidebar.name }}</p>
<div class="textwidget custom-html-widget">
{{ sidebar.content|custom_markdown|safe }}
@ -23,21 +23,21 @@
{% endif %}
{% if most_read_articles %}
<aside id="views-4" class="widget widget_views"><p class="widget-title">Views</p>
<aside id="hot-articles" class="widget widget_views" aria-label="热度榜单内容">
<p class="widget-title">热度榜单</p>
<ul>
{% for a in most_read_articles %}
<li>
<a href="{{ a.get_absolute_url }}" title="{{ a.title }}">
{{ a.title }}
</a> - {{ a.views }} views
</a> - {{ a.views }} 浏览
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
{% if sidebar_categorys %}
<aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms"><p class="widget-title">{% trans 'category' %}</p>
<aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms" aria-label="文章分类内容"><p class="widget-title">{% trans 'category' %}</p>
<ul>
{% for c in sidebar_categorys %}
<li class="cat-item cat-item-184"><a href={{ c.get_absolute_url }}>{{ c.name }}</a>
@ -47,7 +47,7 @@
</aside>
{% endif %}
{% if sidebar_comments and open_site_comment %}
<aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments"><p class="widget-title">{% trans 'recent comments' %}</p>
<aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments" aria-label="评论内容"><p class="widget-title">{% trans 'recent comments' %}</p>
<ul id="recentcomments">
{% for c in sidebar_comments %}
@ -62,7 +62,7 @@
</aside>
{% endif %}
{% if recent_articles %}
<aside id="recent-posts-2" class="widget widget_recent_entries"><p class="widget-title">{% trans 'recent articles' %}</p>
<aside id="recent-posts-2" class="widget widget_recent_entries" aria-label="最新文章内容"><p class="widget-title">{% trans 'recent articles' %}</p>
<ul>
{% for a in recent_articles %}
@ -74,7 +74,7 @@
</aside>
{% endif %}
{% if sidabar_links %}
<aside id="linkcat-0" class="widget widget_links"><p class="widget-title">{% trans 'bookmark' %}</p>
<aside id="linkcat-0" class="widget widget_links" aria-label="书签链接内容"><p class="widget-title">{% trans 'bookmark' %}</p>
<ul class='xoxo blogroll'>
{% for l in sidabar_links %}
<li>
@ -86,25 +86,22 @@
</aside>
{% endif %}
{% if show_google_adsense %}
<aside id="text-2" class="widget widget_text"><p class="widget-title">Google AdSense</p>
<aside id="text-2" class="widget widget_text" aria-label="Google广告内容"><p class="widget-title">Google AdSense</p>
<div class="textwidget">
{{ google_adsense_codes|safe }}
</div>
</aside>
{% endif %}
{% if sidebar_tags %}
<aside id="tag_cloud-2" class="widget widget_tag_cloud"><p class="widget-title">{% trans 'Tag Cloud' %}</p>
<aside id="tag_cloud-2" class="widget widget_tag_cloud" aria-label="标签云内容"><p class="widget-title">{% trans 'Tag Cloud' %}</p>
<div class="tagcloud">
{% for tag,count,size in sidebar_tags %}
<a href="{{ tag.get_absolute_url }}"
class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}"
style="font-size: {{ size }}pt;" title="{{ count }}个话题"> {{ tag.name }}
</a>
<a href="{{ tag.get_absolute_url }}" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
</aside>
{% endif %}
<aside id="text-2" class="widget widget_text"><p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p>
<aside id="text-3" class="widget widget_text" aria-label="GitHub推广内容"><p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p>
<div class="textwidget">
<p><a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
@ -115,12 +112,14 @@
</div>
</aside>
<aside id="meta-3" class="widget widget_meta"><p class="widget-title">{% trans 'Function' %}</p>
<aside id="meta-3" class="widget widget_meta" aria-label="功能导航内容"><p class="widget-title">{% trans 'Function' %}</p>
<ul>
<li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li>
{% if user.is_authenticated %}
<li><a href="{% url "account:logout" %}" rel="nofollow">{% trans 'logout' %}</a>
</li>
<li><a href="{% url 'account:favorites' %}">我的收藏</a></li>
{% else %}
<li><a href="{% url "account:login" %}" rel="nofollow">{% trans 'login' %}</a></li>
@ -133,4 +132,4 @@
</aside>
<div id="rocket" class="show" title="{% trans 'Click me to return to the top' %}"></div>
</div><!-- #secondary -->
</aside><!-- #secondary -->

@ -44,6 +44,8 @@
<link rel='stylesheet' id='twentytwelve-style-css' href='{% static 'blog/css/style.css' %}' type='text/css'
media='all'/>
<link href="{% static 'blog/css/oauth_style.css' %}" rel="stylesheet">
<link rel="stylesheet" href="{% static 'blog/css/custom-theme.css' %}"/>
<link rel="stylesheet" href="{% static 'blog/css/dark-mode.css' %}"/>
{% comment %}<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>{% endcomment %}
<!--[if lt IE 9]>
<link rel='stylesheet' id='twentytwelve-ie-css' href='{% static 'blog/css/ie.css' %}' type='text/css' media='all' />
@ -89,6 +91,9 @@
{% include 'share_layout/nav.html' %}
<button id="theme-toggle" class="theme-toggle" aria-label="切换深浅色">
🌙
</button>
</header><!-- #masthead -->
<div id="main" class="wrapper">

Loading…
Cancel
Save