Merge branch 'YMQ_branch' of https://bdgit.educoder.net/pn39ysei2/project into YMQ_branch

develop
ymq 3 months ago
commit d200412126

Binary file not shown.

@ -1,48 +1,76 @@
from django import forms
#ymq导入Django的forms模块用于创建表单
from django.contrib import admin
#ymq导入Django的admin模块用于后台管理配置
from django.contrib.auth import get_user_model
#ymq导入获取用户模型的函数便于灵活引用用户模型
from django.urls import reverse
#ymq导入reverse函数用于生成URL反向解析
from django.utils.html import format_html
#ymq导入format_html函数用于安全生成HTML内容
from django.utils.translation import gettext_lazy as _
#ymq导入国际化翻译函数将文本标记为可翻译
# Register your models here.
from .models import Article
#ymq从当前应用的models模块导入Article模型
class ArticleForm(forms.ModelForm):
#ymq定义Article模型对应的表单类继承自ModelForm
# body = forms.CharField(widget=AdminPagedownWidget())
#ymq注释掉的代码原本计划为body字段使用AdminPagedownWidget编辑器
class Meta:
#ymqMeta类用于配置表单元数据
model = Article
#ymq指定表单关联的模型为Article
fields = '__all__'
#ymq指定表单包含模型的所有字段
def makr_article_publish(modeladmin, request, queryset):
#ymq定义批量发布文章的动作函数
queryset.update(status='p')
#ymq将选中的文章状态更新为'p'(发布状态)
def draft_article(modeladmin, request, queryset):
#ymq定义批量设为草稿的动作函数
queryset.update(status='d')
#ymq将选中的文章状态更新为'd'(草稿状态)
def close_article_commentstatus(modeladmin, request, queryset):
#ymq定义批量关闭评论的动作函数
queryset.update(comment_status='c')
#ymq将选中的文章评论状态更新为'c'(关闭状态)
def open_article_commentstatus(modeladmin, request, queryset):
#ymq定义批量开启评论的动作函数
queryset.update(comment_status='o')
#ymq将选中的文章评论状态更新为'o'(开启状态)
makr_article_publish.short_description = _('Publish selected articles')
#ymq设置发布动作在admin中的显示名称支持国际化
draft_article.short_description = _('Draft selected articles')
#ymq设置草稿动作在admin中的显示名称支持国际化
close_article_commentstatus.short_description = _('Close article comments')
#ymq设置关闭评论动作在admin中的显示名称支持国际化
open_article_commentstatus.short_description = _('Open article comments')
#ymq设置开启评论动作在admin中的显示名称支持国际化
class ArticlelAdmin(admin.ModelAdmin):
#ymq定义Article模型的admin管理类继承自ModelAdmin
list_per_page = 20
#ymq设置每页显示20条记录
search_fields = ('body', 'title')
#ymq设置可搜索的字段为body和title
form = ArticleForm
#ymq指定使用自定义的ArticleForm表单
list_display = (
'id',
'title',
@ -53,60 +81,93 @@ class ArticlelAdmin(admin.ModelAdmin):
'status',
'type',
'article_order')
#ymq设置列表页显示的字段
list_display_links = ('id', 'title')
#ymq设置列表页中可点击跳转编辑页的字段
list_filter = ('status', 'type', 'category')
#ymq设置可用于筛选的字段
filter_horizontal = ('tags',)
#ymq设置多对多字段的水平筛选器tags字段
exclude = ('creation_time', 'last_modify_time')
#ymq设置编辑页中排除的字段不显示
view_on_site = True
#ymq启用"在站点上查看"功能
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
#ymq注册批量操作动作
def link_to_category(self, obj):
#ymq自定义列表页中分类字段的显示方式转为链接
info = (obj.category._meta.app_label, obj.category._meta.model_name)
#ymq获取分类模型的应用标签和模型名称
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
#ymq生成分类的编辑页URL
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
#ymq返回HTML链接点击可跳转到分类编辑页
link_to_category.short_description = _('category')
#ymq设置自定义字段在列表页的显示名称支持国际化
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
#ymq重写获取表单的方法自定义表单字段
form = super(ArticlelAdmin, self).get_form(request, obj,** kwargs)
#ymq调用父类方法获取表单
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
#ymq限制作者字段只能选择超级用户
return form
#ymq返回修改后的表单
def save_model(self, request, obj, form, change):
#ymq重写保存模型的方法可在此添加自定义保存逻辑
super(ArticlelAdmin, self).save_model(request, obj, form, change)
#ymq调用父类的保存方法完成默认保存
def get_view_on_site_url(self, obj=None):
#ymq重写"在站点上查看"的URL生成方法
if obj:
#ymq如果有具体对象返回对象的完整URL
url = obj.get_full_url()
return url
else:
#ymq如果无对象返回当前站点域名
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
class TagAdmin(admin.ModelAdmin):
#ymq定义Tag模型的admin管理类
exclude = ('slug', 'last_mod_time', 'creation_time')
#ymq编辑页排除slug、最后修改时间和创建时间字段
class CategoryAdmin(admin.ModelAdmin):
#ymq定义Category模型的admin管理类
list_display = ('name', 'parent_category', 'index')
#ymq列表页显示名称、父分类和排序索引字段
exclude = ('slug', 'last_mod_time', 'creation_time')
#ymq编辑页排除slug、最后修改时间和创建时间字段
class LinksAdmin(admin.ModelAdmin):
#ymq定义Links模型的admin管理类
exclude = ('last_mod_time', 'creation_time')
#ymq编辑页排除最后修改时间和创建时间字段
class SideBarAdmin(admin.ModelAdmin):
#ymq定义SideBar模型的admin管理类
list_display = ('name', 'content', 'is_enable', 'sequence')
#ymq列表页显示名称、内容、是否启用和排序序号字段
exclude = ('last_mod_time', 'creation_time')
#ymq编辑页排除最后修改时间和创建时间字段
class BlogSettingsAdmin(admin.ModelAdmin):
#ymq定义BlogSettings模型的admin管理类
pass
#ymq暂未设置特殊配置使用默认admin行为

@ -1,5 +1,7 @@
from django.apps import AppConfig
#ymq导入Django的AppConfig类用于定义应用的配置信息
class BlogConfig(AppConfig):
#ymq定义博客应用的配置类继承自AppConfig
name = 'blog'
#ymq指定应用的名称为'blog'Django通过该名称识别此应用

@ -1,43 +1,65 @@
import logging
#ymq导入logging模块用于日志记录
from django.utils import timezone
#ymq导入Django的timezone模块用于处理时间相关操作
from djangoblog.utils import cache, get_blog_setting
#ymq从项目工具模块导入缓存工具和获取博客设置的函数
from .models import Category, Article
#ymq从当前应用的models模块导入分类和文章模型
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
def seo_processor(requests):
#ymq定义SEO上下文处理器用于向模板全局注入通用数据
key = 'seo_processor'
#ymq缓存键名用于标识当前处理器的缓存数据
value = cache.get(key)
#ymq尝试从缓存中获取数据
if value:
#ymq如果缓存存在直接返回缓存数据
return value
else:
#ymq如果缓存不存在重新生成数据
logger.info('set processor cache.')
#ymq记录日志提示正在设置缓存
setting = get_blog_setting()
#ymq获取博客的全局设置信息
#ymq构建需要传递给模板的上下文数据字典
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_NAME': setting.site_name, # 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示谷歌广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # 谷歌广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述
'SITE_DESCRIPTION': setting.site_description, # 网站描述
'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
# 网站基础URL协议+域名)
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度
'nav_category_list': Category.objects.all(), # 导航分类列表
# 导航页面列表(类型为页面且状态为已发布)
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论
'BEIAN_CODE': setting.beian_code, # 网站备案号
'ANALYTICS_CODE': setting.analytics_code, # 统计分析代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案号
"CURRENT_YEAR": timezone.now().year, # 当前年份(用于页脚等位置)
"GLOBAL_HEADER": setting.global_header, # 全局头部代码
"GLOBAL_FOOTER": setting.global_footer, # 全局底部代码
"COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核
}
cache.set(key, value, 60 * 60 * 10)
#ymq将生成的上下文数据存入缓存有效期10小时60秒*60分*10小时
return value
#ymq返回构建好的上下文数据字典

