@ -14,6 +14,444 @@ 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 :
# 如果有slug字段, 自动生成slug
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地址
"""
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 ) :
"""
抽象方法 , 子类必须实现获取绝对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
"""
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 ) :
"""
获取分类树结构
"""
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 ) :
"""
增加文章浏览量
"""
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 :
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 ) :
"""
获取管理后台的文章编辑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 ) :
"""
获取下一篇已发布的文章
"""
# 下一篇
return Article . objects . filter (
id__gt = self . id , status = ' p ' ) . order_by ( ' id ' ) . first ( )
@cache_decorator ( expiration = 60 * 100 )
def prev_article ( self ) :
"""
获取上一篇已发布的文章
"""
# 前一篇
return Article . objects . filter ( id__lt = self . id , status = ' p ' ) . first ( )
def get_first_image_url ( self ) :
"""
从文章正文中获取第一张图片的URL
: return : 图片URL或空字符串
"""
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 )
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
"""
return reverse (
' blog:category_detail ' , kwargs = {
' category_name ' : self . slug } )
def __str__ ( self ) :
return self . name
@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
@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
class Tag ( BaseModel ) :
""" 文章标签模型 """
name = models . CharField ( _ ( ' tag name ' ) , max_length = 30 , unique = True )
slug = models . SlugField ( default = ' no-slug ' , max_length = 60 , blank = True )
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 )
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
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 ' ) )
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 = ' ' )
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 ) :
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 ( )
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__ )