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.
DjangoBlog-Maintenance-Anal.../src/models.py

456 lines
18 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.

<<<<<<< HEAD
<<<<<<< HEAD
from django.contrib.auth.models import AbstractUser
=======
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
>>>>>>> hyt_branch
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
<<<<<<< HEAD
from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
=======
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):
"""
链接显示类型选择
定义友情链接在网站中的显示位置
"""
I = ('i', _('index')) # 首页显示
L = ('l', _('list')) # 列表页显示
P = ('p', _('post')) # 文章页显示
A = ('a', _('all')) # 所有页面显示
S = ('s', _('slide')) # 幻灯片显示
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) # 最后修改时间
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:
# 自动生成slugURL友好字符串
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):
"""获取完整的URL地址包含域名"""
>>>>>>> hyt_branch
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
<<<<<<< HEAD
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'
=======
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
# 评论模型,存储用户对文章的评论及评论间的嵌套关系
class Comment(models.Model):
body = models.TextField('正文', max_length=300) # 评论内容限制最大长度300字符
creation_time = models.DateTimeField(_('creation time'), default=now) # 评论创建时间,默认当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 评论最后修改时间,默认当前时间
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # 关联Django内置用户模型便于扩展用户系统
verbose_name=_('author'),
on_delete=models.CASCADE) # 级联删除:用户删除时,其评论也会被删除
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 级联删除:文章删除时,其下所有评论也会被删除
parent_comment = models.ForeignKey(
'self', # 自关联,实现评论嵌套回复功能
verbose_name=_('parent comment'),
blank=True,
null=True, # 允许为空,表示该评论是顶级评论(不是回复)
on_delete=models.CASCADE) # 级联删除:父评论删除时,其所有子评论也会被删除
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False) # 评论是否启用(可用于审核功能)
class Meta:
ordering = ['-id'] # 默认按ID降序排列最新评论显示在前面
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id' # 指定通过id字段获取最新记录
def __str__(self):
return self.body
>>>>>>> zh_branch
=======
abstract = True # 抽象基类,不会创建数据库表
@abstractmethod
def get_absolute_url(self):
"""抽象方法获取对象的绝对URL子类必须实现"""
pass
class Article(BaseModel):
"""
文章模型
博客系统的核心模型,存储所有文章内容
"""
# 文章状态选择
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')) # 文章内容使用Markdown编辑器
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):
"""将文章内容转换为字符串"""
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' # 获取最新记录的依据字段
def get_absolute_url(self):
"""获取文章的绝对URL包含年月日信息用于SEO"""
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) # 缓存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
def save(self, *args, **kwargs):
"""保存文章,调用父类保存逻辑"""
super().save(*args, **kwargs)
def viewed(self):
"""增加文章浏览量使用update_fields优化性能"""
self.views += 1
self.save(update_fields=['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:
# 获取已启用的评论并按ID降序排列
comments = self.comment_set.filter(is_enable=True).order_by('-id')
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):
"""获取文章在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) # 缓存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) # 缓存100分钟
def prev_article(self):
"""获取上一篇文章按ID顺序"""
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
从文章内容中提取第一张图片的URL
用于文章列表的缩略图显示
"""
# 使用正则表达式匹配Markdown图片语法
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
class Category(BaseModel):
"""
文章分类模型
用于组织和管理博客文章的类别,支持多级分类结构
"""
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) # URL友好名称
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):
"""获取分类的绝对URL地址使用slug作为URL参数"""
return reverse('blog:category_detail', kwargs={'category_name': self.slug})
def __str__(self):
"""对象的字符串表示"""
return self.name
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
递归获得分类目录的父级
返回从当前分类到根分类的路径,用于面包屑导航
"""
categorys = []
def parse(category):
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
返回所有子分类的列表
"""
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):
"""文章标签模型"""
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使用slug作为URL参数"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_article_count(self):
"""获取该标签下的文章数量使用distinct去重"""
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):
"""友情链接模型"""
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):
return self.name
class SideBar(models.Model):
"""侧边栏模型可以展示一些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'] # 按顺序升序排列
verbose_name = _('sidebar') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def __str__(self):
return self.name
class BlogSettings(models.Model):
"""博客全局配置模型,使用单例模式确保只有一份配置"""
# 网站基本信息
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='') # 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) # 是否显示Google广告
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) # 是否开启全站评论
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='') # 网站统计代码
class Meta:
verbose_name = _('Website configuration') # 单数显示名称
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'))
def save(self, *args, **kwargs):
"""保存配置并清除缓存,确保配置变更立即生效"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear() # 清除所有缓存
>>>>>>> hyt_branch