@ -1,26 +1,37 @@
import time
#ymq导入time模块用于处理时间相关操作如生成唯一ID
import elasticsearch.client
#ymq导入elasticsearch客户端模块用于操作Elasticsearch的Ingest API
from django.conf import settings
#ymq导入Django的settings模块用于获取项目配置
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
#ymq导入elasticsearch-dsl相关类用于定义Elasticsearch文档结构和字段类型
from elasticsearch_dsl.connections import connections
#ymq导入elasticsearch-dsl的连接管理工具用于创建与Elasticsearch的连接
from blog.models import Article
#ymq从blog应用导入Article模型用于同步数据到Elasticsearch
#ymq判断是否启用Elasticsearch检查settings中是否配置了ELASTICSEARCH_DSL
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
#ymq如果启用Elasticsearch创建连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
#ymq创建Elasticsearch客户端实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
#ymq创建Ingest客户端用于管理数据处理管道
c = IngestClient(es)
try:
#ymq尝试获取名为'geoip'的管道,检查是否已存在
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
#ymq如果管道不存在则创建它用于解析IP地址的地理位置信息
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -34,72 +45,85 @@ if ELASTICSEARCH_ENABLED:
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
#ymq定义地理位置信息的内部文档嵌套结构
continent_name = Keyword() # 大陆名称(关键字类型,不分词)
country_iso_code = Keyword() # 国家ISO代码关键字类型
country_name = Keyword() # 国家名称(关键字类型)
location = GeoPoint() # 地理位置坐标(经纬度)
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
#ymq定义用户代理中浏览器信息的内部文档
Family = Keyword() # 浏览器家族如Chrome、Firefox
Version = Keyword() # 浏览器版本
class UserAgentOS(UserAgentBrowser):
#ymq定义用户代理中操作系统信息的内部文档继承浏览器结构
pass
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
#ymq定义用户代理中设备信息的内部文档
Family = Keyword() # 设备家族
Brand = Keyword() # 设备品牌
Model = Keyword() # 设备型号
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
#ymq定义用户代理完整信息的内部文档嵌套结构
browser = Object(UserAgentBrowser, required=False) # 浏览器信息
os = Object(UserAgentOS, required=False) # 操作系统信息
device = Object(UserAgentDevice, required=False) # 设备信息
string = Text() # 原始用户代理字符串
is_bot = Boolean() # 是否为爬虫
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
#ymq定义用于记录性能耗时的Elasticsearch文档
url = Keyword() # 访问的URL关键字类型
time_taken = Long() # 耗时(毫秒)
log_datetime = Date() # 日志记录时间
ip = Keyword() # 访问IP地址
geoip = Object(GeoIp, required=False) # 地理位置信息(嵌套)
useragent = Object(UserAgent, required=False) # 用户代理信息(嵌套)
class Index:
name = 'performance'
#ymq定义索引配置
name = 'performance' # 索引名称
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
}
class Meta:
doc_type = 'ElapsedTime'
doc_type = 'ElapsedTime' # 文档类型Elasticsearch 7.x后逐渐废弃
class ElaspedTimeDocumentManager:
#ymq性能耗时文档的管理类用于索引的创建、删除和数据插入
@staticmethod
def build_index():
#ymq创建索引如果不存在
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
res = client.indices.exists(index="performance") # 检查索引是否存在
if not res:
ElapsedTimeDocument.init()
ElapsedTimeDocument.init() # 初始化索引
@staticmethod
def delete_index():
#ymq删除performance索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
es.indices.delete(index='performance', ignore=[400, 404]) # 忽略不存在的情况
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
ElaspedTimeDocumentManager.build_index()
#ymq创建一条性能耗时记录
ElaspedTimeDocumentManager.build_index() # 确保索引存在
#ymq构建用户代理信息对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@ -116,98 +140,112 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
#ymq创建文档实例使用时间戳作为唯一ID
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000))
1000)) # 毫秒级时间戳作为ID
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
#ymq保存文档时应用geoip管道解析IP地址
doc.save(pipeline="geoip")
class ArticleDocument(Document):
#ymq定义文章信息的Elasticsearch文档用于搜索
#ymqbody和title使用IK分词器max_word分词更细smart更简洁
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
#ymq嵌套作者信息
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#ymq嵌套分类信息
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#ymq嵌套标签信息数组
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
pub_time = Date() # 发布时间
status = Text() # 状态(发布/草稿)
comment_status = Text() # 评论状态(开启/关闭)
type = Text() # 类型(文章/页面)
views = Integer() # 浏览量
article_order = Integer() # 排序序号
class Index:
name = 'blog'
name = 'blog' # 索引名称
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article'
doc_type = 'Article' # 文档类型
class ArticleDocumentManager():
#ymq文章文档的管理类用于索引操作和数据同步
def __init__(self):
#ymq初始化时创建索引
self.create_index()
def create_index(self):
#ymq初始化文章索引
ArticleDocument.init()
def delete_index(self):
#ymq删除blog索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
#ymq将Django模型对象转换为Elasticsearch文档对象
return [
ArticleDocument(
meta={
'id': article.id},
meta={'id': article.id}, # 使用文章ID作为文档ID
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
'id': article.author.id
},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
'id': article.category.id
},
tags=[{'name': t.name, 'id': t.id} for t in article.tags.all()], # 转换多对多标签
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
article_order=article.article_order
) for article in articles
]
def rebuild(self, articles=None):
#ymq重建索引默认同步所有文章可指定文章列表
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
articles = articles if articles else Article.objects.all() # 获取文章数据
docs = self.convert_to_doc(articles) # 转换为文档对象
for doc in docs:
doc.save()
doc.save() # 保存到Elasticsearch
def update_docs(self, docs):
#ymq批量更新文档
for doc in docs:
doc.save()
doc.save()

@ -1,19 +1,36 @@
<<<<<<< HEAD
import logging #导入 Python 标准库的 logging 模块,用于日志记录,方便追踪程序运行过程中的关键信息。
=======
import logging
#ymq导入logging模块用于记录搜索相关日志
>>>>>>> c6856732b39cce6b1aab30e6649dcdb806b75b9f
from django import forms
#ymq导入Django的forms模块用于创建自定义表单
from haystack.forms import SearchForm
#ymq导入Haystack的SearchForm基类扩展实现博客搜索表单
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
class BlogSearchForm(SearchForm):
#ymq定义博客搜索表单类继承自Haystack的SearchForm
querydata = forms.CharField(required=True)
#ymq定义搜索关键词字段required=True表示该字段为必填项
def search(self):
#ymq重写父类的search方法自定义搜索逻辑
datas = super(BlogSearchForm, self).search()
#ymq调用父类search方法获取基础搜索结果
if not self.is_valid():
#ymq如果表单数据验证不通过返回无结果响应
return self.no_query_found()
if self.cleaned_data['querydata']:
#ymq如果存在合法的搜索关键词记录关键词日志
logger.info(self.cleaned_data['querydata'])
return datas
#ymq返回最终的搜索结果集

