yxy添加 modles.py 注释

yxy_branch
严欣怡 4 months ago
parent e3a3a41407
commit 5fc3f6db58

@ -1,123 +1,161 @@
# 导入日志模块,用于记录系统运行日志
import logging
# 导入正则表达式模块用于处理文本中的匹配如提取图片URL
import re
# 导入抽象基类相关工具,用于定义抽象方法
from abc import abstractmethod
# 导入Django配置、异常、模型、URL反转等核心功能
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _ # 国际化翻译工具
# 导入markdown编辑器字段用于文章内容编辑
from mdeditor.fields import MDTextField
# 导入slug生成工具用于生成URL友好的标识符
from uuslug import slugify
# 导入自定义工具:缓存装饰器、缓存操作、获取当前站点信息
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
# 创建当前模块的日志记录器
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', _('index')) # 首页展示
L = ('l', _('list')) # 列表页展示
P = ('p', _('post')) # 文章详情页展示
A = ('a', _('all')) # 所有页面展示
S = ('s', _('slide')) # 幻灯片展示
class BaseModel(models.Model):
"""
模型基类所有其他模型的父类
封装通用字段和方法减少代码重复
"""
# 自增主键ID
id = models.AutoField(primary_key=True)
# 创建时间字段,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间字段,默认为当前时间
last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs):
"""
重写保存方法扩展功能
1. 单独处理文章阅读量更新避免更新其他字段
2. 自动生成slugURL友好标识符
"""
# 判断是否是更新文章阅读量的操作
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字段则自动生成slug基于title或name字段
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
# 优先使用title字段否则使用name字段作为slug源
slug_source = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name')
setattr(self, 'slug', slugify(slug_source)) # 生成URL友好的slug
# 调用父类保存方法
super().save(*args, **kwargs)
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
"""生成包含域名的完整URL用于外部链接或分享"""
site_domain = get_current_site().domain # 获取当前站点域名
full_url = f"https://{site_domain}{self.get_absolute_url()}"
return full_url
class Meta:
abstract = True
abstract = True # 声明为抽象基类,不生成数据库表
@abstractmethod
def get_absolute_url(self):
"""
抽象方法获取模型实例的相对URL
子类必须实现用于生成详情页链接
"""
pass
class Article(BaseModel):
"""文章"""
"""
文章模型
存储博客文章的核心信息
"""
# 文章状态选项:草稿/已发布
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
('d', _('Draft')), # 草稿
('p', _('Published')), # 已发布
)
# 评论状态选项:开启/关闭
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
('o', _('Open')), # 允许评论
('c', _('Close')), # 关闭评论
)
# 内容类型选项:文章/页面(如关于页、联系页)
TYPE = (
('a', _('Article')),
('p', _('Page')),
('a', _('Article')), # 普通文章
('p', _('Page')), # 独立页面
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题(唯一)
body = MDTextField(_('body')) # 文章内容markdown格式
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
_('publish time'), blank=False, null=False, default=now) # 发布时间
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
_('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)
_('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(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
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)
_('order'), blank=False, null=False, default=0) # 文章排序权重
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
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=_('tag'), 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 = _('article') # 模型显示名称(单数)
verbose_name_plural = verbose_name # 模型显示名称(复数)
get_latest_by = 'id' # 按id获取最新记录
def get_absolute_url(self):
"""生成文章详情页的相对URL"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -125,160 +163,174 @@ class Article(BaseModel):
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""获取当前文章所属分类的层级树(含父级分类)"""
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
# 转换为 (分类名称, 分类URL) 的列表
return [(c.name, c.get_absolute_url()) for c in tree]
def save(self, *args, **kwargs):
"""重写保存方法(可扩展,此处直接调用父类方法)"""
super().save(*args, **kwargs)
def viewed(self):
"""增加阅读量并保存仅更新views字段"""
self.views += 1
self.save(update_fields=['views'])
self.save(update_fields=['views']) # 只更新views字段优化性能
def comment_list(self):
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:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
"""获取当前文章的有效评论列表(带缓存)"""
cache_key = f'article_comments_{self.id}'
# 尝试从缓存获取
cached_comments = cache.get(cache_key)
if cached_comments:
logger.info(f'从缓存获取文章评论: {self.id}')
return cached_comments
# 缓存未命中,从数据库查询
comments = self.comment_set.filter(is_enable=True).order_by('-id') # 按ID降序最新在前
cache.set(cache_key, comments, 60 * 100) # 缓存100分钟
logger.info(f'缓存文章评论: {self.id}')
return comments
def get_admin_url(self):
"""获取后台管理编辑页面的URL"""
# 自动获取模型的app标签和模型名称
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
return reverse(f'admin:{info[0]}_{info[1]}_change', args=(self.pk,))
@cache_decorator(expiration=60 * 100)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
"""获取下一篇文章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用于封面图等场景"""
# 正则匹配markdown图片格式: ![描述](URL)
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
return match.group(1) # 返回匹配到的URL
return "" # 无图片时返回空
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
"""
分类模型
用于文章的分类管理支持层级结构父分类
"""
name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称(唯一)
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
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=_('index')) # 排序权重(值越大越靠前)
class Meta:
ordering = ['-index']
ordering = ['-index'] # 按排序权重降序排列
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
"""生成分类详情页的相对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:
递归获取当前分类的层级树含所有父级分类
例如子分类 -> 父分类 -> 顶级分类
"""
categorys = []
def parse(category):
categorys.append(category)
if category.parent_category:
if category.parent_category: # 若存在父分类,继续递归
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
递归获取当前分类的所有子分类含多级子分类
"""
categorys = []
all_categorys = Category.objects.all()
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:
if child not in categorys:
categorys.append(child)
parse(child)
parse(child) # 递归处理子分类
parse(self)
return categorys
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(_('tag name'), 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']
ordering = ['name'] # 按名称升序排列
verbose_name = _('tag')
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)
"""
友情链接模型
存储网站的友情链接信息
"""
name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称(唯一)
link = models.URLField(_('link')) # 链接URL
sequence = models.IntegerField(_('order'), unique=True) # 排序序号(唯一,用于控制显示顺序)
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
_('is show'), default=True, blank=False, null=False) # 是否启用(显示)
show_type = models.CharField(
_('show type'),
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)
default=LinkShowType.I # 默认为首页展示
)
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
ordering = ['sequence']
ordering = ['sequence'] # 按排序序号升序排列
verbose_name = _('link')
verbose_name_plural = verbose_name
@ -287,16 +339,19 @@ 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)
"""
侧边栏模型
用于展示网站侧边栏内容支持HTML
"""
name = models.CharField(_('title'), max_length=100) # 侧边栏标题
content = models.TextField(_('content')) # 侧边栏内容HTML格式
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)
class Meta:
ordering = ['sequence']
ordering = ['sequence'] # 按排序序号升序排列
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
@ -305,59 +360,38 @@ class SideBar(models.Model):
class BlogSettings(models.Model):
"""blog的配置"""
"""
博客配置模型
存储网站的全局设置单例模式仅允许一条记录
"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
_('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 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 seo description'), max_length=1000, null=False, blank=False, default='') # SEO描述
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)
_('site keywords'), max_length=1000, null=False, blank=False, default='') # 网站关键词SEO
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='')
_('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='') # 全局头部HTML代码
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部HTML代码
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
'备案号', max_length=2000, null=True, blank=True, default='') # 网站备案号
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
"网站统计代码", max_length=1000, null=False, blank=False, default='') # 统计代码(如百度统计)
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
'是否显示公安备案号', default=False, null=False) # 是否显示公安备案号
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
'公安备案号', max_length=2000, null=True, blank=True, default='') # 公安备案号
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
'评论是否需要审核', default=False, null=False) # 评论是否需要审核后显示
class Meta:
verbose_name = _('Website configuration')
@ -367,10 +401,15 @@ class BlogSettings(models.Model):
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(_('There can only be one configuration')) # 仅允许一个配置记录
def save(self, *args, **kwargs):
"""保存配置后清除缓存(确保配置实时生效)"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
cache.clear() # 清除所有缓存
Loading…
Cancel
Save