You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Django/doc/blog/models.py

515 lines
26 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 导入Python内置logging模块用于记录模型操作相关日志如缓存命中/设置、数据验证错误)
import logging
# 从abc模块导入abstractmethod装饰器用于定义抽象方法强制子类实现
from abc import abstractmethod
# 导入Django配置模块用于获取项目配置如AUTH_USER_MODEL
from django.conf import settings
# 导入Django数据验证异常类用于自定义数据验证逻辑如博客配置唯一性校验
from django.core.exceptions import ValidationError
# 导入Django模型核心模块用于定义数据模型对应数据库表
from django.db import models
# 导入Django URL反向解析模块用于生成模型的绝对URL
from django.urls import reverse
# 导入Django时区工具用于处理时间字段确保时间戳一致性
from django.utils.timezone import now
# 导入Django国际化翻译工具用于模型字段/选项的多语言支持
from django.utils.translation import gettext_lazy as _
# 导入MDTextField字段来自mdeditor库用于支持Markdown格式的富文本编辑
from mdeditor.fields import MDTextField
# 导入uuslug库的slugify函数用于将中文标题/名称转换为URL友好的slug如"我的博客"→"wo-de-bo-ke"
from uuslug import slugify
# 从自定义工具模块导入缓存相关工具:
# 1. cache_decorator缓存装饰器用于缓存函数返回结果
# 2. cache缓存操作对象用于直接读写缓存
from djangoblog.utils import cache_decorator, cache
# 从自定义工具模块导入获取当前站点信息的函数用于生成完整URL
from djangoblog.utils import get_current_site
# 创建日志记录器日志名称与当前模块一致__name__便于定位日志来源
logger = logging.getLogger(__name__)
# 定义链接显示类型枚举类LinkShowType继承自Django的TextChoices枚举基类
# 作用:规范友情链接的显示位置选项,避免硬编码字符串
class LinkShowType(models.TextChoices):
I = ('i', _('index')) # 首页显示
L = ('l', _('list')) # 列表页显示
P = ('p', _('post')) # 文章详情页显示
A = ('a', _('all')) # 所有页面显示
S = ('s', _('slide')) # 幻灯片区域显示
# 定义抽象基础模型类BaseModel继承自Django的models.Model
# 作用封装所有模型共有的字段和方法如创建时间、修改时间、URL生成避免代码重复
# 注abstract=TrueMeta类中表示该模型为抽象模型不会生成数据库表仅用于被子类继承
class BaseModel(models.Model):
# 主键ID自增整数类型Django默认主键此处显式定义以统一规范
id = models.AutoField(primary_key=True)
# 创建时间DateTimeField类型默认值为当前时间now()支持国际化显示_('creation time')
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间DateTimeField类型默认值为当前时间用于记录数据更新时间
last_modify_time = models.DateTimeField(_('modify time'), default=now)
# 重写save方法扩展保存逻辑处理slug生成和浏览量更新优化
def save(self, *args, **kwargs):
# 判断是否为Article模型的浏览量更新操作
# 1. 实例是Article类的实例
# 2. save方法传入了update_fields参数
# 3. 仅更新views字段浏览量
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
# 如果是浏览量单独更新直接执行SQL更新避免触发完整save流程提升性能
if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views)
# 非浏览量更新场景,执行正常保存逻辑
else:
# 判断当前模型是否有slug字段需要生成URL友好标识的模型如Category、Tag
if 'slug' in self.__dict__:
# 确定slug的生成源优先取title字段如Article无则取name字段如Category、Tag
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
# 调用slugify函数生成slug并赋值给当前实例的slug字段
setattr(self, 'slug', slugify(slug))
# 调用父类的save方法完成数据入库必须调用否则数据不会保存
super().save(*args, **kwargs)
# 生成模型实例的完整URL含域名用于前端跳转、SEO等场景
def get_full_url(self):
# 获取当前站点的域名(如"www.example.com"通过get_current_site工具函数
site = get_current_site().domain
# 拼接完整URL协议默认https+ 域名 + 实例的相对URL通过get_absolute_url获取
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
# 模型元数据配置
class Meta:
abstract = True # 标记为抽象模型,不生成数据库表
# 定义抽象方法get_absolute_url强制子类实现
# 作用每个具体模型必须提供自己的相对URL生成逻辑如文章详情页URL、分类页URL
@abstractmethod
def get_absolute_url(self):
pass
# 定义文章模型Article继承自抽象基础模型BaseModel
class Article(BaseModel):
"""文章模型:存储博客文章/页面数据(如博客文章、关于页、联系页等)"""
# 文章状态选项:元组形式,每个元素为(存储值,显示文本),支持国际化
STATUS_CHOICES = (
('d', _('Draft')), # 'd':草稿状态
('p', _('Published')),# 'p':已发布状态
)
# 评论状态选项:控制文章是否允许评论
COMMENT_STATUS = (
('o', _('Open')), # 'o':开放评论
('c', _('Close')), # 'c':关闭评论
)
# 文章类型选项:区分普通文章和独立页面
TYPE = (
('a', _('Article')), # 'a':普通文章(如博客博文)
('p', _('Page')), # 'p':独立页面(如关于页、隐私政策页)
)
# 文章标题CharField类型最大长度200唯一约束避免重复标题
title = models.CharField(_('title'), max_length=200, unique=True)
# 文章内容MDTextField类型支持Markdown格式编辑富文本
body = MDTextField(_('body'))
# 发布时间DateTimeField类型必填默认值为当前时间用于控制文章发布时间点
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
# 文章状态CharField类型长度1可选值为STATUS_CHOICES默认已发布'p'
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
# 评论状态CharField类型长度1可选值为COMMENT_STATUS默认开放评论'o'
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
# 文章类型CharField类型长度1可选值为TYPE默认普通文章'a'
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
# 浏览量PositiveIntegerField类型仅允许非负整数默认0
views = models.PositiveIntegerField(_('views'), default=0)
# 作者外键关联Django用户模型settings.AUTH_USER_MODEL兼容自定义用户模型
# on_delete=models.CASCADE用户被删除时关联的文章也会被删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
# 文章排序权重IntegerField类型默认0用于自定义文章显示顺序值越大越靠前
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
# 是否显示目录BooleanField类型默认False控制文章详情页是否显示TOC目录
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
# 分类外键关联Category模型自应用内的分类模型
# on_delete=models.CASCADE分类被删除时关联的文章也会被删除
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
# 标签多对多关联Tag模型一篇文章可多个标签一个标签可关联多篇文章允许为空
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
# 辅助方法:返回文章内容字符串(用于需要直接获取纯文本内容的场景)
def body_to_string(self):
return self.body
# 重写__str__方法后台管理界面和打印实例时显示文章标题友好显示
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' # 指定获取最新记录的字段按ID降序
# 实现抽象基类的get_absolute_url方法生成文章的相对URL
def get_absolute_url(self):
# 反向解析'blog:detailbyid'路由传递文章ID、发布年月日作为URL参数
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
# 缓存装饰器缓存结果10小时60*60*10秒避免重复查询数据库
@cache_decorator(60 * 60 * 10)
# 获取文章分类的层级关系(如"技术→Python→Django"
def get_category_tree(self):
# 调用分类模型的get_category_tree方法获取当前文章分类的所有父级分类
tree = self.category.get_category_tree()
# 转换为分类名称分类URL的元组列表用于前端显示分类面包屑
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
# 重写save方法此处仅调用父类方法便于后续扩展自定义逻辑
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# 文章浏览量递增方法:用于文章详情页访问时更新浏览量
def viewed(self):
self.views += 1 # 浏览量+1
# 仅更新views字段通过update_fields参数优化避免更新其他字段
self.save(update_fields=['views'])
# 获取文章的评论列表(已启用的评论)
def comment_list(self):
# 定义缓存键包含文章ID确保不同文章的评论缓存不冲突
cache_key = 'article_comments_{id}'.format(id=self.id)
# 尝试从缓存获取评论列表
value = cache.get(cache_key)
if value:
# 缓存命中:记录日志,直接返回缓存的评论列表
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
# 缓存未命中查询当前文章的已启用评论按ID降序最新评论在前
comments = self.comment_set.filter(is_enable=True).order_by('-id')
# 将评论列表存入缓存有效期100分钟60*100秒
cache.set(cache_key, comments, 60 * 100)
# 记录日志:缓存设置成功
logger.info('set article comments:{id}'.format(id=self.id))
return comments
# 生成文章在Django后台的编辑页URL用于快速跳转到后台编辑
def get_admin_url(self):
# 获取模型的元数据:(应用名,模型名)
info = (self._meta.app_label, self._meta.model_name)
# 反向解析admin的模型修改路由传递文章主键
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
# 缓存装饰器缓存结果100分钟
@cache_decorator(expiration=60 * 100)
# 获取当前文章的下一篇文章已发布状态ID大于当前文章
def next_article(self):
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
# 缓存装饰器缓存结果100分钟
@cache_decorator(expiration=60 * 100)
# 获取当前文章的前一篇文章已发布状态ID小于当前文章
def prev_article(self):
return Article.objects.filter(id__lt=self.id, status='p').first()
# 定义分类模型Category继承自抽象基础模型BaseModel
class Category(BaseModel):
"""文章分类模型:存储博客文章的分类数据(支持层级分类,如父分类→子分类)"""
# 分类名称CharField类型最大长度30唯一约束避免重复分类名
name = models.CharField(_('category name'), max_length=30, unique=True)
# 父分类:自关联外键(分类可作为其他分类的父分类),允许为空(顶级分类)
# on_delete=models.CASCADE父分类被删除时子分类也会被删除
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
# 分类slugURL友好标识默认值'no-slug'用于生成分类页URL
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# 排序索引IntegerField类型默认0用于控制分类显示顺序值越大越靠前
index = models.IntegerField(default=0, verbose_name=_('index'))
# 模型元数据配置
class Meta:
ordering = ['-index'] # 默认排序:按排序索引降序
verbose_name = _('category') # 模型单数显示名称
verbose_name_plural = verbose_name # 模型复数显示名称
# 实现抽象基类的get_absolute_url方法生成分类的相对URL
def get_absolute_url(self):
# 反向解析'blog:category_detail'路由传递分类slug作为参数
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
# 重写__str__方法友好显示分类名称
def __str__(self):
return self.name
# 缓存装饰器缓存结果10小时
@cache_decorator(60 * 60 * 10)
# 递归获取当前分类的所有父级分类(生成分类层级树,如子分类→父分类→顶级分类)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return: 分类实例列表(当前分类 + 所有父级分类)
"""
categorys = []
# 内部递归函数:解析分类的父级
def parse(category):
categorys.append(category) # 将当前分类加入列表
if category.parent_category: # 如果存在父分类,继续递归
parse(category.parent_category)
parse(self) # 从当前分类开始解析
return categorys
# 缓存装饰器缓存结果10小时
@cache_decorator(60 * 60 * 10)
# 递归获取当前分类的所有子级分类(包括子分类的子分类)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return: 分类实例列表(当前分类 + 所有子级分类)
"""
categorys = []
all_categorys = Category.objects.all() # 获取所有分类
# 内部递归函数:解析分类的子级
def parse(category):
if category not in categorys: # 避免重复添加(防止循环引用)
categorys.append(category)
# 查询当前分类的直接子分类
childs = all_categorys.filter(parent_category=category)
for child in childs: # 遍历子分类,递归解析
if category not in categorys:
categorys.append(child)
parse(child)
parse(self) # 从当前分类开始解析
return categorys
# 定义标签模型Tag继承自抽象基础模型BaseModel
class Tag(BaseModel):
"""文章标签模型:存储博客文章的标签数据(用于文章分类和搜索)"""
# 标签名称CharField类型最大长度30唯一约束避免重复标签名
name = models.CharField(_('tag name'), max_length=30, unique=True)
# 标签slugURL友好标识默认值'no-slug'用于生成标签页URL
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# 重写__str__方法友好显示标签名称
def __str__(self):
return self.name
# 实现抽象基类的get_absolute_url方法生成标签的相对URL
def get_absolute_url(self):
# 反向解析'blog:tag_detail'路由传递标签slug作为参数
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
# 缓存装饰器缓存结果10小时
@cache_decorator(60 * 60 * 10)
# 获取当前标签关联的文章数量(去重,避免重复计数)
def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count()
# 模型元数据配置
class Meta:
ordering = ['name'] # 默认排序:按标签名称升序
verbose_name = _('tag') # 模型单数显示名称
verbose_name_plural = verbose_name # 模型复数显示名称
# 定义友情链接模型Links未继承BaseModel单独定义时间字段
class Links(models.Model):
"""友情链接模型:存储博客的友情链接数据"""
# 链接名称CharField类型最大长度30唯一约束避免重复链接名
name = models.CharField(_('link name'), max_length=30, unique=True)
# 链接URLURLField类型自动验证URL格式如http://、https://
link = models.URLField(_('link'))
# 排序序号IntegerField类型唯一约束控制友情链接显示顺序值越小越靠前
sequence = models.IntegerField(_('order'), unique=True)
# 是否启用BooleanField类型默认True控制是否在前端显示该链接
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
# 显示位置CharField类型长度1可选值为LinkShowType枚举默认首页显示'i'
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
# 创建时间DateTimeField类型默认当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间DateTimeField类型默认当前时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
# 模型元数据配置
class Meta:
ordering = ['sequence'] # 默认排序:按排序序号升序
verbose_name = _('link') # 模型单数显示名称
verbose_name_plural = verbose_name # 模型复数显示名称
# 重写__str__方法友好显示链接名称
def __str__(self):
return self.name
# 定义侧边栏模型SideBar未继承BaseModel单独定义时间字段
class SideBar(models.Model):
"""侧边栏模型存储博客侧边栏内容支持自定义HTML内容如公告、广告"""
# 侧边栏标题CharField类型最大长度100
name = models.CharField(_('title'), max_length=100)
# 侧边栏内容TextField类型支持HTML文本如公告、推荐文章列表
content = models.TextField(_('content'))
# 排序序号IntegerField类型唯一约束控制侧边栏显示顺序值越小越靠前
sequence = models.IntegerField(_('order'), unique=True)
# 是否启用BooleanField类型默认True控制是否在前端显示该侧边栏
is_enable = models.BooleanField(_('is enable'), default=True)
# 创建时间DateTimeField类型默认当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间DateTimeField类型默认当前时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
# 模型元数据配置
class Meta:
ordering = ['sequence'] # 默认排序:按排序序号升序
verbose_name = _('sidebar') # 模型单数显示名称
verbose_name_plural = verbose_name # 模型复数显示名称
# 重写__str__方法友好显示侧边栏标题
def __str__(self):
return self.name
# 定义博客配置模型BlogSettings
class BlogSettings(models.Model):
"""博客全局配置模型存储博客的全局设置如站点名称、SEO信息、备案号等"""
# 站点名称CharField类型必填默认空字符串
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
# 站点描述TextField类型必填用于前端显示站点简介如首页底部
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
# 站点SEO描述TextField类型必填用于网页meta标签的description提升搜索引擎排名
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
# 站点关键词TextField类型必填用于网页meta标签的keywords提升搜索引擎排名
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
# 文章摘要长度IntegerField类型默认300控制前端显示文章摘要的字符数
article_sub_length = models.IntegerField(_('article sub length'), default=300)
# 侧边栏文章数量IntegerField类型默认10控制侧边栏显示的最新/热门文章数量
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
# 侧边栏评论数量IntegerField类型默认5控制侧边栏显示的最新评论数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
# 文章页评论数量IntegerField类型默认5控制文章详情页默认显示的评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5)
# 是否显示谷歌广告BooleanField类型默认False控制是否在前端显示谷歌广告
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
# 谷歌广告代码TextField类型可选存储谷歌广告的HTML代码
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
# 是否开放全站评论BooleanField类型默认True控制整个站点是否允许评论
open_site_comment = models.BooleanField(_('open site comment'), default=True)
# 公共头部代码TextField类型可选存储全局头部的自定义HTML如额外CSS、JS
global_header = models.TextField("公共头部", null=True, blank=True, default='')
# 公共尾部代码TextField类型可选存储全局尾部的自定义HTML如备案信息、统计代码
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
# 备案号CharField类型可选存储网站ICP备案号如"粤ICP备xxxx号"
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
# 网站统计代码TextField类型必填存储统计工具的JS代码如百度统计、谷歌分析
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
# 是否显示公安备案号BooleanField类型默认False控制是否显示公安备案信息
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
# 公安备案号TextField类型可选存储公安备案号如"粤公网安备xxxx号"
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
# 评论是否需要审核BooleanField类型默认False控制用户提交的评论是否需管理员审核后显示
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
# 模型元数据配置
class Meta:
verbose_name = _('Website configuration') # 模型单数显示名称(网站配置)
verbose_name_plural = verbose_name # 模型复数显示名称
# 重写__str__方法友好显示站点名称
def __str__(self):
return self.site_name
# 自定义数据验证方法:确保博客配置只能有一条记录(全局唯一配置)
def clean(self):
# 排除当前实例ID后查询是否已有其他配置记录
if BlogSettings.objects.exclude(id=self.id).count():
# 若存在其他记录,抛出验证错误(阻止保存)
raise ValidationError(_('There can only be one configuration'))
# 重写save方法保存配置后清空缓存确保前端能立即获取最新配置
def save(self, *args, **kwargs):
super().save(*args, **kwargs) # 调用父类save方法完成数据入库
from djangoblog.utils import cache # 延迟导入缓存模块,避免循环导入
cache.clear() # 清空所有缓存