@ -1,18 +1,31 @@
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
#ymq从blog.documents导入Elasticsearch相关的文档类和管理器以及启用状态常量
# TODO 参数化
class Command(BaseCommand):
#ymq定义自定义管理命令类继承自BaseCommand
help = 'build search index'
#ymq定义命令的帮助信息使用python manage.py help build_index时显示
def handle(self, *args, **options):
#ymq命令的核心处理方法执行实际的索引构建逻辑
if ELASTICSEARCH_ENABLED:
#ymq仅当Elasticsearch启用时执行以下操作
ElaspedTimeDocumentManager.build_index()
#ymq调用性能耗时文档管理器构建索引若不存在
manager = ElapsedTimeDocument()
manager.init()
#ymq初始化ElapsedTimeDocument对应的索引结构
manager = ArticleDocumentManager()
manager.delete_index()
#ymq删除已存在的文章索引重建前清理
manager.rebuild()
#ymq重建文章索引将数据库中的文章数据同步到Elasticsearch

@ -1,13 +1,20 @@
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.models import Tag, Category
#ymq从blog应用导入Tag标签和Category分类模型
# TODO 参数化
class Command(BaseCommand):
#ymq定义自定义管理命令类继承自BaseCommand
help = 'build search words'
#ymq命令的帮助信息说明该命令用于生成搜索词
def handle(self, *args, **options):
#ymq命令的核心处理方法执行生成搜索词的逻辑
# 从标签和分类中提取名称使用set去重
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
print('\n'.join(datas))
# 按行打印所有去重后的名称(作为搜索词)
print('\n'.join(datas))

@ -1,11 +1,17 @@
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from djangoblog.utils import cache
#ymq从项目工具模块导入缓存工具
class Command(BaseCommand):
#ymq定义清除缓存的自定义命令类继承自BaseCommand
help = 'clear the whole cache'
#ymq命令的帮助信息说明该命令用于清除所有缓存
def handle(self, *args, **options):
cache.clear()
#ymq命令的核心处理方法执行清除缓存操作
cache.clear() # 调用缓存工具的clear方法清除所有缓存数据
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
#ymq向标准输出写入成功信息使用Django的SUCCESS样式通常为绿色

@ -1,40 +1,62 @@
from django.contrib.auth import get_user_model
#ymq导入获取用户模型的函数便于灵活引用用户模型
from django.contrib.auth.hashers import make_password
#ymq导入密码加密函数用于安全存储密码
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.models import Article, Tag, Category
#ymq从blog应用导入文章、标签、分类模型
class Command(BaseCommand):
#ymq定义创建测试数据的自定义命令类继承自BaseCommand
help = 'create test datas'
#ymq命令的帮助信息说明该命令用于创建测试数据
def handle(self, *args, **options):
#ymq命令的核心处理方法执行创建测试数据的逻辑
# 创建或获取测试用户(邮箱、用户名、密码加密存储)
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# 创建或获取父分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# 创建或获取子分类(关联父分类)
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save()
category.save() # 保存子分类
# 创建基础标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 批量创建20篇测试文章
for i in range(1, 20):
# 创建或获取文章(关联分类、作者)
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
title='nice title ' + str(i), # 文章标题带序号
body='nice content ' + str(i), # 文章内容带序号
author=user)[0]
# 创建带序号的标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# 给文章添加标签(包含基础标签和序号标签)
article.tags.add(tag)
article.tags.add(basetag)
article.save()
article.save() # 保存文章
# 清除缓存,确保测试数据立即生效
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))
# 输出成功信息
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -1,16 +1,24 @@
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from djangoblog.spider_notify import SpiderNotify
#ymq导入蜘蛛通知工具类用于向搜索引擎提交URL
from djangoblog.utils import get_current_site
#ymq导入获取当前站点信息的工具函数
from blog.models import Article, Tag, Category
#ymq从blog应用导入文章、标签、分类模型
site = get_current_site().domain
#ymq获取当前站点的域名用于构建完整URL
class Command(BaseCommand):
#ymq定义百度URL提交命令类继承自BaseCommand
help = 'notify baidu url'
#ymq命令的帮助信息说明该命令用于向百度提交URL
def add_arguments(self, parser):
#ymq定义命令参数指定提交的数据类型
parser.add_argument(
'data_type',
type=str,
@ -20,31 +28,46 @@ class Command(BaseCommand):
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
#ymq参数说明article-所有文章tag-所有标签category-所有分类all-全部
def get_full_url(self, path):
#ymq构建包含域名的完整URL
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
type = options['data_type']
self.stdout.write('start get %s' % type)
#ymq命令核心处理方法执行URL收集和提交
type = options['data_type'] # 获取用户指定的数据类型
self.stdout.write('start get %s' % type) # 输出开始收集信息的提示
urls = []
urls = [] # 存储待提交的URL列表
# 根据数据类型收集对应的URL
if type == 'article' or type == 'all':
# 收集已发布文章的URL
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
# 收集所有标签页的URL
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
# 收集所有分类页的URL
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
# 输出待提交的URL数量
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# 调用工具类向百度提交URL
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))
# 输出提交完成的提示
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,47 +1,70 @@
import requests
#ymq导入requests库用于发送HTTP请求测试图片URL有效性
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from django.templatetags.static import static
#ymq导入static标签用于获取静态文件URL
from djangoblog.utils import save_user_avatar
#ymq导入保存用户头像的工具函数
from oauth.models import OAuthUser
#ymq从oauth应用导入OAuthUser模型存储第三方用户信息
from oauth.oauthmanager import get_manager_by_type
#ymq导入获取对应第三方登录管理器的函数
class Command(BaseCommand):
#ymq定义同步用户头像的自定义命令类继承自BaseCommand
help = 'sync user avatar'
#ymq命令的帮助信息说明该命令用于同步用户头像
def test_picture(self, url):
#ymq测试图片URL是否有效状态码200
try:
if requests.get(url, timeout=2).status_code == 200:
return True
return True # URL有效返回True
except:
pass
pass # 异常或状态码非200返回None
def handle(self, *args, **options):
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
#ymq命令核心处理方法执行用户头像同步逻辑
static_url = static("../") # 获取静态文件基础URL
users = OAuthUser.objects.all() # 获取所有第三方用户
self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出待同步用户数量
for u in users:
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
#ymq遍历每个用户进行头像同步
self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户名
url = u.picture # 获取用户当前头像URL
if url:
# 处理已有头像URL的情况
if url.startswith(static_url):
# 头像URL是本地静态文件
if self.test_picture(url):
# 图片有效,跳过同步
continue
else:
# 图片无效,重新获取
if u.metadata:
# 有元数据,通过第三方管理器获取头像
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
url = save_user_avatar(url) # 保存头像并获取本地URL
else:
# 无元数据,使用默认头像
url = static('blog/img/avatar.png')
else:
# 头像URL是外部链接保存到本地
url = save_user_avatar(url)
else:
# 无头像URL使用默认头像
url = static('blog/img/avatar.png')
if url:
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
# 保存更新后的头像URL
self.stdout.write(f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
self.stdout.write('结束同步')
self.stdout.write('结束同步') # 输出同步完成提示

@ -1,42 +1,62 @@
import logging
import time
#ymq导入logging用于日志记录time用于计算页面加载时间
from ipware import get_client_ip
#ymq导入get_client_ip工具用于获取客户端IP地址
from user_agents import parse
#ymq导入parse函数用于解析用户代理字符串
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
#ymq从博客文档模块导入Elasticsearch启用状态和性能日志管理器
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
class OnlineMiddleware(object):
#ymq定义在线中间件类用于记录页面加载性能和访问信息
def __init__(self, get_response=None):
#ymq初始化中间件接收Django的响应处理器
self.get_response = get_response
super().__init__()
def __call__(self, request):
#ymq中间件核心方法处理请求并返回响应
''' page render time '''
start_time = time.time()
response = self.get_response(request)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request)
user_agent = parse(http_user_agent)
#ymq记录页面渲染时间的逻辑
start_time = time.time() # 记录请求处理开始时间
response = self.get_response(request) # 调用后续中间件或视图处理请求
#ymq获取用户代理和IP地址
http_user_agent = request.META.get('HTTP_USER_AGENT', '') # 获取用户代理字符串
ip, _ = get_client_ip(request) # 获取客户端IP地址
user_agent = parse(http_user_agent) # 解析用户代理信息(浏览器、设备等)
#ymq非流式响应才处理流式响应无法修改内容
if not response.streaming:
try:
cast_time = time.time() - start_time
cast_time = time.time() - start_time # 计算页面加载耗时(秒)
#ymq如果启用了Elasticsearch记录性能数据
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2)
url = request.path
time_taken = round((cast_time) * 1000, 2) #ymq: 转换为毫秒并保留两位小数
url = request.path # 获取请求的URL路径
from django.utils import timezone
#ymq调用管理器创建性能日志记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
log_datetime=timezone.now(), #ymq: 记录当前时间
useragent=user_agent, #ymq: 已解析的用户代理信息
ip=ip) #ymq: 客户端IP
#ymq替换响应内容中的<!!LOAD_TIMES!!>标记为实际加载时间保留前5位字符
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
#ymq捕获并记录处理过程中的异常
logger.error("Error OnlineMiddleware: %s" % e)
return response
return response #ymq: 返回处理后的响应

