feat: 添加完整代码注释

master
郭楚扬 3 months ago committed by Gitea
parent d9e4e4c07d
commit fb5a4ea822

@ -1,3 +1,7 @@
"""
用户账户相关模型定义
扩展Django默认用户模型增加博客系统特有功能
"""
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
@ -6,30 +10,50 @@ 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)
"""
博客用户模型
继承Django的AbstractUser扩展博客系统需要的字段
"""
# 用户昵称,用于显示而非用户名
nickname = models.CharField(_('昵称'), max_length=100, blank=True)
# 时间戳字段
creation_time = models.DateTimeField(_('创建时间'), default=now)
last_modify_time = models.DateTimeField(_('最后修改时间'), default=now)
# 用户来源,记录注册渠道
source = models.CharField(_('注册来源'), max_length=100, blank=True)
def get_absolute_url(self):
"""
获取用户主页的URL
用于作者详情页面的链接
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
"""
字符串表示
使用邮箱作为标识因为用户名可能不唯一
"""
return self.email
def get_full_url(self):
"""
获取包含完整域名的用户主页URL
用于分享RSS等需要绝对URL的场景
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'
"""元数据配置"""
ordering = ['-id'] # 按ID降序排列新的用户在前
verbose_name = _('用户') # 单数名称
verbose_name_plural = verbose_name # 复数名称
get_latest_by = 'id' # 获取最新记录的依据字段

@ -1,3 +1,7 @@
"""
用户账户相关视图函数
处理用户注册登录退出密码找回等功能
"""
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
@ -29,176 +33,220 @@ from .models import BlogUser
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
"""
用户注册视图
处理新用户注册流程包括邮箱验证
"""
form_class = RegisterForm # 注册表单类
template_name = 'account/registration_form.html' # 注册页面模板
@method_decorator(csrf_protect) # CSRF保护
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""
表单验证通过后的处理逻辑
创建用户发送验证邮件跳转结果页面
"""
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
user = form.save(False) # 不立即保存,先进行其他操作
user.is_active = False # 新用户需要邮箱验证激活
user.source = 'Register' # 记录注册来源
user.save(True) # 保存用户到数据库
# 构建邮箱验证链接
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 开发环境使用本地域名
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 构建验证邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
emailto=[
user.email,
],
emailto=[user.email],
title='验证您的电子邮箱',
content=content)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
# 跳转到注册结果页面
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
})
# 表单验证失败,重新渲染表单页
return self.render_to_response({'form': form})
class LogoutView(RedirectView):
url = '/login/'
"""
用户退出登录视图
清除用户会话重定向到登录页
"""
url = '/login/' # 退出后重定向的URL
@method_decorator(never_cache)
@method_decorator(never_cache) # 禁止缓存
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
"""处理GET请求执行退出登录操作"""
logout(request) # Django内置退出函数
delete_sidebar_cache() # 清理侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
"""
用户登录视图
处理用户登录认证支持记住登录状态
"""
form_class = LoginForm # 登录表单类
template_name = 'account/login.html' # 登录页面模板
success_url = '/' # 登录成功后的默认跳转地址
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名
login_ttl = 2626560 # 记住登录状态的持续时间(一个月,单位:秒)
@method_decorator(sensitive_post_parameters('password')) # 保护密码参数
@method_decorator(csrf_protect) # CSRF保护
@method_decorator(never_cache) # 禁止缓存
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""添加上下文数据,包括重定向地址"""
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
redirect_to = '/' # 默认重定向到首页
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""表单验证通过后的登录处理"""
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
delete_sidebar_cache() # 清理侧边栏缓存
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
auth.login(self.request, form.get_user()) # 执行登录操作
# 处理"记住我"选项
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
self.request.session.set_expiry(self.login_ttl) # 设置会话过期时间
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
# 登录失败,重新显示表单
return self.render_to_response({'form': form})
def get_success_url(self):
"""获取登录成功后的跳转URL"""
redirect_to = self.request.POST.get(self.redirect_field_name)
# 安全检查确保重定向URL在允许的域名内
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
url=redirect_to, allowed_hosts=[self.request.get_host()]):
redirect_to = self.success_url # 使用默认URL
return redirect_to
def account_result(request):
type = request.GET.get('type')
id = request.GET.get('id')
"""
账户操作结果页面
显示注册结果或邮箱验证结果
"""
type = request.GET.get('type') # 操作类型register或validation
id = request.GET.get('id') # 用户ID
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
# 如果用户已激活,直接跳转首页
if user.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
# 注册成功页面
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden()
user.is_active = True
return HttpResponseForbidden() # 签名验证失败
user.is_active = True # 激活用户
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/')
return HttpResponseRedirect('/') # 参数错误,跳转首页
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
"""
忘记密码视图
通过邮箱重置用户密码
"""
form_class = ForgetPasswordForm # 忘记密码表单
template_name = 'account/forget_password.html' # 模板文件
def form_valid(self, form):
"""表单验证通过后的密码重置处理"""
if form.is_valid():
# 根据邮箱查找用户
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# 使用Django的密码哈希函数设置新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/')
blog_user.save() # 保存新密码
return HttpResponseRedirect('/login/') # 重定向到登录页
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""
忘记密码邮箱验证码视图
发送密码重置验证码到用户邮箱
"""
def post(self, request: HttpRequest):
"""处理POST请求发送验证码邮件"""
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
return HttpResponse("错误的邮箱") # 邮箱格式错误
to_email = form.cleaned_data["email"]
# 生成并发送验证码
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code) # 发送验证邮件
utils.set_code(to_email, code) # 保存验证码(到缓存或数据库)
return HttpResponse("ok")
return HttpResponse("ok") # 发送成功

