diff --git a/src/DjangoBlog/accounts/models.py b/src/DjangoBlog/accounts/models.py
index 3baddbb..21f4f2c 100644
--- a/src/DjangoBlog/accounts/models.py
+++ b/src/DjangoBlog/accounts/models.py
@@ -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' # 获取最新记录的依据字段
\ No newline at end of file
diff --git a/src/DjangoBlog/accounts/views.py b/src/DjangoBlog/accounts/views.py
index ae67aec..9bcfb23 100644
--- a/src/DjangoBlog/accounts/views.py
+++ b/src/DjangoBlog/accounts/views.py
@@ -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 = """
-
请点击下面链接验证您的邮箱
-
- {url}
-
- 再次感谢您!
-
- 如果上面链接无法打开,请将此链接复制至浏览器。
- {url}
- """.format(url=url)
+ 请点击下面链接验证您的邮箱
+ {url}
+ 再次感谢您!
+
+ 如果上面链接无法打开,请将此链接复制至浏览器。
+ {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") # 发送成功
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/models.py b/src/DjangoBlog/blog/models.py
index 579d908..463490c 100644
--- a/src/DjangoBlog/blog/models.py
+++ b/src/DjangoBlog/blog/models.py
@@ -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}"
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/urls.py b/src/DjangoBlog/blog/urls.py
index cb79f4f..f65ae28 100644
--- a/src/DjangoBlog/blog/urls.py
+++ b/src/DjangoBlog/blog/urls.py
@@ -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//',
- views.IndexView.as_view(),
- name='index_page'),
+ # 首页
+ path(r'', views.IndexView.as_view(), name='index'),
+
+ # 首页分页
+ path(r'page//', views.IndexView.as_view(), name='index_page'),
+
+ # 文章详情页(SEO友好URL:/年/月/日/文章ID.html)
path(
r'article////.html',
views.ArticleDetailView.as_view(),
- name='detailbyid'),
+ name='detailbyid'
+ ),
+
+ # 分类详情页
path(
r'category/.html',
views.CategoryDetailView.as_view(),
- name='category_detail'),
+ name='category_detail'
+ ),
+
+ # 分类分页
path(
r'category//.html',
views.CategoryDetailView.as_view(),
- name='category_detail_page'),
+ name='category_detail_page'
+ ),
+
+ # 作者详情页
path(
r'author/.html',
views.AuthorDetailView.as_view(),
- name='author_detail'),
+ name='author_detail'
+ ),
+
+ # 作者文章分页
path(
r'author//.html',
views.AuthorDetailView.as_view(),
- name='author_detail_page'),
+ name='author_detail_page'
+ ),
+
+ # 标签详情页
path(
r'tag/.html',
views.TagDetailView.as_view(),
- name='tag_detail'),
+ name='tag_detail'
+ ),
+
+ # 标签分页
path(
r'tag//.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//',
- views.toggle_like,
- name='toggle_like'),
- path(
- 'favorite//',
- 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//', views.toggle_like, name='toggle_like'),
+
+ # 收藏/取消收藏
+ path('favorite//', views.toggle_favorite, name='toggle_favorite'),
+
+ # 用户收藏列表
+ path('favorites/', views.favorite_list, name='favorite_list'),
+
+ # 用户点赞列表
+ path('likes/', views.like_list, name='like_list'),
+]
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/views.py b/src/DjangoBlog/blog/views.py
index 49dda78..35be4ad 100644
--- a/src/DjangoBlog/blog/views.py
+++ b/src/DjangoBlog/blog/views.py
@@ -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})
\ No newline at end of file
diff --git a/src/DjangoBlog/comments/models.py b/src/DjangoBlog/comments/models.py
index 7c3bbc8..3dd008b 100644
--- a/src/DjangoBlog/comments/models.py
+++ b/src/DjangoBlog/comments/models.py
@@ -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
\ No newline at end of file
diff --git a/src/DjangoBlog/comments/views.py b/src/DjangoBlog/comments/views.py
index ad9b2b9..b96426d 100644
--- a/src/DjangoBlog/comments/views.py
+++ b/src/DjangoBlog/comments/views.py
@@ -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))
\ No newline at end of file
diff --git a/src/DjangoBlog/djangoblog/urls.py b/src/DjangoBlog/djangoblog/urls.py
index 6a9e1de..48aa069 100644
--- a/src/DjangoBlog/djangoblog/urls.py
+++ b/src/DjangoBlog/djangoblog/urls.py
@@ -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)
\ No newline at end of file