@ -1,25 +1,34 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
#ymq该迁移文件由Django 4.1.7自动生成生成时间为2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
#ymq导入Django迁移相关模块、时间工具和markdown编辑器字段
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
initial = True
#ymq标记为初始迁移第一次创建模型时生成
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
#ymq依赖于用户模型确保用户表先创建
]
operations = [
#ymq定义数据库操作列表按顺序执行创建模型的操作
migrations.CreateModel(
#ymq创建BlogSettings模型网站配置
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#ymq自增主键字段
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
@ -35,13 +44,17 @@ class Migration(migrations.Migration):
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
#ymq以上为网站配置的各个字段包含网站基本信息、显示设置、备案信息等
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
#ymq模型的显示名称
},
),
migrations.CreateModel(
#ymq创建Links模型友情链接
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -52,14 +65,18 @@ class Migration(migrations.Migration):
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#ymq友情链接字段包含名称、URL、排序、显示位置等
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
#ymq按排序号升序排列
},
),
migrations.CreateModel(
#ymq创建SideBar模型侧边栏
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -69,14 +86,18 @@ class Migration(migrations.Migration):
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#ymq侧边栏字段包含标题、内容、排序等
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
#ymq按排序号升序排列
},
),
migrations.CreateModel(
#ymq创建Tag模型标签
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
@ -84,14 +105,18 @@ class Migration(migrations.Migration):
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
#ymq标签字段包含名称、URL友好标识slug
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
#ymq按标签名升序排列
},
),
migrations.CreateModel(
#ymq创建Category模型分类
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
@ -101,14 +126,18 @@ class Migration(migrations.Migration):
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
#ymq分类字段支持多级分类自关联外键、权重排序等
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
#ymq按权重降序排列权重越大越靠前
},
),
migrations.CreateModel(
#ymq创建Article模型文章
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
@ -116,6 +145,7 @@ class Migration(migrations.Migration):
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
#ymq使用markdown编辑器字段存储文章正文
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
@ -124,14 +154,19 @@ class Migration(migrations.Migration):
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#ymq关联用户模型外键级联删除
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
#ymq关联分类模型外键级联删除
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
#ymq多对多关联标签模型
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
#ymq先按排序号降序再按发布时间降序
'get_latest_by': 'id',
#ymq按id获取最新记录
},
),
]
]

@ -1,23 +1,34 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
#ymq该迁移文件由Django 4.1.7自动生成生成时间为2023-03-29 06:08
from django.db import migrations, models
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
('blog', '0001_initial'),
#ymq依赖于blog应用的0001_initial迁移文件确保先执行初始迁移
]
operations = [
#ymq定义数据库操作列表添加新字段
migrations.AddField(
#ymq向BlogSettings模型添加global_footer字段
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
#ymq字段类型为文本字段允许为空默认值为空字符串verbose_name为"公共尾部"
),
migrations.AddField(
#ymq向BlogSettings模型添加global_header字段
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
#ymq字段类型为文本字段允许为空默认值为空字符串verbose_name为"公共头部"
),
]
]

@ -1,17 +1,25 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
#ymq该迁移文件由Django 4.2.1自动生成生成时间为2023-05-09 07:45
from django.db import migrations, models
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
#ymq依赖于blog应用的0002号迁移文件确保先执行该迁移
]
operations = [
#ymq定义数据库操作此处为添加字段
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
#ymq向BlogSettings模型添加comment_need_review字段
model_name='blogsettings', # 目标模型名称
name='comment_need_review', # 新字段名称
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
#ymq字段类型为布尔值默认值为False不需要审核后台显示名称为"评论是否需要审核"
),
]
]

@ -1,27 +1,39 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
#ymq该迁移文件由Django 4.2.1自动生成生成时间为2023-05-09 07:51
from django.db import migrations
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
#ymq依赖于blog应用的0003号迁移文件确保先执行该迁移
]
operations = [
#ymq定义数据库操作列表主要是重命名字段
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
#ymq重命名BlogSettings模型的analyticscode字段
model_name='blogsettings', # 目标模型名称
old_name='analyticscode', # 旧字段名
new_name='analytics_code', # 新字段名(改为下划线命名规范)
),
migrations.RenameField(
#ymq重命名BlogSettings模型的beiancode字段
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
new_name='beian_code', # 改为下划线命名规范
),
migrations.RenameField(
#ymq重命名BlogSettings模型的sitename字段
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
new_name='site_name', # 改为下划线命名规范
),
]
]