@ -1,3 +1,7 @@
"""
博客系统核心数据模型定义
包含文章分类标签友情链接等主要数据模型
"""
import logging
import re
from abc import abstractmethod
@ -18,25 +22,40 @@ logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
"""
链接显示类型选择枚举
定义友情链接在不同页面的显示方式
"""
I = ('i', _('首页')) # 首页显示
L = ('l', _('列表页')) # 文章列表页显示
P = ('p', _('文章详情页')) # 文章详情页显示
A = ('a', _('全站显示')) # 所有页面显示
S = ('s', _('幻灯片')) # 幻灯片形式显示
class BaseModel(models.Model):
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
"""
抽象基类模型
为所有模型提供通用的字段和方法
"""
id = models.AutoField(primary_key=True) # 主键ID
creation_time = models.DateTimeField(_('创建时间'), default=now) # 记录创建时间
last_modify_time = models.DateTimeField(_('修改时间'), default=now) # 最后修改时间
def save(self, *args, **kwargs):
"""
重写保存方法添加通用逻辑
包括自动生成slug更新时间戳等
"""
# 检查是否是更新文章浏览量的操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# 直接更新浏览量,避免触发其他信号
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# 自动生成slug字段用于SEO友好的URL
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
@ -45,79 +64,95 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
def get_full_url(self):
"""
获取完整的绝对URL包含域名
用于分享RSS订阅等场景
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True
abstract = True # 抽象类,不会创建数据库表
@abstractmethod
def get_absolute_url(self):
"""抽象方法子类必须实现获取绝对URL的方法"""
pass
class Article(BaseModel):
"""文章"""
"""
文章核心模型
存储博客文章的所有信息支持Markdown格式
"""
# 文章状态选择
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
('d', _('草稿')), # 草稿状态,仅作者可见
('p', _('已发布')), # 已发布状态,所有用户可见
)
# 评论状态选择
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
('o', _('开启')), # 开启评论
('c', _('关闭')), # 关闭评论
)
# 文章类型选择
TYPE = (
('a', _('Article')),
('p', _('Page')),
('a', _('普通文章')), # 普通博客文章
('p', _('页面')), # 静态页面(如关于页面)
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey(
# 基础字段
title = models.CharField(_('标题'), max_length=200, unique=True) # 文章标题,唯一
body = MDTextField(_('正文')) # 文章正文支持Markdown编辑
pub_time = models.DateTimeField(_('发布时间'), blank=False, null=False, default=now) # 发布时间
status = models.CharField(_('状态'), max_length=1, choices=STATUS_CHOICES, default='p') # 文章状态
comment_status = models.CharField(_('评论状态'), max_length=1, choices=COMMENT_STATUS, default='o') # 评论开关
type = models.CharField(_('类型'), max_length=1, choices=TYPE, default='a') # 文章类型
views = models.PositiveIntegerField(_('浏览量'), default=0) # 浏览量统计
author = models.ForeignKey( # 文章作者
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
verbose_name=_('作者'),
blank=False,
null=False,
on_delete=models.CASCADE)
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
category = models.ForeignKey(
on_delete=models.CASCADE) # 作者删除时级联删除文章
# 排序和显示相关字段
article_order = models.IntegerField(_('排序'), blank=False, null=False, default=0) # 文章排序权重
show_toc = models.BooleanField(_('显示目录'), blank=False, null=False, default=False) # 是否显示文章目录
# 分类和标签关系
category = models.ForeignKey( # 文章分类
'Category',
verbose_name=_('category'),
verbose_name=_('分类'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
null=False) # 必须属于一个分类
tags = models.ManyToManyField('Tag', verbose_name=_('标签'), blank=True) # 文章标签,多对多关系
def body_to_string(self):
"""将正文内容转换为字符串"""
return self.body
def __str__(self):
"""字符串表示,返回文章标题"""
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
"""元数据配置"""
ordering = ['-article_order', '-pub_time'] # 默认按排序权重和发布时间降序排列
verbose_name = _('文章') # 单数名称
verbose_name_plural = verbose_name # 复数名称
get_latest_by = 'id' # 获取最新记录的依据字段
def get_absolute_url(self):
"""
获取文章详情页的URL
使用年月日+文章ID的SEO友好URL结构
"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -127,7 +162,16 @@ class Article(BaseModel):
@cache_decorator(expiration=60 * 60 * 2) # 缓存2小时
def get_related_posts(self, count=4):
"""获取相关文章"""
"""
获取相关文章推荐
策略先通过相同标签获取不够则通过相同分类补充还不够则随机推荐
Args:
count: 需要获取的相关文章数量默认4篇
Returns:
list: 相关文章列表
"""
# 通过相同的标签获取相关文章(排除自己)
related_by_tags = Article.objects.filter(
tags__in=self.tags.all(),
@ -161,26 +205,42 @@ class Article(BaseModel):
id=self.id
).exclude(
id__in=[p.id for p in related_posts]
).order_by('?')[:remaining_count] # 随机排序
).order_by('?')[:remaining_count] # 使用order_by('?')随机排序
related_posts.extend(list(random_posts))
return related_posts[:count]
return related_posts[:count] # 确保返回指定数量的文章
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
获取文章所属分类的完整层级树
用于面包屑导航显示
Returns:
list: 包含分类名称和URL的元组列表
"""
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
"""保存文章,调用父类保存逻辑"""
super().save(*args, **kwargs)
def viewed(self):
"""
增加文章浏览量
使用update_fields只更新views字段提高性能
"""
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
"""
获取文章评论列表带缓存
缓存100分钟减少数据库查询压力
"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
@ -188,29 +248,33 @@ class Article(BaseModel):
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
cache.set(cache_key, comments, 60 * 100) # 缓存100分钟
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
"""获取文章在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)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
# 下一篇
"""获取下一篇文章按ID顺序"""
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def prev_article(self):
# 前一篇
"""获取上一篇文章按ID顺序"""
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
从文章正文中提取第一张图片URL
用于文章列表的缩略图显示
Returns:
str: 图片URL或空字符串
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
@ -218,61 +282,87 @@ class Article(BaseModel):
return ""
# ==================== 新增的点赞收藏方法 ====================
def get_like_count(self):
"""获取点赞数量"""
"""获取文章点赞数量"""
return Like.objects.filter(article=self).count()
def get_favorite_count(self):
"""获取收藏数量"""
"""获取文章收藏数量"""
return Favorite.objects.filter(article=self).count()
def is_liked_by_user(self, user):
"""检查用户是否点赞"""
"""
检查指定用户是否点赞了该文章
Args:
user: 用户对象
Returns:
bool: 是否点赞
"""
if not user.is_authenticated:
return False
return Like.objects.filter(user=user, article=self).exists()
def is_favorited_by_user(self, user):
"""检查用户是否收藏"""
"""
检查指定用户是否收藏了该文章
Args:
user: 用户对象
Returns:
bool: 是否收藏
"""
if not user.is_authenticated:
return False
return Favorite.objects.filter(user=user, article=self).exists()
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
"""
文章分类模型
支持多级分类结构
"""
name = models.CharField(_('分类名称'), max_length=30, unique=True) # 分类名称,唯一
parent_category = models.ForeignKey( # 父级分类,实现分类树
'self',
verbose_name=_('parent category'),
verbose_name=_('父级分类'),
blank=True,
null=True,
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index'))
on_delete=models.CASCADE) # 支持无限级分类
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL别名
index = models.IntegerField(default=0, verbose_name=_('排序索引')) # 分类排序
class Meta:
ordering = ['-index']
verbose_name = _('category')
ordering = ['-index'] # 按索引降序排列
verbose_name = _('分类')
verbose_name_plural = verbose_name
def get_absolute_url(self):
"""获取分类详情页URL"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
"""字符串表示,返回分类名称"""
return self.name
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
递归获得分类目录的父级链
用于面包屑导航
Returns:
list: 从当前分类到根分类的列表
"""
categorys = []
def parse(category):
"""递归解析分类层级"""
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
@ -280,16 +370,20 @@ class Category(BaseModel):
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
获得当前分类目录所有子分类
用于分类导航菜单
Returns:
list: 所有子分类列表
"""
categorys = []
all_categorys = Category.objects.all()
def parse(category):
"""递归解析子分类"""
if category not in categorys:
categorys.append(category)
childs = all_categorys.filter(parent_category=category)
@ -303,45 +397,52 @@ class Category(BaseModel):
class Tag(BaseModel):
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
"""
文章标签模型
用于文章的多标签分类
"""
name = models.CharField(_('标签名称'), max_length=30, unique=True) # 标签名称,唯一
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL别名
def __str__(self):
"""字符串表示,返回标签名称"""
return self.name
def get_absolute_url(self):
"""获取标签详情页URL"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_article_count(self):
"""获取该标签下的文章数量"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
verbose_name = _('tag')
ordering = ['name'] # 按名称升序排列
verbose_name = _('标签')
verbose_name_plural = verbose_name
class Links(models.Model):
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
show_type = models.CharField(
_('show type'),
"""
友情链接模型
管理站外友情链接
"""
name = models.CharField(_('链接名称'), max_length=30, unique=True) # 链接名称
link = models.URLField(_('链接地址')) # 链接URL
sequence = models.IntegerField(_('排序'), unique=True) # 显示顺序
is_enable = models.BooleanField(_('是否显示'), default=True, blank=False, null=False) # 是否启用
show_type = models.CharField( # 显示位置
_('显示类型'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
creation_time = models.DateTimeField(_('创建时间'), default=now)
last_mod_time = models.DateTimeField(_('修改时间'), default=now)
class Meta:
ordering = ['sequence']
verbose_name = _('link')
ordering = ['sequence'] # 按排序字段升序排列
verbose_name = _('友情链接')
verbose_name_plural = verbose_name
def __str__(self):
@ -349,17 +450,20 @@ class Links(models.Model):
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(_('is enable'), default=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
"""
侧边栏模型
管理网站侧边栏的自定义内容
"""
name = models.CharField(_('标题'), max_length=100) # 侧边栏标题
content = models.TextField(_('内容')) # HTML内容
sequence = models.IntegerField(_('排序'), unique=True) # 显示顺序
is_enable = models.BooleanField(_('是否启用'), default=True) # 是否启用
creation_time = models.DateTimeField(_('创建时间'), default=now)
last_mod_time = models.DateTimeField(_('修改时间'), default=now)
class Meta:
ordering = ['sequence']
verbose_name = _('sidebar')
ordering = ['sequence'] # 按排序字段升序排列
verbose_name = _('侧边栏')
verbose_name_plural = verbose_name
def __str__(self):
@ -367,118 +471,111 @@ class SideBar(models.Model):
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
article_comment_count = models.IntegerField(_('article comment count'), default=5)
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='')
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
"""
博客全局设置模型
存储博客的各种配置信息单例模式
"""
# 网站基本信息
site_name = models.CharField(_('网站名称'), max_length=200, null=False, blank=False, default='')
site_description = models.TextField(_('网站描述'), max_length=1000, null=False, blank=False, default='')
site_seo_description = models.TextField(_('SEO描述'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField(_('网站关键词'), max_length=1000, null=False, blank=False, default='')
# 内容显示设置
article_sub_length = models.IntegerField(_('文章摘要长度'), default=300) # 文章摘要截取长度
sidebar_article_count = models.IntegerField(_('侧边栏文章数量'), default=10) # 侧边栏最新文章数量
sidebar_comment_count = models.IntegerField(_('侧边栏评论数量'), default=5) # 侧边栏最新评论数量
article_comment_count = models.IntegerField(_('文章评论分页数量'), default=5) # 文章详情页评论分页大小
# 广告和统计
show_google_adsense = models.BooleanField(_('显示广告'), default=False) # 是否显示Google广告
google_adsense_codes = models.TextField(_('广告代码'), max_length=2000, null=True, blank=True, default='') # 广告代码
# 评论设置
open_site_comment = models.BooleanField(_('开启全站评论'), default=True) # 是否开启评论功能
comment_need_review = models.BooleanField('评论是否需要审核', default=False, null=False) # 评论是否需要审核
# 页面布局
global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部HTML
# 备案信息
beian_code = models.CharField('备案号', max_length=2000, null=True, blank=True, default='') # ICP备案号
show_gongan_code = models.BooleanField('是否显示公安备案号', default=False, null=False) # 是否显示公安备案
gongan_beiancode = models.TextField('公安备案号', max_length=2000, null=True, blank=True, default='') # 公安备案号
# 网站统计
analytics_code = models.TextField("网站统计代码", max_length=1000, null=False, blank=False,
default='') # 统计代码如Google Analytics
class Meta:
verbose_name = _('Website configuration')
verbose_name = _('网站配置')
verbose_name_plural = verbose_name
def __str__(self):
return self.site_name
def clean(self):
"""
验证方法确保只能有一个配置实例
"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
raise ValidationError(_('只能有一个网站配置'))
def save(self, *args, **kwargs):
"""保存配置后清空相关缓存"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
cache.clear() # 清除所有缓存,因为配置变更可能影响多个页面
class Like(models.Model):
"""文章点赞模型"""
"""
文章点赞模型
记录用户对文章的点赞关系
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('用户'),
on_delete=models.CASCADE
on_delete=models.CASCADE # 用户删除时删除点赞记录
)
article = models.ForeignKey(
Article,
verbose_name=_('文章'),
on_delete=models.CASCADE
on_delete=models.CASCADE # 文章删除时删除点赞记录
)
created_time = models.DateTimeField(_('点赞时间'), auto_now_add=True)
created_time = models.DateTimeField(_('点赞时间'), auto_now_add=True) # 点赞时间
class Meta:
verbose_name = _('点赞')
verbose_name_plural = verbose_name
unique_together = ('user', 'article')
unique_together = ('user', 'article') # 同一个用户对同一篇文章只能点赞一次
def __str__(self):
return f"{self.user.username} 点赞了 {self.article.title}"
class Favorite(models.Model):
"""文章收藏模型"""
"""
文章收藏模型
记录用户对文章的收藏关系
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('用户'),
on_delete=models.CASCADE
on_delete=models.CASCADE # 用户删除时删除收藏记录
)
article = models.ForeignKey(
Article,
verbose_name=_('文章'),
on_delete=models.CASCADE
on_delete=models.CASCADE # 文章删除时删除收藏记录
)
created_time = models.DateTimeField(_('收藏时间'), auto_now_add=True)
created_time = models.DateTimeField(_('收藏时间'), auto_now_add=True) # 收藏时间
class Meta:
verbose_name = _('收藏')
verbose_name_plural = verbose_name
unique_together = ('user', 'article')
unique_together = ('user', 'article') # 同一个用户对同一篇文章只能收藏一次
def __str__(self):
return f"{self.user.username} 收藏了 {self.article.title}"

@ -1,78 +1,97 @@
"""
博客应用URL配置
定义文章相关的所有URL路由
"""
from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
app_name = "blog"
app_name = "blog" # 应用命名空间
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
# 首页
path(r'', views.IndexView.as_view(), name='index'),
# 首页分页
path(r'page/<int:page>/', views.IndexView.as_view(), name='index_page'),
# 文章详情页SEO友好URL/年/月/日/文章ID.html
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
name='detailbyid'
),
# 分类详情页
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
name='category_detail'
),
# 分类分页
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
name='category_detail_page'
),
# 作者详情页
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
name='author_detail'
),
# 作者文章分页
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
name='author_detail_page'
),
# 标签详情页
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
name='tag_detail'
),
# 标签分页
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
name='tag_detail_page'
),
# 文章归档页缓存1小时
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
path(
r'upload',
views.fileupload,
name='upload'),
path(
r'clean',
views.clean_cache_view,
name='clean'),
path(
'like/<int:article_id>/',
views.toggle_like,
name='toggle_like'),
path(
'favorite/<int:article_id>/',
views.toggle_favorite,
name='toggle_favorite'),
path(
'favorites/',
views.favorite_list,
name='favorite_list'),
path(
'likes/',
views.like_list,
name='like_list'),
]
cache_page(60 * 60)(views.ArchivesView.as_view()), # 缓存1小时
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'),
# ==================== 新增的点赞收藏功能 ====================
# 点赞/取消点赞
path('like/<int:article_id>/', views.toggle_like, name='toggle_like'),
# 收藏/取消收藏
path('favorite/<int:article_id>/', views.toggle_favorite, name='toggle_favorite'),
# 用户收藏列表
path('favorites/', views.favorite_list, name='favorite_list'),
# 用户点赞列表
path('likes/', views.like_list, name='like_list'),
]

