Merge remote-tracking branch 'origin/bjy_branch' into develop

# Conflicts:
#	src/DjangoBlog/blog/admin.py
#	src/DjangoBlog/blog/documents.py
#	src/DjangoBlog/blog/management/commands/ping_baidu.py
#	src/DjangoBlog/blog/management/commands/sync_user_avatar.py
#	src/DjangoBlog/blog/middleware.py
#	src/DjangoBlog/blog/migrations/0001_initial.py
#	src/DjangoBlog/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
#	src/DjangoBlog/blog/tests.py
#	src/DjangoBlog/blog/views.py
dynastxu 3 months ago
commit f285750263

@ -1,3 +1,4 @@
# bjy: 从Django中导入所需的模块和类
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
@ -9,106 +10,157 @@ from django.utils.translation import gettext_lazy as _
from .models import Article
# bjy: 为Article模型创建一个自定义的ModelForm
class ArticleForm(forms.ModelForm):
# bjy: 示例如果使用Pagedown编辑器可以取消下面这行的注释
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
# bjy: 指定这个表单对应的模型是Article
model = Article
# bjy: 表示在表单中包含模型的所有字段
fields = '__all__'
# bjy: 定义一个admin动作用于将选中的文章发布
def makr_article_publish(queryset):
# bjy: 批量更新查询集中所有文章的状态为'p'(已发布)
queryset.update(status='p')
# bjy: 定义一个admin动作用于将选中的文章设为草稿
def draft_article(queryset):
# bjy: 批量更新查询集中所有文章的状态为'd'(草稿)
queryset.update(status='d')
# bjy: 定义一个admin动作用于关闭选中文章的评论功能
def close_article_commentstatus(queryset):
# bjy: 批量更新查询集中所有文章的评论状态为'c'(关闭)
queryset.update(comment_status='c')
# bjy: 定义一个admin动作用于开启选中文章的评论功能
def open_article_commentstatus(queryset):
# bjy: 批量更新查询集中所有文章的评论状态为'o'(开启)
queryset.update(comment_status='o')
# bjy: 为admin动作设置在后台显示的描述文本
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')
# bjy: 为Article模型自定义Admin管理界面
class ArticlelAdmin(admin.ModelAdmin):
# bjy: 设置每页显示20条记录
list_per_page = 20
# bjy: 启用搜索功能搜索范围包括文章内容body和标题title
search_fields = ('body', 'title')
# bjy: 指定使用的自定义表单
form = ArticleForm
# bjy: 在列表视图中显示的字段
list_display = (
'id',
'title',
'author',
'link_to_category',
'link_to_category', # bjy: 自定义方法,显示指向分类的链接
'creation_time',
'views',
'status',
'type',
'article_order')
# bjy: 设置列表视图中可点击进入编辑页面的链接字段
list_display_links = ('id', 'title')
# bjy: 启用右侧筛选栏,可按状态、类型、分类进行筛选
list_filter = ('status', 'type', 'category')
# bjy: 启用日期层次导航,按创建时间进行分层
date_hierarchy = 'creation_time'
# bjy: 为多对多字段tags提供一个水平筛选的界面
filter_horizontal = ('tags',)
# bjy: 在编辑页面中排除的字段,这些字段将自动处理
exclude = ('creation_time', 'last_modify_time')
# bjy: 在列表页面显示“在站点上查看”的按钮
view_on_site = True
# bjy: 将自定义的admin动作添加到动作下拉列表中
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
# bjy: 对于外键字段author, category显示为一个输入框用于输入ID而不是下拉列表
raw_id_fields = ('author', 'category',)
# bjy: 自定义方法,用于在列表页面显示一个指向文章分类的链接
def link_to_category(self, obj):
# bjy: 获取分类模型的app_label和model_name用于构建admin URL
info = (obj.category._meta.app_label, obj.category._meta.model_name)
# bjy: 生成指向该分类编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
# bjy: 使用format_html安全地生成HTML链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
# bjy: 设置该方法在列表页面列标题的显示文本
link_to_category.short_description = _('category')
# bjy: 重写get_form方法用于动态修改表单
def get_form(self, request, obj=None, **kwargs):
# bjy: 获取父类的表单
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# bjy: 修改author字段的查询集只显示超级用户
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
# bjy: 重写save_model方法在保存模型时执行额外操作
def save_model(self, request, obj, form, change):
# bjy: 调用父类的save_model方法执行默认保存操作
super(ArticlelAdmin, self).save_model(request, obj, form, change)
# bjy: 重写get_view_on_site_url方法自定义“在站点上查看”的URL
def get_view_on_site_url(self, obj=None):
if obj:
# bjy: 如果对象存在则调用模型的get_full_url方法获取URL
url = obj.get_full_url()
return url
else:
# bjy: 如果对象不存在例如在添加新对象时则返回网站首页URL
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
# bjy: 为Tag模型自定义Admin管理界面
class TagAdmin(admin.ModelAdmin):
# bjy: 在编辑页面中排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# bjy: 为Category模型自定义Admin管理界面
class CategoryAdmin(admin.ModelAdmin):
# bjy: 在列表视图中显示的字段
list_display = ('name', 'parent_category', 'index')
# bjy: 在编辑页面中排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# bjy: 为Links模型自定义Admin管理界面
class LinksAdmin(admin.ModelAdmin):
# bjy: 在编辑页面中排除的字段
exclude = ('last_mod_time', 'creation_time')
# bjy: 为SideBar模型自定义Admin管理界面
class SideBarAdmin(admin.ModelAdmin):
# bjy: 在列表视图中显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# bjy: 在编辑页面中排除的字段
exclude = ('last_mod_time', 'creation_time')
# bjy: 为BlogSettings模型自定义Admin管理界面
class BlogSettingsAdmin(admin.ModelAdmin):
# bjy: 使用默认配置,无需自定义
pass

@ -1,5 +1,8 @@
# bjy: 从Django中导入AppConfig基类用于配置应用程序
from django.apps import AppConfig
# bjy: 定义一个名为BlogConfig的配置类它继承自AppConfig
class BlogConfig(AppConfig):
# bjy: 指定这个配置类对应的应用程序名称通常是Python包的路径
name = 'blog'

@ -1,43 +1,76 @@
# bjy: 导入日志模块
import logging
# bjy: 从Django中导入时区工具
from django.utils import timezone
# bjy: 从项目工具模块中导入缓存和获取博客设置的函数
from djangoblog.utils import cache, get_blog_setting
# bjy: 从当前应用的models中导入Category和Article模型
from .models import Category, Article
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个上下文处理器,用于在所有模板中注入全局变量
def seo_processor(requests):
# bjy: 定义一个缓存键名
key = 'seo_processor'
# bjy: 尝试从缓存中获取数据
value = cache.get(key)
# bjy: 如果缓存中存在数据,则直接返回
if value:
return value
else:
# bjy: 如果缓存中没有数据,则记录一条日志
logger.info('set processor cache.')
# bjy: 获取博客的设置对象
setting = get_blog_setting()
# bjy: 构建一个包含所有SEO和全局设置的字典
value = {
# bjy: 网站名称
'SITE_NAME': setting.site_name,
# bjy: 是否显示Google AdSense广告
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
# bjy: Google AdSense的广告代码
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
# bjy: 网站的SEO描述
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
# bjy: 网站的普通描述
'SITE_DESCRIPTION': setting.site_description,
# bjy: 网站的关键词
'SITE_KEYWORDS': setting.site_keywords,
# bjy: 网站的完整基础URL协议+域名)
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
# bjy: 文章列表页的摘要长度
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
# bjy: 用于导航栏的所有分类列表
'nav_category_list': Category.objects.all(),
# bjy: 用于导航栏的所有已发布的“页面”类型的文章
'nav_pages': Article.objects.filter(
type='p',
status='p'),
type='p', # bjy: 类型为'p'page
status='p'), # bjy: 状态为'p'published
# bjy: 是否开启全站评论功能
'OPEN_SITE_COMMENT': setting.open_site_comment,
# bjy: 网站的ICP备案号
'BEIAN_CODE': setting.beian_code,
# bjy: 网站统计代码如Google Analytics
'ANALYTICS_CODE': setting.analytics_code,
# bjy: 公安备案号
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
# bjy: 是否显示公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code,
# bjy: 当前年份,用于页脚版权信息
"CURRENT_YEAR": timezone.now().year,
# bjy: 全局页头HTML代码
"GLOBAL_HEADER": setting.global_header,
# bjy: 全局页脚HTML代码
"GLOBAL_FOOTER": setting.global_footer,
# bjy: 评论是否需要审核
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
# bjy: 将构建好的字典存入缓存缓存时间为10小时60*60*10秒
cache.set(key, value, 60 * 60 * 10)
# bjy: 返回这个字典,它将被注入到所有模板的上下文中
return value