@ -1,20 +1,27 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
#ymq该迁移文件由Django 4.2.5自动生成生成时间为2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
#ymq导入Django迁移相关模块、时间工具和markdown编辑器字段
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
#ymq依赖于用户模型和blog应用的0004号迁移文件
]
operations = [
#ymq定义数据库操作列表包含模型选项修改、字段删除、添加和修改
# 修改模型的元数据选项主要是verbose_name的国际化调整
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
@ -35,6 +42,8 @@ class Migration(migrations.Migration):
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# 删除旧的时间字段(命名方式调整)
migrations.RemoveField(
model_name='article',
name='created_time',
@ -67,6 +76,8 @@ class Migration(migrations.Migration):
model_name='tag',
name='last_mod_time',
),
# 添加新的时间字段统一命名为creation_time和last_modify_time
migrations.AddField(
model_name='article',
name='creation_time',
@ -107,6 +118,8 @@ class Migration(migrations.Migration):
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Article模型的字段属性主要是verbose_name国际化
migrations.AlterField(
model_name='article',
name='article_order',
@ -167,6 +180,8 @@ class Migration(migrations.Migration):
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
# 修改BlogSettings模型的字段属性verbose_name国际化
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
@ -222,6 +237,8 @@ class Migration(migrations.Migration):
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
# 修改Category模型的字段属性
migrations.AlterField(
model_name='category',
name='index',
@ -237,6 +254,8 @@ class Migration(migrations.Migration):
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
# 修改Links模型的字段属性
migrations.AlterField(
model_name='links',
name='is_enable',
@ -267,6 +286,8 @@ class Migration(migrations.Migration):
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
# 修改SideBar模型的字段属性
migrations.AlterField(
model_name='sidebar',
name='content',
@ -292,9 +313,11 @@ class Migration(migrations.Migration):
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Tag模型的字段属性
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]
]

@ -1,17 +1,23 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
#ymq该迁移文件由Django 4.2.7自动生成生成时间为2024年1月26日02:41
from django.db import migrations
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
#ymq依赖于blog应用的0005号迁移文件确保先执行该迁移
]
operations = [
#ymq定义数据库操作此处为修改模型选项
migrations.AlterModelOptions(
name='blogsettings',
#ymq修改BlogSettings模型的显示名称改为英文"Website configuration"
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]
]

@ -1,6 +1,7 @@
import logging
import re
from abc import abstractmethod
#ymq导入logging用于日志记录re用于正则表达式操作abstractmethod用于定义抽象方法
from django.conf import settings
from django.core.exceptions import ValidationError
@ -8,36 +9,43 @@ from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField
from uuslug import slugify
from mdeditor.fields import MDTextField # 导入markdown编辑器字段
from uuslug import slugify # 导入slug生成工具
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
from djangoblog.utils import cache_decorator, cache # 导入缓存相关工具
from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
class LinkShowType(models.TextChoices):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
#ymq定义链接展示位置的枚举类
I = ('i', _('index')) # 首页展示
L = ('l', _('list')) # 列表页展示
P = ('p', _('post')) # 文章页展示
A = ('a', _('all')) # 所有页面展示
S = ('s', _('slide')) # 幻灯片展示
class BaseModel(models.Model):
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
#ymq定义模型基类封装公共字段和方法抽象类
id = models.AutoField(primary_key=True) # 自增主键
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs):
#ymq重写保存方法处理slug生成和特殊更新逻辑
# 判断是否是更新文章浏览量的操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# 单独处理浏览量更新,提高性能
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# 自动生成slug用于URL友好化
if 'slug' in self.__dict__:
# 根据title或name字段生成slug
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
@ -45,79 +53,88 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
def get_full_url(self):
#ymq生成包含域名的完整URL
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True
abstract = True # 声明为抽象模型,不生成数据库表
@abstractmethod
def get_absolute_url(self):
#ymq抽象方法子类必须实现用于生成模型实例的URL
pass
class Article(BaseModel):
"""文章"""
"""文章模型"""
# 状态选项:草稿/已发布
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
# 评论状态选项:开启/关闭
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
# 类型选项:文章/页面
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题
body = MDTextField(_('body')) # 文章内容使用markdown编辑器
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
_('publish time'), blank=False, null=False, default=now) # 发布时间
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
default='p') # 发布状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
default='o') # 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型
views = models.PositiveIntegerField(_('views'), default=0) # 浏览量
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
on_delete=models.CASCADE) # 关联作者(外键)
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
_('order'), blank=False, null=False, default=0) # 排序序号
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
null=False) # 关联分类(外键)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 关联标签(多对多)
def body_to_string(self):
#ymq返回文章内容字符串
return self.body
def __str__(self):
#ymq模型实例的字符串表示文章标题
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
ordering = ['-article_order', '-pub_time'] # 默认排序:先按排序号降序,再按发布时间降序
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
get_latest_by = 'id' # 按id获取最新记录
def get_absolute_url(self):
#ymq生成文章详情页的URL
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -125,21 +142,24 @@ class Article(BaseModel):
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
#ymq获取当前文章所属分类的层级结构含父级分类
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
#ymq重写保存方法可扩展自定义逻辑
super().save(*args, **kwargs)
def viewed(self):
#ymq增加浏览量并保存
self.views += 1
self.save(update_fields=['views'])
self.save(update_fields=['views']) # 只更新views字段提高性能
def comment_list(self):
#ymq获取文章的评论列表带缓存
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
@ -147,67 +167,64 @@ class Article(BaseModel):
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
cache.set(cache_key, comments, 60 * 100) # 缓存100分钟
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
#ymq生成文章在admin后台的编辑URL
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
# 下一篇
#ymq获取下一篇文章ID更大的已发布文章
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def prev_article(self):
# 前一篇
#ymq获取上一篇文章ID更小的已发布文章
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
"""从文章内容中提取第一张图片的URL"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # 匹配markdown图片语法
if match:
return match.group(1)
return ""
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
"""文章分类模型"""
name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index'))
on_delete=models.CASCADE) # 父分类(自关联,支持多级分类)
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好化标识
index = models.IntegerField(default=0, verbose_name=_('index')) # 排序索引
class Meta:
ordering = ['-index']
ordering = ['-index'] # 按索引降序排列
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
#ymq生成分类详情页的URL
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
#ymq模型实例的字符串表示分类名称
return self.name
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
"""递归获取当前分类的所有父级分类,形成层级结构"""
categorys = []
def parse(category):
@ -218,12 +235,9 @@ class Category(BaseModel):
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
"""获取当前分类的所有子分类(含多级子分类)"""
categorys = []
all_categorys = Category.objects.all()
@ -241,136 +255,143 @@ class Category(BaseModel):
class Tag(BaseModel):
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
"""文章标签模型"""
name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好化标识
def __str__(self):
#ymq模型实例的字符串表示标签名称
return self.name
def get_absolute_url(self):
#ymq生成标签详情页的URL
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_article_count(self):
#ymq获取该标签关联的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
ordering = ['name'] # 按名称排序
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model):
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
"""友情链接模型"""
name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称
link = models.URLField(_('link')) # 链接URL
sequence = models.IntegerField(_('order'), unique=True) # 排序序号
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
_('is show'), default=True, blank=False, null=False) # 是否显示
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
default=LinkShowType.I) # 展示位置
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
ordering = ['sequence']
ordering = ['sequence'] # 按排序序号排列
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
#ymq模型实例的字符串表示链接名称
return self.name
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(_('is enable'), default=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
"""侧边栏模型可展示自定义HTML内容"""
name = models.CharField(_('title'), max_length=100) # 侧边栏标题
content = models.TextField(_('content')) # 侧边栏内容HTML
sequence = models.IntegerField(_('order'), unique=True) # 排序序号
is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
ordering = ['sequence']
ordering = ['sequence'] # 按排序序号排列
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
#ymq模型实例的字符串表示侧边栏标题
return self.name
class BlogSettings(models.Model):
"""blog的配置"""
"""博客全局配置模型"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
default='') # 网站名称
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
default='') # 网站描述
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
_('site seo description'), max_length=1000, null=False, blank=False, default='') # SEO描述
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
article_comment_count = models.IntegerField(_('article comment count'), default=5)
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
default='') # 网站关键词
article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示谷歌广告
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='')
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
_('adsense code'), max_length=2000, null=True, blank=True, default='') # 谷歌广告代码
open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启网站评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部代码
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部代码
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
default='') # 网站备案号
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
default='') # 统计分析代码
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
'是否显示公安备案号', default=False, null=False) # 是否显示公安备案号
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
default='') # 公安备案号
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
'评论是否需要审核', default=False, null=False) # 评论是否需要审核
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
def __str__(self):
#ymq模型实例的字符串表示网站名称
return self.site_name
def clean(self):
#ymq数据验证确保全局配置只能有一条记录
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
#ymq保存配置后清除缓存确保配置立即生效
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
cache.clear()

