zjp注释 #29

Closed
phm9gvnzi wants to merge 16 commits from zjp_branch into master

42
.gitignore vendored

@ -1,42 +0,0 @@
# 系统和用户目录
Application Data/
Cookies/
Local Settings/
My Documents/
NetHood/
PrintHood/
Recent/
SendTo/
Templates/
「开始」菜单/
AppData/
Contacts/
Desktop/
Documents/
Downloads/
Favorites/
Links/
Music/
NTUSER.*
OneDrive/
Pictures/
Saved Games/
Searches/
Videos/
WPS Cloud Files/
wechat_files/
# 临时文件和IDE配置
.bash_history
.eclipse/
.gitconfig
.idlerc/
.matplotlib/
.p2/
.ssh/
.vscode/
eclipse-workspace/
eclipse/
ntuser.*

2
.idea/.gitignore vendored

@ -1,5 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

@ -5,8 +5,4 @@
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/software-engineering-methodology-djq-branch(1).iml" filepath="$PROJECT_DIR$/.idea/software-engineering-methodology-djq-branch(1).iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/Django.iml" filepath="$PROJECT_DIR$/.idea/Django.iml" />
</modules>
</component>
</project>

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,6 @@
<<<<<<< HEAD
这是我的个人分支说明
=======
# software-engineering-methodology
>>>>>>> f783378e06d6abd4513ad3220bf6f630b2fb7263

