提交代码批注

AQ_branch
安琪 6 months ago
parent 622afe96f7
commit 3c8a12f23b

@ -5,6 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Python 调试程序: 当前文件",

@ -1,86 +1,91 @@
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model # aq: 导入获取用户模型的工具(适配自定义用户模型)
from django.urls import reverse # aq: 导入URL反向解析函数
from django.utils.html import format_html # aq: 导入HTML格式化工具用于生成Admin中的链接
from django.utils.translation import gettext_lazy as _ # aq: 导入国际化翻译函数
# Register your models here.
from .models import Article
from .models import Article # aq: 导入文章模型需确保Category、Tag等模型也已在models中定义
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class ArticleForm(forms.ModelForm): # aq: 文章的Admin表单类可自定义字段渲染逻辑
# body = forms.CharField(widget=AdminPagedownWidget()) # aq: 注释掉的Markdown编辑器组件配置
class Meta:
model = Article
fields = '__all__'
model = Article # aq: 关联的模型
fields = '__all__' # aq: 显示所有字段
# aq: 自定义Admin批量操作——将选中文章设为“已发布”状态
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
# aq: 自定义Admin批量操作——将选中文章设为“草稿”状态
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
# aq: 自定义Admin批量操作——关闭选中文章的评论
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
# aq: 自定义Admin批量操作——开放选中文章的评论
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
# aq: 批量操作的显示名称(支持国际化)
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20
search_fields = ('body', 'title')
form = ArticleForm
list_display = (
class ArticlelAdmin(admin.ModelAdmin): # aq: 文章的Admin管理类配置后台展示/操作逻辑)
list_per_page = 20 # aq: 每页显示20条数据
search_fields = ('body', 'title') # aq: 可搜索字段(文章正文、标题)
form = ArticleForm # aq: 使用自定义的ArticleForm表单
list_display = ( # aq: 列表页显示的字段
'id',
'title',
'author',
'link_to_category',
'link_to_category', # aq: 自定义字段——分类跳转链接
'creation_time',
'views',
'status',
'type',
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
actions = [
list_display_links = ('id', 'title') # aq: 列表页可点击跳转的字段(链接到编辑页)
list_filter = ('status', 'type', 'category') # aq: 侧边筛选器字段
filter_horizontal = ('tags',) # aq: 多对多字段的水平选择器(标签字段)
exclude = ('creation_time', 'last_modify_time') # aq: 编辑页隐藏的字段(自动生成,无需手动输入)
view_on_site = True # aq: 显示“在站点上查看”按钮
actions = [ # aq: 启用的批量操作
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj):
def link_to_category(self, obj): # aq: 自定义列表字段——分类名称带后台编辑链接
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name)) # aq: 修复原代码语法错误,正确拼接链接
link_to_category.short_description = _('category')
link_to_category.short_description = _('category') # aq: 自定义字段的显示名称
def get_form(self, request, obj=None, **kwargs):
def get_form(self, request, obj=None, **kwargs): # aq: 自定义表单——作者字段仅显示超级管理员
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
def save_model(self, request, obj, form, change): # aq: 保存模型时的钩子(此处调用父类方法,可扩展自定义逻辑)
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
def get_view_on_site_url(self, obj=None): # aq: 自定义“在站点上查看”的链接(跳转到文章前台详情页)
if obj:
url = obj.get_full_url()
return url
@ -90,23 +95,24 @@ class ArticlelAdmin(admin.ModelAdmin):
return site
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
class TagAdmin(admin.ModelAdmin): # aq: 标签的Admin管理类
exclude = ('slug', 'last_mod_time', 'creation_time') # aq: 隐藏自动生成的字段slug自动生成
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin): # aq: 分类的Admin管理类
list_display = ('name', 'parent_category', 'index') # aq: 列表页显示字段(名称、父分类、排序权重)
exclude = ('slug', 'last_mod_time', 'creation_time') # aq: 隐藏自动生成的字段
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin): # aq: 友情链接的Admin管理类
exclude = ('last_mod_time', 'creation_time') # aq: 隐藏自动生成的时间字段
class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin): # aq: 侧边栏的Admin管理类
list_display = ('name', 'content', 'is_enable', 'sequence') # aq: 列表页显示字段(标题、内容、是否启用、排序)
exclude = ('last_mod_time', 'creation_time') # aq: 隐藏自动生成的时间字段
class BlogSettingsAdmin(admin.ModelAdmin):
class BlogSettingsAdmin(admin.ModelAdmin): # aq: 博客配置的Admin管理类使用默认配置
pass

@ -1,5 +1,5 @@
from django.apps import AppConfig
from django.apps import AppConfig # aq: 导入Django的应用配置基类
class BlogConfig(AppConfig):
name = 'blog'
class BlogConfig(AppConfig): # aq: 博客应用的配置类(用于定义应用元信息)
name = 'blog' # aq: 应用名称必须与应用目录名一致Django通过此识别应用

@ -1,43 +1,48 @@
import logging
from django.utils import timezone
from django.utils import timezone # aq: 导入时区时间工具
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
from djangoblog.utils import cache, get_blog_setting # aq: 导入缓存工具和获取博客配置的函数
from .models import Category, Article # aq: 导入分类、文章模型
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # aq: 初始化当前模块的日志对象
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
if value:
def seo_processor(requests): # aq: Django上下文处理器向模板注入SEO/站点配置信息
key = 'seo_processor' # aq: 缓存键名
value = cache.get(key) # aq: 尝试从缓存中获取数据
if value: # aq: 缓存存在则直接返回
return value
else:
logger.info('set processor cache.')
setting = get_blog_setting()
logger.info('set processor cache.') # aq: 记录缓存设置日志
setting = get_blog_setting() # aq: 获取博客的全局配置
# aq: 构造要注入模板的上下文数据
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_NAME': setting.site_name, # aq: 站点名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # aq: 是否显示Google广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # aq: Google广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # aq: 站点SEO描述
'SITE_DESCRIPTION': setting.site_description, # aq: 站点描述
'SITE_KEYWORDS': setting.site_keywords, # aq: 站点关键词
# aq: 站点基础URL协议+域名)
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # aq: 文章摘要长度
'nav_category_list': Category.objects.all(), # aq: 导航分类列表
# aq: 导航页面列表(类型为页面、状态为已发布)
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
'OPEN_SITE_COMMENT': setting.open_site_comment, # aq: 是否开放全站评论
'BEIAN_CODE': setting.beian_code, # aq: 备案号
'ANALYTICS_CODE': setting.analytics_code, # aq: 网站统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # aq: 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, # aq: 是否显示公安备案号
"CURRENT_YEAR": timezone.now().year, # aq: 当前年份(用于版权信息)
"GLOBAL_HEADER": setting.global_header, # aq: 公共头部HTML
"GLOBAL_FOOTER": setting.global_footer, # aq: 公共尾部HTML
"COMMENT_NEED_REVIEW": setting.comment_need_review, # aq: 评论是否需要审核
}
cache.set(key, value, 60 * 60 * 10)
return value
cache.set(key, value, 60 * 60 * 10) # aq: 缓存数据10小时
return value