@ -1,13 +1,20 @@
from haystack import indexes
#ymq导入Haystack的indexes模块用于定义搜索索引
from blog.models import Article
#ymq从blog应用导入Article模型为其创建搜索索引
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
#ymq定义文章搜索索引类继承自SearchIndex和Indexable
#ymq: document=True表示该字段是主要搜索字段use_template=True表示使用模板定义字段内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
#ymq指定该索引对应的模型
return Article
def index_queryset(self, using=None):
return self.get_model().objects.filter(status='p')
#ymq定义需要被索引的数据集
#ymq: 只索引状态为'p'(已发布)的文章
return self.get_model().objects.filter(status='p')

@ -23,15 +23,18 @@ from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__)
register = template.Library()
#ymq注册模板标签库用于在Django模板中使用自定义标签和过滤器
@register.simple_tag(takes_context=True)
def head_meta(context):
#ymq自定义简单标签用于生成页面头部元信息通过插件钩子处理
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
#ymq格式化时间仅时间部分使用settings中定义的TIME_FORMAT
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
@ -41,6 +44,7 @@ def timeformat(data):
@register.simple_tag
def datetimeformat(data):
#ymq格式化日期时间使用settings中定义的DATE_TIME_FORMAT
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
@ -51,11 +55,13 @@ def datetimeformat(data):
@register.filter()
@stringfilter
def custom_markdown(content):
#ymq将内容转换为Markdown格式并标记为安全HTML用于文章内容
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
def get_markdown_toc(content):
#ymq获取Markdown内容的目录TOC并标记为安全HTML
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@ -64,6 +70,7 @@ def get_markdown_toc(content):
@register.filter()
@stringfilter
def comment_markdown(content):
#ymq处理评论内容的Markdown转换并过滤不安全HTML标签
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@ -76,6 +83,7 @@ def truncatechars_content(content):
:param content:
:return:
"""
#ymq按网站设置的长度截断文章内容保留HTML标签
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
@ -85,8 +93,8 @@ def truncatechars_content(content):
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
#ymq截断内容为150字符并去除HTML标签用于生成纯文本摘要
from django.utils.html import strip_tags
return strip_tags(content)[:150]
@ -97,12 +105,13 @@ def load_breadcrumb(article):
:param article:
:return:
"""
#ymq生成文章面包屑导航数据包含分类层级和网站名称
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
site = get_current_site().domain
names.append((blogsetting.site_name, '/'))
names = names[::-1]
names = names[::-1] # 反转列表,使层级从网站到当前分类
return {
'names': names,
@ -118,6 +127,7 @@ def load_articletags(article):
:param article:
:return:
"""
#ymq获取文章关联的标签列表包含标签URL、文章数和随机样式
tags = article.tags.all()
tags_list = []
for tag in tags:
@ -137,6 +147,7 @@ def load_sidebar(user, linktype):
加载侧边栏
:return:
"""
#ymq加载侧边栏数据带缓存包含文章列表、分类、标签等
value = cache.get("sidebar" + linktype)
if value:
value['user'] = user
@ -145,6 +156,7 @@ def load_sidebar(user, linktype):
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
# 获取最近文章、分类、热门文章等数据
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = Category.objects.all()
@ -157,8 +169,8 @@ def load_sidebar(user, linktype):
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
# 处理标签云(按文章数计算字体大小)
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
@ -166,7 +178,6 @@ def load_sidebar(user, linktype):
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags)
@ -185,6 +196,7 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
# 缓存侧边栏数据3小时
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
@ -198,6 +210,7 @@ def load_article_metas(article, user):
:param article:
:return:
"""
#ymq加载文章元信息作者、发布时间等供模板使用
return {
'article': article,
'user': user
@ -206,9 +219,11 @@ def load_article_metas(article, user):
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
#ymq生成分页导航链接支持首页、标签、作者、分类等不同页面类型
previous_url = ''
next_url = ''
if page_type == '':
# 首页分页
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number})
@ -218,6 +233,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
'blog:index_page', kwargs={
'page': previous_number})
if page_type == '分类标签归档':
# 标签页分页
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
@ -234,6 +250,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
'page': previous_number,
'tag_name': tag.slug})
if page_type == '作者文章归档':
# 作者页分页
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
@ -250,6 +267,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
'author_name': tag_name})
if page_type == '分类目录归档':
# 分类页分页
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
@ -281,6 +299,7 @@ def load_article_detail(article, isindex, user):
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
#ymq加载文章详情数据区分列表页显示摘要和详情页显示全文
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
@ -292,35 +311,35 @@ def load_article_detail(article, isindex, user):
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""
"""获得gravatar头像URL"""
#ymq获取用户头像URL优先使用第三方登录头像否则使用Gravatar
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
else:
# 检查是否有第三方登录用户的头像
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
# 生成Gravatar头像URL
email = email.encode('utf-8')
default = static('blog/img/avatar.png')
default = static('blog/img/avatar.png') # 默认头像
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
cache.set(cachekey, url, 60 * 60 * 10)
cache.set(cachekey, url, 60 * 60 * 10) # 缓存头像URL 10小时
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
"""获得gravatar头像"""
"""获得gravatar头像img标签"""
#ymq生成头像img标签调用gravatar_url获取URL
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' %
@ -335,10 +354,12 @@ def query(qs, **kwargs):
...
{% endfor %}
"""
return qs.filter(**kwargs)
#ymq模板中过滤查询集的标签支持动态传参过滤
return qs.filter(** kwargs)
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
#ymq字符串拼接过滤器将两个参数转换为字符串并拼接
return str(arg1) + str(arg2)