@ -1,46 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -1,253 +1,213 @@
import time # 用于生成时间戳作为文档ID
import elasticsearch.client # Elasticsearch客户端工具
from django.conf import settings # 导入Django项目配置
# 导入Elasticsearch DSL相关模块用于定义文档结构和字段类型
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections # 用于创建Elasticsearch连接
from elasticsearch_dsl.connections import connections
from blog.models import Article # 导入Django博客文章模型
from blog.models import Article
# 检查是否启用了Elasticsearch通过判断配置中是否有ELASTICSEARCH_DSL
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# 创建Elasticsearch连接连接地址从Django配置中获取
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch # 导入Elasticsearch客户端
from elasticsearch import Elasticsearch
# 初始化Elasticsearch客户端
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient # 导入Ingest API客户端用于处理数据管道
from elasticsearch.client import IngestClient
c = IngestClient(es)
try:
# 检查是否存在名为'geoip'的数据管道用于解析IP地址的地理位置信息
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
# 若不存在,则创建'geoip'管道通过IP地址添加地理位置信息
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info", # 管道描述添加IP的地理信息
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip" # 基于文档中的'ip'字段解析地理信息
"field" : "ip"
}
}
]
}''')
# 内部文档类存储IP地址解析后的地理位置信息嵌套在ElapsedTimeDocument中
class GeoIp(InnerDoc):
continent_name = Keyword() # 大陆名称Keyword类型精确匹配不分词
country_iso_code = Keyword() # 国家ISO代码如CN、US
country_name = Keyword() # 国家名称
location = GeoPoint() # 经纬度坐标Elasticsearch的地理点类型
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
# 内部文档类存储用户代理中的浏览器信息嵌套在UserAgent中
class UserAgentBrowser(InnerDoc):
Family = Keyword() # 浏览器家族如Chrome、Firefox
Version = Keyword() # 浏览器版本
Family = Keyword()
Version = Keyword()
# 内部文档类:存储用户代理中的操作系统信息(继承浏览器信息结构)
class UserAgentOS(UserAgentBrowser):
pass # 结构与浏览器一致包含Family系统家族和Version系统版本
pass
# 内部文档类存储用户代理中的设备信息嵌套在UserAgent中
class UserAgentDevice(InnerDoc):
Family = Keyword() # 设备家族如iPhone、Windows
Brand = Keyword() # 设备品牌如Apple、Samsung
Model = Keyword() # 设备型号如iPhone 13
Family = Keyword()
Brand = Keyword()
Model = Keyword()
# 内部文档类存储用户代理User-Agent完整信息嵌套在ElapsedTimeDocument中
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False) # 浏览器信息(可选)
os = Object(UserAgentOS, required=False) # 操作系统信息(可选)
device = Object(UserAgentDevice, required=False) # 设备信息(可选)
string = Text() # 原始User-Agent字符串
is_bot = Boolean() # 是否为爬虫机器人
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
# Elasticsearch文档类记录性能耗时信息如接口响应时间
class ElapsedTimeDocument(Document):
url = Keyword() # 请求URL精确匹配
time_taken = Long() # 耗时(毫秒)
log_datetime = Date() # 日志记录时间
ip = Keyword() # 访问者IP地址
geoip = Object(GeoIp, required=False) # 地理位置信息由geoip管道解析可选
useragent = Object(UserAgent, required=False) # 用户代理信息(可选)
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class Index:
name = 'performance' # 索引名称:存储性能数据
name = 'performance'
settings = {
"number_of_shards": 1, # 主分片数量
"number_of_replicas": 0 # 副本分片数量单节点环境设为0
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'ElapsedTime' # 文档类型Elasticsearch 7.x后可省略
doc_type = 'ElapsedTime'
# 管理类处理ElapsedTimeDocument的索引创建、删除和数据插入
class ElaspedTimeDocumentManager:
@staticmethod
def build_index():
"""创建performance索引若不存在"""
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 检查索引是否存在
res = client.indices.exists(index="performance")
if not res:
# 初始化索引根据ElapsedTimeDocument的定义创建映射
ElapsedTimeDocument.init()
@staticmethod
def delete_index():
"""删除performance索引"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 忽略400索引不存在和404请求错误的错误
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
"""创建一条性能日志文档并保存到Elasticsearch"""
# 确保索引已创建
ElaspedTimeDocumentManager.build_index()
# 构建用户代理信息对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family # 浏览器家族
ua.browser.Version = useragent.browser.version_string # 浏览器版本
ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family # 操作系统家族
ua.os.Version = useragent.os.version_string # 操作系统版本
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family # 设备家族
ua.device.Brand = useragent.device.brand # 设备品牌
ua.device.Model = useragent.device.model # 设备型号
ua.string = useragent.ua_string # 原始User-Agent字符串
ua.is_bot = useragent.is_bot # 是否为爬虫
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# 创建性能日志文档
doc = ElapsedTimeDocument(
meta={
# 用当前时间戳毫秒级作为文档ID确保唯一性
'id': int(round(time.time() * 1000))
'id': int(
round(
time.time() *
1000))
},
url=url, # 请求URL
time_taken=time_taken, # 耗时
log_datetime=log_datetime, # 记录时间
useragent=ua, # 用户代理信息
ip=ip # 访问IP
)
# 保存文档时应用'geoip'管道自动解析IP的地理位置
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
# Elasticsearch文档类存储博客文章信息用于全文搜索
class ArticleDocument(Document):
# 文章内容使用IK分词器ik_max_word最大粒度分词ik_smart智能分词
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 文章标题(同上,支持中文分词搜索)
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 作者信息(嵌套对象)
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 作者昵称
'id': Integer() # 作者ID
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 分类信息(嵌套对象)
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 分类名称
'id': Integer() # 分类ID
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 标签信息(嵌套对象列表)
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 标签名称
'id': Integer() # 标签ID
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
pub_time = Date() # 发布时间
status = Text() # 文章状态(如发布、草稿)
comment_status = Text() # 评论状态(如允许、关闭)
type = Text() # 文章类型(如原创、转载)
views = Integer() # 浏览量
article_order = Integer() # 文章排序权重
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
class Index:
name = 'blog' # 索引名称:存储博客文章数据
name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article' # 文档类型
doc_type = 'Article'
# 管理类处理ArticleDocument的索引创建、删除、数据同步
class ArticleDocumentManager():
def __init__(self):
"""初始化时创建blog索引若不存在"""
self.create_index()
def create_index(self):
"""创建blog索引根据ArticleDocument的定义"""
ArticleDocument.init()
def delete_index(self):
"""删除blog索引"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
"""将Django的Article模型对象列表转换为ArticleDocument列表"""
return [
ArticleDocument(
meta={'id': article.id}, # 用文章ID作为文档ID
body=article.body, # 文章内容
title=article.title, # 文章标题
meta={
'id': article.id},
body=article.body,
title=article.title,
author={
'nickname': article.author.username, # 作者用户名
'id': article.author.id # 作者ID
},
'nickname': article.author.username,
'id': article.author.id},
category={
'name': article.category.name, # 分类名称
'id': article.category.id # 分类ID
},
# 标签列表遍历文章的tags多对多字段
tags=[{'name': t.name, 'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time, # 发布时间
status=article.status, # 文章状态
comment_status=article.comment_status, # 评论状态
type=article.type, # 文章类型
views=article.views, # 浏览量
article_order=article.article_order # 排序权重
) for article in articles
]
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
"""重建blog索引将文章数据同步到Elasticsearch默认同步所有文章"""
ArticleDocument.init() # 确保索引结构正确
# 若未指定文章列表,则同步所有文章
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
# 转换为文档列表
docs = self.convert_to_doc(articles)
# 批量保存文档
for doc in docs:
doc.save()
def update_docs(self, docs):
"""更新文档列表(批量保存)"""
for doc in docs:
doc.save()
doc.save()

@ -1,88 +1,42 @@
iimport logging
import logging
import time
# 用于获取客户端IP地址的工具
from ipware import get_client_ip
# 用于解析用户代理(浏览器/设备信息)的工具
from user_agents import parse
# 导入Elasticsearch相关配置和文档管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# 初始化日志记录器,用于记录中间件运行过程中的信息和错误
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
"""
自定义Django中间件用于
1. 计算页面渲染耗时
2. 收集访问日志IP用户代理访问时间等
3. 当启用Elasticsearch时将访问性能数据存入搜索引擎
4. 在响应内容中替换特定标记为页面加载时间
"""
def __init__(self, get_response=None):
"""
中间件初始化方法
:param get_response: Django框架传入的下一个响应处理函数用于构建中间件链
"""
self.get_response = get_response
# 调用父类初始化方法兼容Python 2.x在Python 3中可省略
super().__init__()
def __call__(self, request):
"""
中间件核心处理方法在请求到达视图前和响应返回客户端前执行
:param request: Django请求对象包含客户端请求的所有信息
:return: 经过处理的Django响应对象
"""
# 记录请求处理开始时间(用于计算页面渲染耗时)
''' page render time '''
start_time = time.time()
# 调用下一个中间件或视图函数,获取响应对象
response = self.get_response(request)
# 从请求头中获取用户代理字符串(包含浏览器、设备等信息)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# 通过ipware工具获取客户端IP地址返回元组(ip地址, 是否为公开IP)
ip, _ = get_client_ip(request)
# 解析用户代理字符串,生成结构化的用户代理对象(方便提取浏览器)
user_agent = parse(http_user_agent)
# 非流式响应如普通HTML页面排除文件下载等流式响应才进行处理
if not response.streaming:
try:
# 计算页面渲染总耗时(当前时间 - 开始时间)
cast_time = time.time() - start_time
# 如果启用了Elasticsearch将访问性能数据存入搜索引擎
if ELASTICSEARCH_ENABLED:
# 转换耗时为毫秒并保留两位小数
time_taken = round((cast_time) * 1000, 2)
# 获取当前请求的URL路径
url = request.path
# 导入Django时区工具获取当前时间
from django.utils import timezone
# 通过文档管理器创建并保存访问记录
ElaspedTimeDocumentManager.create(
url=url, # 访问的URL
time_taken=time_taken, # 页面加载耗时(毫秒)
log_datetime=timezone.now(), # 访问时间
useragent=user_agent, # 用户代理信息(浏览器/设备)
ip=ip # 客户端IP地址
)
# 将响应内容中的<!!LOAD_TIMES!!>标记替换为实际渲染耗时保留前5位字符
# 注意仅适用于文本类型响应如HTML二进制响应会跳过
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', # 待替换的二进制标记
str.encode(str(cast_time)[:5]) # 转换为二进制的耗时字符串
)
# 捕获处理过程中的所有异常,避免中间件错误导致请求失败
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error in OnlineMiddleware: %s" % e)
logger.error("Error OnlineMiddleware: %s" % e)
# 返回处理后的响应对象
return response

@ -18,6 +18,7 @@ logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
# 定义友情链接显示类型的枚举类,分别对应首页、列表页、文章页、所有页面、轮播
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
@ -26,98 +27,118 @@ class LinkShowType(models.TextChoices):
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) # 自增主键
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs):
"""
重写save方法处理slug字段如果模型有slug和title/name字段并调用父类save方法
同时处理仅更新views字段的特殊情况
"""
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))
slug_source = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name')
setattr(self, 'slug', slugify(slug_source))
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
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'))
title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题,唯一
body = MDTextField(_('body')) # 文章内容使用MDTextField支持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')
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)
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) # 文章分类外键关联Category模型
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 文章标签多对多关联Tag模型
def body_to_string(self):
"""将文章内容转换为字符串返回"""
return self.body
def __str__(self):
"""自定义字符串表示,返回文章标题"""
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
ordering = ['-article_order', '-pub_time'] # 排序规则先按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,
@ -127,19 +148,26 @@ class Article(BaseModel):
@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):
"""重写save方法调用父类save方法"""
super().save(*args, **kwargs)
def viewed(self):
"""文章被浏览时浏览量加1并保存"""
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:
@ -152,24 +180,25 @@ class Article(BaseModel):
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)
def next_article(self):
# 下一篇
"""获取下一篇文章id大于当前文章且已发布的第一篇并缓存"""
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 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图片语法中的图片链接
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
@ -178,35 +207,38 @@ class Article(BaseModel):
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) # 分类的slug用于URL
index = models.IntegerField(default=0, verbose_name=_('index')) # 分类排序序号
class Meta:
ordering = ['-index']
ordering = ['-index'] # 按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 = []
@ -221,8 +253,7 @@ class Category(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
递归获取当前分类的所有子分类包括子分类的子分类等并缓存
"""
categorys = []
all_categorys = Category.objects.all()
@ -241,136 +272,156 @@ 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(_('tag name'), max_length=30, unique=True) # 标签名称,唯一
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 标签的slug用于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)
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) # 显示类型关联LinkShowType枚举
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
ordering = ['sequence']
ordering = ['sequence'] # 按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)
"""
侧边栏模型类用于展示自定义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'] # 按sequence排序
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
"""自定义字符串表示,返回侧边栏标题"""
return self.name
class BlogSettings(models.Model):
"""blog的配置"""
"""
博客配置模型类存储网站的各种配置信息
"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
default='') # 网站名称
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
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)
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)
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='') # Google广告代码
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='')
default='') # 网站备案号
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
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='')
default='') # 公安备案号
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
'评论是否需要审核', 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):
"""
重写save方法保存后清除缓存使配置变更立即生效
"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
cache.clear()

@ -1,13 +1,26 @@
# 从haystack框架导入索引相关的模块用于实现全文搜索功能
from haystack import indexes
# 导入当前项目中blog应用的Article模型该模型对应需要被索引的文章数据
from blog.models import Article
# 定义ArticleIndex类继承自SearchIndex和Indexable用于配置Article模型的搜索索引
# SearchIndex提供索引的核心功能定义了如何从模型中提取数据构建索引
# Indexable标识该类可被索引要求实现get_model方法来指定关联的模型
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
# 定义一个text字段作为文档的主要索引字段document=True表示这是主要搜索字段
# use_template=True表示使用模板来定义该字段需要索引的内容通常在templates/search/indexes/blog/article_text.txt中配置
# 该字段会聚合模型中需要被搜索的字段(如标题、正文等),作为全文搜索的基础
text = indexes.CharField(document=True, use_template=True)
# 实现Indexable接口的方法返回当前索引关联的模型类
# 作用告诉haystack该索引对应的数据来自哪个模型
def get_model(self):
return Article
# 定义需要被索引的查询集(即哪些数据会被纳入搜索范围)
# using参数用于指定搜索引擎多引擎场景下使用默认None表示使用默认引擎
# 这里返回状态为'p'(假设表示"已发布")的文章,确保只有已发布的内容可被搜索
def index_queryset(self, using=None):
return self.get_model().objects.filter(status='p')
return self.get_model().objects.filter(status='p')
Loading…
Cancel
Save