|
|
|
|
@ -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}"
|