@ -1,73 +1,101 @@
import os
#ymq导入os模块用于文件路径和文件操作
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
#ymq导入文件上传相关类用于模拟文件上传测试
from django.core.management import call_command
#ymq导入call_command用于调用Django管理命令
from django.core.paginator import Paginator
#ymq导入分页类用于测试分页功能
from django.templatetags.static import static
#ymq导入static标签用于获取静态文件路径
from django.test import Client, RequestFactory, TestCase
#ymq导入测试相关类Client用于模拟HTTP请求TestCase提供测试框架
from django.urls import reverse
#ymq导入reverse用于反向解析URL
from django.utils import timezone
#ymq导入timezone用于处理时间相关测试数据
from accounts.models import BlogUser
#ymq从accounts应用导入用户模型
from blog.forms import BlogSearchForm
#ymq从blog应用导入搜索表单
from blog.models import Article, Category, Tag, SideBar, Links
#ymq从blog应用导入模型类用于测试数据创建和查询
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
#ymq导入自定义模板标签函数用于测试模板标签功能
from djangoblog.utils import get_current_site, get_sha256
#ymq导入工具函数用于获取站点信息和加密
from oauth.models import OAuthUser, OAuthConfig
#ymq从oauth应用导入模型用于测试第三方登录相关功能
# Create your tests here.
class ArticleTest(TestCase):
#ymq定义文章相关的测试类继承自TestCase
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
#ymq测试前置方法在每个测试方法执行前运行初始化测试客户端和工厂
self.client = Client() # 创建测试客户端用于模拟HTTP请求
self.factory = RequestFactory() # 创建请求工厂,用于构造请求对象
def test_validate_article(self):
site = get_current_site().domain
#ymq测试文章相关功能的完整性包括创建、查询、页面访问等
site = get_current_site().domain # 获取当前站点域名
# 创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.save()
user.set_password("liangliangyy") # 设置用户密码
user.is_staff = True # 设为 staff允许访问admin
user.is_superuser = True # 设为超级用户
user.save() # 保存用户
# 测试用户个人页面访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 200) # 断言页面正常响应
# 测试admin相关页面访问未登录状态
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
# 创建侧边栏测试数据
s = SideBar()
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.save()
# 创建分类测试数据
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
# 创建标签测试数据
tag = Tag()
tag.name = "nicetag"
tag.save()
# 创建文章测试数据
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' # 类型为文章
article.status = 'p' # 状态为已发布
article.save()
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
# 测试文章标签关联
self.assertEqual(0, article.tags.count()) # 初始无标签
article.tags.add(tag) # 添加标签
article.save()
self.assertEqual(1, article.tags.count())
self.assertEqual(1, article.tags.count()) # 断言标签已添加
# 批量创建文章(用于测试分页)
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@ -79,56 +107,73 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
# 测试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)
call_command("build_index") # 调用命令构建索引
response = self.client.get('/search', {'q': 'nicetitle'}) # 模拟搜索请求
self.assertEqual(response.status_code, 200) # 断言搜索页面正常响应
# 测试文章详情页访问
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎
# 测试标签页访问
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试分类页访问
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试搜索功能
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# 测试文章模板标签
s = load_articletags(article)
self.assertIsNotNone(s)
self.assertIsNotNone(s) # 断言标签返回非空
# 用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
# 测试归档页面访问
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# 测试不同类型的分页功能
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
self.check_pagination(p, '', '') # 全部文章分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
self.check_pagination(p, '分类标签归档', tag.slug) # 标签文章分页
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
self.check_pagination(p, '作者文章归档', 'liangliangyy') # 作者文章分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
self.check_pagination(p, '分类目录归档', category.slug) # 分类文章分页
# 测试搜索表单
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify
f.search() # 调用搜索方法
# 测试百度蜘蛛通知
SpiderNotify.baidu_notify([article.get_full_url()])
# 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
u = gravatar_url('liangliangyy@gmail.com') # 获取头像URL
u = gravatar('liangliangyy@gmail.com') # 生成头像HTML
# 测试友情链接页面
link = Links(
sequence=1,
name="lylinux",
@ -136,57 +181,75 @@ class ArticleTest(TestCase):
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# 测试RSS订阅和站点地图
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# 测试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/')
def check_pagination(self, p, type, value):
#ymq测试分页功能的辅助方法
for page in range(1, p.num_pages + 1):
# 调用分页模板标签获取分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
self.assertIsNotNone(s) # 断言分页信息非空
# 测试上一页链接
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# 测试下一页链接
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
#ymq测试图片上传功能
import requests
# 下载测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png') # 保存路径
with open(imagepath, 'wb') as file:
file.write(rsp.content)
file.write(rsp.content) # 保存图片
# 测试未授权上传
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
self.assertEqual(rsp.status_code, 403) # 断言被拒绝
# 生成上传签名(模拟授权)
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# 模拟带签名的上传请求
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
self.assertEqual(rsp.status_code, 200) # 断言上传成功
os.remove(imagepath) # 清理测试文件
# 测试用户头像保存和邮件发送工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
send_email(['qq@qq.com'], 'testTitle', 'testContent') # 测试发送邮件
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
'https://www.python.org/static/img/python-logo.png') # 测试保存头像
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
#ymq测试错误页面404
rsp = self.client.get('/eee') # 访问不存在的URL
self.assertEqual(rsp.status_code, 404) # 断言返回404
def test_commands(self):
#ymq测试Django管理命令
# 创建测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -194,13 +257,15 @@ class ArticleTest(TestCase):
user.is_staff = True
user.is_superuser = True
user.save()
# 创建OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# 创建OAuth用户关联
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@ -211,7 +276,8 @@ class ArticleTest(TestCase):
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
# 创建另一个OAuth用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -221,12 +287,15 @@ class ArticleTest(TestCase):
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
# 测试Elasticsearch索引构建命令如果启用
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
# 测试其他管理命令
call_command("ping_baidu", "all") # 百度ping通知
call_command("create_testdata") # 创建测试数据
call_command("clear_cache") # 清理缓存
call_command("sync_user_avatar") # 同步用户头像
call_command("build_search_words") # 构建搜索词

@ -1,62 +1,92 @@
from django.urls import path
#ymq导入Django的path函数用于定义URL路由
from django.views.decorators.cache import cache_page
#ymq导入缓存装饰器用于对视图进行缓存
from . import views
#ymq从当前应用导入views模块引用视图函数/类
app_name = "blog"
#ymq定义应用命名空间避免URL名称冲突
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
#ymq首页URL映射到IndexView视图类名称为'index'
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
#ymq分页首页URL接收整数类型的page参数名称为'index_page'
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
#ymq文章详情页URL接收年、月、日、文章ID参数名称为'detailbyid'
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
#ymq分类详情页URL接收slug类型的分类名称参数名称为'category_detail'
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
#ymq分类分页详情页URL接收分类名称和页码参数名称为'category_detail_page'
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
#ymq作者详情页URL接收作者名称参数名称为'author_detail'
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
#ymq作者分页详情页URL接收作者名称和页码参数名称为'author_detail_page'
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
#ymq标签详情页URL接收slug类型的标签名称参数名称为'tag_detail'
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
#ymq标签分页详情页URL接收标签名称和页码参数名称为'tag_detail_page'
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
#ymq归档页面URL使用cache_page装饰器缓存1小时60*60秒名称为'archives'
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
#ymq友情链接页面URL映射到LinkListView视图类名称为'links'
path(
r'upload',
views.fileupload,
name='upload'),
#ymq文件上传URL映射到fileupload视图函数名称为'upload'
path(
r'clean',
views.clean_cache_view,
name='clean'),
]
#ymq清理缓存URL映射到clean_cache_view视图函数名称为'clean'
]

