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.
tentest/doc/DjangoBlog/blog/models.py

654 lines
17 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.

import logging
import re
from abc import abstractmethod
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 mdeditor.fields import MDTextField
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):
"""
mk:
友情链接显示类型枚举
属性:
I: 首页显示
L: 列表页显示
P: 文章页显示
A: 全部页面显示
S: 幻灯片显示
"""
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
class BaseModel(models.Model):
"""
mk:
基础模型类,提供通用字段和方法
属性:
id: 主键ID
creation_time: 创建时间
last_modify_time: 最后修改时间
"""
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):
"""
mk:
保存模型实例
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
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:
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs)
def get_full_url(self):
"""
mk:
获取完整的URL地址
Returns:
str: 完整的URL地址
"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True
@abstractmethod
def get_absolute_url(self):
"""
mk:
抽象方法获取绝对URL地址
Returns:
str: 绝对URL地址
"""
pass
class Article(BaseModel):
"""
mk:
文章模型类
属性:
title: 标题
body: 正文内容
pub_time: 发布时间
status: 状态(草稿/发布)
comment_status: 评论状态(开启/关闭)
type: 类型(文章/页面)
views: 浏览量
author: 作者
article_order: 排序
show_toc: 是否显示目录
category: 分类
tags: 标签
"""
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
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(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
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(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self):
"""
mk:
将正文内容转换为字符串
Returns:
str: 正文内容字符串
"""
return self.body
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 文章标题
"""
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def get_absolute_url(self):
"""
mk:
获取文章详情页的绝对URL
Returns:
str: 文章详情页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
})
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
mk:
获取分类树结构
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):
"""
mk:
保存文章实例
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
super().save(*args, **kwargs)
def viewed(self):
"""
mk:
增加文章浏览量并保存
"""
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
"""
mk:
获取文章评论列表,带缓存功能
Returns:
QuerySet: 评论查询集
"""
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
def get_admin_url(self):
"""
mk:
获取后台管理URL
Returns:
str: 后台管理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)
def next_article(self):
"""
mk:
获取下一篇已发布的文章
Returns:
Article|None: 下一篇文章对象或None
"""
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
"""
mk:
获取上一篇已发布的文章
Returns:
Article|None: 上一篇文章对象或None
"""
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
mk:
从文章正文中提取第一张图片的URL
Returns:
str: 第一张图片的URL如果没有找到则返回空字符串
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
class Category(BaseModel):
"""
mk:
文章分类模型类
属性:
name: 分类名称
parent_category: 父级分类
slug: URL别名
index: 排序索引
"""
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'))
class Meta:
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
"""
mk:
获取分类详情页的绝对URL
Returns:
str: 分类详情页URL
"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 分类名称
"""
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
mk:
递归获取分类目录的父级分类树
Returns:
list: 包含当前分类及其所有父级分类的列表
"""
categorys = []
def parse(category):
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
mk:
获取当前分类目录的所有子分类
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)
for child in childs:
if category not in categorys:
categorys.append(child)
parse(child)
parse(self)
return categorys
class Tag(BaseModel):
"""
mk:
文章标签模型类
属性:
name: 标签名称
slug: URL别名
"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 标签名称
"""
return self.name
def get_absolute_url(self):
"""
mk:
获取标签详情页的绝对URL
Returns:
str: 标签详情页URL
"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
"""
mk:
获取使用该标签的文章数量
Returns:
int: 文章数量
"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model):
"""
mk:
友情链接模型类
属性:
name: 链接名称
link: 链接地址
sequence: 排序
is_enable: 是否启用
show_type: 显示类型
creation_time: 创建时间
last_mod_time: 最后修改时间
"""
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'),
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)
class Meta:
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 链接名称
"""
return self.name
class SideBar(models.Model):
"""
mk:
侧边栏模型类用于展示HTML内容
属性:
name: 标题
content: 内容
sequence: 排序
is_enable: 是否启用
creation_time: 创建时间
last_mod_time: 最后修改时间
"""
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)
class Meta:
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 侧边栏标题
"""
return self.name
class BlogSettings(models.Model):
"""
mk:
博客配置模型类
属性:
site_name: 网站名称
site_description: 网站描述
site_seo_description: SEO描述
site_keywords: SEO关键词
article_sub_length: 文章摘要长度
sidebar_article_count: 侧边栏文章数量
sidebar_comment_count: 侧边栏评论数量
article_comment_count: 文章评论数量
show_google_adsense: 是否显示Google广告
google_adsense_codes: Google广告代码
open_site_comment: 是否开启网站评论
global_header: 公共头部代码
global_footer: 公共尾部代码
beian_code: 备案号
analytics_code: 网站统计代码
show_gongan_code: 是否显示公安备案号
gongan_beiancode: 公安备案号
comment_need_review: 评论是否需要审核
"""
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)
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
def __str__(self):
"""
mk:
返回对象的字符串表示
Returns:
str: 网站名称
"""
return self.site_name
def clean(self):
"""
mk:
数据验证,确保只存在一个配置实例
Raises:
ValidationError: 当已存在配置实例时抛出异常
"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
"""
mk:
保存配置并清除缓存
Args:
*args: 位置参数
**kwargs: 关键字参数
Returns:
None
"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()