@ -1,26 +1,40 @@
# bjy: 导入时间模块
import time
# bjy: 导入Elasticsearch的客户端模块和异常类
import elasticsearch.client
import elasticsearch.exceptions
# bjy: 导入Django的设置
from django.conf import settings
# bjy: 从elasticsearch_dsl中导入文档、内部文档、字段类型和连接管理器
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
# bjy: 从blog应用中导入Article模型
from blog.models import Article
# bjy: 检查Django设置中是否配置了ELASTICSEARCH_DSL以决定是否启用Elasticsearch功能
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
# bjy: 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
# bjy: 根据Django设置创建到Elasticsearch的连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
# bjy: 导入并实例化Elasticsearch客户端
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# bjy: 导入并实例化Ingest客户端用于管理管道
from elasticsearch.client import IngestClient
c = IngestClient(es)
# bjy: 尝试获取名为'geoip'的管道
try:
c.get_pipeline('geoip')
# bjy: 如果管道不存在,则创建它
except elasticsearch.exceptions.NotFoundError:
# bjy: 创建一个geoip管道用于根据IP地址添加地理位置信息
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -33,58 +47,90 @@ if ELASTICSEARCH_ENABLED:
}''')
# bjy: 定义一个内部文档InnerDoc结构用于存储IP地理位置信息
class GeoIp(InnerDoc):
# bjy: 大洲名称
continent_name = Keyword()
# bjy: 国家ISO代码
country_iso_code = Keyword()
# bjy: 国家名称
country_name = Keyword()
# bjy: 地理坐标(经纬度)
location = GeoPoint()
# bjy: 定义内部文档用于存储用户代理User-Agent中的浏览器信息
class UserAgentBrowser(InnerDoc):
# bjy: 浏览器家族如Chrome, Firefox
Family = Keyword()
# bjy: 浏览器版本
Version = Keyword()
# bjy: 定义内部文档,用于存储用户代理中的操作系统信息
class UserAgentOS(UserAgentBrowser):
# bjy: 继承自UserAgentBrowser结构相同
pass
# bjy: 定义内部文档,用于存储用户代理中的设备信息
class UserAgentDevice(InnerDoc):
# bjy: 设备家族如iPhone, Android
Family = Keyword()
# bjy: 设备品牌如Apple, Samsung
Brand = Keyword()
# bjy: 设备型号如iPhone 12
Model = Keyword()
# bjy: 定义内部文档,用于存储完整的用户代理信息
class UserAgent(InnerDoc):
# bjy: 嵌套浏览器信息
browser = Object(UserAgentBrowser, required=False)
# bjy: 嵌套操作系统信息
os = Object(UserAgentOS, required=False)
# bjy: 嵌套设备信息
device = Object(UserAgentDevice, required=False)
# bjy: 原始User-Agent字符串
string = Text()
# bjy: 是否为爬虫或机器人
is_bot = Boolean()
# bjy: 定义一个Elasticsearch文档用于存储页面性能数据如响应时间
class ElapsedTimeDocument(Document):
# bjy: 请求的URL
url = Keyword()
# bjy: 请求耗时(毫秒)
time_taken = Long()
# bjy: 日志记录时间
log_datetime = Date()
# bjy: 客户端IP地址
ip = Keyword()
# bjy: 嵌套的IP地理位置信息
geoip = Object(GeoIp, required=False)
# bjy: 嵌套的用户代理信息
useragent = Object(UserAgent, required=False)
class Index:
# bjy: 指定索引名称为'performance'
name = 'performance'
# bjy: 设置索引的分片和副本数
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
# bjy: 指定文档类型
doc_type = 'ElapsedTime'
# bjy: 定义一个管理类用于操作ElapsedTimeDocument索引
class ElaspedTimeDocumentManager:
@staticmethod
def build_index():
# bjy: 如果索引不存在,则创建它
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
@ -93,13 +139,16 @@ class ElaspedTimeDocumentManager:
@staticmethod
def delete_index():
# bjy: 删除'performance'索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
# bjy: 确保索引存在
ElaspedTimeDocumentManager.build_index()
# bjy: 构建UserAgent内部文档对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@ -116,8 +165,10 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# bjy: 创建ElapsedTimeDocument文档实例
doc = ElapsedTimeDocument(
meta={
# bjy: 使用当前时间的毫秒数作为文档ID
'id': int(
round(
time.time() *
@ -127,60 +178,81 @@ class ElaspedTimeDocumentManager:
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
# bjy: 保存文档,并使用'geoip'管道处理IP地址
doc.save(pipeline="geoip")
# bjy: 定义一个Elasticsearch文档用于存储博客文章数据以支持全文搜索
class ArticleDocument(Document):
# bjy: 文章内容使用ik分词器进行索引和搜索
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# bjy: 文章标题使用ik分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# bjy: 作者信息,为一个对象类型
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# bjy: 分类信息,为一个对象类型
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# bjy: 标签信息,为一个对象类型
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# bjy: 发布时间
pub_time = Date()
# bjy: 文章状态
status = Text()
# bjy: 评论状态
comment_status = Text()
# bjy: 文章类型
type = Text()
# bjy: 浏览量
views = Integer()
# bjy: 文章排序权重
article_order = Integer()
class Index:
# bjy: 指定索引名称为'blog'
name = 'blog'
# bjy: 设置索引的分片和副本数
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
# bjy: 指定文档类型
doc_type = 'Article'
# bjy: 定义一个管理类用于操作ArticleDocument索引
class ArticleDocumentManager:
def __init__(self):
# bjy: 初始化时创建索引
self.create_index()
@staticmethod
def create_index():
# bjy: 创建'blog'索引
ArticleDocument.init()
@staticmethod
def delete_index():
# bjy: 删除'blog'索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
@staticmethod
def convert_to_doc(articles):
# bjy: 将Django的Article查询集转换为ArticleDocument对象列表
return [
ArticleDocument(
meta={
@ -205,13 +277,16 @@ class ArticleDocumentManager:
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
# bjy: 重建索引。如果未提供articles则使用所有文章
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
# bjy: 遍历并保存每个文档
for doc in docs:
doc.save()
@staticmethod
def update_docs(docs):
# bjy: 更新一组文档
for doc in docs:
doc.save()

@ -1,19 +1,32 @@
# bjy: 导入日志模块
import logging
# bjy: 从Django中导入表单模块
from django import forms
# bjy: 从haystack一个Django搜索框架中导入基础搜索表单
from haystack.forms import SearchForm
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个自定义的博客搜索表单继承自Haystack的SearchForm
class BlogSearchForm(SearchForm):
# bjy: 定义一个名为querydata的字符字段用于接收用户输入的搜索关键词并设置为必填
querydata = forms.CharField(required=True)
# bjy: 重写search方法用于执行搜索逻辑
def search(self):
# bjy: 调用父类的search方法执行默认的搜索并返回结果集
datas = super(BlogSearchForm, self).search()
# bjy: 检查表单数据是否有效
if not self.is_valid():
# bjy: 如果表单无效则调用no_query_found方法通常返回一个空的结果集
return self.no_query_found()
# bjy: 如果用户在querydata字段中输入了内容
if self.cleaned_data['querydata']:
# bjy: 将用户输入的搜索关键词记录到日志中
logger.info(self.cleaned_data['querydata'])
# bjy: 返回搜索结果
return datas

@ -1,18 +1,23 @@
from django.core.management.base import BaseCommand
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
# bjy: 定义一个继承自BaseCommand的命令类用于执行构建搜索索引的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"构建搜索索引"
help = 'build search index'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 检查Elasticsearch功能是否已启用确保在启用状态下才执行索引操作
if ELASTICSEARCH_ENABLED:
# bjy: 调用ElapsedTimeDocumentManager的类方法构建用于记录耗时的文档索引
ElaspedTimeDocumentManager.build_index()
# bjy: 创建ElapsedTimeDocument的实例并调用其init方法进行初始化可能是数据同步或设置
manager = ElapsedTimeDocument()
manager.init()
# bjy: 创建ArticleDocumentManager的实例用于管理文章的搜索索引
manager = ArticleDocumentManager()
# bjy: 删除现有的文章索引,为重建做准备,防止旧数据冲突
manager.delete_index()
# bjy: 重新构建文章索引将数据库中的最新文章数据同步到Elasticsearch
manager.rebuild()

@ -1,13 +1,20 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从当前应用的models模块导入Tag和Category模型用于获取数据
from blog.models import Tag, Category
# TODO 参数化
# bjy: 定义一个继承自BaseCommand的命令类用于执行构建搜索词的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"构建搜索词"
help = 'build search words'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 使用集合推导式获取所有Tag和Category的name字段并自动去重
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
# bjy: 将去重后的搜索词集合中的每个元素用换行符连接,并打印到标准输出
print('\n'.join(datas))

@ -1,11 +1,18 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从项目工具模块导入cache实例用于操作缓存
from djangoblog.utils import cache
# bjy: 定义一个继承自BaseCommand的命令类用于执行清空缓存的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"清空所有缓存"
help = 'clear the whole cache'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 调用cache实例的clear方法清空所有缓存
cache.clear()
# bjy: 使用成功样式向标准输出写入操作成功的信息,并附带换行符
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -1,40 +1,65 @@
# bjy: 从Django的auth模块导入get_user_model函数用于动态获取当前项目激活的用户模型
from django.contrib.auth import get_user_model
# bjy: 从Django的auth模块导入make_password函数用于创建加密后的密码哈希
from django.contrib.auth.hashers import make_password
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从当前应用的models模块导入Article, Tag, Category模型用于创建测试数据
from blog.models import Article, Tag, Category
# bjy: 定义一个继承自BaseCommand的命令类用于执行创建测试数据的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"创建测试数据"
help = 'create test datas'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 获取或创建一个测试用户,如果不存在则创建,密码已加密
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# bjy: 获取或创建一个父级分类parent_category为None表示它是顶级分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# bjy: 获取或创建一个子分类,并设置其父分类为上面创建的父级分类
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
# bjy: 显式保存子分类实例确保数据已写入数据库虽然get_or_create通常会保存
category.save()
# bjy: 创建一个基础标签,所有文章都将共用此标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# bjy: 循环19次创建19篇测试文章和对应的标签
for i in range(1, 20):
# bjy: 获取或创建一篇文章,关联到上面创建的分类、用户,并设置标题和内容
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
# bjy: 为每篇文章创建一个专属标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# bjy: 将专属标签和基础标签都添加到当前文章的标签集合中
article.tags.add(tag)
article.tags.add(basetag)
# bjy: 保存文章,使标签关联生效
article.save()
# bjy: 导入项目的cache工具用于清理缓存
from djangoblog.utils import cache
# bjy: 清空所有缓存,以确保新创建的数据能被正确加载
cache.clear()
# bjy: 使用成功样式向标准输出写入操作完成的信息
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -1,16 +1,23 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
from djangoblog.spider_notify import SpiderNotify
# bjy: 从项目工具模块导入get_current_site函数用于获取当前站点域名等信息
from djangoblog.utils import get_current_site
# bjy: 获取当前站点的域名用于拼接完整URL
site = get_current_site().domain
# bjy: 定义一个继承自BaseCommand的命令类用于执行通知百度抓取URL的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"通知百度URL"
help = 'notify baidu url'
# bjy: 为命令添加参数,允许用户指定通知的数据类型
def add_arguments(self, parser):
# bjy: 添加一个名为data_type的位置参数类型为字符串且只能从给定的选项中选择
parser.add_argument(
'data_type',
type=str,
@ -21,31 +28,43 @@ class Command(BaseCommand):
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
# bjy: 定义一个辅助方法用于根据路径拼接完整的URL
@staticmethod
def get_full_url(path):
# bjy: 使用https协议和当前站点域名拼接完整URL
url = "https://{site}{path}".format(site=site, path=path)
return url
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 获取用户指定的data_type参数决定通知哪些类型的URL
type = options['data_type']
# bjy: 输出开始获取指定类型URL的信息
self.stdout.write('start get %s' % type)
# bjy: 初始化一个空列表用于收集所有待通知的URL
urls = []
# bjy: 如果类型为article或all则收集所有已发布文章的完整URL
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
# bjy: 如果类型为tag或all则收集所有标签的完整URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
# bjy: 如果类型为category或all则收集所有分类的完整URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
# bjy: 输出开始通知URL的数量信息使用成功样式
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# bjy: 调用SpiderNotify的百度通知方法将收集到的URL发送给百度
SpiderNotify.baidu_notify(urls)
# bjy: 输出通知完成的信息,使用成功样式
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,48 +1,76 @@
# bjy: 导入requests库用于发起HTTP请求检测头像URL是否可访问
import requests
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从Django模板标签模块导入static函数用于生成静态文件的URL
from django.templatetags.static import static
# bjy: 从项目工具模块导入save_user_avatar函数用于保存用户头像到本地
from djangoblog.utils import save_user_avatar
# bjy: 从oauth应用导入OAuthUser模型用于获取所有OAuth用户数据
from oauth.models import OAuthUser
# bjy: 从oauth应用导入get_manager_by_type函数用于根据OAuth类型获取对应的管理器
from oauth.oauthmanager import get_manager_by_type
# bjy: 定义一个继承自BaseCommand的命令类用于执行同步用户头像的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"同步用户头像"
help = 'sync user avatar'
# bjy: 定义一个辅助方法用于测试给定的URL是否可访问返回200状态码
@staticmethod
def test_picture(url):
try:
# bjy: 尝试GET请求设置2秒超时如果状态码为200则返回True
if requests.get(url, timeout=2).status_code == 200:
return True
except:
# bjy: 任何异常都视为不可访问,静默忽略
pass
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 获取项目静态文件的基础URL用于判断头像是否为本地静态文件
static_url = static("../")
# bjy: 获取所有OAuth用户
users = OAuthUser.objects.all()
# bjy: 输出开始同步用户头像的总数信息
self.stdout.write(f'开始同步{len(users)}个用户头像')
# bjy: 遍历每个用户,进行头像同步
for u in users:
# bjy: 输出当前正在同步的用户昵称
self.stdout.write(f'开始同步:{u.nickname}')
# bjy: 获取用户当前的头像URL
url = u.picture
# bjy: 如果头像URL不为空则执行同步逻辑
if url:
# bjy: 如果当前头像URL是本地静态文件路径
if url.startswith(static_url):
# bjy: 测试该静态文件是否可访问,若可访问则跳过此用户
if self.test_picture(url):
continue
else:
# bjy: 如果不可访问且用户有metadata信息则尝试通过OAuth管理器重新获取头像URL并保存
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
# bjy: 如果没有metadata则使用默认头像
url = static('blog/img/avatar.png')
else:
# bjy: 如果头像URL不是本地静态文件则直接保存到本地
url = save_user_avatar(url)
else:
# bjy: 如果头像URL为空则使用默认头像
url = static('blog/img/avatar.png')
# bjy: 如果最终得到的URL不为空则更新用户头像并保存
if url:
# bjy: 输出同步完成后的用户昵称和头像URL
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
# bjy: 输出同步全部结束的信息
self.stdout.write('结束同步')

@ -1,42 +1,68 @@
# bjy: 导入日志模块
import logging
# bjy: 导入时间模块,用于计算页面渲染时间
import time
# bjy: 从ipware库导入get_client_ip函数用于获取客户端真实IP
from ipware import get_client_ip
# bjy: 从user_agents库导入parse函数用于解析User-Agent字符串
from user_agents import parse
# bjy: 从blog应用的documents模块中导入Elasticsearch是否启用的标志和性能文档管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个中间件类,用于记录页面性能和在线用户信息
class OnlineMiddleware(object):
# bjy: Django 1.10+ 兼容的初始化方法
def __init__(self, get_response=None):
# bjy: 保存get_response可调用对象它是Django请求-响应链中的下一个处理器
self.get_response = get_response
# bjy: 调用父类的初始化方法
super().__init__()
# bjy: 中间件的核心调用方法,每个请求都会经过这里
def __call__(self, request):
""" page render time """
# bjy: 记录页面渲染开始时间
start_time = time.time()
# bjy: 调用下一个中间件或视图,获取响应对象
response = self.get_response(request)
# bjy: 从请求头中获取User-Agent字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# bjy: 使用ipware库获取客户端的IP地址
ip, _ = get_client_ip(request)
# bjy: 解析User-Agent字符串得到结构化的用户代理信息
user_agent = parse(http_user_agent)
# bjy: 检查响应是否为流式响应(如文件下载),如果不是,则进行处理
if not response.streaming:
try:
# bjy: 计算页面渲染耗时(秒)
cast_time = time.time() - start_time
# bjy: 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
# bjy: 将耗时转换为毫秒并四舍五入
time_taken = round(cast_time * 1000, 2)
# bjy: 获取请求的URL路径
url = request.path
# bjy: 导入Django的时区工具
from django.utils import timezone
# bjy: 调用文档管理器将性能数据保存到Elasticsearch
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# bjy: 将页面渲染耗时替换到响应内容的特定占位符<!!LOAD_TIMES!!>中
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
# bjy: 捕获并记录处理过程中可能发生的任何异常
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
# bjy: 返回最终的响应对象
return response

@ -1,23 +1,35 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
# bjy: 此文件由Django 4.1.7于2023-03-29 06:08自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations和models用于定义迁移操作和模型字段
from django.db import migrations, models
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0001_initial迁移确保基础表已存在
dependencies = [
('blog', '0001_initial'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作1为BlogSettings模型添加一个名为'global_footer'的字段
migrations.AddField(
# bjy: 指定要操作的模型名称
model_name='blogsettings',
# bjy: 指定新字段的名称
name='global_footer',
# bjy: 定义新字段的类型和属性文本类型可为空默认空字符串并设置verbose_name
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
# bjy: 操作2为BlogSettings模型添加一个名为'global_header'的字段
migrations.AddField(
# bjy: 指定要操作的模型名称
model_name='blogsettings',
# bjy: 指定新字段的名称
name='global_header',
# bjy: 定义新字段的类型和属性文本类型可为空默认空字符串并设置verbose_name
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -1,17 +1,25 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
# bjy: 此文件由Django 4.2.1于2023-05-09 07:45自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations和models用于定义迁移操作和模型字段
from django.db import migrations, models
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0002_blogsettings_global_footer_and_more迁移
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作为BlogSettings模型添加一个新字段
migrations.AddField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定新字段的名称为'comment_need_review'
name='comment_need_review',
# bjy: 定义新字段的类型和属性布尔类型默认值为False并设置verbose_name
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -1,27 +1,43 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
# bjy: 此文件由Django 4.2.1于2023-05-09 07:51自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations用于定义迁移操作
from django.db import migrations
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0003_blogsettings_comment_need_review迁移
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作1重命名BlogSettings模型中的一个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'analyticscode'
old_name='analyticscode',
# bjy: 指定字段的新名称为'analytics_code'
new_name='analytics_code',
),
# bjy: 操作2重命名BlogSettings模型中的另一个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'beiancode'
old_name='beiancode',
# bjy: 指定字段的新名称为'beian_code'
new_name='beian_code',
),
# bjy: 操作3重命名BlogSettings模型中的第三个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'sitename'
old_name='sitename',
# bjy: 指定字段的新名称为'site_name'
new_name='site_name',
),
]

@ -1,17 +1,24 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
# bjy: 此文件由Django 4.2.7于2024-01-26 02:41自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations用于定义迁移操作
from django.db import migrations
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0005迁移
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作修改BlogSettings模型的Meta选项更新verbose_name为英文
migrations.AlterModelOptions(
# bjy: 指定要操作的模型名称为'blogsettings'
name='blogsettings',
# bjy: 更新模型的verbose_name和verbose_name_plural为'Website configuration'
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -1,49 +1,74 @@
# bjy: 导入日志模块
import logging
# bjy: 导入正则表达式模块
import re
# bjy: 导入抽象基类模块,用于定义抽象方法
from abc import abstractmethod
# bjy: 从Django中导入设置、异常、模型、URL反向解析、时区和国际化工具
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 _
# bjy: 从Django MDEditor中导入Markdown文本字段
from mdeditor.fields import MDTextField
# bjy: 从uuslug中导入slugify函数用于生成URL友好的slug
from uuslug import slugify
# bjy: 从项目工具模块中导入缓存装饰器和缓存对象
from djangoblog.utils import cache_decorator, cache
# bjy: 从项目工具模块中导入获取当前站点的函数
from djangoblog.utils import get_current_site
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个文本选择类,用于链接显示类型
class LinkShowType(models.TextChoices):
# bjy: 首页
I = ('i', _('index'))
# bjy: 列表页
L = ('l', _('list'))
# bjy: 文章页
P = ('p', _('post'))
# bjy: 所有页面
A = ('a', _('all'))
# bjy: 幻灯片
S = ('s', _('slide'))
# bjy: 定义一个基础模型类,作为其他模型的父类
class BaseModel(models.Model):
# bjy: 自增主键
id = models.AutoField(primary_key=True)
# bjy: 创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('modify time'), default=now)
# bjy: 重写save方法以实现自定义逻辑
def save(self, *args, **kwargs):
# bjy: 检查是否是更新文章浏览量的操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# bjy: 如果是则直接更新数据库中的views字段避免触发其他save逻辑
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# bjy: 如果模型有slug字段则根据title或name自动生成slug
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
# bjy: 调用父类的save方法
super().save(*args, **kwargs)
# bjy: 获取模型的完整URL包括域名
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
@ -51,72 +76,96 @@ class BaseModel(models.Model):
return url
class Meta:
# bjy: 设置为抽象模型,不会在数据库中创建表
abstract = True
# bjy: 定义一个抽象方法,要求子类必须实现
@abstractmethod
def get_absolute_url(self):
pass
# bjy: 定义文章模型
class Article(BaseModel):
"""文章"""
# bjy: 文章状态选择
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
# bjy: 评论状态选择
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
# bjy: 文章类型选择
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
# bjy: 文章标题,唯一
title = models.CharField(_('title'), max_length=200, unique=True)
# bjy: 文章正文使用Markdown编辑器
body = MDTextField(_('body'))
# bjy: 发布时间
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
# bjy: 文章状态
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
# bjy: 评论状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
# bjy: 文章类型
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
# bjy: 浏览量
views = models.PositiveIntegerField(_('views'), default=0)
# bjy: 作者,外键关联到用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
# bjy: 文章排序权重
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
# bjy: 是否显示目录
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
# bjy: 分类外键关联到Category模型
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
# bjy: 标签多对多关联到Tag模型
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
# bjy: 将body字段转换为字符串
def body_to_string(self):
return self.body
# bjy: 定义文章的字符串表示
def __str__(self):
return self.title
class Meta:
# bjy: 默认排序方式
ordering = ['-article_order', '-pub_time']
# bjy: 模型的单数和复数名称
verbose_name = _('article')
verbose_name_plural = verbose_name
# bjy: 指定按哪个字段获取最新对象
get_latest_by = 'id'
# bjy: 获取文章的绝对URL
def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
@ -125,6 +174,7 @@ class Article(BaseModel):
'day': self.creation_time.day
})
# bjy: 获取文章的分类树路径,带缓存
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
tree = self.category.get_category_tree()
@ -132,13 +182,16 @@ class Article(BaseModel):
return names
# bjy: 重写save方法
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# bjy: 增加浏览量
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
# bjy: 获取文章的评论列表,带缓存
def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
@ -151,21 +204,25 @@ class Article(BaseModel):
logger.info('set article comments:{id}'.format(id=self.id))
return comments
# bjy: 获取文章在Admin后台的编辑URL
def get_admin_url(self):
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
# bjy: 获取下一篇文章,带缓存
@cache_decorator(expiration=60 * 100)
def next_article(self):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
# bjy: 获取上一篇文章,带缓存
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
# bjy: 从文章正文中提取第一张图片的URL
def get_first_image_url(self):
"""
Get the first image url from article.body.
@ -177,31 +234,40 @@ class Article(BaseModel):
return ""
# bjy: 定义分类模型
class Category(BaseModel):
"""文章分类"""
# bjy: 分类名称,唯一
name = models.CharField(_('category name'), max_length=30, unique=True)
# bjy: 父分类,自关联外键
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
# bjy: URL友好的别名
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# bjy: 排序索引
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
# bjy: 按索引降序排列
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
# bjy: 获取分类的绝对URL
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
# bjy: 定义分类的字符串表示
def __str__(self):
return self.name
# bjy: 递归获取分类的所有父级分类,带缓存
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
@ -218,6 +284,7 @@ class Category(BaseModel):
parse(self)
return categorys
# bjy: 获取当前分类的所有子分类,带缓存
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
@ -240,136 +307,186 @@ class Category(BaseModel):
return categorys
# bjy: 定义标签模型
class Tag(BaseModel):
"""文章标签"""
# bjy: 标签名称,唯一
name = models.CharField(_('tag name'), max_length=30, unique=True)
# bjy: URL友好的别名
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# bjy: 定义标签的字符串表示
def __str__(self):
return self.name
# bjy: 获取标签的绝对URL
def get_absolute_url(self):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
# bjy: 获取使用该标签的文章数量,带缓存
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
# bjy: 按名称排序
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
# bjy: 定义友情链接模型
class Links(models.Model):
"""友情链接"""
# bjy: 链接名称,唯一
name = models.CharField(_('link name'), max_length=30, unique=True)
# bjy: 链接URL
link = models.URLField(_('link'))
# bjy: 排序权重,唯一
sequence = models.IntegerField(_('order'), unique=True)
# bjy: 是否启用
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
# bjy: 显示类型
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
# bjy: 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
# bjy: 按排序权重升序排列
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
# bjy: 定义链接的字符串表示
def __str__(self):
return self.name
# bjy: 定义侧边栏模型
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
# bjy: 侧边栏标题
name = models.CharField(_('title'), max_length=100)
# bjy: 侧边栏内容HTML
content = models.TextField(_('content'))
# bjy: 排序权重,唯一
sequence = models.IntegerField(_('order'), unique=True)
# bjy: 是否启用
is_enable = models.BooleanField(_('is enable'), default=True)
# bjy: 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
# bjy: 按排序权重升序排列
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
# bjy: 定义侧边栏的字符串表示
def __str__(self):
return self.name
# bjy: 定义博客设置模型
class BlogSettings(models.Model):
"""blog的配置"""
# bjy: 网站名称
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
# bjy: 网站描述
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
# bjy: SEO描述
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
# bjy: 网站关键词
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
# bjy: 文章摘要长度
article_sub_length = models.IntegerField(_('article sub length'), default=300)
# bjy: 侧边栏文章数量
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
# bjy: 侧边栏评论数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
# bjy: 文章页评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5)
# bjy: 是否显示Google AdSense
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
# bjy: Google AdSense代码
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
# bjy: 是否开启全站评论
open_site_comment = models.BooleanField(_('open site comment'), default=True)
# bjy: 公共头部HTML代码
global_header = models.TextField("公共头部", null=True, blank=True, default='')
# bjy: 公共尾部HTML代码
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
# bjy: ICP备案号
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
# bjy: 网站统计代码
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
# bjy: 是否显示公安备案号
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
# bjy: 公安备案号
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
# bjy: 评论是否需要审核
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
class Meta:
# bjy: 模型的单数和复数名称
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
# bjy: 定义设置的字符串表示
def __str__(self):
return self.site_name
# bjy: 重写clean方法用于模型验证
def clean(self):
# bjy: 确保数据库中只能有一条配置记录
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
# bjy: 重写save方法保存后清除缓存
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from djangoblog.utils import cache

@ -1,13 +1,21 @@
# bjy: 从haystack框架中导入indexes模块用于创建搜索索引
from haystack import indexes
# bjy: 从blog应用中导入Article模型
from blog.models import Article
# bjy: 为Article模型定义一个搜索索引类
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
# bjy: 定义一个主文本字段,`document=True`表示这是搜索的主要字段
# bjy: `use_template=True`表示该字段的内容将由一个模板来生成
text = indexes.CharField(document=True, use_template=True)
# bjy: `get_model`方法必须实现,用于返回此索引对应的模型类
def get_model(self):
return Article
# bjy: `index_queryset`方法定义了哪些模型实例应该被建立索引
def index_queryset(self, using=None):
# bjy: 这里只返回状态为'p'(已发布)的文章
return self.get_model().objects.filter(status='p')

@ -1,9 +1,17 @@
/* bjy: 定义一个名为.button的CSS类用于设置按钮的通用样式 */
.button {
/* bjy: 移除按钮的默认边框 */
border: none;
/* bjy: 设置按钮的内边距上下4像素左右80像素 */
padding: 4px 80px;
/* bjy: 设置按钮内部文本的水平居中对齐 */
text-align: center;
/* bjy: 移除文本装饰(如下划线),通常用于链接样式的按钮 */
text-decoration: none;
/* bjy: 将按钮设置为行内块级元素,使其可以设置宽高并与其他元素在同一行显示 */
display: inline-block;
/* bjy: 设置按钮内部文本的字体大小为16像素 */
font-size: 16px;
/* bjy: 设置按钮的外边距上下4像素左右2像素用于控制按钮之间的间距 */
margin: 4px 2px;
}
}

@ -1,45 +1,76 @@
// bjy: 声明一个全局变量wait用于倒计时初始值为60秒
let wait = 60;
// bjy: 定义一个名为time的函数用于处理按钮的倒计时效果
// bjy: 参数o代表触发倒计时的按钮元素
function time(o) {
// bjy: 如果倒计时结束wait为0
if (wait == 0) {
// bjy: 移除按钮的disabled属性使其重新可点击
o.removeAttribute("disabled");
// bjy: 将按钮的显示文本恢复为“获取验证码”
o.value = "获取验证码";
// bjy: 重置倒计时变量为60以便下次使用
wait = 60
// bjy: 结束函数执行
return false
} else {
// bjy: 如果倒计时未结束,禁用按钮,防止重复点击
o.setAttribute("disabled", true);
// bjy: 更新按钮的显示文本,显示剩余的倒计时秒数
o.value = "重新发送(" + wait + ")";
// bjy: 倒计时秒数减一
wait--;
// bjy: 设置一个1秒1000毫秒后执行的定时器
setTimeout(function () {
// bjy: 定时器回调函数中递归调用time函数实现每秒更新一次倒计时
time(o)
},
1000)
}
}
// bjy: 为ID为"btn"的元素绑定点击事件处理函数
document.getElementById("btn").onclick = function () {
// bjy: 使用jQuery选择器获取邮箱输入框元素
let id_email = $("#id_email")
// bjy: 使用jQuery选择器获取CSRF令牌的值用于Django的POST请求安全验证
let token = $("*[name='csrfmiddlewaretoken']").val()
// bjy: 将this即被点击的按钮的引用保存到ts变量中以便在AJAX回调中使用
let ts = this
// bjy: 使用jQuery选择器获取用于显示错误信息的元素
let myErr = $("#myErr")
// bjy: 使用jQuery发起一个AJAX请求
$.ajax(
{
// bjy: 请求的URL地址
url: "/forget_password_code/",
// bjy: 请求的类型为POST
type: "POST",
// bjy: 发送到服务器的数据包含邮箱和CSRF令牌
data: {
"email": id_email.val(),
"csrfmiddlewaretoken": token
},
// bjy: 定义请求成功时的回调函数result是服务器返回的数据
success: function (result) {
// bjy: 如果服务器返回的结果不是"ok"(表示发送失败或有错误)
if (result != "ok") {
// bjy: 移除页面上可能存在的旧错误提示
myErr.remove()
// bjy: 在邮箱输入框后面动态添加一个错误提示列表,显示服务器返回的错误信息
id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>")
// bjy: 结束函数执行
return
}
// bjy: 如果发送成功,移除页面上可能存在的旧错误提示
myErr.remove()
// bjy: 调用time函数开始按钮的倒计时效果
time(ts)
},
// bjy: 定义请求失败时的回调函数e是错误对象
error: function (e) {
// bjy: 弹出一个警告框,提示用户发送失败
alert("发送失败,请重试")
}
}

@ -8,44 +8,64 @@
* details, see https://creativecommons.org/licenses/by/3.0/.
*/
// Intended to prevent false-positive bug reports about Bootstrap not working properly in old versions of IE due to folks testing using IE's unreliable emulation modes.
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function () {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict';
// bjy: 定义一个函数用于从用户代理字符串中获取IE的模拟版本号
function emulatedIEMajorVersion() {
// bjy: 使用正则表达式匹配用户代理字符串中的 "MSIE x.x" 部分
var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
// bjy: 如果匹配不到说明不是IE或版本号无法识别返回null
if (groups === null) {
return null
}
// bjy: 将匹配到的版本号字符串(如 "10.0")转换为整数
var ieVersionNum = parseInt(groups[1], 10)
// bjy: 取整数部分作为主版本号
var ieMajorVersion = Math.floor(ieVersionNum)
// bjy: 返回模拟的IE主版本号
return ieMajorVersion
}
// bjy: 定义一个函数用于检测当前浏览器实际运行的IE版本即使它处于旧版IE的模拟模式下
function actualNonEmulatedIEMajorVersion() {
// Detects the actual version of IE in use, even if it's in an older-IE emulation mode.
// IE JavaScript conditional compilation docs: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx
// @cc_on docs: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx
// bjy: 此函数通过IE特有的JScript条件编译来检测真实版本
// bjy: IE JavaScript条件编译文档: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx
// bjy: @cc_on 文档: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx
// bjy: 创建一个新的Function其内容是IE的条件编译语句用于获取JScript版本
var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line
// bjy: 如果jscriptVersion未定义说明是IE11或更高版本且不在模拟模式下
if (jscriptVersion === undefined) {
return 11 // IE11+ not in emulation mode
}
// bjy: 如果JScript版本小于9则判断为IE8或更低版本
if (jscriptVersion < 9) {
return 8 // IE8 (or lower; haven't tested on IE<8)
}
// bjy: 否则返回JScript版本这对应于IE9或IE10在任何模式下或IE11在非IE11模式下
return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode
}
// bjy: 获取当前浏览器的用户代理字符串
var ua = window.navigator.userAgent
// bjy: 检查用户代理中是否包含'Opera'或'Presto'Opera的旧版渲染引擎
if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) {
return // Opera, which might pretend to be IE
}
// bjy: 调用函数获取模拟的IE版本号
var emulated = emulatedIEMajorVersion()
// bjy: 如果模拟版本为null说明不是IE浏览器直接返回
if (emulated === null) {
return // Not IE
}
// bjy: 调用函数获取实际的IE版本号
var nonEmulated = actualNonEmulatedIEMajorVersion()
// bjy: 比较模拟版本和实际版本如果不相同说明IE正处于模拟模式下
if (emulated !== nonEmulated) {
// bjy: 弹出一个警告框提示用户当前正处于IE模拟模式并警告其行为可能与真实旧版IE不同
window.alert('WARNING: You appear to be using IE' + nonEmulated + ' in IE' + emulated + ' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!')
}
})();

@ -7,17 +7,27 @@
// See the Getting Started docs for more information:
// http://getbootstrap.com/getting-started/#support-ie10-width
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function () {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict';
// bjy: 检查当前浏览器的用户代理字符串User Agent是否匹配IEMobile/10.0
// bjy: 这是为了专门识别运行在Windows Phone 8上的IE10移动版浏览器
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
// bjy: 如果匹配,则创建一个新的<style>元素
var msViewportStyle = document.createElement('style')
// bjy: 向这个<style>元素中添加一个CSS规则
msViewportStyle.appendChild(
// bjy: 创建一个包含CSS规则的文本节点
document.createTextNode(
// bjy: CSS规则@-ms-viewport是IE10的一个特有规则用于设置视口大小
// bjy: width:auto!important 覆盖了IE10错误的默认设置强制视口宽度为设备宽度
'@-ms-viewport{width:auto!important}'
)
)
// bjy: 将这个新创建的<style>元素添加到文档的<head>部分使CSS规则生效
document.querySelector('head').appendChild(msViewportStyle)
}
})();
})();

@ -1,13 +1,16 @@
/*
Styles for older IE versions (previous to IE9).
bjy: IEIE9
*/
/* bjy: 为body元素设置浅灰色背景 */
body {
background-color: #e6e6e6;
}
/* bjy: 当body没有自定义背景时设置背景为白色 */
body.custom-background-empty {
background-color: #fff;
}
/* bjy: 在没有自定义背景或背景为白色的页面上,移除网站容器的阴影、边距和内边距 */
body.custom-background-empty .site,
body.custom-background-white .site {
box-shadow: none;
@ -15,14 +18,17 @@ body.custom-background-white .site {
margin-top: 0;
padding: 0;
}
/* bjy: 隐藏辅助性文本和屏幕阅读器专用文本,通过裁剪使其在视觉上不可见 */
.assistive-text,
.site .screen-reader-text {
clip: rect(1px 1px 1px 1px);
}
/* bjy: 在全宽布局下,使内容区域占满整个宽度,并取消浮动 */
.full-width .site-content {
float: none;
width: 100%;
}
/* bjy: 防止在IE8中带有height和width属性的图片被拉伸设置width为auto */
img.size-full,
img.size-large,
img.header-image,
@ -32,15 +38,18 @@ img[class*="wp-image-"],
img[class*="attachment-"] {
width: auto; /* Prevent stretching of full-size and large-size images with height and width attributes in IE8 */
}
/* bjy: 作者头像向左浮动,并设置上边距 */
.author-avatar {
float: left;
margin-top: 8px;
margin-top: 0.571428571rem;
}
/* bjy: 作者描述向右浮动宽度占80% */
.author-description {
float: right;
width: 80%;
}
/* bjy: 网站主容器样式:居中、最大宽度、阴影、溢出隐藏、内边距 */
.site {
box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
margin: 48px auto;
@ -48,27 +57,33 @@ img[class*="attachment-"] {
overflow: hidden;
padding: 0 40px;
}
/* bjy: 网站内容区域向左浮动宽度约为65.1% */
.site-content {
float: left;
width: 65.104166667%;
}
/* bjy: 在首页模板、附件页面或全宽布局下内容区域宽度为100% */
body.template-front-page .site-content,
body.attachment .site-content,
body.full-width .site-content {
width: 100%;
}
/* bjy: 小工具区域向右浮动宽度约为26.04% */
.widget-area {
float: right;
width: 26.041666667%;
}
/* bjy: 网站标题和副标题左对齐 */
.site-header h1,
.site-header h2 {
text-align: left;
}
/* bjy: 网站标题的字体大小和行高 */
.site-header h1 {
font-size: 26px;
line-height: 1.846153846;
}
/* bjy: 主导航菜单样式:上下边框、行内块显示、左对齐、全宽 */
.main-navigation ul.nav-menu,
.main-navigation div.nav-menu > ul {
border-bottom: 1px solid #ededed;
@ -77,32 +92,39 @@ body.full-width .site-content {
text-align: left;
width: 100%;
}
/* bjy: 重置主导航ul的默认外边距和文本缩进 */
.main-navigation ul {
margin: 0;
text-indent: 0;
}
/* bjy: 主导航的链接和列表项设置为行内块,无文本装饰 */
.main-navigation li a,
.main-navigation li {
display: inline-block;
text-decoration: none;
}
/* bjy: 针对IE7的特殊处理将主导航的链接和列表项设置为行内 */
.ie7 .main-navigation li a,
.ie7 .main-navigation li {
display: inline;
}
/* bjy: 主导航链接样式:无边框、颜色、行高、大写转换 */
.main-navigation li a {
border-bottom: 0;
color: #6a6a6a;
line-height: 3.692307692;
text-transform: uppercase;
}
/* bjy: 主导航链接悬停时颜色变黑 */
.main-navigation li a:hover {
color: #000;
}
/* bjy: 主导航列表项样式:右边距、相对定位 */
.main-navigation li {
margin: 0 40px 0 0;
position: relative;
}
/* bjy: 主导航下拉子菜单样式:绝对定位、隐藏(通过裁剪) */
.main-navigation li ul {
margin: 0;
padding: 0;
@ -114,17 +136,20 @@ body.full-width .site-content {
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
/* bjy: 针对IE7的下拉菜单特殊处理使用display:none隐藏 */
.ie7 .main-navigation li ul {
clip: inherit;
display: none;
left: 0;
overflow: visible;
}
/* bjy: 二级及更深层的下拉菜单定位 */
.main-navigation li ul ul,
.ie7 .main-navigation li ul ul {
top: 0;
left: 100%;
}
/* bjy: 当鼠标悬停或聚焦于主导航项时,显示其子菜单 */
.main-navigation ul li:hover > ul,
.main-navigation ul li:focus > ul,
.main-navigation .focus > ul {
@ -134,10 +159,12 @@ body.full-width .site-content {
height: inherit;
width: inherit;
}
/* bjy: 针对IE7当鼠标悬停或聚焦时使用display:block显示子菜单 */
.ie7 .main-navigation ul li:hover > ul,
.ie7 .main-navigation ul li:focus > ul {
display: block;
}
/* bjy: 下拉菜单中的链接样式:背景、边框、块级显示、字体、内边距、宽度 */
.main-navigation li ul li a {
background: #efefef;
border-bottom: 1px solid #ededed;
@ -147,10 +174,12 @@ body.full-width .site-content {
padding: 8px 10px;
width: 180px;
}
/* bjy: 下拉菜单链接悬停时的背景和颜色 */
.main-navigation li ul li a:hover {
background: #e3e3e3;
color: #444;
}
/* bjy: 当前菜单项或其祖先项的链接样式:加粗 */
.main-navigation .current-menu-item > a,
.main-navigation .current-menu-ancestor > a,
.main-navigation .current_page_item > a,
@ -158,39 +187,48 @@ body.full-width .site-content {
color: #636363;
font-weight: bold;
}
/* bjy: 隐藏主导航的移动端菜单切换按钮 */
.main-navigation .menu-toggle {
display: none;
}
/* bjy: 文章标题的字体大小 */
.entry-header .entry-title {
font-size: 22px;
}
/* bjy: 评论表单中文本输入框的宽度 */
#respond form input[type="text"] {
width: 46.333333333%;
}
/* bjy: 评论表单中文本域的宽度 */
#respond form textarea.blog-textarea {
width: 79.666666667%;
}
/* bjy: 首页模板的内容区域和文章设置溢出隐藏 */
.template-front-page .site-content,
.template-front-page article {
overflow: hidden;
}
/* bjy: 首页模板中有特色图片的文章向左浮动宽度约为47.92% */
.template-front-page.has-post-thumbnail article {
float: left;
width: 47.916666667%;
}
/* bjy: 首页模板中的特色图片向右浮动宽度约为47.92% */
.entry-page-image {
float: right;
margin-bottom: 0;
width: 47.916666667%;
}
/* IE Front Page Template Widget fix */
/* bjy: IE首页模板小工具修复清除浮动 */
.template-front-page .widget-area {
clear: both;
}
/* bjy: IE首页模板小工具修复设置小工具宽度为100%,无边框 */
.template-front-page .widget {
width: 100% !important;
border: none;
}
/* bjy: 首页模板小工具区域的布局:左浮动、宽度、边距 */
.template-front-page .widget-area .widget,
.template-front-page .first.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets {
@ -198,10 +236,12 @@ body.full-width .site-content {
margin-bottom: 24px;
width: 51.875%;
}
/* bjy: 首页模板特定小工具的布局:清除右浮动 */
.template-front-page .second.front-widgets,
.template-front-page .widget-area .widget:nth-child(odd) {
clear: right;
}
/* bjy: 首页模板另一组小工具的布局:右浮动、宽度、边距 */
.template-front-page .first.front-widgets,
.template-front-page .second.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets {
@ -209,65 +249,71 @@ body.full-width .site-content {
margin: 0 0 24px;
width: 39.0625%;
}
/* bjy: 双侧边栏首页模板的小工具布局:取消浮动,宽度自适应 */
.template-front-page.two-sidebars .widget,
.template-front-page.two-sidebars .widget:nth-child(even) {
float: none;
width: auto;
}
/* add input font for <IE9 Password Box to make the bullets show up */
/* bjy: 为IE9以下的密码输入框添加字体以确保密码圆点显示正常 */
input[type="password"] {
font-family: Helvetica, Arial, sans-serif;
}
/* RTL overrides for IE7 and IE8
/* bjy: RTLIE7IE8
-------------------------------------------------------------- */
/* bjy: RTL布局下网站标题右对齐 */
.rtl .site-header h1,
.rtl .site-header h2 {
text-align: right;
}
/* bjy: RTL布局下小工具区域和作者描述向左浮动 */
.rtl .widget-area,
.rtl .author-description {
float: left;
}
/* bjy: RTL布局下作者头像和内容区域向右浮动 */
.rtl .author-avatar,
.rtl .site-content {
float: right;
}
/* bjy: RTL布局下主导航菜单右对齐 */
.rtl .main-navigation ul.nav-menu,
.rtl .main-navigation div.nav-menu > ul {
text-align: right;
}
/* bjy: RTL布局下下拉菜单项的左边距 */
.rtl .main-navigation ul li ul li,
.rtl .main-navigation ul li ul li ul li {
margin-left: 40px;
margin-right: auto;
}
/* bjy: RTL布局下二级下拉菜单位于父菜单的左侧 */
.rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
/* bjy: IE7 RTL布局下二级下拉菜单位置调整 */
.ie7 .rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
/* bjy: IE7 RTL布局下为主导航列表项设置堆叠顺序 */
.ie7 .rtl .main-navigation ul li {
z-index: 99;
}
/* bjy: IE7 RTL布局下一级下拉菜单位于父菜单上方 */
.ie7 .rtl .main-navigation li ul {
position: absolute;
bottom: 100%;
right: 0;
z-index: 1;
}
/* bjy: IE7 RTL布局下主导航列表项的边距调整 */
.ie7 .rtl .main-navigation li {
margin-right: auto;
margin-left: 40px;
}
.ie7 .rtl .main-navigation li ul ul ul {
position: relative;
z-index: 1;
}
marg

@ -1,36 +1,43 @@
/* Make clicks pass-through */
/* bjy: 使点击事件可以穿透进度条元素,不影响下方页面的交互 */
#nprogress {
pointer-events: none;
}
/* bjy: 进度条主体样式 */
#nprogress .bar {
/* bjy: 进度条背景色为红色 */
background: red;
/* bjy: 固定定位,使进度条始终在页面顶部 */
position: fixed;
/* bjy: 设置较高的堆叠顺序,确保进度条在最上层 */
z-index: 1031;
top: 0;
left: 0;
/* bjy: 进度条宽度和高度 */
width: 100%;
height: 2px;
}
/* Fancy blur effect */
/* bjy: 进度条右侧的“闪光”或“模糊”效果 */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
/* bjy: 使用box-shadow创建一个发光的阴影效果 */
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1.0;
/* bjy: 对peg元素进行轻微的旋转和位移增加动感 */
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
/* bjy: 进度指示器(右上角的旋转图标)样式。注释提示:删除这些样式可以去掉旋转图标 */
#nprogress .spinner {
display: block;
position: fixed;
@ -39,36 +46,42 @@
right: 15px;
}
/* bjy: 旋转图标本身的样式 */
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
/* bjy: 设置边框,顶部和左侧为红色,形成旋转动画的视觉效果 */
border: solid 2px transparent;
border-top-color: red;
border-left-color: red;
border-radius: 50%;
/* bjy: 应用旋转动画 */
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
/* bjy: 自定义父容器样式,用于将进度条限制在特定区域内 */
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
/* bjy: 当进度条在自定义父容器内时将其定位方式从fixed改为absolute */
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
/* bjy: 定义旋转动画的关键帧兼容旧版WebKit内核浏览器 */
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
/* bjy: 定义旋转动画的关键帧(标准语法) */
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

@ -1,305 +1,378 @@
/* bjy: Google 图标样式,定义背景图位置 */
.icon-sn-google {
background-position: 0 -28px;
}
/* bjy: Google 背景图标样式,定义背景色和背景图位置 */
.icon-sn-bg-google {
background-color: #4285f4;
background-position: 0 0;
}
/* bjy: Google 字体图标如FontAwesome样式定义颜色 */
.fa-sn-google {
color: #4285f4;
}
/* bjy: GitHub 图标样式 */
.icon-sn-github {
background-position: -28px -28px;
}
/* bjy: GitHub 背景图标样式 */
.icon-sn-bg-github {
background-color: #333;
background-position: -28px 0;
}
/* bjy: GitHub 字体图标样式 */
.fa-sn-github {
color: #333;
}
/* bjy: 微博图标样式 */
.icon-sn-weibo {
background-position: -56px -28px;
}
/* bjy: 微博背景图标样式 */
.icon-sn-bg-weibo {
background-color: #e90d24;
background-position: -56px 0;
}
/* bjy: 微博字体图标样式 */
.fa-sn-weibo {
color: #e90d24;
}
/* bjy: QQ图标样式 */
.icon-sn-qq {
background-position: -84px -28px;
}
/* bjy: QQ背景图标样式 */
.icon-sn-bg-qq {
background-color: #0098e6;
background-position: -84px 0;
}
/* bjy: QQ字体图标样式 */
.fa-sn-qq {
color: #0098e6;
}
/* bjy: Twitter图标样式 */
.icon-sn-twitter {
background-position: -112px -28px;
}
/* bjy: Twitter背景图标样式 */
.icon-sn-bg-twitter {
background-color: #50abf1;
background-position: -112px 0;
}
/* bjy: Twitter字体图标样式 */
.fa-sn-twitter {
color: #50abf1;
}
/* bjy: Facebook图标样式 */
.icon-sn-facebook {
background-position: -140px -28px;
}
/* bjy: Facebook背景图标样式 */
.icon-sn-bg-facebook {
background-color: #4862a3;
background-position: -140px 0;
}
/* bjy: Facebook字体图标样式 */
.fa-sn-facebook {
color: #4862a3;
}
/* bjy: 人人网图标样式 */
.icon-sn-renren {
background-position: -168px -28px;
}
/* bjy: 人人网背景图标样式 */
.icon-sn-bg-renren {
background-color: #197bc8;
background-position: -168px 0;
}
/* bjy: 人人网字体图标样式 */
.fa-sn-renren {
color: #197bc8;
}
/* bjy: 腾讯微博图标样式 */
.icon-sn-tqq {
background-position: -196px -28px;
}
/* bjy: 腾讯微博背景图标样式 */
.icon-sn-bg-tqq {
background-color: #1f9ed2;
background-position: -196px 0;
}
/* bjy: 腾讯微博字体图标样式 */
.fa-sn-tqq {
color: #1f9ed2;
}
/* bjy: 豆瓣图标样式 */
.icon-sn-douban {
background-position: -224px -28px;
}
/* bjy: 豆瓣背景图标样式 */
.icon-sn-bg-douban {
background-color: #279738;
background-position: -224px 0;
}
/* bjy: 豆瓣字体图标样式 */
.fa-sn-douban {
color: #279738;
}
/* bjy: 微信图标样式 */
.icon-sn-weixin {
background-position: -252px -28px;
}
/* bjy: 微信背景图标样式 */
.icon-sn-bg-weixin {
background-color: #00b500;
background-position: -252px 0;
}
/* bjy: 微信字体图标样式 */
.fa-sn-weixin {
color: #00b500;
}
/* bjy: 省略号图标样式 */
.icon-sn-dotted {
background-position: -280px -28px;
}
/* bjy: 省略号背景图标样式 */
.icon-sn-bg-dotted {
background-color: #eee;
background-position: -280px 0;
}
/* bjy: 省略号字体图标样式 */
.fa-sn-dotted {
color: #eee;
}
/* bjy: 网站图标样式 */
.icon-sn-site {
background-position: -308px -28px;
}
/* bjy: 网站背景图标样式 */
.icon-sn-bg-site {
background-color: #00b500;
background-position: -308px 0;
}
/* bjy: 网站字体图标样式 */
.fa-sn-site {
color: #00b500;
}
/* bjy: LinkedIn图标样式 */
.icon-sn-linkedin {
background-position: -336px -28px;
}
/* bjy: LinkedIn背景图标样式 */
.icon-sn-bg-linkedin {
background-color: #0077b9;
background-position: -336px 0;
}
/* bjy: LinkedIn字体图标样式 */
.fa-sn-linkedin {
color: #0077b9;
}
/* bjy: 所有社交网络图标的通用样式 */
[class*=icon-sn-] {
display: inline-block;
/* bjy: 设置背景图片为一张包含所有图标的SVG精灵图 */
background-image: url('../img/icon-sn.svg');
background-repeat: no-repeat;
width: 28px;
height: 28px;
vertical-align: middle;
/* bjy: 设置背景图大小高度固定为56px宽度自适应 */
background-size: auto 56px;
}
/* bjy: 所有社交网络图标在鼠标悬停时的效果 */
[class*=icon-sn-]:hover {
opacity: .8;
/* bjy: 兼容旧版IE的透明度滤镜 */
filter: alpha(opacity=80);
}
/* bjy: Google按钮样式 */
.btn-sn-google {
background: #4285f4;
}
/* bjy: Google按钮在激活、聚焦或悬停时的样式 */
.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
background: #2a75f3;
}
/* bjy: GitHub按钮样式 */
.btn-sn-github {
background: #333;
}
/* bjy: GitHub按钮在激活、聚焦或悬停时的样式 */
.btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover {
background: #262626;
}
/* bjy: 微博按钮样式 */
.btn-sn-weibo {
background: #e90d24;
}
/* bjy: 微博按钮在激活、聚焦或悬停时的样式 */
.btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover {
background: #d10c20;
}
/* bjy: QQ按钮样式 */
.btn-sn-qq {
background: #0098e6;
}
/* bjy: QQ按钮在激活、聚焦或悬停时的样式 */
.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
background: #0087cd;
}
/* bjy: Twitter按钮样式 */
.btn-sn-twitter {
background: #50abf1;
}
/* bjy: Twitter按钮在激活、聚焦或悬停时的样式 */
.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
background: #38a0ef;
}
/* bjy: Facebook按钮样式 */
.btn-sn-facebook {
background: #4862a3;
}
/* bjy: Facebook按钮在激活、聚焦或悬停时的样式 */
.btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover {
background: #405791;
}
/* bjy: 人人网按钮样式 */
.btn-sn-renren {
background: #197bc8;
}
/* bjy: 人人网按钮在激活、聚焦或悬停时的样式 */
.btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover {
background: #166db1;
}
/* bjy: 腾讯微博按钮样式 */
.btn-sn-tqq {
background: #1f9ed2;
}
/* bjy: 腾讯微博按钮在激活、聚焦或悬停时的样式 */
.btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover {
background: #1c8dbc;
}
/* bjy: 豆瓣按钮样式 */
.btn-sn-douban {
background: #279738;
}
/* bjy: 豆瓣按钮在激活、聚焦或悬停时的样式 */
.btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover {
background: #228330;
}
/* bjy: 微信按钮样式 */
.btn-sn-weixin {
background: #00b500;
}
/* bjy: 微信按钮在激活、聚焦或悬停时的样式 */
.btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover {
background: #009c00;
}
/* bjy: 省略号按钮样式 */
.btn-sn-dotted {
background: #eee;
}
/* bjy: 省略号按钮在激活、聚焦或悬停时的样式 */
.btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover {
background: #e1e1e1;
}
/* bjy: 网站按钮样式 */
.btn-sn-site {
background: #00b500;
}
/* bjy: 网站按钮在激活、聚焦或悬停时的样式 */
.btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover {
background: #009c00;
}
/* bjy: LinkedIn按钮样式 */
.btn-sn-linkedin {
background: #0077b9;
}
/* bjy: LinkedIn按钮在激活、聚焦或悬停时的样式 */
.btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover {
background: #0067a0;
}
/* bjy: 所有社交网络按钮的通用样式,包括其状态 */
[class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover {
border: none;
color: #fff;
}
/* bjy: “更多”按钮的特殊样式,移除内边距 */
.btn-sn-more {
padding: 0;
}
/* bjy: “更多”按钮及其状态的特殊样式,移除阴影 */
.btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover {
box-shadow: none;
}
/* bjy: 当社交网络图标被用作按钮内部元素时,移除其背景色 */
[class*=btn-sn-] [class*=icon-sn-] {
background-color: transparent;
}
}

@ -1,40 +1,59 @@
/**
* Created by liangliang on 2016/11/20.
* bjy: Created by liangliang on 2016/11/20.
*/
// bjy: 定义一个函数,用于处理评论回复功能
// bjy: parentid参数是要回复的评论的ID
function do_reply(parentid) {
// bjy: 在控制台打印父评论ID用于调试
console.log(parentid);
// bjy: 将父评论ID设置到隐藏的表单字段中
$("#id_parent_comment_id").val(parentid)
// bjy: 将评论表单移动到指定评论的下方
$("#commentform").appendTo($("#div-comment-" + parentid));
// bjy: 隐藏“发表评论”的标题
$("#reply-title").hide();
// bjy: 显示“取消回复”的链接
$("#cancel_comment").show();
}
// bjy: 定义一个函数,用于取消评论回复
function cancel_reply() {
// bjy: 显示“发表评论”的标题
$("#reply-title").show();
// bjy: 隐藏“取消回复”的链接
$("#cancel_comment").hide();
// bjy: 清空隐藏的父评论ID字段
$("#id_parent_comment_id").val('')
// bjy: 将评论表单移回原来的位置
$("#commentform").appendTo($("#respond"));
}
// bjy: 页面加载时启动NProgress进度条
NProgress.start();
// bjy: 设置进度条进度为40%
NProgress.set(0.4);
//Increment
// bjy: 设置一个定时器每1000毫秒1秒增加一点进度
var interval = setInterval(function () {
NProgress.inc();
}, 1000);
// bjy: 当文档结构加载完成时
$(document).ready(function () {
// bjy: 完成进度条加载
NProgress.done();
// bjy: 清除定时器
clearInterval(interval);
});
/** 侧边栏回到顶部 */
/** bjy: 侧边栏回到顶部功能 */
// bjy: 获取火箭图标的jQuery对象
var rocket = $('#rocket');
// bjy: 监听窗口的滚动事件并使用debounce函数进行防抖处理
$(window).on('scroll', debounce(slideTopSet, 300));
// bjy: 定义一个防抖函数,用于限制函数的执行频率
function debounce(func, wait) {
var timeout;
return function () {
@ -43,49 +62,67 @@ function debounce(func, wait) {
};
}
// bjy: 定义一个函数,根据滚动位置来决定是否显示“回到顶部”按钮
function slideTopSet() {
// bjy: 获取当前文档滚动的垂直距离
var top = $(document).scrollTop();
// bjy: 如果滚动距离大于200像素
if (top > 200) {
// bjy: 显示火箭图标(通过添加'show'类)
rocket.addClass('show');
} else {
// bjy: 否则,隐藏火箭图标(通过移除'show'类)
rocket.removeClass('show');
}
}
// bjy: 监听文档上的点击事件,如果点击的是火箭图标
$(document).on('click', '#rocket', function (event) {
// bjy: 为火箭图标添加'move'类,触发向上飞行的动画
rocket.addClass('move');
// bjy: 使用animate动画将页面平滑滚动回顶部
$('body, html').animate({
scrollTop: 0
}, 800);
});
// bjy: 监听标准动画结束事件
$(document).on('animationEnd', function () {
// bjy: 动画结束后延迟400毫秒移除'move'类,以便下次可以再次触发
setTimeout(function () {
rocket.removeClass('move');
}, 400);
});
// bjy: 监听webkit内核的动画结束事件用于兼容性
$(document).on('webkitAnimationEnd', function () {
// bjy: 动画结束后延迟400毫秒移除'move'类
setTimeout(function () {
rocket.removeClass('move');
}, 400);
});
// bjy: 当整个页面(包括图片等资源)加载完成后执行
window.onload = function () {
// bjy: 获取页面上所有“回复”链接
var replyLinks = document.querySelectorAll(".comment-reply-link");
// bjy: 遍历所有“回复”链接
for (var i = 0; i < replyLinks.length; i++) {
// bjy: 为每个链接绑定点击事件
replyLinks[i].onclick = function () {
// bjy: 从链接的data-pk属性中获取要回复的评论ID
var pk = this.getAttribute("data-pk");
// bjy: 调用do_reply函数来处理回复逻辑
do_reply(pk);
};
}
};
// bjy: 以下代码被注释掉,可能用于国际化语言切换功能
// $(document).ready(function () {
// var form = $('#i18n-form');
// var selector = $('.i18n-select');
// selector.on('change', function () {
// form.submit();
// });
// });
// });

@ -1,102 +1,130 @@
/**
* MathJax 智能加载器
* 检测页面是否包含数学公式如果有则动态加载和配置MathJax
* bjy: MathJax 智能加载器
* bjy: 检测页面是否包含数学公式如果有则动态加载和配置MathJax
*/
// bjy: 使用立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function() {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict';
/**
* 检测页面是否包含数学公式
* bjy: 检测页面是否包含数学公式
* @returns {boolean} 是否包含数学公式
*/
function hasMathFormulas() {
// bjy: 获取页面的全部文本内容
const content = document.body.textContent || document.body.innerText || '';
// 检测常见的数学公式语法
// bjy: 使用正则表达式检测常见的数学公式语法(如$...$、$$...$$、\begin{}等)
return /\$.*?\$|\$\$.*?\$\$|\\begin\{.*?\}|\\end\{.*?\}|\\[a-zA-Z]+\{/.test(content);
}
/**
* 配置MathJax
* bjy: 配置MathJax
*/
function configureMathJax() {
// bjy: 在全局window对象上设置MathJax配置
window.MathJax = {
// bjy: 配置TeX解析器
tex: {
// 行内公式和块级公式分隔符
// bjy: 定义行内公式的分隔符为$
inlineMath: [['$', '$']],
// bjy: 定义块级公式的分隔符为$$
displayMath: [['$$', '$$']],
// 处理转义字符和LaTeX环境
// bjy: 启用对转义字符(如\$)的处理
processEscapes: true,
// bjy: 启用对LaTeX环境如\begin{equation})的处理
processEnvironments: true,
// 自动换行
// bjy: 使用AMS美国数学学会的标签规则来自动编号
tags: 'ams'
},
// bjy: 配置处理选项
options: {
// 跳过这些HTML标签避免处理代码块等
// bjy: 跳过这些HTML标签避免在代码块、脚本等非文本内容中渲染公式
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'],
// CSS类控制
// bjy: 定义包含'tex2jax_ignore'类的元素将被忽略
ignoreHtmlClass: 'tex2jax_ignore',
// bjy: 定义只有包含'tex2jax_process'类的元素才会被处理
processHtmlClass: 'tex2jax_process'
},
// 启动配置
// bjy: 配置MathJax的启动行为
startup: {
// bjy: 当MathJax准备就绪时执行的回调函数
ready() {
console.log('MathJax配置完成开始初始化...');
// bjy: 执行MathJax的默认就绪流程
MathJax.startup.defaultReady();
// 处理特定区域的数学公式
// bjy: 获取需要渲染数学公式的特定区域元素
const contentEl = document.getElementById('content');
const commentsEl = document.getElementById('comments');
// bjy: 创建一个Promise数组用于管理异步渲染任务
const promises = [];
// bjy: 如果内容区域存在,则将其加入渲染队列
if (contentEl) {
promises.push(MathJax.typesetPromise([contentEl]));
}
// bjy: 如果评论区存在,也将其加入渲染队列
if (commentsEl) {
promises.push(MathJax.typesetPromise([commentsEl]));
}
// 等待所有渲染完成
// bjy: 等待所有区域的公式都渲染完成
Promise.all(promises).then(() => {
console.log('MathJax渲染完成');
// 触发自定义事件通知其他脚本MathJax已就绪
// bjy: 触发一个名为'mathjaxReady'的自定义事件通知其他脚本MathJax已就绪
document.dispatchEvent(new CustomEvent('mathjaxReady'));
}).catch(error => {
// bjy: 如果渲染过程中出现错误,则打印错误信息
console.error('MathJax渲染失败:', error);
});
}
},
// 输出配置
// bjy: 配置CHTML输出CommonHTML一种在所有现代浏览器中工作的输出格式
chtml: {
// bjy: 设置公式缩放比例
scale: 1,
// bjy: 设置最小缩放比例
minScale: 0.5,
// bjy: 不强制公式高度与周围字体匹配
matchFontHeight: false,
// bjy: 设置块级公式居中对齐
displayAlign: 'center',
// bjy: 设置块级公式缩进为0
displayIndent: '0'
}
};
}
/**
* 加载MathJax库
* bjy: 加载MathJax库
*/
function loadMathJax() {
console.log('检测到数学公式开始加载MathJax...');
// bjy: 创建一个<script>元素用于加载MathJax
const script = document.createElement('script');
// bjy: 设置MathJax的CDN地址
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
// bjy: 异步加载脚本
script.async = true;
// bjy: 延迟执行脚本直到HTML文档解析完毕
script.defer = true;
// bjy: 定义脚本加载成功时的回调函数
script.onload = function() {
console.log('MathJax库加载成功');
};
// bjy: 定义脚本加载失败时的回调函数
script.onerror = function() {
console.error('MathJax库加载失败尝试备用CDN...');
// 备用CDN
// bjy: 备用CDN加载方案
const fallbackScript = document.createElement('script');
// bjy: 首先加载polyfill以确保旧版浏览器兼容性
fallbackScript.src = 'https://polyfill.io/v3/polyfill.min.js?features=es6';
fallbackScript.onload = function() {
// bjy: polyfill加载成功后再加载备用CDN的MathJax
const mathJaxScript = document.createElement('script');
mathJaxScript.src = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML';
mathJaxScript.async = true;
@ -104,39 +132,46 @@
};
document.head.appendChild(fallbackScript);
};
// bjy: 将创建的<script>元素添加到文档的<head>中,开始加载
document.head.appendChild(script);
}
/**
* 初始化函数
* bjy: 初始化函数
*/
function init() {
// 等待DOM完全加载
// bjy: 检查DOM是否已经加载完成
if (document.readyState === 'loading') {
// bjy: 如果还在加载中则等待DOMContentLoaded事件后再执行init
document.addEventListener('DOMContentLoaded', init);
return;
}
// 检测是否需要加载MathJax
// bjy: 检测页面内容是否需要加载MathJax
if (hasMathFormulas()) {
// 先配置,再加载
// bjy: 如果需要,则先配置,再加载
configureMathJax();
loadMathJax();
} else {
// bjy: 如果不需要,则在控制台输出信息并跳过
console.log('未检测到数学公式跳过MathJax加载');
}
}
// 提供重新渲染的全局方法,供动态内容使用
// bjy: 提供一个全局方法,用于在动态加载内容后重新渲染数学公式
window.rerenderMathJax = function(element) {
// bjy: 检查MathJax是否已加载并且typesetPromise方法是否存在
if (window.MathJax && window.MathJax.typesetPromise) {
// bjy: 确定要渲染的目标元素如果未提供则默认为整个body
const target = element || document.body;
// bjy: 调用MathJax的typesetPromise方法对指定元素进行渲染并返回Promise
return window.MathJax.typesetPromise([target]);
}
// bjy: 如果MathJax未准备好则返回一个已解决的Promise
return Promise.resolve();
};
// 启动初始化
// bjy: 启动初始化流程
init();
})();

@ -1,53 +1,75 @@
/**
* Handles toggling the navigation menu for small screens and
* accessibility for submenu items.
* bjy: Handles toggling the navigation menu for small screens and
* bjy: accessibility for submenu items.
*/
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
( function() {
// bjy: 获取主导航元素
var nav = document.getElementById( 'site-navigation' ), button, menu;
// bjy: 如果主导航元素不存在,则直接退出函数
if ( ! nav ) {
return;
}
// bjy: 获取导航内的按钮元素(通常是移动端的菜单切换按钮)
button = nav.getElementsByTagName( 'button' )[0];
// bjy: 获取导航内的菜单列表ul元素
menu = nav.getElementsByTagName( 'ul' )[0];
// bjy: 如果按钮不存在,则直接退出函数
if ( ! button ) {
return;
}
// Hide button if menu is missing or empty.
// bjy: 如果菜单不存在或为空,则隐藏切换按钮
if ( ! menu || ! menu.childNodes.length ) {
button.style.display = 'none';
return;
}
// bjy: 为按钮绑定点击事件
button.onclick = function() {
// bjy: 确保菜单列表有 'nav-menu' 这个基础类名
if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
menu.className = 'nav-menu';
}
// bjy: 检查按钮是否已经处于激活toggled-on状态
if ( -1 !== button.className.indexOf( 'toggled-on' ) ) {
// bjy: 如果是,则移除按钮和菜单的 'toggled-on' 类,以关闭菜单
button.className = button.className.replace( ' toggled-on', '' );
menu.className = menu.className.replace( ' toggled-on', '' );
} else {
// bjy: 如果不是,则给按钮和菜单添加 'toggled-on' 类,以打开菜单
button.className += ' toggled-on';
menu.className += ' toggled-on';
}
};
} )();
// Better focus for hidden submenu items for accessibility.
// bjy: Better focus for hidden submenu items for accessibility.
// bjy: 使用另一个IIFE并将jQuery作为参数传入以安全地使用$
( function( $ ) {
// bjy: 在主导航区域内为所有链接a标签绑定焦点和失焦事件
$( '.main-navigation' ).find( 'a' ).on( 'focus.twentytwelve blur.twentytwelve', function() {
// bjy: 当链接获得或失去焦点时切换其父级菜单项li的 'focus' 类
// bjy: 这主要用于通过键盘Tab键导航时高亮显示当前所在的菜单项
$( this ).parents( '.menu-item, .page_item' ).toggleClass( 'focus' );
} );
// bjy: 检测设备是否支持触摸事件,用于处理移动端的菜单交互
if ( 'ontouchstart' in window ) {
// bjy: 在body上监听触摸开始事件但委托给有子菜单的菜单项的链接
$('body').on( 'touchstart.twentytwelve', '.menu-item-has-children > a, .page_item_has_children > a', function( e ) {
// bjy: 获取当前触摸的链接的父级li元素
var el = $( this ).parent( 'li' );
// bjy: 如果该菜单项还没有 'focus' 类(即子菜单未展开)
if ( ! el.hasClass( 'focus' ) ) {
// bjy: 阻止链接的默认跳转行为,因为第一次点击是展开子菜单
e.preventDefault();
// bjy: 切换当前菜单项的 'focus' 类,展开子菜单
el.toggleClass( 'focus' );
// bjy: 移除其他同级菜单项的 'focus' 类,确保一次只展开一个子菜单
el.siblings( '.focus').removeClass( 'focus' );
}
} );

@ -1,37 +1,56 @@
/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* @license MIT */
/* bjy: NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* bjy: @license MIT */
// bjy: UMDUniversal Module Definition模式包装使库能在多种模块系统AMD, CommonJS, 浏览器全局变量)下工作
;(function(root, factory) {
// bjy: 如果环境支持AMD如RequireJS则使用define定义模块
if (typeof define === 'function' && define.amd) {
define(factory);
// bjy: 如果环境支持CommonJS如Node.js则将模块导出
} else if (typeof exports === 'object') {
module.exports = factory();
// bjy: 否则将库挂载到全局对象浏览器中的window
} else {
root.NProgress = factory();
}
// bjy: 传入this在浏览器中为window作为root并调用工厂函数
})(this, function() {
// bjy: 创建NProgress对象作为库的命名空间
var NProgress = {};
// bjy: 定义NProgress的版本号
NProgress.version = '0.2.0';
// bjy: 定义默认配置项并挂载到NProgress.settings上
var Settings = NProgress.settings = {
// bjy: 进度条最小值,防止进度条在开始时看起来像没动
minimum: 0.08,
// bjy: 动画缓动函数
easing: 'linear',
// bjy: 进度条定位方式,由程序自动检测
positionUsing: '',
// bjy: 动画速度(毫秒)
speed: 200,
// bjy: 是否开启自动递增trickle效果
trickle: true,
// bjy: 自动递增的频率(毫秒)
trickleSpeed: 200,
// bjy: 是否显示右上角的加载旋转图标
showSpinner: true,
// bjy: 进度条条形的选择器
barSelector: '[role="bar"]',
// bjy: 加载旋转图标的选择器
spinnerSelector: '[role="spinner"]',
// bjy: 进度条的父容器默认为body
parent: 'body',
// bjy: 进度条的HTML模板
template: '<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'
};
/**
* Updates configuration.
* bjy: 更新配置
*
* NProgress.configure({
* minimum: 0.1
@ -39,130 +58,154 @@
*/
NProgress.configure = function(options) {
var key, value;
// bjy: 遍历传入的配置项
for (key in options) {
value = options[key];
// bjy: 将有效的配置项更新到Settings对象中
if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
}
// bjy: 返回NProgress自身支持链式调用
return this;
};
/**
* Last number.
* bjy: 存储当前进度状态的变量
*/
NProgress.status = null;
/**
* Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
* bjy: 设置进度条的状态`n`是一个从`0.0``1.0`的数字
*
* NProgress.set(0.4);
* NProgress.set(1.0);
*/
NProgress.set = function(n) {
// bjy: 检查进度条是否已经启动
var started = NProgress.isStarted();
// bjy: 将进度值n限制在minimum和1之间
n = clamp(n, Settings.minimum, 1);
// bjy: 更新状态如果进度为1则将status设为null表示完成
NProgress.status = (n === 1 ? null : n);
// bjy: 如果进度条未渲染,则先渲染它
var progress = NProgress.render(!started),
bar = progress.querySelector(Settings.barSelector),
speed = Settings.speed,
ease = Settings.easing;
// bjy: 触发重排以确保后续的CSS过渡效果能正确应用
progress.offsetWidth; /* Repaint */
// bjy: 使用队列来管理动画,确保动画按顺序执行
queue(function(next) {
// Set positionUsing if it hasn't already been set
// bjy: 如果定位方式未设置则自动检测最佳的CSS定位方式
if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
// Add transition
// bjy: 为进度条添加CSS过渡效果更新其位置
css(bar, barPositionCSS(n, speed, ease));
// bjy: 如果进度达到100%
if (n === 1) {
// Fade out
// bjy: 先设置一个无过渡的状态并设置opacity为1然后触发重排
css(progress, {
transition: 'none',
opacity: 1
});
progress.offsetWidth; /* Repaint */
// bjy: 延迟后,添加淡出动画
setTimeout(function() {
css(progress, {
transition: 'all ' + speed + 'ms linear',
opacity: 0
});
// bjy: 在淡出动画完成后,移除进度条元素,并执行队列的下一个任务
setTimeout(function() {
NProgress.remove();
next();
}, speed);
}, speed);
} else {
// bjy: 如果未完成,则在动画持续时间后执行下一个任务
setTimeout(next, speed);
}
});
// bjy: 返回NProgress自身支持链式调用
return this;
};
// bjy: 检查进度条是否已经启动即status是否为数字
NProgress.isStarted = function() {
return typeof NProgress.status === 'number';
};
/**
* Shows the progress bar.
* This is the same as setting the status to 0%, except that it doesn't go backwards.
* bjy: 显示进度条
* bjy: 这与将状态设置为0%相同只是它不会向后退
*
* NProgress.start();
*
*/
NProgress.start = function() {
// bjy: 如果进度条未启动则将其设置为0%
if (!NProgress.status) NProgress.set(0);
// bjy: 定义一个递归函数用于实现trickle自动递增效果
var work = function() {
setTimeout(function() {
// bjy: 如果进度条已被手动关闭,则停止递增
if (!NProgress.status) return;
// bjy: 调用trickle方法增加一点进度
NProgress.trickle();
// bjy: 递归调用自身
work();
}, Settings.trickleSpeed);
};
// bjy: 如果配置中开启了trickle则开始执行
if (Settings.trickle) work();
return this;
};
/**
* Hides the progress bar.
* This is the *sort of* the same as setting the status to 100%, with the
* difference being `done()` makes some placebo effect of some realistic motion.
* bjy: 隐藏进度条
* bjy: 这与将状态设置为100%类似`done()`会制造一些更逼真的动画效果
*
* NProgress.done();
*
* If `true` is passed, it will show the progress bar even if its hidden.
* bjy: 如果传入`true`即使进度条是隐藏的它也会显示并完成
*
* NProgress.done(true);
*/
NProgress.done = function(force) {
// bjy: 如果没有强制完成且进度条未启动,则直接返回
if (!force && !NProgress.status) return this;
// bjy: 先增加一点随机进度然后设置进度为100%
return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
};
/**
* Increments by a random amount.
* bjy: 以一个随机量增加进度
*/
NProgress.inc = function(amount) {
var n = NProgress.status;
// bjy: 如果进度条未启动,则启动它
if (!n) {
return NProgress.start();
} else if(n > 1) {
// bjy: 如果进度已超过100%,不做任何事
} else {
// bjy: 如果没有指定增加的量,则根据当前进度计算一个合适的增量
if (typeof amount !== 'number') {
if (n >= 0 && n < 0.2) { amount = 0.1; }
else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
@ -171,18 +214,20 @@
else { amount = 0; }
}
// bjy: 计算新的进度值并限制在0到0.994之间防止达到100%后还有动画
n = clamp(n + amount, 0, 0.994);
return NProgress.set(n);
}
};
// bjy: trickle方法内部调用inc
NProgress.trickle = function() {
return NProgress.inc();
};
/**
* Waits for all supplied jQuery promises and
* increases the progress as the promises resolve.
* bjy: 等待所有提供的jQuery promise解决
* bjy: 并在promise解决时增加进度
*
* @param $promise jQUery Promise
*/
@ -190,23 +235,30 @@
var initial = 0, current = 0;
NProgress.promise = function($promise) {
// bjy: 如果promise不存在或已经解决则直接返回
if (!$promise || $promise.state() === "resolved") {
return this;
}
// bjy: 如果这是第一个promise则启动进度条
if (current === 0) {
NProgress.start();
}
// bjy: 增加总promise计数和当前计数
initial++;
current++;
// bjy: 为promise添加always回调无论成功或失败都会执行
$promise.always(function() {
// bjy: 当前计数减一
current--;
// bjy: 如果所有promise都已完成
if (current === 0) {
initial = 0;
NProgress.done();
} else {
// bjy: 否则根据已完成的promise比例更新进度条
NProgress.set((initial - current) / initial);
}
});
@ -217,55 +269,65 @@
})();
/**
* (Internal) renders the progress bar markup based on the `template`
* setting.
* bjy: (内部) 根据`template`设置渲染进度条的HTML标记
*/
NProgress.render = function(fromStart) {
// bjy: 如果进度条已经渲染,则直接返回已存在的元素
if (NProgress.isRendered()) return document.getElementById('nprogress');
// bjy: 给html元素添加'nprogress-busy'类,可用于样式控制
addClass(document.documentElement, 'nprogress-busy');
// bjy: 创建进度条的容器div
var progress = document.createElement('div');
progress.id = 'nprogress';
// bjy: 使用模板设置其innerHTML
progress.innerHTML = Settings.template;
var bar = progress.querySelector(Settings.barSelector),
// bjy: 如果是从头开始,则初始位置在-100%,否则根据当前状态计算位置
perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
parent = document.querySelector(Settings.parent),
spinner;
// bjy: 设置进度条条的初始位置和过渡效果
css(bar, {
transition: 'all 0 linear',
transform: 'translate3d(' + perc + '%,0,0)'
});
// bjy: 如果配置中不显示旋转图标,则将其移除
if (!Settings.showSpinner) {
spinner = progress.querySelector(Settings.spinnerSelector);
spinner && removeElement(spinner);
}
// bjy: 如果父容器不是body则给父容器添加自定义类
if (parent != document.body) {
addClass(parent, 'nprogress-custom-parent');
}
// bjy: 将进度条元素添加到父容器中
parent.appendChild(progress);
return progress;
};
/**
* Removes the element. Opposite of render().
* bjy: 移除元素与render()相反
*/
NProgress.remove = function() {
// bjy: 移除html和父容器上的辅助类
removeClass(document.documentElement, 'nprogress-busy');
removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent');
// bjy: 从DOM中移除进度条元素
var progress = document.getElementById('nprogress');
progress && removeElement(progress);
};
/**
* Checks if the progress bar is rendered.
* bjy: 检查进度条是否已渲染
*/
NProgress.isRendered = function() {
@ -273,35 +335,36 @@
};
/**
* Determine which positioning CSS rule to use.
* bjy: 确定使用哪种定位CSS规则
*/
NProgress.getPositioningCSS = function() {
// Sniff on document.body.style
// bjy: 检查body的style属性以嗅探浏览器支持
var bodyStyle = document.body.style;
// Sniff prefixes
// bjy: 嗅探浏览器支持的CSS前缀
var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
('MozTransform' in bodyStyle) ? 'Moz' :
('msTransform' in bodyStyle) ? 'ms' :
('OTransform' in bodyStyle) ? 'O' : '';
// bjy: 如果支持3D变换则使用translate3d
if (vendorPrefix + 'Perspective' in bodyStyle) {
// Modern browsers with 3D support, e.g. Webkit, IE10
return 'translate3d';
// bjy: 如果只支持2D变换则使用translate
} else if (vendorPrefix + 'Transform' in bodyStyle) {
// Browsers without 3D support, e.g. IE9
return 'translate';
// bjy: 否则回退到使用margin性能较差
} else {
// Browsers without translate() support, e.g. IE7-8
return 'margin';
}
};
/**
* Helpers
* bjy: 辅助函数
*/
// bjy: 将一个数值限制在最小值和最大值之间
function clamp(n, min, max) {
if (n < min) return min;
if (n > max) return max;
@ -309,8 +372,7 @@
}
/**
* (Internal) converts a percentage (`0..1`) to a bar translateX
* percentage (`-100%..0%`).
* bjy: (内部) 将百分比 (`0..1`) 转换为进度条的translateX百分比 (`-100%..0%`)
*/
function toBarPerc(n) {
@ -319,13 +381,14 @@
/**
* (Internal) returns the correct CSS for changing the bar's
* position given an n percentage, and speed and ease from Settings
* bjy: (内部) 返回用于改变进度条位置的CSS
* bjy: 根据给定的百分比n以及Settings中的速度和缓动函数
*/
function barPositionCSS(n, speed, ease) {
var barCSS;
// bjy: 根据定位方式生成不同的CSS
if (Settings.positionUsing === 'translate3d') {
barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
} else if (Settings.positionUsing === 'translate') {
@ -334,18 +397,20 @@
barCSS = { 'margin-left': toBarPerc(n)+'%' };
}
// bjy: 添加过渡效果
barCSS.transition = 'all '+speed+'ms '+ease;
return barCSS;
}
/**
* (Internal) Queues a function to be executed.
* bjy: (内部) 将一个函数排队等待执行
*/
var queue = (function() {
var pending = [];
// bjy: 从队列中取出第一个函数并执行
function next() {
var fn = pending.shift();
if (fn) {
@ -353,30 +418,35 @@
}
}
// bjy: 返回一个函数,用于向队列中添加新任务
return function(fn) {
pending.push(fn);
// bjy: 如果这是队列中唯一的任务,则立即开始执行
if (pending.length == 1) next();
};
})();
/**
* (Internal) Applies css properties to an element, similar to the jQuery
* css method.
* bjy: (内部) 将CSS属性应用到元素上类似于jQuery的css方法
*
* While this helper does assist with vendor prefixed property names, it
* does not perform any manipulation of values prior to setting styles.
* bjy: 虽然这个辅助函数有助于处理带供应商前缀的属性名
* bjy: 但它在设置样式之前不会对值进行任何操作
*/
var css = (function() {
// bjy: 常见的CSS前缀列表
var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
// bjy: 缓存已检测的CSS属性名
cssProps = {};
// bjy: 将连字符格式的字符串转换为驼峰格式
function camelCase(string) {
return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
return letter.toUpperCase();
});
}
// bjy: 获取带正确前缀的CSS属性名
function getVendorProp(name) {
var style = document.body.style;
if (name in style) return name;
@ -384,6 +454,7 @@
var i = cssPrefixes.length,
capName = name.charAt(0).toUpperCase() + name.slice(1),
vendorName;
// bjy: 遍历前缀,检查哪个前缀的属性被支持
while (i--) {
vendorName = cssPrefixes[i] + capName;
if (vendorName in style) return vendorName;
@ -392,34 +463,39 @@
return name;
}
// bjy: 获取最终的样式属性名(带缓存)
function getStyleProp(name) {
name = camelCase(name);
return cssProps[name] || (cssProps[name] = getVendorProp(name));
}
// bjy: 应用单个CSS属性到元素
function applyCss(element, prop, value) {
prop = getStyleProp(prop);
element.style[prop] = value;
}
// bjy: 暴露的css函数支持单个或多个属性设置
return function(element, properties) {
var args = arguments,
prop,
value;
// bjy: 如果传入两个参数element, properties对象
if (args.length == 2) {
for (prop in properties) {
value = properties[prop];
if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
}
} else {
// bjy: 如果传入三个参数element, prop, value
applyCss(element, args[1], args[2]);
}
}
})();
/**
* (Internal) Determines if an element or space separated list of class names contains a class name.
* bjy: (内部) 判断一个元素或空格分隔的类名字符串是否包含某个类名
*/
function hasClass(element, name) {
@ -428,7 +504,7 @@
}
/**
* (Internal) Adds a class to an element.
* bjy: (内部) 给一个元素添加类名
*/
function addClass(element, name) {
@ -437,12 +513,12 @@
if (hasClass(oldList, name)) return;
// Trim the opening space.
// bjy: 去掉开头的空格
element.className = newList.substring(1);
}
/**
* (Internal) Removes a class from an element.
* bjy: (内部) 从一个元素移除类名
*/
function removeClass(element, name) {
@ -451,17 +527,16 @@
if (!hasClass(element, name)) return;
// Replace the class name.
// bjy: 替换掉要移除的类名
newList = oldList.replace(' ' + name + ' ', ' ');
// Trim the opening and closing spaces.
// bjy: 去掉开头和结尾的空格
element.className = newList.substring(1, newList.length - 1);
}
/**
* (Internal) Gets a space separated list of the class names on the element.
* The list is wrapped with a single space on each end to facilitate finding
* matches within the list.
* bjy: (内部) 获取元素上所有类名的空格分隔列表
* bjy: 列表的首尾都包裹一个空格以便于在列表中查找匹配项
*/
function classList(element) {
@ -469,12 +544,13 @@
}
/**
* (Internal) Removes an element from the DOM.
* bjy: (内部) 从DOM中移除一个元素
*/
function removeElement(element) {
element && element.parentNode && element.parentNode.removeChild(element);
}
// bjy: 返回NProgress对象
return NProgress;
});

@ -1,5 +1,7 @@
# bjy: 导入操作系统接口模块
import os
# bjy: 从Django中导入设置、文件上传、命令调用、分页器、静态文件、测试工具、URL反向解析和时区工具
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
@ -9,30 +11,40 @@ from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
# bjy: 从项目中导入用户模型、博客表单、博客模型、自定义模板标签和工具函数
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
# bjy: 从项目中导入OAuth相关模型
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
# bjy: 在此处创建测试。
# bjy: 定义一个针对文章功能的测试类继承自Django的TestCase
class ArticleTest(TestCase):
# bjy: setUp方法在每个测试方法执行前运行用于初始化测试环境
def setUp(self):
# bjy: 创建一个测试客户端实例,用于模拟浏览器请求
self.client = Client()
# bjy: 创建一个请求工厂实例,用于生成请求对象
self.factory = RequestFactory()
# bjy: 定义一个测试方法,用于验证文章相关的功能
def test_validate_article(self):
get_current_site().domain
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
# bjy: 设置用户密码
user.set_password("liangliangyy")
# bjy: 设置用户为员工和管理员
user.is_staff = True
user.is_superuser = True
user.save()
# bjy: 测试用户详情页是否能正常访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
self.client.get('/admin/servermanager/emailsendlog/')
@ -44,16 +56,19 @@ class ArticleTest(TestCase):
s.is_enable = True
s.save()
# bjy: 创建并保存一个分类实例
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
# bjy: 创建并保存一个标签实例
tag = Tag()
tag.name = "nicetag"
tag.save()
# bjy: 创建并保存一篇文章
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
@ -63,11 +78,15 @@ class ArticleTest(TestCase):
article.status = 'p'
article.save()
# bjy: 验证文章初始标签数量为0
self.assertEqual(0, article.tags.count())
# bjy: 给文章添加标签并保存
article.tags.add(tag)
article.save()
# bjy: 验证文章标签数量变为1
self.assertEqual(1, article.tags.count())
# bjy: 循环创建20篇文章用于测试分页等功能
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@ -79,96 +98,126 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
# bjy: 如果启用了Elasticsearch则重建索引并测试搜索功能
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
# bjy: 测试文章详情页
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# bjy: 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
# bjy: 测试标签页
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
# bjy: 测试分类页
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# bjy: 测试搜索页
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# bjy: 测试加载文章标签的模板标签
s = load_articletags(article)
self.assertIsNotNone(s)
# bjy: 以超级用户身份登录
self.client.login(username='liangliangyy', password='liangliangyy')
# bjy: 测试文章归档页
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# bjy: 测试文章列表的分页信息
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
# bjy: 测试按标签筛选后的文章分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
# bjy: 测试按作者筛选后的文章分页
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
# bjy: 测试按分类筛选后的文章分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
# bjy: 测试搜索表单
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
# bjy: 测试百度通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
# bjy: 测试获取Gravatar头像的模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
gravatar_url('liangliangyy@gmail.com')
gravatar('liangliangyy@gmail.com')
# bjy: 创建并保存一个友情链接
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save()
# bjy: 测试友情链接页面
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# bjy: 测试RSS订阅页面
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
# bjy: 测试网站地图
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# bjy: 测试一些Admin后台的删除和变更操作
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/')
# bjy: 辅助方法,用于检查分页导航链接是否正确
def check_pagination(self, p, type, value):
# bjy: 遍历所有页码
for page in range(1, p.num_pages + 1):
# bjy: 加载当前页的分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
# bjy: 如果存在上一页链接,则测试其可访问性
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# bjy: 如果存在下一页链接,则测试其可访问性
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
# bjy: 测试图片上传功能
def test_image(self):
# bjy: 下载一个网络图片到本地
import requests
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# bjy: 测试无签名上传预期返回403
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# bjy: 生成签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# bjy: 使用签名上传图片
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
@ -176,18 +225,23 @@ class ArticleTest(TestCase):
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
# bjy: 删除本地临时图片
os.remove(imagepath)
# bjy: 测试发送邮件和保存用户头像的工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
# bjy: 测试404错误页面
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
# bjy: 测试自定义的管理命令
@staticmethod
def test_commands():
# bjy: 创建一个超级用户(如果不存在)
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -196,12 +250,14 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
# bjy: 创建并保存一个OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# bjy: 创建并保存一个OAuth用户关联到超级用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@ -213,6 +269,7 @@ class ArticleTest(TestCase):
}'''
u.save()
# bjy: 创建另一个OAuth用户用于测试
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -223,9 +280,11 @@ class ArticleTest(TestCase):
}'''
u.save()
# bjy: 如果启用了Elasticsearch则重建索引
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
# bjy: 调用并测试一系列自定义管理命令
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")

@ -1,60 +1,77 @@
# bjy: 从Django中导入路径函数和缓存页面装饰器
from django.urls import path
from django.views.decorators.cache import cache_page
# bjy: 从当前应用中导入视图模块
from . import views
# bjy: 定义应用的命名空间用于在模板中反向解析URL时避免冲突
app_name = "blog"
# bjy: 定义URL模式列表
urlpatterns = [
# bjy: 首页路由指向IndexView视图
path(
r'',
views.IndexView.as_view(),
name='index'),
# bjy: 首页分页路由指向IndexView视图并接收页码参数
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
# bjy: 文章详情页路由包含年、月、日和文章ID指向ArticleDetailView视图
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
# bjy: 分类详情页路由接收分类的slug指向CategoryDetailView视图
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
# bjy: 分类详情页分页路由接收分类slug和页码指向CategoryDetailView视图
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
# bjy: 作者详情页路由接收作者名指向AuthorDetailView视图
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
# bjy: 作者详情页分页路由接收作者名和页码指向AuthorDetailView视图
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
# bjy: 标签详情页路由接收标签的slug指向TagDetailView视图
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
# bjy: 标签详情页分页路由接收标签slug和页码指向TagDetailView视图
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
# bjy: 文章归档页路由使用cache_page装饰器缓存1小时指向ArchivesView视图
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
# bjy: 友情链接页路由指向LinkListView视图
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
# bjy: 文件上传路由指向fileupload视图函数
path(
r'upload',
views.fileupload,
name='upload'),
# bjy: 清除缓存路由指向clean_cache_view视图函数
path(
r'clean',
views.clean_cache_view,

@ -1,7 +1,9 @@
# bjy: 导入日志、操作系统和UUID模块
import logging
import os
import uuid
# bjy: 从Django中导入设置、分页器、HTTP响应、快捷函数、静态文件、时区、国际化、CSRF豁免和基于类的视图
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
@ -13,51 +15,62 @@ 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
# bjy: 从Haystack中导入搜索视图
from haystack.views import SearchView
# bjy: 从项目中导入博客模型、评论表单、插件管理器和工具函数
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.utils import cache, get_blog_setting, get_sha256
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个基于类的文章列表视图,作为其他列表视图的基类
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
# bjy: 指定渲染的模板文件
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
# bjy: 指定在模板中使用的上下文变量名
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
# bjy: 页面类型,用于在模板中显示不同的标题
page_type = ''
# bjy: 每页显示的文章数量,从设置中获取
paginate_by = settings.PAGINATE_BY
# bjy: URL中分页参数的名称
page_kwarg = 'page'
# bjy: 友情链接的显示类型,默认为列表页
link_type = LinkShowType.L
# bjy: 获取视图的缓存键(此方法未使用)
def get_view_cache_key(self):
return self.request.get['pages']
# bjy: 属性,用于获取当前页码
@property
def page_number(self):
page_kwarg = self.page_kwarg
# bjy: 从URL参数或GET参数中获取页码默认为1
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
# bjy: 抽象方法要求子类实现用于获取queryset的缓存键
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
# bjy: 抽象方法,要求子类实现,用于获取实际的数据集
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
# bjy: 从缓存中获取数据集,如果缓存不存在则查询数据库并存入缓存
def get_queryset_from_cache(self, cache_key):
"""
缓存页面数据
@ -69,11 +82,13 @@ class ArticleListView(ListView):
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
# bjy: 调用子类实现的get_queryset_data方法获取数据
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
# bjy: 重写父类方法从缓存中获取queryset
def get_queryset(self):
"""
重写默认从缓存获取数据
@ -83,11 +98,13 @@ class ArticleListView(ListView):
value = self.get_queryset_from_cache(key)
return value
# bjy: 重写父类方法,向上下文中添加链接类型
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
# bjy: 首页视图继承自ArticleListView
class IndexView(ArticleListView):
"""
首页
@ -95,15 +112,18 @@ class IndexView(ArticleListView):
# 友情链接类型
link_type = LinkShowType.I
# bjy: 实现父类的抽象方法,获取首页的文章数据
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
# bjy: 实现父类的抽象方法,生成首页的缓存键
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
# bjy: 文章详情页视图
class ArticleDetailView(DetailView):
"""
文章详情页面
@ -113,14 +133,21 @@ class ArticleDetailView(DetailView):
pk_url_kwarg = 'article_id'
context_object_name = "article"
# bjy: 重写父类方法,向上下文中添加额外的数据
def get_context_data(self, **kwargs):
# bjy: 创建评论表单实例
comment_form = CommentForm()
# bjy: 获取文章的所有评论
article_comments = self.object.comment_list()
# bjy: 筛选出父评论(顶级评论)
parent_comments = article_comments.filter(parent_comment=None)
# bjy: 获取博客设置
blog_setting = get_blog_setting()
# bjy: 对父评论进行分页
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
# bjy: 从GET参数中获取评论页码
page = self.request.GET.get('comment_page', '1')
# bjy: 校验页码是否为有效数字
if not page.isnumeric():
page = 1
else:
@ -130,55 +157,68 @@ class ArticleDetailView(DetailView):
if page > paginator.num_pages:
page = paginator.num_pages
# bjy: 获取当前页的评论对象
p_comments = paginator.page(page)
# bjy: 获取下一页和上一页的页码
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# bjy: 如果存在下一页则构建下一页的URL
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
# bjy: 如果存在上一页则构建上一页的URL
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# bjy: 将评论表单和评论数据添加到上下文
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
# bjy: 添加上一篇和下一篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# bjy: 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# 触发文章详情加载钩子,让插件可以添加额外的上下文数据
# bjy: 触发文章详情加载钩子,让插件可以添加额外的上下文数据
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request)
# Action Hook, 通知插件"文章详情已获取"
# bjy: Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
return context
# bjy: 分类详情页视图
class CategoryDetailView(ArticleListView):
"""
分类目录列表
"""
page_type = "分类目录归档"
# bjy: 实现父类的抽象方法,获取分类下的文章数据
def get_queryset_data(self):
slug = self.kwargs['category_name']
# bjy: 根据slug获取分类对象如果不存在则返回404
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
# bjy: 获取该分类及其所有子分类的名称列表
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# bjy: 筛选出属于这些分类的所有已发布文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
# bjy: 实现父类的抽象方法,生成分类页的缓存键
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
@ -188,10 +228,12 @@ class CategoryDetailView(ArticleListView):
categoryname=categoryname, page=self.page_number)
return cache_key
# bjy: 重写父类方法,向上下文中添加分类名称
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
# bjy: 处理多级分类的情况,只取最后一部分
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
@ -200,12 +242,14 @@ class CategoryDetailView(ArticleListView):
return super(CategoryDetailView, self).get_context_data(**kwargs)
# bjy: 作者详情页视图
class AuthorDetailView(ArticleListView):
"""
作者详情页
"""
page_type = '作者文章归档'
# bjy: 实现父类的抽象方法,生成作者页的缓存键
def get_queryset_cache_key(self):
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
@ -213,12 +257,14 @@ class AuthorDetailView(ArticleListView):
author_name=author_name, page=self.page_number)
return cache_key
# bjy: 实现父类的抽象方法,获取作者的文章数据
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
# bjy: 重写父类方法,向上下文中添加作者名称
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
@ -226,12 +272,14 @@ class AuthorDetailView(ArticleListView):
return super(AuthorDetailView, self).get_context_data(**kwargs)
# bjy: 标签详情页视图
class TagDetailView(ArticleListView):
"""
标签列表页面
"""
page_type = '分类标签归档'
# bjy: 实现父类的抽象方法,获取标签下的文章数据
def get_queryset_data(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
@ -241,6 +289,7 @@ class TagDetailView(ArticleListView):
tags__name=tag_name, type='a', status='p')
return article_list
# bjy: 实现父类的抽象方法,生成标签页的缓存键
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
@ -250,6 +299,7 @@ class TagDetailView(ArticleListView):
tag_name=tag_name, page=self.page_number)
return cache_key
# bjy: 重写父类方法,向上下文中添加标签名称
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name
@ -258,32 +308,40 @@ class TagDetailView(ArticleListView):
return super(TagDetailView, self).get_context_data(**kwargs)
# bjy: 文章归档页视图
class ArchivesView(ArticleListView):
"""
文章归档页面
"""
page_type = '文章归档'
# bjy: 归档页不分页
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
# bjy: 实现父类的抽象方法,获取所有已发布文章
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
# bjy: 实现父类的抽象方法,生成归档页的缓存键
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
# bjy: 友情链接页视图
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
# bjy: 重写queryset只获取已启用的链接
def get_queryset(self):
return Links.objects.filter(is_enable=True)
# bjy: 自定义的Elasticsearch搜索视图
class EsSearchView(SearchView):
# bjy: 重写get_context方法自定义搜索结果的上下文
def get_context(self):
paginator, page = self.build_page()
context = {
@ -293,6 +351,7 @@ class EsSearchView(SearchView):
"paginator": paginator,
"suggestion": None,
}
# bjy: 如果启用了拼写建议,则添加到上下文
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())
@ -300,6 +359,7 @@ class EsSearchView(SearchView):
return context
# bjy: 文件上传视图使用csrf_exempt豁免CSRF验证
@csrf_exempt
def fileupload(request):
"""
@ -308,38 +368,52 @@ def fileupload(request):
:return:
"""
if request.method == 'POST':
# bjy: 从GET参数中获取签名
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
# bjy: 验证签名是否正确
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
# bjy: 遍历所有上传的文件
for filename in request.FILES:
# bjy: 按年/月/日创建目录
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
# bjy: 判断文件是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
# bjy: 如果目录不存在则创建
if not os.path.exists(base_dir):
os.makedirs(base_dir)
# bjy: 生成唯一的文件名并拼接保存路径
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# bjy: 安全检查,防止路径遍历攻击
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# bjy: 将文件内容写入磁盘
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# bjy: 如果是图片,则进行压缩优化
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
# bjy: 生成文件的静态URL
url = static(savepath)
response.append(url)
# bjy: 返回包含所有文件URL的响应
return HttpResponse(response)
else:
# bjy: 非POST请求返回错误信息
return HttpResponse("only for post")
# bjy: 自定义404错误处理视图
def page_not_found_view(
request,
exception,
@ -354,6 +428,7 @@ def page_not_found_view(
status=404)
# bjy: 自定义500服务器错误处理视图
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
@ -362,6 +437,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
status=500)
# bjy: 自定义403权限拒绝错误处理视图
def permission_denied_view(
request,
exception,
@ -374,6 +450,7 @@ def permission_denied_view(
'statuscode': '403'}, status=403)
# bjy: 清除缓存的视图
def clean_cache_view():
cache.clear()
return HttpResponse('ok')

Loading…
Cancel
Save