@ -1,6 +1,7 @@
import logging
import os
import uuid
#ymq导入日志、文件操作、UUID生成相关模块
from django.conf import settings
from django.core.paginator import Paginator
@ -14,17 +15,24 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
#ymq导入Django核心组件、视图类、HTTP响应类等
from blog.models import Article, Category, LinkShowType, Links, Tag
#ymq从blog应用导入模型类
from comments.forms import CommentForm
#ymq从comments应用导入评论表单
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
#ymq导入插件钩子相关模块用于扩展文章功能
from djangoblog.utils import cache, get_blog_setting, get_sha256
#ymq导入工具函数用于缓存、获取博客设置和加密
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
class ArticleListView(ListView):
#ymq文章列表基础视图类继承自Django的ListView
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
@ -33,15 +41,17 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
paginate_by = settings.PAGINATE_BY # 分页大小,从配置中获取
page_kwarg = 'page' # 页码参数名
link_type = LinkShowType.L # 链接展示类型
def get_view_cache_key(self):
#ymq获取视图缓存键未实际使用预留方法
return self.request.get['pages']
@property
def page_number(self):
#ymq获取当前页码从URL参数或默认值
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
@ -51,13 +61,13 @@ class ArticleListView(ListView):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
raise NotImplementedError() # 强制子类实现该方法
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
raise NotImplementedError() # 强制子类实现该方法
def get_queryset_from_cache(self, cache_key):
'''
@ -70,8 +80,8 @@ class ArticleListView(ListView):
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
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
@ -80,46 +90,53 @@ class ArticleListView(ListView):
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
key = self.get_queryset_cache_key() # 获取缓存键
value = self.get_queryset_from_cache(key) # 从缓存获取数据
return value
def get_context_data(self, **kwargs):
#ymq扩展上下文数据添加链接类型
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
return super(ArticleListView, self).get_context_data(** kwargs)
class IndexView(ArticleListView):
'''
首页
首页视图
'''
# 友情链接类型
# 友情链接类型:首页展示
link_type = LinkShowType.I
def get_queryset_data(self):
#ymq获取首页文章列表已发布的文章
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
#ymq生成首页缓存键包含页码
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
文章详情页面视图
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
template_name = 'blog/article_detail.html' # 详情页模板
model = Article # 关联模型
pk_url_kwarg = 'article_id' # URL中主键参数名
context_object_name = "article" # 模板中上下文变量名
def get_context_data(self, **kwargs):
comment_form = CommentForm()
#ymq扩展文章详情页的上下文数据
comment_form = CommentForm() # 初始化评论表单
# 获取文章评论列表
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
parent_comments = article_comments.filter(parent_comment=None) # 过滤顶级评论
blog_setting = get_blog_setting() # 获取博客设置
# 评论分页处理
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
@ -135,26 +152,32 @@ class ArticleDetailView(DetailView):
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
# 生成评论分页链接
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 向上下文添加数据
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
# 上一篇/下一篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
# 触发插件钩子:文章详情已获取
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
# 应用插件过滤器:修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
@ -163,23 +186,27 @@ class ArticleDetailView(DetailView):
class CategoryDetailView(ArticleListView):
'''
分类目录列表
分类目录列表视图
'''
page_type = "分类目录归档"
page_type = "分类目录归档" # 页面类型标识
def get_queryset_data(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
#ymq获取指定分类下的文章列表
slug = self.kwargs['category_name'] # 从URL获取分类别名
category = get_object_or_404(Category, slug=slug) # 获取分类对象
categoryname = category.name
self.categoryname = categoryname
# 获取所有子分类名称
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# 查询属于当前分类及子分类的已发布文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
#ymq生成分类列表缓存键
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
@ -189,59 +216,65 @@ class CategoryDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
#ymq扩展分类页上下文数据
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
categoryname = categoryname.split('/')[-1] # 处理多级分类名称
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
return super(CategoryDetailView, self).get_context_data(** kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
作者详情页视图
'''
page_type = '作者文章归档'
page_type = '作者文章归档' # 页面类型标识
def get_queryset_cache_key(self):
#ymq生成作者文章列表缓存键
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
author_name = slugify(self.kwargs['author_name']) # 作者名转slug
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
#ymq获取指定作者的文章列表
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
author__username=author_name, type='a', status='p') # 过滤已发布的文章
return article_list
def get_context_data(self, **kwargs):
#ymq扩展作者页上下文数据
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
return super(AuthorDetailView, self).get_context_data(** kwargs)
class TagDetailView(ArticleListView):
'''
标签列表页面
标签列表页面视图
'''
page_type = '分类标签归档'
page_type = '分类标签归档' # 页面类型标识
def get_queryset_data(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
#ymq获取指定标签的文章列表
slug = self.kwargs['tag_name'] # 从URL获取标签别名
tag = get_object_or_404(Tag, slug=slug) # 获取标签对象
tag_name = tag.name
self.name = tag_name
# 查询包含当前标签的已发布文章
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
def get_queryset_cache_key(self):
#ymq生成标签文章列表缓存键
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@ -251,101 +284,118 @@ class TagDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
#ymq扩展标签页上下文数据
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
return super(TagDetailView, self).get_context_data(** kwargs)
class ArchivesView(ArticleListView):
'''
文章归档页面
文章归档页面视图
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
page_type = '文章归档' # 页面类型标识
paginate_by = None # 不分页
page_kwarg = None # 无页码参数
template_name = 'blog/article_archives.html' # 归档页模板
def get_queryset_data(self):
#ymq获取所有已发布文章用于归档
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
#ymq生成归档页缓存键
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
#ymq友情链接列表视图
model = Links # 关联模型
template_name = 'blog/links_list.html' # 链接列表模板
def get_queryset(self):
#ymq只获取启用的友情链接
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
#ymqElasticsearch搜索视图继承自Haystack的SearchView
def get_context(self):
paginator, page = self.build_page()
#ymq构建搜索结果页面的上下文数据
paginator, page = self.build_page() # 处理分页
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
"query": self.query, # 搜索关键词
"form": self.form, # 搜索表单
"page": page, # 当前页数据
"paginator": paginator, # 分页器
"suggestion": None, # 搜索建议(默认无)
}
# 如果启用拼写建议,添加建议内容
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
context.update(self.extra_context()) # 添加额外上下文
return context
@csrf_exempt
@csrf_exempt # 禁用CSRF保护用于外部调用
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
图片/文件上传接口需验证签名
:param request:
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
sign = request.GET.get('sign', None) # 获取签名参数
if not sign:
return HttpResponseForbidden()
return HttpResponseForbidden() # 无签名则拒绝
# 验证签名双重SHA256加密对比
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
return HttpResponseForbidden() # 签名错误则拒绝
response = [] # 存储上传后的文件URL
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
timestr = timezone.now().strftime('%Y/%m/%d') # 按日期组织文件
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名
fname = u''.join(str(filename))
# 判断是否为图片
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)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
os.makedirs(base_dir) # 目录不存在则创建
# 生成唯一文件名UUID+原扩展名)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全校验:防止路径遍历攻击
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 图片压缩处理
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
image.save(savepath, quality=20, optimize=True) # 压缩质量为20
# 生成文件访问URL
url = static(savepath)
response.append(url)
return HttpResponse(response)
return HttpResponse(response) # 返回所有上传文件的URL
else:
return HttpResponse("only for post")
return HttpResponse("only for post") # 只允许POST方法
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
#ymq404错误处理视图
if exception:
logger.error(exception)
logger.error(exception) # 记录错误日志
url = request.get_full_path()
return render(request,
template_name,
@ -355,6 +405,7 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'):
#ymq500错误处理视图
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -366,8 +417,9 @@ def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
#ymq403错误处理视图
if exception:
logger.error(exception)
logger.error(exception) # 记录错误日志
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
@ -375,5 +427,6 @@ def permission_denied_view(
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
#ymq清理缓存的视图
cache.clear() # 清除所有缓存
return HttpResponse('ok') # 返回成功响应
Loading…
Cancel
Save