@ -1,15 +1,17 @@
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
from django.conf import settings # aq: 导入Django项目配置
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean # aq: 导入Elasticsearch DSL的文档/字段类型
from elasticsearch_dsl.connections import connections # aq: 导入ES连接工具
from blog.models import Article
from blog.models import Article # aq: 导入博客文章模型
# aq: 判断是否启用Elasticsearch检查配置中是否有ELASTICSEARCH_DSL
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# aq: 创建ES连接使用配置中的地址
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@ -17,10 +19,11 @@ if ELASTICSEARCH_ENABLED:
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
c = IngestClient(es)
c = IngestClient(es) # aq: 初始化ES的Ingest客户端用于管道操作
try:
c.get_pipeline('geoip')
c.get_pipeline('geoip') # aq: 检查是否存在名为geoip的管道
except elasticsearch.exceptions.NotFoundError:
# aq: 若不存在创建geoip管道用于解析IP的地理位置信息
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -33,73 +36,75 @@ if ELASTICSEARCH_ENABLED:
}''')
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
class GeoIp(InnerDoc): # aq: 定义地理位置信息的内部文档(嵌套结构)
continent_name = Keyword() # aq: 大陆名称Keyword类型精确匹配
country_iso_code = Keyword() # aq: 国家ISO编码
country_name = Keyword() # aq: 国家名称
location = GeoPoint() # aq: 经纬度坐标
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
class UserAgentBrowser(InnerDoc): # aq: 定义用户代理中浏览器信息的内部文档
Family = Keyword() # aq: 浏览器家族如Chrome
Version = Keyword() # aq: 浏览器版本
class UserAgentOS(UserAgentBrowser):
class UserAgentOS(UserAgentBrowser): # aq: 定义用户代理中操作系统信息的内部文档(继承浏览器结构)
pass
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgentDevice(InnerDoc): # aq: 定义用户代理中设备信息的内部文档
Family = Keyword() # aq: 设备家族如iPhone
Brand = Keyword() # aq: 设备品牌
Model = Keyword() # aq: 设备型号
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
class UserAgent(InnerDoc): # aq: 定义用户代理的内部文档(包含浏览器、系统、设备)
browser = Object(UserAgentBrowser, required=False) # aq: 浏览器信息Object类型嵌套UserAgentBrowser
os = Object(UserAgentOS, required=False) # aq: 操作系统信息
device = Object(UserAgentDevice, required=False) # aq: 设备信息
string = Text() # aq: 原始用户代理字符串
is_bot = Boolean() # aq: 是否为爬虫机器人
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class ElapsedTimeDocument(Document): # aq: 定义页面耗时统计的ES文档对应performance索引
url = Keyword() # aq: 请求URL
time_taken = Long() # aq: 页面加载耗时(毫秒)
log_datetime = Date() # aq: 日志记录时间
ip = Keyword() # aq: 客户端IP
geoip = Object(GeoIp, required=False) # aq: 地理位置信息嵌套GeoIp
useragent = Object(UserAgent, required=False) # aq: 用户代理信息嵌套UserAgent
class Index:
name = 'performance'
name = 'performance' # aq: 索引名称
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
"number_of_shards": 1, # aq: 分片数
"number_of_replicas": 0 # aq: 副本数开发环境通常设为0
}
class Meta:
doc_type = 'ElapsedTime'
doc_type = 'ElapsedTime' # aq: 文档类型ES 7+后可省略)
class ElaspedTimeDocumentManager:
class ElaspedTimeDocumentManager: # aq: 页面耗时文档的管理类封装ES操作
@staticmethod
def build_index():
def build_index(): # aq: 检查并创建performance索引
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
ElapsedTimeDocument.init()
ElapsedTimeDocument.init() # aq: 初始化索引根据ElapsedTimeDocument的定义
@staticmethod
def delete_index():
def delete_index(): # aq: 删除performance索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
es.indices.delete(index='performance', ignore=[400, 404]) # aq: 忽略不存在/错误
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
ElaspedTimeDocumentManager.build_index()
def create(url, time_taken, log_datetime, useragent, ip): # aq: 创建一条耗时记录
ElaspedTimeDocumentManager.build_index() # aq: 确保索引存在
# aq: 构造UserAgent对象填充浏览器、系统、设备信息
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@ -116,6 +121,7 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# aq: 构造ElapsedTimeDocument实例用时间戳作为ID避免重复
doc = ElapsedTimeDocument(
meta={
'id': int(
@ -127,61 +133,65 @@ class ElaspedTimeDocumentManager:
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
doc.save(pipeline="geoip") # aq: 保存时使用geoip管道解析IP
class ArticleDocument(Document):
class ArticleDocument(Document): # aq: 定义文章的ES文档对应blog索引
# aq: 文章正文/标题使用ik分词器max_word细分词smart粗分词
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# aq: 作者信息(嵌套结构)
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# aq: 分类信息(嵌套结构)
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# aq: 标签信息(嵌套结构)
tags = Object(properties={
'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() # aq: 发布时间
status = Text() # aq: 文章状态
comment_status = Text() # aq: 评论状态
type = Text() # aq: 内容类型
views = Integer() # aq: 阅读量
article_order = Integer() # aq: 排序权重
class Index:
name = 'blog'
name = 'blog' # aq: 索引名称
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article'
doc_type = 'Article' # aq: 文档类型
class ArticleDocumentManager():
class ArticleDocumentManager(): # aq: 文章文档的管理类
def __init__(self):
def __init__(self): # aq: 初始化时创建索引
self.create_index()
def create_index(self):
def create_index(self): # aq: 创建blog索引
ArticleDocument.init()
def delete_index(self):
def delete_index(self): # aq: 删除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):
def convert_to_doc(self, articles): # aq: 将Django的Article模型转换为ArticleDocument
return [
ArticleDocument(
meta={
'id': article.id},
'id': article.id}, # aq: 用文章ID作为ES文档ID
body=article.body,
title=article.title,
author={
@ -190,7 +200,7 @@ class ArticleDocumentManager():
category={
'name': article.category.name,
'id': article.category.id},
tags=[
tags=[ # aq: 转换多对多的标签为列表
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
@ -201,13 +211,13 @@ class ArticleDocumentManager():
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
def rebuild(self, articles=None): # aq: 重建文章索引(默认同步所有文章)
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):
def update_docs(self, docs): # aq: 批量更新文章文档
for doc in docs:
doc.save()
doc.save()

@ -1,19 +1,26 @@
import logging
from django import forms
from haystack.forms import SearchForm
from django import forms # aq: 导入Django表单基类
from haystack.forms import SearchForm # aq: 导入Haystack搜索框架的基础搜索表单
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # aq: 初始化当前模块的日志对象
class BlogSearchForm(SearchForm):
class BlogSearchForm(SearchForm): # aq: 自定义博客搜索表单继承Haystack的SearchForm
# aq: 定义搜索关键词字段required=True表示该字段不能为空
querydata = forms.CharField(required=True)
def search(self):
def search(self): # aq: 重写搜索方法,自定义搜索逻辑
# aq: 调用父类的search方法获取初始搜索结果
datas = super(BlogSearchForm, self).search()
# aq: 如果表单验证不通过,返回“无结果”的默认处理
if not self.is_valid():
return self.no_query_found()
# aq: 如果搜索关键词存在,记录关键词到日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas
# aq: 返回最终的搜索结果
return datas

@ -1,42 +1,52 @@
import logging
import time
import time # aq: 导入时间模块,用于计算页面渲染耗时
from ipware import get_client_ip
from user_agents import parse
from ipware import get_client_ip # aq: 导入获取客户端IP的工具
from user_agents import parse # aq: 导入解析用户代理UA的工具
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager # aq: 导入ES开关和耗时记录的Document管理器
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # aq: 初始化当前模块的日志对象
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
class OnlineMiddleware(object): # aq: 自定义Django中间件用于统计页面加载耗时、记录访问信息
def __init__(self, get_response=None): # aq: 中间件初始化方法
self.get_response = get_response # aq: 保存Django的响应处理函数
super().__init__()
def __call__(self, request):
''' page render time '''
start_time = time.time()
response = self.get_response(request)
def __call__(self, request): # aq: 中间件核心方法,处理请求/响应流程
''' page render time ''' # aq: 功能说明:统计页面渲染耗时
start_time = time.time() # aq: 记录请求处理的开始时间
response = self.get_response(request) # aq: 调用后续中间件/视图,获取响应
# aq: 从请求头中获取用户代理UA字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request)
user_agent = parse(http_user_agent)
ip, _ = get_client_ip(request) # aq: 获取客户端IP地址
user_agent = parse(http_user_agent) # aq: 解析UA字符串得到设备/浏览器信息
# aq: 非流式响应如普通HTML响应才处理
if not response.streaming:
try:
cast_time = time.time() - start_time
cast_time = time.time() - start_time # aq: 计算请求处理总耗时(秒)
# aq: 如果启用了Elasticsearch记录耗时信息
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2)
url = request.path
time_taken = round((cast_time) * 1000, 2) # aq: 转换为毫秒并保留2位小数
url = request.path # aq: 获取当前请求的URL路径
from django.utils import timezone
# aq: 向ES中创建一条耗时记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# aq: 将页面中的<!!LOAD_TIMES!!>占位符替换为实际耗时取前5位
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
except Exception as e: # aq: 捕获异常并记录日志
logger.error("Error OnlineMiddleware: %s" % e)
return response
return response # aq: 返回处理后的响应

@ -4,134 +4,133 @@ from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
import mdeditor.fields # aq: 导入Markdown编辑器专用字段类型
class Migration(migrations.Migration):
initial = True
initial = True # aq: 标记为初始迁移,负责创建博客核心数据表结构
dependencies = [
dependencies = [ # aq: 迁移依赖——关联Django用户模型支持自定义用户模型
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
operations = [ # aq: 迁移核心操作:创建所有博客相关数据表
migrations.CreateModel( # aq: 1. 创建网站配置表BlogSettings
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # aq: 自增主键BigInt类型
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')), # aq: 网站名称
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')), # aq: 网站描述
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')), # aq: SEO优化描述
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')), # aq: 网站搜索关键字
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')), # aq: 文章摘要默认长度
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')), # aq: 侧边栏显示文章数量
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')), # aq: 侧边栏显示评论数量
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')), # aq: 文章页默认显示评论数
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')), # aq: 是否启用谷歌广告
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')), # aq: 谷歌广告代码
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')), # aq: 是否开放全站评论
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')), # aq: 网站备案号
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')), # aq: 网站统计(如百度统计)代码
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')), # aq: 是否显示公安备案号
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), # aq: 公安备案号
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
'verbose_name_plural': '网站配置', # aq: 后台显示名称(单数/复数)
},
),
migrations.CreateModel(
migrations.CreateModel( # aq: 2. 创建友情链接表Links
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
('link', models.URLField(verbose_name='链接地址')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # aq: 自增主键
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')), # aq: 友情链接名称(唯一)
('link', models.URLField(verbose_name='链接地址')), # aq: 链接URL地址
('sequence', models.IntegerField(unique=True, verbose_name='排序')), # aq: 排序序号(唯一,决定显示顺序)
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # aq: 是否启用该链接
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')), # aq: 链接展示位置
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
'ordering': ['sequence'], # aq: 按排序序号升序排列
},
),
migrations.CreateModel(
migrations.CreateModel( # aq: 3. 创建侧边栏表SideBar
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='标题')),
('content', models.TextField(verbose_name='内容')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # aq: 自增主键
('name', models.CharField(max_length=100, verbose_name='标题')), # aq: 侧边栏标题
('content', models.TextField(verbose_name='内容')), # aq: 侧边栏内容支持HTML
('sequence', models.IntegerField(unique=True, verbose_name='排序')), # aq: 排序序号(唯一)
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')), # aq: 是否启用该侧边栏
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
'ordering': ['sequence'], # aq: 按排序序号升序排列
},
),
migrations.CreateModel(
migrations.CreateModel( # aq: 4. 创建标签表Tag
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('id', models.AutoField(primary_key=True, serialize=False)), # aq: 自增主键Int类型
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')), # aq: 标签名称(唯一)
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), # aq: URL友好的slug名称自动生成
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
'ordering': ['name'], # aq: 按标签名称升序排列
},
),
migrations.CreateModel(
migrations.CreateModel( # aq: 5. 创建分类表Category
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
('id', models.AutoField(primary_key=True, serialize=False)), # aq: 自增主键
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')), # aq: 分类名称(唯一)
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), # aq: URL友好的slug名称
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), # aq: 排序权重(数值越大越靠前)
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), # aq: 父分类(自关联,支持多级分类)
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
'ordering': ['-index'], # aq: 按权重降序排列
},
),
migrations.CreateModel(
migrations.CreateModel( # aq: 6. 创建文章表Article——核心数据表
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
('id', models.AutoField(primary_key=True, serialize=False)), # aq: 自增主键
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), # aq: 文章标题(唯一)
('body', mdeditor.fields.MDTextField(verbose_name='正文')), # aq: 文章正文Markdown格式
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')), # aq: 发布时间
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')), # aq: 文章状态(草稿/发表)
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')), # aq: 评论状态(打开/关闭)
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')), # aq: 内容类型(文章/独立页面)
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')), # aq: 阅读量(非负整数)
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), # aq: 文章排序权重
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), # aq: 是否显示文章目录
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), # aq: 关联作者(用户模型)
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), # aq: 关联分类(多对一)
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), # aq: 关联标签(多对多)
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
'ordering': ['-article_order', '-pub_time'], # aq: 先按排序权重降序,再按发布时间降序
'get_latest_by': 'id', # aq: 按ID字段获取最新记录
},
),
]
]

@ -1,23 +1,21 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
from django.db import migrations, models # aq: 导入Django数据库迁移核心模块
class Migration(migrations.Migration):
dependencies = [
class Migration(migrations.Migration): # aq: 数据库迁移类,用于修改数据库表结构
dependencies = [ # aq: 迁移依赖关系需先执行0001_initial初始迁移
('blog', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
operations = [ # aq: 迁移具体操作列表
migrations.AddField( # aq: 给BlogSettings模型新增字段
model_name='blogsettings', # aq: 目标模型(博客配置模型)
name='global_footer', # aq: 新增字段名公共尾部HTML
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), # aq: 字段类型为文本,允许为空,默认值空字符串,后台显示名称“公共尾部”
),
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
migrations.AddField( # aq: 给BlogSettings模型新增字段
model_name='blogsettings', # aq: 目标模型(博客配置模型)
name='global_header', # aq: 新增字段名公共头部HTML
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), # aq: 字段类型为文本,允许为空,默认值空字符串,后台显示名称“公共头部”
),
]
]

@ -1,17 +1,17 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
from django.db import migrations, models # aq: 导入Django迁移相关模块
class Migration(migrations.Migration):
dependencies = [
class Migration(migrations.Migration): # aq: 数据库迁移类,用于更新数据库结构
dependencies = [ # aq: 迁移依赖需先执行0002号迁移
('blog', '0002_blogsettings_global_footer_and_more'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
operations = [ # aq: 迁移操作列表
migrations.AddField( # aq: 新增字段操作
model_name='blogsettings', # aq: 目标模型(博客配置模型)
name='comment_need_review', # aq: 新增字段名(评论是否需要审核)
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), # aq: 字段类型布尔值默认False后台显示名称
),
]
]

@ -1,27 +1,26 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
from django.db import migrations # aq: 导入Django数据库迁移核心模块
class Migration(migrations.Migration):
dependencies = [
class Migration(migrations.Migration): # aq: 数据库迁移类,用于修改字段名称
dependencies = [ # aq: 迁移依赖——需先执行0003号迁移新增评论审核字段
('blog', '0003_blogsettings_comment_need_review'),
]
operations = [
operations = [ # aq: 迁移操作列表——批量重命名BlogSettings模型的字段统一为下划线命名规范
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
model_name='blogsettings', # aq: 目标模型(博客配置模型)
old_name='analyticscode', # aq: 原字段名(驼峰命名)
new_name='analytics_code', # aq: 新字段名下划线命名符合Python规范
),
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
old_name='beiancode', # aq: 原字段名(驼峰命名)
new_name='beian_code', # aq: 新字段名(下划线命名)
),
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
old_name='sitename', # aq: 原字段名(驼峰命名)
new_name='site_name', # aq: 新字段名(下划线命名)
),
]
]

@ -4,17 +4,17 @@ from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
import mdeditor.fields # aq: 导入Markdown编辑器字段类型
class Migration(migrations.Migration):
dependencies = [
dependencies = [ # aq: 迁移依赖——关联用户模型+依赖0004号字段重命名迁移
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
operations = [ # aq: 迁移核心操作——统一模型配置字段名、verbose_name国际化
# aq: 1. 修改模型选项将verbose_name改为英文适配国际化
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
@ -35,6 +35,8 @@ class Migration(migrations.Migration):
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# aq: 2. 删除旧的时间字段(驼峰命名)
migrations.RemoveField(
model_name='article',
name='created_time',
@ -67,15 +69,17 @@ class Migration(migrations.Migration):
model_name='tag',
name='last_mod_time',
),
# aq: 3. 新增统一命名的时间字段(下划线命名+英文verbose_name
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # aq: 创建时间
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), # aq: 最后修改时间
),
migrations.AddField(
model_name='category',
@ -107,6 +111,8 @@ class Migration(migrations.Migration):
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# aq: 4. 修改所有字段的verbose_name为英文适配国际化翻译
migrations.AlterField(
model_name='article',
name='article_order',
@ -297,4 +303,4 @@ class Migration(migrations.Migration):
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]
]

@ -1,17 +1,15 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
from django.db import migrations # aq: 导入Django数据库迁移核心模块
class Migration(migrations.Migration):
dependencies = [
class Migration(migrations.Migration): # aq: 数据库迁移类,用于修改模型元信息
dependencies = [ # aq: 迁移依赖——需先执行0005号迁移统一其他模型国际化配置
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
operations = [
operations = [ # aq: 迁移核心操作——修改BlogSettings模型的verbose_name适配国际化
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
name='blogsettings', # aq: 目标模型(博客配置模型)
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, # aq: 中英文统一为“网站配置”的英文表述,保持全局一致性
),
]
]

@ -1,123 +1,126 @@
import logging
import re
from abc import abstractmethod
from abc import abstractmethod # aq: 导入抽象方法装饰器,用于定义抽象基类
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 slugify import slugify
from django.conf import settings # aq: 导入Django项目的配置对象
from django.core.exceptions import ValidationError # aq: 导入Django的验证错误类
from django.db import models # aq: 导入Django的模型基类用于定义数据库表
from django.urls import reverse # aq: 导入URL反向解析函数生成URL
from django.utils.timezone import now # aq: 导入当前时间函数(带时区)
from django.utils.translation import gettext_lazy as _ # aq: 导入国际化翻译函数
from mdeditor.fields import MDTextField # aq: 导入Markdown编辑器字段
from slugify import slugify # aq: 导入slug生成工具将标题转为URL友好格式
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
from djangoblog.utils import cache_decorator, cache # aq: 导入自定义缓存装饰器和缓存工具
from djangoblog.utils import get_current_site # aq: 导入获取当前站点域名的工具
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # aq: 初始化日志对象,用于记录当前模块的日志
class LinkShowType(models.TextChoices):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
class LinkShowType(models.TextChoices): # aq: 定义链接的展示位置枚举类
I = ('i', _('index')) # aq: 首页展示
L = ('l', _('list')) # aq: 列表页展示
P = ('p', _('post')) # aq: 文章详情页展示
A = ('a', _('all')) # aq: 所有页面展示
S = ('s', _('slide')) # aq: 轮播区域展示
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)
class BaseModel(models.Model): # aq: 抽象基类模型,封装公共字段和方法
id = models.AutoField(primary_key=True) # aq: 自增主键ID
creation_time = models.DateTimeField(_('creation time'), default=now) # aq: 记录创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # aq: 记录最后修改时间
def save(self, *args, **kwargs):
def save(self, *args, **kwargs): # aq: 重写保存方法,扩展功能
# aq: 判断是否是更新文章阅读量的操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# aq: 仅更新阅读量(避免触发其他逻辑)
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# aq: 如果模型有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))
super().save(*args, **kwargs)
super().save(*args, **kwargs) # aq: 调用父类保存方法
def get_full_url(self):
site = get_current_site().domain
def get_full_url(self): # aq: 生成模型对象的完整URL含域名
site = get_current_site().domain # aq: 获取当前站点域名
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True
abstract = True # aq: 标记为抽象类,不会生成数据库表
@abstractmethod
@abstractmethod # aq: 定义抽象方法,子类必须实现
def get_absolute_url(self):
pass
class Article(BaseModel):
class Article(BaseModel): # aq: 文章模型继承BaseModel
"""文章"""
STATUS_CHOICES = (
STATUS_CHOICES = ( # aq: 文章状态选项(草稿/已发布)
('d', _('Draft')),
('p', _('Published')),
)
COMMENT_STATUS = (
COMMENT_STATUS = ( # aq: 评论状态选项(开放/关闭)
('o', _('Open')),
('c', _('Close')),
)
TYPE = (
TYPE = ( # aq: 内容类型选项(文章/页面)
('a', _('Article')),
('p', _('Page')),
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
pub_time = models.DateTimeField(
title = models.CharField(_('title'), max_length=200, unique=True) # aq: 文章标题(唯一)
body = MDTextField(_('body')) # aq: 文章正文Markdown格式
pub_time = models.DateTimeField( # aq: 发布时间
_('publish time'), blank=False, null=False, default=now)
status = models.CharField(
status = models.CharField( # aq: 文章状态
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
comment_status = models.CharField(
comment_status = models.CharField( # aq: 评论状态
_('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(
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # aq: 内容类型
views = models.PositiveIntegerField(_('views'), default=0) # aq: 阅读量(非负整数)
author = models.ForeignKey( # aq: 文章作者(关联用户模型)
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
article_order = models.IntegerField(
article_order = models.IntegerField( # aq: 文章排序权重
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
category = models.ForeignKey(
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # aq: 是否显示目录
category = models.ForeignKey( # aq: 文章分类关联Category模型
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # aq: 文章标签多对多关联Tag
def body_to_string(self):
def body_to_string(self): # aq: 返回正文内容的字符串形式
return self.body
def __str__(self):
def __str__(self): # aq: 模型实例的字符串表示(返回标题)
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'] # aq: 默认排序先按order降序再按发布时间降序
verbose_name = _('article') # aq: 模型的可读名称
verbose_name_plural = verbose_name # aq: 模型的复数可读名称
get_latest_by = 'id' # aq: 获取最新记录的字段
def get_absolute_url(self):
def get_absolute_url(self): # aq: 实现抽象方法生成文章详情页URL
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -125,85 +128,82 @@ class Article(BaseModel):
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
@cache_decorator(60 * 60 * 10) # aq: 缓存10小时
def get_category_tree(self): # aq: 获取文章分类的层级结构(含父分类)
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):
def save(self, *args, **kwargs): # aq: 重写保存方法(这里调用父类方法)
super().save(*args, **kwargs)
def viewed(self):
def viewed(self): # aq: 增加阅读量仅更新views字段
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
def comment_list(self): # aq: 获取文章的评论列表(带缓存)
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)
comments = self.comment_set.filter(is_enable=True).order_by('-id') # aq: 筛选启用的评论按ID降序
cache.set(cache_key, comments, 60 * 100) # aq: 缓存100分钟
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
def get_admin_url(self): # aq: 生成文章在后台管理的编辑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):
# 下一篇
@cache_decorator(expiration=60 * 100) # aq: 缓存100分钟
def next_article(self): # aq: 获取下一篇文章ID更大、已发布
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
@cache_decorator(expiration=60 * 100) # aq: 缓存100分钟
def prev_article(self): # aq: 获取上一篇文章ID更小、已发布
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
def get_first_image_url(self): # aq: 从正文提取第一张图片的URL
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # aq: 匹配Markdown图片语法
if match:
return match.group(1)
return ""
class Category(BaseModel):
class Category(BaseModel): # aq: 分类模型继承BaseModel
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
name = models.CharField(_('category name'), max_length=30, unique=True) # aq: 分类名称(唯一)
parent_category = models.ForeignKey( # aq: 父分类(自关联)
'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'))
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # aq: 分类的slugURL友好名称
index = models.IntegerField(default=0, verbose_name=_('index')) # aq: 分类排序权重
class Meta:
ordering = ['-index']
ordering = ['-index'] # aq: 默认按index降序排序
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
def get_absolute_url(self): # aq: 实现抽象方法生成分类详情页URL
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
def __str__(self): # aq: 模型实例的字符串表示(返回分类名称)
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
@cache_decorator(60 * 60 * 10) # aq: 缓存10小时
def get_category_tree(self): # aq: 递归获取分类的父级层级结构
"""
递归获得分类目录的父级
:return:
@ -218,8 +218,8 @@ class Category(BaseModel):
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
@cache_decorator(60 * 60 * 10) # aq: 缓存10小时
def get_sub_categorys(self): # aq: 递归获取当前分类的所有子分类
"""
获得当前分类目录所有子集
:return:
@ -230,7 +230,7 @@ class Category(BaseModel):
def parse(category):
if category not in categorys:
categorys.append(category)
childs = all_categorys.filter(parent_category=category)
childs = all_categorys.filter(parent_category=category) # aq: 筛选子分类
for child in childs:
if category not in categorys:
categorys.append(child)
@ -240,137 +240,137 @@ class Category(BaseModel):
return categorys
class Tag(BaseModel):
class Tag(BaseModel): # aq: 标签模型继承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) # aq: 标签名称(唯一)
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # aq: 标签的slug
def __str__(self):
def __str__(self): # aq: 模型实例的字符串表示(返回标签名称)
return self.name
def get_absolute_url(self):
def get_absolute_url(self): # aq: 实现抽象方法生成标签详情页URL
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
@cache_decorator(60 * 60 * 10) # aq: 缓存10小时
def get_article_count(self): # aq: 获取该标签关联的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
ordering = ['name'] # aq: 默认按名称排序
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model):
class Links(models.Model): # aq: 友情链接模型
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(
name = models.CharField(_('link name'), max_length=30, unique=True) # aq: 链接名称(唯一)
link = models.URLField(_('link')) # aq: 链接URL
sequence = models.IntegerField(_('order'), unique=True) # aq: 排序序号(唯一)
is_enable = models.BooleanField( # aq: 是否启用该链接
_('is show'), default=True, blank=False, null=False)
show_type = models.CharField(
show_type = models.CharField( # aq: 链接展示位置关联LinkShowType枚举
_('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)
creation_time = models.DateTimeField(_('creation time'), default=now) # aq: 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # aq: 最后修改时间
class Meta:
ordering = ['sequence']
ordering = ['sequence'] # aq: 按排序序号升序排列
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
def __str__(self): # aq: 模型实例的字符串表示(返回链接名称)
return self.name
class SideBar(models.Model):
class SideBar(models.Model): # aq: 侧边栏模型
"""侧边栏,可以展示一些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(_('title'), max_length=100) # aq: 侧边栏标题
content = models.TextField(_('content')) # aq: 侧边栏内容HTML
sequence = models.IntegerField(_('order'), unique=True) # aq: 排序序号(唯一)
is_enable = models.BooleanField(_('is enable'), default=True) # aq: 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) # aq: 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # aq: 最后修改时间
class Meta:
ordering = ['sequence']
ordering = ['sequence'] # aq: 按排序序号升序排列
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
def __str__(self): # aq: 模型实例的字符串表示(返回侧边栏标题)
return self.name
class BlogSettings(models.Model):
class BlogSettings(models.Model): # aq: 博客配置模型
"""blog的配置"""
site_name = models.CharField(
site_name = models.CharField( # aq: 站点名称
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
site_description = models.TextField(
site_description = models.TextField( # aq: 站点描述
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
site_seo_description = models.TextField(
site_seo_description = models.TextField( # aq: 站点SEO描述
_('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField(
site_keywords = models.TextField( # aq: 站点关键词
_('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(
article_sub_length = models.IntegerField(_('article sub length'), default=300) # aq: 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # aq: 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # aq: 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) # aq: 文章页评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # aq: 是否显示Google广告
google_adsense_codes = models.TextField( # aq: Google广告代码
_('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(
open_site_comment = models.BooleanField(_('open site comment'), default=True) # aq: 是否开放全站评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') # aq: 公共头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # aq: 公共尾部HTML
beian_code = models.CharField( # aq: 备案号
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField(
analytics_code = models.TextField( # aq: 网站统计代码
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField(
show_gongan_code = models.BooleanField( # aq: 是否显示公安备案号
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField(
gongan_beiancode = models.TextField( # aq: 公安备案号
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField(
comment_need_review = models.BooleanField( # aq: 评论是否需要审核
'评论是否需要审核', default=False, null=False)
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
def __str__(self):
def __str__(self): # aq: 模型实例的字符串表示(返回站点名称)
return self.site_name
def clean(self):
def clean(self): # aq: 验证方法,确保仅存在一个配置实例
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
def save(self, *args, **kwargs): # aq: 重写保存方法,保存后清空缓存
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
cache.clear()

@ -1,13 +1,14 @@
from haystack import indexes
from haystack import indexes # aq: 导入Haystack搜索框架的索引基类
from blog.models import Article
from blog.models import Article # aq: 导入博客核心文章模型
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
class ArticleIndex(indexes.SearchIndex, indexes.Indexable): # aq: 文章搜索索引类继承Haystack核心索引类
# aq: 核心搜索字段document=True标记为主字段use_template=True表示使用模板定义搜索范围
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
def get_model(self): # aq: 必须实现的方法指定索引关联的Django模型
return Article
def index_queryset(self, using=None):
return self.get_model().objects.filter(status='p')
def index_queryset(self, using=None): # aq: 定义索引的查询范围(仅索引已发布的文章)
return self.get_model().objects.filter(status='p') # aq: status='p'对应文章“已发布”状态

@ -1,73 +1,84 @@
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.paginator import Paginator
from django.templatetags.static import static
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from accounts.models import BlogUser
from blog.forms import BlogSearchForm
from blog.models import Article, Category, Tag, SideBar, Links
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
from djangoblog.utils import get_current_site, get_sha256
from oauth.models import OAuthUser, OAuthConfig
from django.conf import settings # aq: 导入Django项目配置
from django.core.files.uploadedfile import SimpleUploadedFile # aq: 导入简单文件上传类,用于测试图片上传
from django.core.management import call_command # aq: 导入Django命令调用工具用于测试自定义命令
from django.core.paginator import Paginator # aq: 导入分页工具,用于测试分页逻辑
from django.templatetags.static import static # aq: 导入静态文件路径生成工具
from django.test import Client, RequestFactory, TestCase # aq: 导入Django测试核心类模拟客户端、请求工厂、测试用例
from django.urls import reverse # aq: 导入URL反向解析函数
from django.utils import timezone # aq: 导入时区时间工具
from accounts.models import BlogUser # aq: 导入用户模型(自定义用户表)
from blog.forms import BlogSearchForm # aq: 导入博客搜索表单
from blog.models import Article, Category, Tag, SideBar, Links # aq: 导入博客核心模型
from blog.templatetags.blog_tags import load_pagination_info, load_articletags # aq: 导入自定义模板标签
from djangoblog.utils import get_current_site, get_sha256 # aq: 导入工具函数获取站点、SHA256加密
from oauth.models import OAuthUser, OAuthConfig # aq: 导入OAuth相关模型第三方登录
# Create your tests here.
class ArticleTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
def test_validate_article(self):
site = get_current_site().domain
class ArticleTest(TestCase): # aq: 文章模块综合测试类(覆盖模型、视图、表单、模板标签等)
def setUp(self): # aq: 测试初始化方法,每次测试前执行
self.client = Client() # aq: 初始化测试客户端模拟HTTP请求
self.factory = RequestFactory() # aq: 初始化请求工厂(构造自定义请求)
def test_validate_article(self): # aq: 核心测试方法——验证文章相关全流程(创建、访问、搜索等)
site = get_current_site().domain # aq: 获取当前站点域名
# aq: 创建/获取测试超级用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.set_password("liangliangyy") # aq: 设置用户密码
user.is_staff = True # aq: 标记为工作人员(可访问后台)
user.is_superuser = True # aq: 标记为超级管理员
user.save()
# aq: 测试访问用户详情页
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 200) # aq: 断言状态码为200正常访问
# aq: 测试访问后台相关页面(无需登录,仅验证路由存在)
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
# aq: 创建测试侧边栏
s = SideBar()
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.sequence = 1 # aq: 排序序号
s.name = 'test' # aq: 侧边栏标题
s.content = 'test content' # aq: 侧边栏内容
s.is_enable = True # aq: 启用侧边栏
s.save()
# aq: 创建测试分类
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.name = "category" # aq: 分类名称
category.creation_time = timezone.now() # aq: 创建时间
category.last_mod_time = timezone.now() # aq: 最后修改时间
category.save()
# aq: 创建测试标签
tag = Tag()
tag.name = "nicetag"
tag.name = "nicetag" # aq: 标签名称
tag.save()
# aq: 创建测试文章
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.title = "nicetitle" # aq: 文章标题
article.body = "nicecontent" # aq: 文章正文
article.author = user # aq: 关联作者
article.category = category # aq: 关联分类
article.type = 'a' # aq: 类型为“文章”
article.status = 'p' # aq: 状态为“已发布”
article.save()
# aq: 测试文章标签关联(初始无标签,添加后断言数量)
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
article.save()
self.assertEqual(1, article.tags.count())
# aq: 批量创建20篇测试文章用于测试分页
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@ -79,32 +90,44 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
# aq: 若启用Elasticsearch测试搜索功能
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
call_command("build_index") # aq: 执行创建索引命令
response = self.client.get('/search', {'q': 'nicetitle'}) # aq: 发起搜索请求
self.assertEqual(response.status_code, 200)
# aq: 测试访问文章详情页
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# aq: 测试爬虫通知功能(百度收录通知)
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
# aq: 测试访问标签、分类详情页
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# aq: 测试搜索不存在的关键词(验证页面正常响应)
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# aq: 测试自定义模板标签load_articletags
s = load_articletags(article)
self.assertIsNotNone(s)
self.assertIsNotNone(s) # aq: 断言返回结果非空
# aq: 测试用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
# aq: 测试访问归档页面
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# aq: 测试各类分页场景(全部文章、标签归档、作者归档、分类归档)
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
@ -119,16 +142,19 @@ class ArticleTest(TestCase):
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
# aq: 测试搜索表单(空查询)
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify
# aq: 测试百度爬虫通知批量URL
SpiderNotify.baidu_notify([article.get_full_url()])
# aq: 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
u = gravatar_url('liangliangyy@gmail.com') # aq: 生成Gravatar头像URL
u = gravatar('liangliangyy@gmail.com') # aq: 生成Gravatar头像HTML
# aq: 创建测试友情链接并测试访问
link = Links(
sequence=1,
name="lylinux",
@ -137,56 +163,71 @@ class ArticleTest(TestCase):
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# aq: 测试RSS订阅和站点地图
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# aq: 测试后台文章删除、日志访问
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
def check_pagination(self, p, type, value): # aq: 分页测试辅助方法
for page in range(1, p.num_pages + 1):
# aq: 获取分页信息(通过自定义模板标签)
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
self.assertIsNotNone(s) # aq: 断言分页信息非空
# aq: 测试上一页链接(存在则访问)
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# aq: 测试下一页链接(存在则访问)
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
import requests
def test_image(self): # aq: 测试图片上传功能
import requests # aq: 导入requests库用于下载测试图片
# aq: 下载Python官方Logo作为测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png') # aq: 图片保存路径
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# aq: 未带签名访问上传接口预期403禁止访问
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# aq: 生成上传签名双重SHA256加密
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# aq: 构造文件上传数据
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
# aq: 带签名上传图片(跟随重定向)
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
self.assertEqual(rsp.status_code, 200) # aq: 断言上传成功
os.remove(imagepath) # aq: 删除本地测试图片
# aq: 测试邮件发送和用户头像保存工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
send_email(['qq@qq.com'], 'testTitle', 'testContent') # aq: 发送测试邮件
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
'https://www.python.org/static/img/python-logo.png') # aq: 保存远程头像
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_errorpage(self): # aq: 测试错误页面404页面
rsp = self.client.get('/eee') # aq: 访问不存在的路由
self.assertEqual(rsp.status_code, 404) # aq: 断言返回404状态码
def test_commands(self):
def test_commands(self): # aq: 测试自定义Django管理命令
# aq: 创建测试超级用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -195,23 +236,26 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
# aq: 创建测试OAuth配置QQ登录
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# aq: 创建测试OAuth用户关联本地用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
u.user = user
u.picture = static("/blog/img/avatar.png")
u.picture = static("/blog/img/avatar.png") # aq: 本地静态头像
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
}''' # aq: OAuth用户元数据
u.save()
# aq: 创建另一个测试OAuth用户远程头像
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -222,11 +266,12 @@ class ArticleTest(TestCase):
}'''
u.save()
# aq: 执行各类自定义命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
call_command("build_index") # aq: 构建ES搜索索引
call_command("ping_baidu", "all") # aq: 通知百度收录
call_command("create_testdata") # aq: 创建测试数据
call_command("clear_cache") # aq: 清空缓存
call_command("sync_user_avatar") # aq: 同步用户头像
call_command("build_search_words") # aq: 构建搜索关键词

@ -1,62 +1,75 @@
from django.urls import path
from django.views.decorators.cache import cache_page
from django.views.decorators.cache import cache_page # aq: 导入缓存装饰器,用于页面缓存
from . import views
from . import views # aq: 导入当前app的视图模块
app_name = "blog"
app_name = "blog" # aq: 定义URL命名空间避免路由名称冲突
urlpatterns = [
# aq: 首页路由默认第1页
path(
r'',
views.IndexView.as_view(),
name='index'),
# aq: 首页分页路由(指定页码)
path(
r'page/<int:page>/',
r'page/<<int:page>/',
views.IndexView.as_view(),
name='index_page'),
# aq: 文章详情页路由(按日期+文章ID访问
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
r'article/<<int:year>/<<int:month>/<<int:day>/<<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
# aq: 分类详情页路由默认第1页
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
# aq: 分类详情页分页路由
path(
r'category/<slug:category_name>/<int:page>.html',
r'category/<slug:category_name>/<<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
# aq: 作者文章列表路由默认第1页
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
# aq: 作者文章列表分页路由
path(
r'author/<author_name>/<int:page>.html',
r'author/<author_name>/<<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
# aq: 标签文章列表路由默认第1页
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
# aq: 标签文章列表分页路由
path(
r'tag/<slug:tag_name>/<int:page>.html',
r'tag/<slug:tag_name>/<<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
# aq: 文章归档页面缓存1小时减轻服务器压力
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
# aq: 友情链接页面路由
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
# aq: 图片上传接口路由
path(
r'upload',
views.fileupload,
name='upload'),
# aq: 缓存清理接口路由
path(
r'clean',
views.clean_cache_view,
name='clean'),
]
]

@ -2,26 +2,26 @@ import logging
import os
import uuid
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.conf import settings # aq: 导入Django项目全局配置
from django.core.paginator import Paginator # aq: 导入分页工具,用于评论分页
from django.http import HttpResponse, HttpResponseForbidden # aq: 导入HTTP响应类正常响应/禁止访问)
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from django.shortcuts import render # aq: 导入模板渲染函数
from django.templatetags.static import static # aq: 生成静态文件访问URL
from django.utils import timezone # aq: 导入时区时间工具,用于文件存储目录命名
from django.utils.translation import gettext_lazy as _ # aq: 导入国际化翻译函数
from django.views.decorators.csrf import csrf_exempt # aq: 豁免CSRF验证适配外部上传调用
from django.views.generic.detail import DetailView # aq: 导入详情页通用视图基类
from django.views.generic.list import ListView # aq: 导入列表页通用视图基类
from haystack.views import SearchView # aq: 导入Haystack搜索视图基类
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
from blog.models import Article, Category, LinkShowType, Links, Tag # aq: 导入博客核心数据模型
from comments.forms import CommentForm # aq: 导入评论表单类
from djangoblog.plugin_manage import hooks # aq: 导入插件钩子管理工具
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME # aq: 导入文章内容钩子常量
from djangoblog.utils import cache, get_blog_setting, get_sha256 # aq: 导入工具函数(缓存/博客配置/加密)
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # aq: 初始化当前模块日志对象
class ArticleListView(ListView):
@ -33,31 +33,31 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
paginate_by = settings.PAGINATE_BY # aq: 每页显示数量(读取全局配置)
page_kwarg = 'page' # aq: 分页参数名URL中用page传递页码
link_type = LinkShowType.L # aq: 友情链接展示类型(列表页默认)
def get_view_cache_key(self):
return self.request.get['pages']
return self.request.get['pages'] # aq: 原代码语法错误get应为GET保留原逻辑
@property
def page_number(self):
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
return page # aq: 计算当前页码优先URL参数其次GET参数默认1
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
raise NotImplementedError() # aq: 抽象方法,强制子类实现
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
raise NotImplementedError() # aq: 抽象方法,强制子类实现
def get_queryset_from_cache(self, cache_key):
'''
@ -71,7 +71,7 @@ class ArticleListView(ListView):
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
cache.set(cache_key, article_list) # aq: 缓存查询结果
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
@ -82,10 +82,10 @@ class ArticleListView(ListView):
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
return value # aq: 重写父类方法,优先从缓存获取数据
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
kwargs['linktype'] = self.link_type # aq: 向模板添加友情链接类型标识
return super(ArticleListView, self).get_context_data(**kwargs)
@ -94,33 +94,34 @@ class IndexView(ArticleListView):
首页
'''
# 友情链接类型
link_type = LinkShowType.I
link_type = LinkShowType.I # aq: 首页专属友情链接类型
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
return article_list # aq: 查询首页文章(已发布的普通文章)
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
return cache_key # aq: 生成首页缓存键(含页码,区分不同分页)
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
template_name = 'blog/article_detail.html' # aq: 详情页模板
model = Article # aq: 关联的模型类
pk_url_kwarg = 'article_id' # aq: URL中主键参数名对应路由的article_id
context_object_name = "article" # aq: 模板中文章对象的变量名
def get_context_data(self, **kwargs):
comment_form = CommentForm()
comment_form = CommentForm() # aq: 初始化评论表单
article_comments = self.object.comment_list() # aq: 获取文章评论(带缓存)
parent_comments = article_comments.filter(parent_comment=None) # aq: 筛选顶级评论
blog_setting = get_blog_setting() # aq: 获取博客全局配置
paginator = Paginator(parent_comments, blog_setting.article_comment_count) # aq: 评论分页
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
@ -147,8 +148,8 @@ class ArticleDetailView(DetailView):
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
kwargs['next_article'] = self.object.next_article # aq: 下一篇文章
kwargs['prev_article'] = self.object.prev_article # aq: 上一篇文章
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
@ -158,18 +159,18 @@ class ArticleDetailView(DetailView):
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context
return context # aq: 向模板添加评论、上下篇等扩展数据
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
page_type = "分类目录归档" # aq: 页面类型标识(模板展示用)
def get_queryset_data(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
slug = self.kwargs['category_name'] # aq: 从URL获取分类slug
category = get_object_or_404(Category, slug=slug) # aq: 获取分类不存在返回404
categoryname = category.name
self.categoryname = categoryname
@ -177,7 +178,7 @@ class CategoryDetailView(ArticleListView):
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
return article_list # aq: 查询分类及子分类下的已发布文章
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
@ -186,60 +187,60 @@ class CategoryDetailView(ArticleListView):
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
return cache_key # aq: 生成分类缓存键(含分类名+页码)
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
categoryname = categoryname.split('/')[-1] # aq: 处理多级分类,取最后一级名称
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
return super(CategoryDetailView, self).get_context_data(**kwargs) # aq: 向模板添加分类标识
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
page_type = '作者文章归档' # aq: 页面类型标识
def get_queryset_cache_key(self):
from slugify import slugify
author_name = slugify(self.kwargs['author_name'])
author_name = slugify(self.kwargs['author_name']) # aq: 作者名转slug适配URL
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
return cache_key # aq: 生成作者缓存键含作者名slug+页码)
def get_queryset_data(self):
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
return article_list # aq: 查询指定作者的已发布文章
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
return super(AuthorDetailView, self).get_context_data(**kwargs) # aq: 向模板添加作者标识
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档'
page_type = '分类标签归档' # aq: 页面类型标识
def get_queryset_data(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
slug = self.kwargs['tag_name'] # aq: 从URL获取标签slug
tag = get_object_or_404(Tag, slug=slug) # aq: 获取标签不存在返回404
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
return article_list # aq: 查询指定标签的已发布文章
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
@ -248,56 +249,56 @@ class TagDetailView(ArticleListView):
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
return cache_key
return cache_key # aq: 生成标签缓存键(含标签名+页码)
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
return super(TagDetailView, self).get_context_data(**kwargs) # aq: 向模板添加标签标识
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
page_type = '文章归档' # aq: 页面类型标识
paginate_by = None # aq: 归档页不分页
page_kwarg = None # aq: 无分页参数
template_name = 'blog/article_archives.html' # aq: 归档页专用模板
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
return Article.objects.filter(status='p').all() # aq: 查询所有已发布文章
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
return cache_key # aq: 归档页缓存键(固定值)
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
model = Links # aq: 关联友情链接模型
template_name = 'blog/links_list.html' # aq: 友情链接模板
def get_queryset(self):
return Links.objects.filter(is_enable=True)
return Links.objects.filter(is_enable=True) # aq: 筛选启用的友情链接
class EsSearchView(SearchView):
def get_context(self):
paginator, page = self.build_page()
paginator, page = self.build_page() # aq: 构建搜索结果分页
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
"query": self.query, # aq: 搜索关键词
"form": self.form, # aq: 搜索表单
"page": page, # aq: 当前页搜索结果
"paginator": paginator, # aq: 分页器
"suggestion": None, # aq: 拼写建议(默认无)
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
context.update(self.extra_context()) # aq: 添加额外上下文
return context
return context # aq: 向模板添加搜索相关数据
@csrf_exempt
@ -308,17 +309,17 @@ def fileupload(request):
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
sign = request.GET.get('sign', None) # aq: 获取上传签名(防止非法上传)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
return HttpResponseForbidden() # aq: 签名无效返回403
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
timestr = timezone.now().strftime('%Y/%m/%d') # aq: 按日期创建存储目录
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 # aq: 判断是否为图片
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
@ -327,17 +328,17 @@ def fileupload(request):
return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
wfile.write(chunk) # aq: 分块写入文件
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
image.save(savepath, quality=20, optimize=True) # aq: 图片压缩质量20%
url = static(savepath) # aq: 生成文件静态URL
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
return HttpResponse("only for post") # aq: 仅支持POST请求
def page_not_found_view(
@ -345,13 +346,13 @@ def page_not_found_view(
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
logger.error(exception) # aq: 记录异常日志
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
status=404) # aq: 404页面处理视图
def server_error_view(request, template_name='blog/error_page.html'):
@ -359,7 +360,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
status=500) # aq: 500页面处理视图
def permission_denied_view(
@ -367,13 +368,13 @@ def permission_denied_view(
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
logger.error(exception) # aq: 记录异常日志
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
'statuscode': '403'}, status=403) # aq: 403页面处理视图
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
return HttpResponse('ok')
Loading…
Cancel
Save