|
|
# 导入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=True(Meta类中)表示该模型为抽象模型,不会生成数据库表,仅用于被子类继承
|
|
|
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)
|
|
|
# 分类slug:URL友好标识,默认值'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)
|
|
|
# 标签slug:URL友好标识,默认值'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)
|
|
|
# 链接URL:URLField类型,自动验证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() # 清空所有缓存 |