@ -1,3 +1,7 @@
"""
博客系统视图函数定义
处理文章展示分类标签搜索等核心功能
"""
import logging
import os
import uuid
@ -25,23 +29,33 @@ 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'
# 分页设置
paginate_by = settings.PAGINATE_BY # 每页显示文章数量从settings读取
page_kwarg = 'page' # URL中页码参数名称
# 链接显示类型,控制友情链接的显示位置
link_type = LinkShowType.L
def get_view_cache_key(self):
"""获取视图缓存键已弃用使用get_queryset_cache_key代替"""
return self.request.get['pages']
@property
def page_number(self):
"""获取当前页码的属性方法"""
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
@ -49,21 +63,23 @@ class ArticleListView(ListView):
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
子类重写获得queryset的缓存key
每个子类必须实现此方法以生成唯一的缓存键
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
子类重写获取queryset的数据
定义具体的查询逻辑返回文章查询集
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
缓存页面数据提高列表页加载速度
:param cache_key: 缓存key
:return:
:return: 文章查询集
'''
value = cache.get(cache_key)
if value:
@ -71,57 +87,89 @@ class ArticleListView(ListView):
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
cache.set(cache_key, article_list) # 设置缓存,默认过期时间
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
重写默认方法从缓存获取数据
缓存未命中时从数据库查询并缓存结果
:return: 文章查询集
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
"""添加上下文数据,传递链接类型到模板"""
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
'''
首页
首页视图
显示最新的文章列表是博客的入口页面
'''
# 友情链接类型
# 友情链接类型:首页显示
link_type = LinkShowType.I
def get_queryset_data(self):
"""获取首页文章数据:所有已发布的普通文章(非页面)"""
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
"""生成首页缓存键index_页码支持分页缓存"""
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
文章详情页面视图
显示单篇文章的完整内容评论相关文章等信息
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
template_name = 'blog/article_detail.html' # 详情页模板
model = Article # 关联的文章模型
pk_url_kwarg = 'article_id' # URL中文章ID的参数名
context_object_name = "article" # 模板中文章对象的变量名
def get(self, request, *args, **kwargs):
"""重写get方法在获取文章时增加浏览量防刷机制"""
response = super().get(request, *args, **kwargs)
# 增加浏览量使用Cookie防刷
cookie_name = f'article_{self.object.id}_viewed'
if not request.COOKIES.get(cookie_name):
self.object.viewed() # 增加浏览量
response.set_cookie(cookie_name, 'true', max_age=60 * 60 * 24) # 24小时内不重复计数
return response
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:
@ -131,56 +179,84 @@ class ArticleDetailView(DetailView):
if page > paginator.num_pages:
page = paginator.num_pages
# 获取当前页的评论
p_comments = paginator.page(page)
# 生成分页导航URL
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
# 构建评论分页URL
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['related_posts'] = self.object.get_related_posts(4)
# 添加上下文数据
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['related_posts'] = self.object.get_related_posts(4) # 相关文章推荐
# 调用父类方法构建基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# 触发文章详情加载钩子,让插件可以添加额外的上下文数据
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request)
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# 添加点赞和收藏状态(用户相关功能)
user = self.request.user
if user.is_authenticated:
kwargs['is_liked'] = article.is_liked_by_user(user)
kwargs['is_favorited'] = article.is_favorited_by_user(user)
else:
kwargs['is_liked'] = False
kwargs['is_favorited'] = False
kwargs['like_count'] = article.get_like_count()
kwargs['favorite_count'] = article.get_favorite_count()
return context
class CategoryDetailView(ArticleListView):
'''
分类目录列表
分类目录列表视图
显示指定分类下的所有文章
'''
page_type = "分类目录归档"
page_type = "分类目录归档" # 页面类型标识
def get_queryset_data(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
"""获取分类下的文章数据,包括子分类的文章"""
slug = self.kwargs['category_name'] # 从URL获取分类slug
category = get_object_or_404(Category, slug=slug) # 获取分类对象
categoryname = category.name
self.categoryname = categoryname
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):
"""生成分类页缓存键category_list_分类名_页码"""
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
@ -190,89 +266,67 @@ class CategoryDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
"""添加上下文数据:页面类型和分类名称"""
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
categoryname = categoryname.split('/')[-1] # 处理多层分类名
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
kwargs['tag_name'] = categoryname # 在模板中统一使用tag_name变量
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
作者详情页视图
显示指定作者的所有文章
'''
page_type = '作者文章归档'
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get(self, request, *args, **kwargs):
"""重写get方法在获取文章时增加浏览量"""
response = super().get(request, *args, **kwargs)
# 增加浏览量使用Cookie防刷
cookie_name = f'article_{self.object.id}_viewed'
if not request.COOKIES.get(cookie_name):
self.object.viewed()
response.set_cookie(cookie_name, 'true', max_age=60 * 60 * 24) # 24小时
return response
page_type = '作者文章归档' # 页面类型标识
def get_queryset_cache_key(self):
"""生成作者页缓存键author_作者名_页码"""
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
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
kwargs['tag_name'] = author_name # 在模板中统一使用tag_name变量
return super(AuthorDetailView, self).get_context_data(**kwargs)
# 添加点赞和收藏状态
article = self.object
user = self.request.user
if user.is_authenticated:
kwargs['is_liked'] = article.is_liked_by_user(user)
kwargs['is_favorited'] = article.is_favorited_by_user(user)
else:
kwargs['is_liked'] = False
kwargs['is_favorited'] = False
kwargs['like_count'] = article.get_like_count()
kwargs['favorite_count'] = article.get_favorite_count()
class TagDetailView(ArticleListView):
'''
标签列表页面
标签列表页面视图
显示指定标签下的所有文章
'''
page_type = '分类标签归档'
page_type = '分类标签归档' # 页面类型标识
def get_queryset_data(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
"""获取标签下的所有已发布文章"""
slug = self.kwargs['tag_name'] # 从URL获取标签slug
tag = get_object_or_404(Tag, slug=slug) # 获取标签对象
tag_name = tag.name
self.name = tag_name
self.name = tag_name # 保存标签名供其他方法使用
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
tags__name=tag_name, type='a', status='p') # 多对多关系查询
return article_list
def get_queryset_cache_key(self):
"""生成标签页缓存键tag_标签名_页码"""
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@ -282,7 +336,7 @@ class TagDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
"""添加上下文数据:页面类型和标签名称"""
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
@ -291,39 +345,55 @@ class TagDetailView(ArticleListView):
class ArchivesView(ArticleListView):
'''
文章归档页面
文章归档页面视图
显示所有文章的按时间归档列表
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
page_type = '文章归档' # 页面类型标识
paginate_by = None # 归档页不分页,显示所有文章
page_kwarg = None # 无页码参数
template_name = 'blog/article_archives.html' # 专用模板
def get_queryset_data(self):
"""获取所有已发布文章(用于归档显示)"""
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
"""生成归档页缓存键archives单页不分页"""
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
"""
友情链接页面视图
显示所有启用的友情链接
"""
model = Links # 关联的友情链接模型
template_name = 'blog/links_list.html' # 链接页模板
def get_queryset(self):
"""获取所有启用的友情链接,按排序字段排列"""
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
"""
Elasticsearch搜索视图
提供全文搜索功能
"""
def get_context(self):
paginator, page = self.build_page()
"""构建搜索结果的上下文数据"""
paginator, page = self.build_page() # 构建分页
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
"query": self.query, # 搜索关键词
"form": self.form, # 搜索表单
"page": page, # 当前页数据
"paginator": paginator, # 分页器
"suggestion": None, # 搜索建议
}
# 添加拼写建议(如果搜索引擎支持)
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
@ -331,52 +401,72 @@ class EsSearchView(SearchView):
return context
@csrf_exempt
@csrf_exempt # 免除CSRF保护用于文件上传
def fileupload(request):
"""
文件上传接口图床功能
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
:param request: HTTP请求对象
:return: 上传文件的URL列表
"""
if request.method == 'POST':
# 验证签名,防止未授权上传
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
# 处理所有上传的文件
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
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)
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)
image.save(savepath, quality=20, optimize=True) # 压缩质量20%
url = static(savepath) # 生成静态文件URL
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
return HttpResponse("only for post") # 只支持POST请求
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
"""
404页面未找到错误处理视图
"""
if exception:
logger.error(exception)
logger.error(exception) # 记录错误日志
url = request.get_full_path()
return render(request,
template_name,
@ -386,6 +476,9 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'):
"""
500服务器错误处理视图
"""
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -397,8 +490,11 @@ def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
"""
403权限拒绝错误处理视图
"""
if exception:
logger.error(exception)
logger.error(exception) # 记录错误日志
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
@ -406,33 +502,44 @@ def permission_denied_view(
def clean_cache_view(request):
"""
清理缓存视图管理员功能
用于手动清除所有缓存
"""
cache.clear()
return HttpResponse('ok')
# blog/views.py - 添加以下视图
# ==================== 新增的点赞收藏功能视图 ====================
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from .models import Like, Favorite
@login_required
@login_required # 需要登录
def toggle_like(request, article_id):
"""点赞/取消点赞"""
"""
点赞/取消点赞功能
AJAX接口返回JSON响应
"""
if request.method == 'POST':
article = get_object_or_404(Article, id=article_id)
# 使用get_or_create实现切换功能
like, created = Like.objects.get_or_create(
user=request.user,
article=article
)
# 如果已存在(不是新创建的),则删除(取消点赞)
if not created:
like.delete()
is_liked = False
else:
is_liked = True
# 获取更新后的点赞数量
like_count = article.get_like_count()
return JsonResponse({
@ -446,21 +553,27 @@ def toggle_like(request, article_id):
@login_required
def toggle_favorite(request, article_id):
"""收藏/取消收藏"""
"""
收藏/取消收藏功能
AJAX接口返回JSON响应
"""
if request.method == 'POST':
article = get_object_or_404(Article, id=article_id)
# 使用get_or_create实现切换功能
favorite, created = Favorite.objects.get_or_create(
user=request.user,
article=article
)
# 如果已存在(不是新创建的),则删除(取消收藏)
if not created:
favorite.delete()
is_favorited = False
else:
is_favorited = True
# 获取更新后的收藏数量
favorite_count = article.get_favorite_count()
return JsonResponse({
@ -474,13 +587,21 @@ def toggle_favorite(request, article_id):
@login_required
def favorite_list(request):
"""用户的收藏列表"""
"""
用户的收藏列表页面
显示当前用户收藏的所有文章
"""
# 使用select_related优化查询避免N+1问题
favorites = Favorite.objects.filter(user=request.user).select_related('article')
return render(request, 'blog/favorite_list.html', {'favorites': favorites})
@login_required
def like_list(request):
"""用户的点赞列表"""
"""
用户的点赞列表页面
显示当前用户点赞的所有文章
"""
# 使用select_related优化查询避免N+1问题
likes = Like.objects.filter(user=request.user).select_related('article')
return render(request, 'blog/like_list.html', {'likes': likes})
return render(request, 'blog/like_list.html', {'likes': likes})

@ -1,3 +1,7 @@
"""
评论系统数据模型定义
支持多级评论回复功能
"""
from django.conf import settings
from django.db import models
from django.utils.timezone import now
@ -6,34 +10,55 @@ 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)
"""
评论模型
支持对文章的评论和评论之间的回复
"""
# 评论内容
body = models.TextField('正文', max_length=300) # 评论正文限制300字
# 时间戳
creation_time = models.DateTimeField(_('创建时间'), default=now)
last_modify_time = models.DateTimeField(_('最后修改时间'), default=now)
# 关联关系
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
verbose_name=_('作者'),
on_delete=models.CASCADE) # 用户删除时删除其所有评论
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
verbose_name=_('文章'),
on_delete=models.CASCADE) # 文章删除时删除其所有评论
# 父级评论,实现评论回复功能
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
'self', # 自关联
verbose_name=_('父级评论'),
blank=True,
null=True,
on_delete=models.CASCADE)
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
on_delete=models.CASCADE) # 父评论删除时删除子评论
# 评论状态
is_enable = models.BooleanField(
_('是否启用'),
default=False, # 默认需要审核
blank=False,
null=False
)
class Meta:
ordering = ['-id']
verbose_name = _('comment')
"""元数据配置"""
ordering = ['-id'] # 按ID降序新的评论在前
verbose_name = _('评论')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def __str__(self):
return self.body
"""
字符串表示
返回评论内容的前50个字符
"""
return self.body[:50] + '...' if len(self.body) > 50 else self.body

@ -1,4 +1,7 @@
# Create your views here.
"""
评论系统视图函数
处理评论的提交验证和显示
"""
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
@ -13,51 +16,70 @@ from .models import Comment
class CommentPostView(FormView):
form_class = CommentForm
template_name = 'blog/article_detail.html'
"""
评论提交视图
处理用户评论的提交和验证
"""
form_class = CommentForm # 评论表单类
template_name = 'blog/article_detail.html' # 错误时使用的模板
@method_decorator(csrf_protect)
@method_decorator(csrf_protect) # CSRF保护
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
"""处理GET请求重定向到文章详情页的评论区域"""
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
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)
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
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
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.parent_comment = parent_comment # 设置父评论
comment.save(True) # 保存评论到数据库
comment.save(True)
# 重定向到文章详情页的特定评论位置
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))
"%s#div-comment-%d" % # 使用锚点定位到新评论
(article.get_absolute_url(), comment.pk))

@ -1,78 +1,103 @@
"""djangoblog URL Configuration
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
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
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'))
"""
DjangoBlog项目主URL配置
定义项目的所有URL路由和全局配置
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.i18n import i18n_patterns # 国际化URL支持
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.contrib.sitemaps.views import sitemap # 站点地图
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from haystack.views import search_view_factory # 搜索视图工厂
from django.http import JsonResponse
import time
import time # 时间相关功能
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.admin_site import admin_site # 自定义Admin站点
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # ES搜索表单
from djangoblog.feeds import DjangoBlogFeed # RSS订阅
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# 站点地图配置
sitemaps = {
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
'blog': ArticleSiteMap, # 文章站点地图
'Category': CategorySiteMap, # 分类站点地图
'Tag': TagSiteMap, # 标签站点地图
'User': UserSiteMap, # 用户站点地图
'static': StaticViewSitemap # 静态页面站点地图
}
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
# 全局错误处理视图配置
handler404 = 'blog.views.page_not_found_view' # 404错误处理
handler500 = 'blog.views.server_error_view' # 500错误处理
handle403 = 'blog.views.permission_denied_view' # 403错误处理
def health_check(request):
"""
健康检查接口
简单返回服务健康状态
用于负载均衡健康检查和监控系统
:param request: HTTP请求对象
:return: JSON格式的健康状态响应
"""
return JsonResponse({
'status': 'healthy',
'timestamp': time.time()
'status': 'healthy', # 服务状态
'timestamp': time.time() # 当前时间戳
})
# 基础URL模式不包含语言前缀
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
path('health/', health_check, name='health_check'),
path('i18n/', include('django.conf.urls.i18n')), # 国际化URL支持
path('health/', health_check, name='health_check'), # 健康检查端点
]
# 国际化URL模式包含语言前缀
urlpatterns += i18n_patterns(
# 管理员后台
re_path(r'^admin/', admin_site.urls),
# 博客应用URL
re_path(r'', include('blog.urls', namespace='blog')),
# Markdown编辑器URL
re_path(r'mdeditor/', include('mdeditor.urls')),
# 评论系统URL
re_path(r'', include('comments.urls', namespace='comment')),
# 用户账户URL
re_path(r'', include('accounts.urls', namespace='account')),
# OAuth认证URL
re_path(r'', include('oauth.urls', namespace='oauth')),
# 站点地图URL
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
# RSS订阅源
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
# 搜索功能使用Elasticsearch
re_path('^search', search_view_factory(
view_class=EsSearchView,
form_class=ElasticSearchModelSearchForm),
name='search'),
# 服务器管理URL
re_path(r'', include('servermanager.urls', namespace='servermanager')),
# OwnTracks位置追踪URL
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
, prefix_default_language=False) # 不强制默认语言前缀
# 静态文件服务(开发环境)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# 媒体文件服务(开发环境)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
document_root=settings.MEDIA_ROOT)
Loading…
Cancel
Save