Compare commits

..

59 Commits

Author SHA1 Message Date
pu685px73 b84a4636b8 ADD file via upload
4 months ago
pu685px73 cc72205f7b ADD file via upload
4 months ago
pu685px73 59198b6ef4 ADD file via upload
4 months ago
pu685px73 14ae1ee140 ADD file via upload
4 months ago
pu685px73 aa3da56be8 ADD file via upload
4 months ago
pu685px73 94f9530594 ADD file via upload
4 months ago
pu685px73 302503775c ADD file via upload
4 months ago
pu685px73 ed4ec24e39 ADD file via upload
4 months ago
pu685px73 ad6a8da984 ADD file via upload
4 months ago
pu685px73 dd7abc979c ADD file via upload
4 months ago
pu685px73 ddd8b6ca42 ADD file via upload
4 months ago
pu685px73 1306cc26df ADD file via upload
4 months ago
pu685px73 7f58515a0d ADD file via upload
4 months ago
pu685px73 9c1b640398 ADD file via upload
4 months ago
pu685px73 1313ac2702 ADD file via upload
4 months ago
pu685px73 ef41edfb9f ADD file via upload
4 months ago
pu685px73 f707a5cfb2 ADD file via upload
4 months ago
pu685px73 bbe481bd73 ADD file via upload
4 months ago
pu685px73 19e3e20724 ADD file via upload
4 months ago
pu685px73 e3d1462ab5 ADD file via upload
4 months ago
pu685px73 ae7972a233 ADD file via upload
4 months ago
pu685px73 343b352824 ADD file via upload
4 months ago
pu685px73 0a71263a98 ADD file via upload
4 months ago
pu685px73 892e93c728 ADD file via upload
4 months ago
pu685px73 8788617476 ADD file via upload
4 months ago
pu685px73 63c669b1e8 ADD file via upload
4 months ago
pu685px73 ca64e1e03e ADD file via upload
4 months ago
pu685px73 e7fe4f236a ADD file via upload
4 months ago
pu685px73 c1da8d414f ADD file via upload
4 months ago
pu685px73 ff4ae7672f Delete 'src/comments/__pycache__/ admin.cpython-312.pyc'
4 months ago
pu685px73 5d794c150b ADD file via upload
4 months ago
pu685px73 b8e3bc6fc0 Delete 'src/comments/__pycache__/ __init__.cpython-312.pyc'
4 months ago
pu685px73 1dc56841bc ADD file via upload
4 months ago
pu685px73 f05deb4f4b ADD file via upload
4 months ago
pu685px73 cbb33c4ddc ADD file via upload
4 months ago
pu685px73 778adb3c98 Delete 'comment'
4 months ago
pu685px73 f633cd6dc7 ADD file via upload
4 months ago
pu685px73 3c05c696ff Delete 'src/comments'
4 months ago
pu685px73 a87f385f6e 1
4 months ago
pu685px73 0a380a6067 Delete 'src/__pycache__'
4 months ago
pu685px73 775e437dd3 Add __pycache__
4 months ago
pu685px73 2853092d52 Delete 'src/__pycache__'
4 months ago
pu685px73 4f326fcbcc ADD file via upload
4 months ago
pu685px73 bd325ddb00 Delete 's'
4 months ago
pu685px73 12316252a7 ADD file via upload
4 months ago
pu685px73 4856f61de3 Delete 'src/1'
4 months ago
pu685px73 37c3621f54 Delete 'src/bb'
4 months ago
pu685px73 f91f89b055 Add bb
4 months ago
pu685px73 c03d77d5ce Delete 'ddd'
4 months ago
pu685px73 7dad061f41 Delete 'bb'
4 months ago
pu685px73 dd131d8217 ADD file via upload
4 months ago
pu685px73 4c20cda373 ADD file via upload
4 months ago
pu685px73 f52ab27273 Add 1
4 months ago
pu685px73 c512dc8c63 ADD file via upload
4 months ago
pu685px73 54a46fb553 ADD file via upload
4 months ago
pu685px73 6257f61e35 Merge pull request '2' (#6) from zjy_branch into develop
5 months ago
pjf284zq7 bed7097354 Merge pull request '1' (#5) from lm_branch into develop
5 months ago
pu685px73 547d184482 Update README.md
5 months ago
pu685px73 971c23ee3b Update README.md
5 months ago

@ -1,2 +1,2 @@
# home
# homework

@ -0,0 +1 @@
基于Python的Hello World

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,262 +0,0 @@
#!/usr/bin/env python
#yyb encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
#yyb 获取日志记录器
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""#yyb 获取最新的文章ID和评论ID"""
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""#yyb 计算字符串的SHA256哈希值"""
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
"""#yyb 缓存装饰器,用于缓存函数结果
Args:
expiration: 缓存过期时间默认3分钟
"""
def wrapper(func):
def news(*args, **kwargs):
try:
#yyb 尝试从视图获取缓存键
view = args[0]
key = view.get_cache_key()
except:
key = None
if not key:
#yyb 如果没有缓存键,根据函数和参数生成唯一键
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
if value is not None:
#yyb logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
if value is None:
#yyb 如果函数返回None设置默认缓存值
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
#yyb 刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
#yyb 创建模拟请求对象
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
#yyb 获取缓存键并删除缓存
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
"""#yyb 获取当前站点(带缓存)"""
site = Site.objects.get_current()
return site
class CommonMarkdown:
"""#yyb Markdown处理工具类"""
@staticmethod
def _convert_markdown(value):
"""#yyb 内部方法转换Markdown文本为HTML"""
md = markdown.Markdown(
extensions=[
'extra', #yyb 额外扩展
'codehilite', #yyb 代码高亮
'toc', #yyb 目录生成
'tables', #yyb 表格支持
]
)
body = md.convert(value) #yyb 转换Markdown为HTML
toc = md.toc #yyb 获取目录
return body, toc
@staticmethod
def get_markdown_with_toc(value):
"""#yyb 获取带目录的Markdown转换结果"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
"""#yyb 获取Markdown转换结果不带目录"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""#yyb 发送邮件(通过信号机制)"""
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""#yyb 生成6位随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
"""#yyb 将字典转换为URL参数字符串"""
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
"""#yyb 获取博客设置(带缓存)"""
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
#yyb 如果数据库中没有博客设置,创建默认设置
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value) #yyb 设置缓存
return value
def save_user_avatar(url):
'''
#yyb 保存用户头像到本地
:param url:头像url
:return: 本地路径
'''
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2) #yyb 下载头像
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir) #yyb 创建头像目录
#yyb 检查文件是否为图片格式
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext #yyb 生成唯一文件名
logger.info('保存用户头像:' + basedir + save_filename)
#yyb 保存头像文件
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename) #yyb 返回静态文件URL
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png') #yyb 返回默认头像
def delete_sidebar_cache():
"""#yyb 删除侧边栏相关缓存"""
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
def delete_view_cache(prefix, keys):
"""#yyb 删除视图缓存"""
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
"""#yyb 获取资源URL静态文件URL"""
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
#yyb HTML清理允许的标签
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
#yyb HTML清理允许的属性
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
"""#yyb 清理HTML移除不安全的标签和属性"""
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -1,3 +0,0 @@
#yyb 设置Django应用的默认配置类
#yyb 当Django启动时会自动使用这里指定的应用配置类
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -1,82 +0,0 @@
#yyb 导入Django管理站点相关模块
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
#yyb 导入各个应用的admin模块和模型
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
"""#yyb 自定义DjangoBlog管理站点"""
#yyb 管理站点头部标题
site_header = 'djangoblog administration'
#yyb 管理站点页面标题
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
"""#yyb 初始化管理站点"""
super().__init__(name)
def has_permission(self, request):
"""#yyb 检查用户权限:只允许超级用户访问"""
return request.user.is_superuser
#yyb 以下是注释掉的URL配置示例可用于添加自定义管理页面
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
# from blog.views import refresh_memcache
#
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# return urls + my_urls
#yyb 创建DjangoBlog管理站点实例
admin_site = DjangoBlogAdminSite(name='admin')
#yyb 注册博客相关的模型和管理类
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
#yyb 注册服务器管理相关的模型
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
#yyb 注册用户账户模型
admin_site.register(BlogUser, BlogUserAdmin)
#yyb 注册评论模型
admin_site.register(Comment, CommentAdmin)
#yyb 注册OAuth认证相关模型
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
#yyb 注册OwnTracks位置跟踪模型
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
#yyb 注册Django站点模型
admin_site.register(Site, SiteAdmin)
#yyb 注册Django日志条目模型
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,21 +0,0 @@
#yyb 导入Django应用配置基类
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
"""#yyb Djangoblog应用的配置类"""
#yyb 设置默认的自动主键字段类型为BigAutoField64位自增整数
default_auto_field = 'django.db.models.BigAutoField'
#yyb 指定应用名称
name = 'djangoblog'
def ready(self):
"""#yyb 应用准备就绪时执行的方法"""
#yyb 调用父类的ready方法
super().ready()
#yyb 导入并加载插件
#yyb 这里在应用启动时自动加载所有插件
from .plugin_manage.loader import load_plugins
load_plugins()

@ -1,163 +0,0 @@
#yyb 导入必要的Python和Django模块
import _thread
import logging
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
#yyb 导入自定义模型和工具函数
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
#yyb 获取logger实例
logger = logging.getLogger(__name__)
#yyb 定义自定义信号
#yyb OAuth用户登录信号传递用户id
oauth_user_login_signal = django.dispatch.Signal(['id'])
#yyb 发送邮件信号,传递收件人、标题和内容
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
"""#yyb 发送邮件信号处理器"""
#yyb 从信号参数中获取邮件相关信息
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
#yyb 创建邮件消息对象
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html" #yyb 设置邮件内容类型为HTML
#yyb 记录邮件发送日志
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
try:
#yyb 尝试发送邮件
result = msg.send()
log.send_result = result > 0 #yyb 发送成功结果为True
except Exception as e:
#yyb 记录发送失败日志
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save() #yyb 保存邮件发送日志
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""#yyb OAuth用户登录信号处理器"""
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
#yyb 如果用户头像不在当前站点域名下,则保存头像到本地
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
#yyb 删除侧边栏缓存
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
"""#yyb 模型保存后的通用回调函数"""
clearcache = False
#yyb 如果是LogEntryDjango管理员日志直接返回
if isinstance(instance, LogEntry):
return
#yyb 检查实例是否有get_full_url方法通常是有URL的模型
if 'get_full_url' in dir(instance):
#yyb 判断是否只是更新浏览量
is_update_views = update_fields == {'views'}
#yyb 如果不是测试环境且不是更新浏览量,则通知搜索引擎
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url]) #yyb 通知百度搜索引擎
except Exception as ex:
logger.error("notify sipder", ex)
#yyb 如果不是更新浏览量,设置清除缓存标志
if not is_update_views:
clearcache = True
#yyb 如果是评论模型
if isinstance(instance, Comment):
#yyb 如果评论是启用的
if instance.is_enable:
path = instance.article.get_absolute_url()
site = get_current_site().domain
#yyb 处理站点域名(移除端口号)
if site.find(':') > 0:
site = site[0:site.find(':')]
#yyb 使文章详情页缓存过期
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
#yyb 删除SEO处理器缓存
if cache.get('seo_processor'):
cache.delete('seo_processor')
#yyb 删除文章评论缓存
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
#yyb 删除侧边栏缓存
delete_sidebar_cache()
#yyb 删除文章评论视图缓存
delete_view_cache('article_comments', [str(instance.article.pk)])
#yyb 在新线程中发送评论通知邮件
_thread.start_new_thread(send_comment_email, (instance,))
#yyb 如果需要清除缓存
if clearcache:
cache.clear() #yyb 清除所有缓存
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
"""#yyb 用户登录/登出信号处理器"""
if user and user.username:
logger.info(user) #yyb 记录用户信息
delete_sidebar_cache() #yyb 删除侧边栏缓存
# cache.clear() #yyb 注释掉的清除所有缓存代码

@ -1,217 +0,0 @@
#yyb 导入必要的模块和类
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
#yyb 导入自定义的Elasticsearch文档和模型
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
#yyb 获取日志记录器
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
"""#yyb Elasticsearch搜索后端实现"""
def __init__(self, connection_alias, **connection_options):
"""#yyb 初始化Elasticsearch后端"""
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager() #yyb 文章文档管理器
self.include_spelling = True #yyb 是否包含拼写建议
def _get_models(self, iterable):
"""#yyb 获取模型实例并转换为文档"""
#yyb 如果有提供模型列表则使用,否则获取所有文章
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models) #yyb 将模型转换为Elasticsearch文档
return docs
def _create(self, models):
"""#yyb 创建索引并重建文档"""
self.manager.create_index() #yyb 创建Elasticsearch索引
docs = self._get_models(models)
self.manager.rebuild(docs) #yyb 重建所有文档
def _delete(self, models):
"""#yyb 删除文档"""
for m in models:
m.delete()
return True
def _rebuild(self, models):
"""#yyb 重建索引文档"""
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs) #yyb 更新文档
def update(self, index, iterable, commit=True):
"""#yyb 更新文档"""
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
"""#yyb 移除指定对象"""
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
"""#yyb 清空索引"""
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""#yyb 获取搜索建议词,如果没有找到建议词则返回原搜索词"""
#yyb 构建搜索建议查询
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
#yyb 处理建议结果
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"]) #yyb 使用建议词
else:
keywords.append(suggest["text"]) #yyb 使用原词
return ' '.join(keywords)
@log_query #yyb 记录查询日志的装饰器
def search(self, query_string, **kwargs):
"""#yyb 执行搜索查询"""
logger.info('search query_string:' + query_string)
#yyb 获取分页参数
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
#yyb 推荐词搜索:如果启用建议,则获取建议词
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
#yyb 构建搜索查询在标题和正文中匹配设置最小匹配度70%
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
#yyb 执行搜索过滤已发布的状态为p且类型为a的文章
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset] #yyb 不返回源文档内容,只返回元数据
results = search.execute()
hits = results['hits'].total #yyb 总命中数
raw_results = []
#yyb 处理搜索结果
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
#yyb 创建搜索结果对象
result = result_class(
app_label,
model_name,
raw_result['_id'], #yyb 文档ID
raw_result['_score'], #yyb 相关性分数
**additional_fields)
raw_results.append(result)
facets = {}
#yyb 如果查询词与建议词不同,则设置拼写建议
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results, #yyb 搜索结果列表
'hits': hits, #yyb 总命中数
'facets': facets, #yyb 分面搜索数据
'spelling_suggestion': spelling_suggestion, #yyb 拼写建议
}
class ElasticSearchQuery(BaseSearchQuery):
"""#yyb Elasticsearch查询构建器"""
def _convert_datetime(self, date):
"""#yyb 转换日期时间格式"""
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S')) #yyb 包含时间的格式
else:
return force_str(date.strftime('%Y%m%d000000')) #yyb 只包含日期的格式
def clean(self, query_fragment):
"""
#yyb 清理用户输入的查询片段,转义保留字符
#yyb Whoosh 1.X与此不同不再使用反斜杠转义保留字符
#yyb 而是应该引用整个单词。
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
#yyb 处理保留字
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
#yyb 处理保留字符
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word #yyb 用引号包围包含保留字符的单词
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
"""#yyb 构建查询片段"""
return value.query_string
def get_count(self):
"""#yyb 获取搜索结果数量"""
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
"""#yyb 获取拼写建议"""
return self._spelling_suggestion
def build_params(self, spelling_query=None):
"""#yyb 构建查询参数"""
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
"""#yyb Elasticsearch模型搜索表单"""
def search(self):
"""#yyb 执行搜索,根据参数决定是否使用建议搜索"""
#yyb 是否建议搜索从请求数据中获取is_suggest参数
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search() #yyb 调用父类搜索方法
return sqs
class ElasticSearchEngine(BaseEngine):
"""#yyb Elasticsearch搜索引擎"""
backend = ElasticSearchBackend #yyb 指定后端类
query = ElasticSearchQuery #yyb 指定查询类

@ -1,59 +0,0 @@
#yyb 导入Django RSS订阅相关模块
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
#yyb 导入自定义模型和工具
from blog.models import Article
from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
"""#yyb DjangoBlog的RSS订阅源类"""
#yyb 指定Feed类型为RSS 2.0
feed_type = Rss201rev2Feed
#yyb Feed的描述信息
description = '大巧无工,重剑无锋.'
#yyb Feed的标题
title = "且听风吟 大巧无工,重剑无锋. "
#yyb Feed的链接地址
link = "/feed/"
def author_name(self):
"""#yyb 获取作者名称 - 返回第一个用户的昵称"""
return get_user_model().objects.first().nickname
def author_link(self):
"""#yyb 获取作者链接 - 返回第一个用户的绝对URL"""
return get_user_model().objects.first().get_absolute_url()
def items(self):
"""#yyb 获取要在Feed中显示的项目列表"""
#yyb 返回最近发布的5篇文章过滤条件类型为'article'且状态为'published'
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""#yyb 获取单个项目的标题"""
return item.title
def item_description(self, item):
"""#yyb 获取单个项目的描述 - 将Markdown内容转换为HTML"""
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
"""#yyb 获取Feed的版权信息"""
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""#yyb 获取单个项目的链接"""
return item.get_absolute_url()
def item_guid(self, item):
"""#yyb 获取单个项目的全局唯一标识符(当前未实现)"""
#yyb 注意:这个方法目前没有返回值,可能需要根据需求实现
#yyb 通常应该返回一个唯一标识项目的字符串如文章的ID或永久链接
pass

@ -1,114 +0,0 @@
#yyb 导入Django管理相关模块
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
"""#yyb Django管理员日志条目的自定义管理界面"""
#yyb 列表页过滤器配置:按内容类型过滤
list_filter = [
'content_type'
]
#yyb 搜索字段配置:可按对象表示和变更消息搜索
search_fields = [
'object_repr',
'change_message'
]
#yyb 列表页中可点击的链接字段
list_display_links = [
'action_time',
'get_change_message',
]
#yyb 列表页显示的字段
list_display = [
'action_time', #yyb 操作时间
'user_link', #yyb 用户链接(自定义)
'content_type', #yyb 内容类型
'object_link', #yyb 对象链接(自定义)
'get_change_message', #yyb 变更消息
]
def has_add_permission(self, request):
"""#yyb 禁止添加新的日志条目"""
return False
def has_change_permission(self, request, obj=None):
"""#yyb 修改权限控制只允许超级用户或具有特定权限的用户查看不允许POST修改"""
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
"""#yyb 禁止删除日志条目"""
return False
def object_link(self, obj):
"""#yyb 生成对象链接的显示"""
object_link = escape(obj.object_repr) #yyb 转义对象表示字符串
content_type = obj.content_type
#yyb 如果不是删除操作且内容类型存在,尝试生成可点击的链接
if obj.action_flag != DELETION and content_type is not None:
try:
#yyb 构建管理员修改页面的URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
#yyb 创建HTML链接
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
#yyb 如果无法生成URL保持原样
pass
return mark_safe(object_link) #yyb 标记为安全HTML
#yyb 设置对象链接字段的排序和显示名称
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def user_link(self, obj):
"""#yyb 生成用户链接的显示"""
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user)) #yyb 转义用户字符串
try:
#yyb 构建用户修改页面的URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
#yyb 创建HTML链接
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
#yyb 如果无法生成URL保持原样
pass
return mark_safe(user_link) #yyb 标记为安全HTML
#yyb 设置用户链接字段的排序和显示名称
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
def get_queryset(self, request):
"""#yyb 获取查询集预取content_type关系以提高性能"""
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
def get_actions(self, request):
"""#yyb 获取可用的批量操作,移除删除选中操作"""
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected'] #yyb 移除批量删除选项
return actions

@ -1,345 +0,0 @@
"""
#yyb Django settings for djangoblog project.
#yyb Generated by 'django-admin startproject' using Django 1.10.2.
#yyb For more information on this file, see
#yyb https://docs.djangoproject.com/en/1.10/topics/settings/
#yyb For the full list of settings and their values, see
#yyb https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
"""#yyb 将环境变量转换为布尔值"""
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
#yyb 构建项目内部路径BASE_DIR / 'subdir'
BASE_DIR = Path(__file__).resolve().parent.parent
#yyb 快速开发配置 - 不适用于生产环境
#yyb 参见 https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
#yyb 安全警告:在生产环境中保持密钥保密!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
#yyb 安全警告:在生产环境中不要开启调试模式!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
#yyb DEBUG = False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' #yyb 检测是否在测试模式
#yyb 允许的主机名
#yyb ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
#yyb Django 4.0新增配置受信任的CSRF来源
CSRF_TRUSTED_ORIGINS = ['http://example.com']
#yyb 应用定义
INSTALLED_APPS = [
#yyb 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig', #yyb 使用简化的管理员配置
'django.contrib.auth', #yyb 认证系统
'django.contrib.contenttypes', #yyb 内容类型框架
'django.contrib.sessions', #yyb 会话框架
'django.contrib.messages', #yyb 消息框架
'django.contrib.staticfiles', #yyb 静态文件管理
'django.contrib.sites', #yyb 站点框架
'django.contrib.sitemaps', #yyb 站点地图
'mdeditor', #yyb Markdown编辑器
'haystack', #yyb 搜索框架
'blog', #yyb 博客应用
'accounts', #yyb 账户应用
'comments', #yyb 评论应用
'oauth', #yyb OAuth认证
'servermanager', #yyb 服务器管理
'owntracks', #yyb 位置跟踪
'compressor', #yyb 静态文件压缩
'djangoblog' #yyb 主应用
]
#yyb 中间件配置
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', #yyb 安全中间件
'django.contrib.sessions.middleware.SessionMiddleware', #yyb 会话中间件
'django.middleware.locale.LocaleMiddleware', #yyb 国际化中间件
'django.middleware.gzip.GZipMiddleware', #yyb Gzip压缩
#yyb 'django.middleware.cache.UpdateCacheMiddleware', #yyb 缓存更新(注释)
'django.middleware.common.CommonMiddleware', #yyb 通用中间件
#yyb 'django.middleware.cache.FetchFromCacheMiddleware', #yyb 缓存获取(注释)
'django.middleware.csrf.CsrfViewMiddleware', #yyb CSRF保护
'django.contrib.auth.middleware.AuthenticationMiddleware', #yyb 认证中间件
'django.contrib.messages.middleware.MessageMiddleware', #yyb 消息中间件
'django.middleware.clickjacking.XFrameOptionsMiddleware', #yyb 点击劫持保护
'django.middleware.http.ConditionalGetMiddleware', #yyb 条件GET请求
'blog.middleware.OnlineMiddleware' #yyb 自定义在线用户中间件
]
#yyb 根URL配置
ROOT_URLCONF = 'djangoblog.urls'
#yyb 模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates', #yyb Django模板引擎
'DIRS': [os.path.join(BASE_DIR, 'templates')], #yyb 模板目录
'APP_DIRS': True, #yyb 启用应用模板目录
'OPTIONS': {
'context_processors': [ #yyb 上下文处理器
'django.template.context_processors.debug', #yyb 调试信息
'django.template.context_processors.request', #yyb 请求对象
'django.contrib.auth.context_processors.auth', #yyb 认证信息
'django.contrib.messages.context_processors.messages', #yyb 消息框架
'blog.context_processors.seo_processor' #yyb 自定义SEO处理器
],
},
},
]
#yyb WSGI应用配置
WSGI_APPLICATION = 'djangoblog.wsgi.application'
#yyb 数据库配置
#yyb https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', #yyb MySQL数据库引擎
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', #yyb 数据库名
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'django_user', #yyb 用户名
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'wzm216921', #yyb 密码
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', #yyb 主机
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306), #yyb 端口
'OPTIONS': {
'charset': 'utf8mb4'}, #yyb 字符集配置
}}
#yyb 密码验证
#yyb https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', #yyb 用户属性相似性验证
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', #yyb 最小长度验证
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', #yyb 常见密码验证
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', #yyb 数字密码验证
},
]
#yyb 国际化配置
LANGUAGES = (
('en', _('English')), #yyb 英语
('zh-hans', _('Simplified Chinese')), #yyb 简体中文
('zh-hant', _('Traditional Chinese')), #yyb 繁体中文
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'), #yyb 本地化文件路径
)
LANGUAGE_CODE = 'zh-hans' #yyb 默认语言
TIME_ZONE = 'Asia/Shanghai' #yyb 时区
USE_I18N = True #yyb 启用国际化
USE_L10N = True #yyb 启用本地化
USE_TZ = False #yyb 不使用时区支持
#yyb 静态文件 (CSS, JavaScript, Images)
#yyb https://docs.djangoproject.com/en/1.10/howto/static-files/
#yyb Haystack搜索配置
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', #yyb Whoosh搜索引擎
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), #yyb 索引路径
},
}
#yyb 自动更新搜索索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
#yyb 允许用户使用邮箱或用户名登录
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') #yyb 静态文件收集目录
STATIC_URL = '/static/' #yyb 静态文件URL
STATICFILES = os.path.join(BASE_DIR, 'static') #yyb 静态文件目录
AUTH_USER_MODEL = 'accounts.BlogUser' #yyb 自定义用户模型
LOGIN_URL = '/login/' #yyb 登录URL
TIME_FORMAT = '%Y-%m-%d %H:%M:%S' #yyb 时间格式
DATE_TIME_FORMAT = '%Y-%m-%d' #yyb 日期格式
#yyb bootstrap颜色样式
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
#yyb 分页设置
PAGINATE_BY = 10
#yyb HTTP缓存超时时间
CACHE_CONTROL_MAX_AGE = 2592000
#yyb 缓存设置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', #yyb 本地内存缓存
'TIMEOUT': 10800, #yyb 缓存超时时间3小时
'LOCATION': 'unique-snowflake', #yyb 缓存位置标识
}
}
#yyb 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache', #yyb Redis缓存后端
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', #yyb Redis连接URL
}
}
SITE_ID = 1 #yyb 站点ID
#yyb 百度站长平台通知URL
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
#yyb 邮箱配置:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' #yyb SMTP后端
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) #yyb 是否使用TLS
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) #yyb 是否使用SSL
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' #yyb SMTP主机
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) #yyb SMTP端口
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') #yyb 邮箱用户
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') #yyb 邮箱密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER #yyb 默认发件人
SERVER_EMAIL = EMAIL_HOST_USER #yyb 服务器邮箱
#yyb 设置debug=false不会处理异常邮件通知
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] #yyb 管理员邮箱
#yyb 微信管理员密码两次MD5加密
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
#yyb 日志配置
LOG_PATH = os.path.join(BASE_DIR, 'logs') #yyb 日志路径
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True) #yyb 创建日志目录
LOGGING = {
'version': 1, #yyb 日志配置版本
'disable_existing_loggers': False, #yyb 不禁用现有日志记录器
'root': {
'level': 'INFO', #yyb 根日志级别
'handlers': ['console', 'log_file'], #yyb 处理器
},
'formatters': { #yyb 日志格式
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', #yyb 详细格式
}
},
'filters': { #yyb 过滤器
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse', #yyb 要求调试模式为False
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue', #yyb 要求调试模式为True
},
},
'handlers': { #yyb 处理器
'log_file': {
'level': 'INFO', #yyb 日志级别
'class': 'logging.handlers.TimedRotatingFileHandler', #yyb 按时间轮转的文件处理器
'filename': os.path.join(LOG_PATH, 'djangoblog.log'), #yyb 日志文件路径
'when': 'D', #yyb 按天轮转
'formatter': 'verbose', #yyb 使用详细格式
'interval': 1, #yyb 间隔1天
'delay': True, #yyb 延迟创建
'backupCount': 5, #yyb 保留5个备份
'encoding': 'utf-8' #yyb 文件编码
},
'console': { #yyb 控制台处理器
'level': 'DEBUG',
'filters': ['require_debug_true'], #yyb 仅在调试模式下生效
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': { #yyb 空处理器
'class': 'logging.NullHandler',
},
'mail_admins': { #yyb 管理员邮件处理器
'level': 'ERROR',
'filters': ['require_debug_false'], #yyb 仅在非调试模式下生效
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': { #yyb 日志记录器
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True, #yyb 向上传播
},
'django.request': { #yyb Django请求日志
'handlers': ['mail_admins'], #yyb 发送邮件给管理员
'level': 'ERROR',
'propagate': False, #yyb 不向上传播
}
}
}
#yyb 静态文件查找器
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', #yyb 文件系统查找器
'django.contrib.staticfiles.finders.AppDirectoriesFinder', #yyb 应用目录查找器
#yyb other
'compressor.finders.CompressorFinder', #yyb 压缩文件查找器
)
COMPRESS_ENABLED = True #yyb 启用压缩
#yyb COMPRESS_OFFLINE = True #yyb 离线压缩(注释)
#yyb CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
#yyb 从相对URL创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
#yyb CSS压缩器
'compressor.filters.cssmin.CSSMinFilter'
]
#yyb JS压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter' #yyb JS压缩器
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') #yyb 媒体文件根目录
MEDIA_URL = '/media/' #yyb 媒体文件URL
X_FRAME_OPTIONS = 'SAMEORIGIN' #yyb 帧选项:同源策略
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' #yyb 默认自增字段类型
#yyb Elasticsearch配置
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') #yyb Elasticsearch主机
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', #yyb Elasticsearch引擎
},
}
#yyb 插件系统
PLUGINS_DIR = BASE_DIR / 'plugins' #yyb 插件目录
ACTIVE_PLUGINS = [ #yyb 激活的插件列表
'article_copyright', #yyb 文章版权
'reading_time', #yyb 阅读时间
'external_links', #yyb 外部链接
'view_count', #yyb 浏览量统计
'seo_optimizer' #yyb SEO优化
]

@ -1,82 +0,0 @@
#yyb 导入Django站点地图相关模块
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
#yyb 导入博客相关模型
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
"""#yyb 静态页面站点地图"""
priority = 0.5 #yyb 优先级0.0-1.0
changefreq = 'daily' #yyb 更新频率:每天
def items(self):
"""#yyb 返回包含在站点地图中的项目列表"""
return ['blog:index', ] #yyb 博客首页
def location(self, item):
"""#yyb 返回每个项目的绝对URL"""
return reverse(item) #yyb 通过反向解析生成URL
class ArticleSiteMap(Sitemap):
"""#yyb 文章站点地图"""
changefreq = "monthly" #yyb 更新频率:每月
priority = "0.6" #yyb 优先级0.6
def items(self):
"""#yyb 返回所有已发布的文章"""
return Article.objects.filter(status='p') #yyb 状态为'p'(已发布)的文章
def lastmod(self, obj):
"""#yyb 返回文章的最后修改时间"""
return obj.last_modify_time #yyb 文章的最后修改时间
class CategorySiteMap(Sitemap):
"""#yyb 分类站点地图"""
changefreq = "Weekly" #yyb 更新频率:每周
priority = "0.6" #yyb 优先级0.6
def items(self):
"""#yyb 返回所有分类"""
return Category.objects.all() #yyb 所有分类
def lastmod(self, obj):
"""#yyb 返回分类的最后修改时间"""
return obj.last_modify_time #yyb 分类的最后修改时间
class TagSiteMap(Sitemap):
"""#yyb 标签站点地图"""
changefreq = "Weekly" #yyb 更新频率:每周
priority = "0.3" #yyb 优先级0.3(标签页优先级较低)
def items(self):
"""#yyb 返回所有标签"""
return Tag.objects.all() #yyb 所有标签
def lastmod(self, obj):
"""#yyb 返回标签的最后修改时间"""
return obj.last_modify_time #yyb 标签的最后修改时间
class UserSiteMap(Sitemap):
"""#yyb 用户站点地图"""
changefreq = "Weekly" #yyb 更新频率:每周
priority = "0.3" #yyb 优先级0.3(用户页优先级较低)
def items(self):
"""#yyb 返回所有有文章的作者(去重)"""
#yyb 获取所有文章的作者并通过set去重再转换为列表
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
"""#yyb 返回用户的注册时间"""
return obj.date_joined #yyb 用户的注册时间

@ -1,41 +0,0 @@
#yyb 导入必要的模块
import logging
import requests
from django.conf import settings
#yyb 获取日志记录器
logger = logging.getLogger(__name__)
class SpiderNotify():
"""#yyb 搜索引擎爬虫通知类"""
@staticmethod
def baidu_notify(urls):
"""
#yyb 向百度站长平台提交链接,通知百度爬虫抓取更新内容
Args:
urls: 需要通知的URL列表
"""
try:
#yyb 将URL列表转换为换行分隔的字符串格式
data = '\n'.join(urls)
#yyb 向百度站长平台API提交URL数据
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
#yyb 记录API返回结果
logger.info(result.text)
except Exception as e:
#yyb 记录通知过程中的错误
logger.error(e)
@staticmethod
def notify(url):
"""
#yyb 通用的爬虫通知方法(目前仅支持百度)
Args:
url: 需要通知的URL
"""
SpiderNotify.baidu_notify(url)

@ -1,44 +0,0 @@
#yyb 导入Django测试框架
from django.test import TestCase
#yyb 导入工具函数
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
"""#yyb DjangoBlog应用测试类"""
def setUp(self):
"""#yyb 测试初始化方法"""
#yyb 可以在这里设置测试数据,当前为空
pass
def test_utils(self):
"""#yyb 测试工具函数"""
#yyb 测试SHA256加密函数
md5 = get_sha256('test')
self.assertIsNotNone(md5) #yyb 断言加密结果不为空
#yyb 测试Markdown转换函数
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c) #yyb 断言Markdown转换结果不为空
#yyb 测试字典转URL参数字符串函数
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data) #yyb 断言转换结果不为空

@ -1,73 +0,0 @@
"""#yyb djangoblog URL 配置
#yyb `urlpatterns` 列表将 URL 路由到视图。更多信息请参阅:
#yyb https://docs.djangoproject.com/en/1.10/topics/http/urls/
#yyb 示例:
#yyb 函数视图
#yyb 1. 导入: from my_app import views
#yyb 2. 添加 URL 到 urlpatterns: url(r'^$', views.home, name='home')
#yyb 基于类的视图
#yyb 1. 导入: from other_app.views import Home
#yyb 2. 添加 URL 到 urlpatterns: url(r'^$', Home.as_view(), name='home')
#yyb 包含其他 URLconf
#yyb 1. 导入 include() 函数: from django.conf.urls import url, include
#yyb 2. 添加 URL 到 urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns #yyb 国际化URL模式
from django.conf.urls.static import static #yyb 静态文件服务
from django.contrib.sitemaps.views import sitemap #yyb 站点地图视图
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory #yyb Haystack搜索视图工厂
#yyb 导入自定义视图和组件
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site #yyb 自定义管理员站点
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm #yyb Elasticsearch搜索表单
from djangoblog.feeds import DjangoBlogFeed #yyb RSS订阅源
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
#yyb 站点地图配置字典
sitemaps = {
'blog': ArticleSiteMap, #yyb 文章站点地图
'Category': CategorySiteMap, #yyb 分类站点地图
'Tag': TagSiteMap, #yyb 标签站点地图
'User': UserSiteMap, #yyb 用户站点地图
'static': StaticViewSitemap #yyb 静态页面站点地图
}
#yyb 自定义错误处理视图
handler404 = 'blog.views.page_not_found_view' #yyb 404页面未找到
handler500 = 'blog.views.server_error_view' #yyb 500服务器错误
handle403 = 'blog.views.permission_denied_view' #yyb 403权限拒绝
#yyb 基本URL模式
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), #yyb 国际化URL
]
#yyb 添加国际化URL模式
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls), #yyb 自定义管理员后台URL
re_path(r'', include('blog.urls', namespace='blog')), #yyb 博客应用URL
re_path(r'mdeditor/', include('mdeditor.urls')), #yyb Markdown编辑器URL
re_path(r'', include('comments.urls', namespace='comment')), #yyb 评论应用URL
re_path(r'', include('accounts.urls', namespace='account')), #yyb 账户应用URL
re_path(r'', include('oauth.urls', namespace='oauth')), #yyb OAuth认证URL
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, #yyb 站点地图XML
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()), #yyb RSS订阅源URL
re_path(r'^rss/$', DjangoBlogFeed()), #yyb RSS订阅源别名
#yyb 搜索URL使用自定义的Elasticsearch视图和表单
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')), #yyb 服务器管理URL
re_path(r'', include('owntracks.urls', namespace='owntracks')), #yyb OwnTracks位置跟踪URL
prefix_default_language=False #yyb 不在默认语言的URL前添加语言前缀
) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) #yyb 静态文件服务
#yyb 调试模式下启用媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -1,262 +0,0 @@
#!/usr/bin/env python
#yyb encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
#yyb 获取日志记录器
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""#yyb 获取最新的文章ID和评论ID"""
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""#yyb 计算字符串的SHA256哈希值"""
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
"""#yyb 缓存装饰器,用于缓存函数结果
Args:
expiration: 缓存过期时间默认3分钟
"""
def wrapper(func):
def news(*args, **kwargs):
try:
#yyb 尝试从视图获取缓存键
view = args[0]
key = view.get_cache_key()
except:
key = None
if not key:
#yyb 如果没有缓存键,根据函数和参数生成唯一键
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
if value is not None:
#yyb logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
if value is None:
#yyb 如果函数返回None设置默认缓存值
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
#yyb 刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
#yyb 创建模拟请求对象
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
#yyb 获取缓存键并删除缓存
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
"""#yyb 获取当前站点(带缓存)"""
site = Site.objects.get_current()
return site
class CommonMarkdown:
"""#yyb Markdown处理工具类"""
@staticmethod
def _convert_markdown(value):
"""#yyb 内部方法转换Markdown文本为HTML"""
md = markdown.Markdown(
extensions=[
'extra', #yyb 额外扩展
'codehilite', #yyb 代码高亮
'toc', #yyb 目录生成
'tables', #yyb 表格支持
]
)
body = md.convert(value) #yyb 转换Markdown为HTML
toc = md.toc #yyb 获取目录
return body, toc
@staticmethod
def get_markdown_with_toc(value):
"""#yyb 获取带目录的Markdown转换结果"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
"""#yyb 获取Markdown转换结果不带目录"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""#yyb 发送邮件(通过信号机制)"""
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""#yyb 生成6位随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
"""#yyb 将字典转换为URL参数字符串"""
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
"""#yyb 获取博客设置(带缓存)"""
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
#yyb 如果数据库中没有博客设置,创建默认设置
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value) #yyb 设置缓存
return value
def save_user_avatar(url):
'''
#yyb 保存用户头像到本地
:param url:头像url
:return: 本地路径
'''
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2) #yyb 下载头像
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir) #yyb 创建头像目录
#yyb 检查文件是否为图片格式
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext #yyb 生成唯一文件名
logger.info('保存用户头像:' + basedir + save_filename)
#yyb 保存头像文件
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename) #yyb 返回静态文件URL
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png') #yyb 返回默认头像
def delete_sidebar_cache():
"""#yyb 删除侧边栏相关缓存"""
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
def delete_view_cache(prefix, keys):
"""#yyb 删除视图缓存"""
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
"""#yyb 获取资源URL静态文件URL"""
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
#yyb HTML清理允许的标签
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
#yyb HTML清理允许的属性
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
"""#yyb 清理HTML移除不安全的标签和属性"""
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -1,585 +0,0 @@
#yyb encoding: utf-8
#yyb 文件编码声明
from __future__ import absolute_import, division, print_function, unicode_literals
#yyb 兼容Python 2和3的导入
import json
import os
import re
import shutil
import threading
import warnings
import six
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from datetime import datetime
from django.utils.encoding import force_str
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
from haystack.constants import DJANGO_CT, DJANGO_ID, ID
from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
from haystack.inputs import Clean, Exact, PythonData, Raw
from haystack.models import SearchResult
from haystack.utils import get_identifier, get_model_ct
from haystack.utils import log as logging
from haystack.utils.app_loading import haystack_get_model
from jieba.analyse import ChineseAnalyzer #yyb 中文分词器
from whoosh import index
from whoosh.analysis import StemmingAnalyzer
from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
from whoosh.fields import ID as WHOOSH_ID
from whoosh.filedb.filestore import FileStorage, RamStorage
from whoosh.highlight import ContextFragmenter, HtmlFormatter
from whoosh.highlight import highlight as whoosh_highlight
from whoosh.qparser import QueryParser
from whoosh.searching import ResultsPage
from whoosh.writing import AsyncWriter
try:
import whoosh
except ImportError:
#yyb 如果Whoosh没有安装抛出缺失依赖异常
raise MissingDependency(
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
#yyb 处理最低版本要求
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
raise MissingDependency(
"The 'whoosh' backend requires version 2.5.0 or greater.")
#yyb 日期时间正则表达式,用于解析日期时间字符串
DATETIME_REGEX = re.compile(
'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$')
LOCALS = threading.local() #yyb 线程本地存储
LOCALS.RAM_STORE = None
class WhooshHtmlFormatter(HtmlFormatter):
"""
#yyb 简化的Whoosh HTML格式化器
#yyb 我们使用它来在不同后端之间获得一致的结果。
#yyb 具体来说Solr、Xapian和Elasticsearch都使用这种格式化。
"""
template = '<%(tag)s>%(t)s</%(tag)s>'
class WhooshSearchBackend(BaseSearchBackend):
"""#yyb Whoosh搜索引擎后端实现"""
#yyb Whoosh保留的特殊用途单词
RESERVED_WORDS = (
'AND',
'NOT',
'OR',
'TO',
)
#yyb Whoosh保留的特殊用途字符
#yyb '\\'必须放在前面,以免覆盖其他斜杠替换
RESERVED_CHARACTERS = (
'\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
'[', ']', '^', '"', '~', '*', '?', ':', '.',
)
def __init__(self, connection_alias, **connection_options):
super(
WhooshSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.setup_complete = False #yyb 设置完成标志
self.use_file_storage = True #yyb 是否使用文件存储
self.post_limit = getattr(
connection_options,
'POST_LIMIT',
128 * 1024 * 1024) #yyb 帖子大小限制默认128MB
self.path = connection_options.get('PATH') #yyb 索引存储路径
if connection_options.get('STORAGE', 'file') != 'file':
self.use_file_storage = False #yyb 不使用文件存储,使用内存存储
if self.use_file_storage and not self.path:
raise ImproperlyConfigured(
"You must specify a 'PATH' in your settings for connection '%s'." %
connection_alias)
self.log = logging.getLogger('haystack') #yyb 日志记录器
def setup(self):
"""
#yyb 延迟加载,直到需要时才设置
"""
from haystack import connections
new_index = False #yyb 是否创建新索引
#yyb 确保索引目录存在
if self.use_file_storage and not os.path.exists(self.path):
os.makedirs(self.path)
new_index = True
if self.use_file_storage and not os.access(self.path, os.W_OK):
raise IOError(
"The path to your Whoosh index '%s' is not writable for the current user/group." %
self.path)
#yyb 选择存储类型:文件存储或内存存储
if self.use_file_storage:
self.storage = FileStorage(self.path)
else:
global LOCALS
if getattr(LOCALS, 'RAM_STORE', None) is None:
LOCALS.RAM_STORE = RamStorage()
self.storage = LOCALS.RAM_STORE
#yyb 构建schema和内容字段名
self.content_field_name, self.schema = self.build_schema(
connections[self.connection_alias].get_unified_index().all_searchfields())
self.parser = QueryParser(self.content_field_name, schema=self.schema) #yyb 查询解析器
#yyb 创建或打开索引
if new_index is True:
self.index = self.storage.create_index(self.schema)
else:
try:
self.index = self.storage.open_index(schema=self.schema)
except index.EmptyIndexError:
self.index = self.storage.create_index(self.schema)
self.setup_complete = True #yyb 标记设置完成
def build_schema(self, fields):
"""#yyb 构建Whoosh索引schema"""
schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True), #yyb 唯一标识符
DJANGO_CT: WHOOSH_ID(stored=True), #yyb Django内容类型
DJANGO_ID: WHOOSH_ID(stored=True), #yyb Django对象ID
}
#yyb 获取Haystack硬编码的键数量
initial_key_count = len(schema_fields)
content_field_name = '' #yyb 内容字段名
for field_name, field_class in fields.items():
if field_class.is_multivalued: #yyb 多值字段
if field_class.indexed is False:
schema_fields[field_class.index_fieldname] = IDLIST(
stored=True, field_boost=field_class.boost)
else:
schema_fields[field_class.index_fieldname] = KEYWORD(
stored=True, commas=True, scorable=True, field_boost=field_class.boost)
elif field_class.field_type in ['date', 'datetime']: #yyb 日期时间字段
schema_fields[field_class.index_fieldname] = DATETIME(
stored=field_class.stored, sortable=True)
elif field_class.field_type == 'integer': #yyb 整数字段
schema_fields[field_class.index_fieldname] = NUMERIC(
stored=field_class.stored, numtype=int, field_boost=field_class.boost)
elif field_class.field_type == 'float': #yyb 浮点数字段
schema_fields[field_class.index_fieldname] = NUMERIC(
stored=field_class.stored, numtype=float, field_boost=field_class.boost)
elif field_class.field_type == 'boolean': #yyb 布尔字段
#yyb Field boost在1.8.2版本中不支持BOOLEAN
schema_fields[field_class.index_fieldname] = BOOLEAN(
stored=field_class.stored)
elif field_class.field_type == 'ngram': #yyb N-gram字段
schema_fields[field_class.index_fieldname] = NGRAM(
minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
elif field_class.field_type == 'edge_ngram': #yyb 边缘N-gram字段
schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start',
stored=field_class.stored,
field_boost=field_class.boost)
else:
#yyb 默认使用中文分析器的文本字段
#yyb 原代码使用StemmingAnalyzer现改为ChineseAnalyzer以支持中文分词
schema_fields[field_class.index_fieldname] = TEXT(
stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
if field_class.document is True: #yyb 主文档字段
content_field_name = field_class.index_fieldname
schema_fields[field_class.index_fieldname].spelling = True #yyb 启用拼写建议
#yyb 如果没有找到字段,优雅地失败
if len(schema_fields) <= initial_key_count:
raise SearchBackendError(
"No fields were found in any search_indexes. Please correct this before attempting to search.")
return (content_field_name, Schema(**schema_fields))
def update(self, index, iterable, commit=True):
"""#yyb 更新索引文档"""
if not self.setup_complete:
self.setup()
self.index = self.index.refresh() #yyb 刷新索引
writer = AsyncWriter(self.index) #yyb 异步写入器
for obj in iterable:
try:
doc = index.full_prepare(obj) #yyb 准备文档
except SkipDocument:
self.log.debug(u"Indexing for object `%s` skipped", obj)
else:
#yyb 确保所有值都是Unicode因为Whoosh只接受Unicode
for key in doc:
doc[key] = self._from_python(doc[key])
#yyb Whoosh 2.5.0+不支持文档boost
if 'boost' in doc:
del doc['boost']
try:
writer.update_document(**doc) #yyb 更新文档
except Exception as e:
if not self.silently_fail:
raise
#yyb 记录对象标识符但不包含实际对象,避免处理日志消息时产生编码错误
self.log.error(
u"%s while preparing object for update" %
e.__class__.__name__,
exc_info=True,
extra={
"data": {
"index": index,
"object": get_identifier(obj)}})
if len(iterable) > 0:
#yyb 目前无论如何都要提交,否则会遇到锁定问题
writer.commit()
def remove(self, obj_or_string, commit=True):
"""#yyb 从索引中移除文档"""
if not self.setup_complete:
self.setup()
self.index = self.index.refresh()
whoosh_id = get_identifier(obj_or_string)
try:
#yyb 通过查询删除文档
self.index.delete_by_query(
q=self.parser.parse(
u'%s:"%s"' %
(ID, whoosh_id)))
except Exception as e:
if not self.silently_fail:
raise
self.log.error(
"Failed to remove document '%s' from Whoosh: %s",
whoosh_id,
e,
exc_info=True)
def clear(self, models=None, commit=True):
"""#yyb 清空索引"""
if not self.setup_complete:
self.setup()
self.index = self.index.refresh()
if models is not None:
assert isinstance(models, (list, tuple))
try:
if models is None:
self.delete_index() #yyb 完全删除索引
else:
models_to_delete = []
for model in models:
models_to_delete.append(
u"%s:%s" %
(DJANGO_CT, get_model_ct(model)))
#yyb 通过查询删除指定模型的文档
self.index.delete_by_query(
q=self.parser.parse(
u" OR ".join(models_to_delete)))
except Exception as e:
if not self.silently_fail:
raise
if models is not None:
self.log.error(
"Failed to clear Whoosh index of models '%s': %s",
','.join(models_to_delete),
e,
exc_info=True)
else:
self.log.error(
"Failed to clear Whoosh index: %s", e, exc_info=True)
def delete_index(self):
"""#yyb 删除整个索引"""
#yyb 根据Whoosh邮件列表如果要清除索引中的所有内容直接删除索引文件更高效
if self.use_file_storage and os.path.exists(self.path):
shutil.rmtree(self.path)
elif not self.use_file_storage:
self.storage.clean()
#yyb 重新创建所有内容
self.setup()
def optimize(self):
"""#yyb 优化索引"""
if not self.setup_complete:
self.setup()
self.index = self.index.refresh()
self.index.optimize()
def calculate_page(self, start_offset=0, end_offset=None):
"""#yyb 计算分页信息"""
#yyb 防止Whoosh抛出错误。需要end_offset大于0
if end_offset is not None and end_offset <= 0:
end_offset = 1
#yyb 确定页码
page_num = 0
if end_offset is None:
end_offset = 1000000 #yyb 默认大数
if start_offset is None:
start_offset = 0
page_length = end_offset - start_offset #yyb 页面长度
if page_length and page_length > 0:
page_num = int(start_offset / page_length)
#yyb 递增因为Whoosh使用基于1的页码
page_num += 1
return page_num, page_length
@log_query #yyb 记录查询日志的装饰器
def search(
self,
query_string,
sort_by=None,
start_offset=0,
end_offset=None,
fields='',
highlight=False,
facets=None,
date_facets=None,
query_facets=None,
narrow_queries=None,
spelling_query=None,
within=None,
dwithin=None,
distance_point=None,
models=None,
limit_to_registered_models=None,
result_class=None,
**kwargs):
"""#yyb 执行搜索查询"""
if not self.setup_complete:
self.setup()
#yyb 零长度查询应该返回无结果
if len(query_string) == 0:
return {
'results': [],
'hits': 0,
}
query_string = force_str(query_string) #yyb 确保查询字符串是字符串
#yyb 单字符查询(非通配符)会被停用词过滤器捕获,应该返回零结果
if len(query_string) <= 1 and query_string != u'*':
return {
'results': [],
'hits': 0,
}
reverse = False #yyb 是否反转排序
if sort_by is not None:
#yyb 确定是否需要反转结果以及Whoosh是否可以处理被要求排序的字段
#yyb 反转是一个全有或全无的操作
sort_by_list = []
reverse_counter = 0
for order_by in sort_by:
if order_by.startswith('-'): #yyb 降序排序
reverse_counter += 1
if reverse_counter and reverse_counter != len(sort_by):
raise SearchBackendError("Whoosh requires all order_by fields"
" to use the same sort direction")
for order_by in sort_by:
if order_by.startswith('-'):
sort_by_list.append(order_by[1:]) #yyb 移除负号
if len(sort_by_list) == 1:
reverse = True
else:
sort_by_list.append(order_by)
if len(sort_by_list) == 1:
reverse = False
sort_by = sort_by_list[0] #yyb Whoosh只支持单字段排序
#yyb Whoosh不支持分面搜索发出警告
if facets is not None:
warnings.warn(
"Whoosh does not handle faceting.",
Warning,
stacklevel=2)
if date_facets is not None:
warnings.warn(
"Whoosh does not handle date faceting.",
Warning,
stacklevel=2)
if query_facets is not None:
warnings.warn(
"Whoosh does not handle query faceting.",
Warning,
stacklevel=2)
narrowed_results = None
self.index = self.index.refresh()
#yyb 限制到注册的模型
if limit_to_registered_models is None:
limit_to_registered_models = getattr(
settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
#yyb 构建模型选择列表
if models and len(models):
model_choices = sorted(get_model_ct(model) for model in models)
elif limit_to_registered_models:
#yyb 使用窄查询,将结果限制为当前路由器处理的模型
model_choices = self.build_models_list()
else:
model_choices = []
#yyb 如果有模型选择,添加到窄查询中
if len(model_choices) > 0:
if narrow_queries is None:
narrow_queries = set()
narrow_queries.add(' OR '.join(
['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
narrow_searcher = None
#yyb 处理窄查询
if narrow_queries is not None:
#yyb 可能很昂贵在Whoosh中没有看到其他方法...
narrow_searcher = self.index.searcher()
for nq in narrow_queries:
recent_narrowed_results = narrow_searcher.search(
self.parser.parse(force_str(nq)), limit=None)
if len(recent_narrowed_results) <= 0:
return {
'results': [],
'hits': 0,
}
if narrowed_results:
narrowed_results.filter(recent_narrowed_results)
else:
narrowed_results = recent_narrowed_results
self.index = self.index.refresh()
#yyb 如果索引中有文档,执行搜索
if self.index.doc_count():
searcher = self.index.searcher() #yyb 创建搜索器
parsed_query = self.parser.parse(query_string) #yyb 解析查询
#yyb 如果查询无效/包含停用词,优雅地恢复
if parsed_query is None:
return {
'results': [],
'hits': 0,
}
page_num, page_length = self.calculate_page(
start_offset, end_offset)
search_kwargs = {
'pagelen': page_length, #yyb 页面长度
'sortedby': sort_by, #yyb 排序字段
'reverse': reverse, #yyb 是否反转
}
#yyb 处理结果已被窄化的情况
if narrowed_results is not None:
search_kwargs['filter'] = narrowed_results
try:
#yyb 执行分页搜索
raw_page = searcher.search_page(
parsed_query,
page_num,
**search_kwargs
)
except ValueError:
if not self.silently_fail:
raise
return {
'results': [],
'hits': 0,
'spelling_suggestion': None,
}
#yyb 由于Whoosh 2.5.1的问题,如果请求的页码过高,它会返回错误的页面
if raw_page.pagenum < page_num:
return {
'results': [],
'hits': 0,
'spelling_suggestion': None,
}
#yyb 处理搜索结果
results = self._process_results(
raw_page,
highlight=highlight,
query_string=query_string,
spelling_query=spelling_query,
result_class=result_class)
searcher.close() #yyb 关闭搜索器
if hasattr(narrow_searcher, 'close'):
narrow_searcher.close()
return results
else:
#yyb 如果没有文档,处理拼写建议
if self.include_spelling:
if spelling_query:
spelling_suggestion = self.create_spelling_suggestion(
spelling_query)
else:
spelling_suggestion = self.create_spelling_suggestion(
query_string)
else:
spelling_suggestion = None
return {
'results': [],
'hits': 0,
'spelling_suggestion': spelling_suggestion,
}
def more_like_this(
self,
model_instance,
additional_query_string=None,
start_offset=0,
end_offset=None,
models=None,
limit_to_registered_models=None,
result_class=None,
**kwargs

@ -1,20 +0,0 @@
"""
#yyb WSGI config for djangoblog project.
#yyb 它将WSGI可调用对象暴露为名为``application``的模块级变量。
#yyb 有关此文件的更多信息,请参阅:
#yyb https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os
#yyb 导入Django的WSGI应用获取函数
from django.core.wsgi import get_wsgi_application
#yyb 设置Django的默认设置模块
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
#yyb 获取WSGI应用实例
#yyb 这个application变量将被WSGI服务器如Gunicorn、uWSGI使用来服务Django应用
application = get_wsgi_application()

@ -1,3 +0,0 @@
#yyb 设置Django应用的默认配置类
#yyb 当Django启动时会自动使用这里指定的应用配置类
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

Binary file not shown.

@ -1,21 +0,0 @@
#yyb 导入Django应用配置基类
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
"""#yyb Djangoblog应用的配置类"""
#yyb 设置默认的自动主键字段类型为BigAutoField64位自增整数
default_auto_field = 'django.db.models.BigAutoField'
#yyb 指定应用名称
name = 'djangoblog'
def ready(self):
"""#yyb 应用准备就绪时执行的方法"""
#yyb 调用父类的ready方法
super().ready()
#yyb 导入并加载插件
#yyb 这里在应用启动时自动加载所有插件
from .plugin_manage.loader import load_plugins
load_plugins()

@ -1,163 +0,0 @@
#yyb 导入必要的Python和Django模块
import _thread
import logging
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
#yyb 导入自定义模型和工具函数
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
#yyb 获取logger实例
logger = logging.getLogger(__name__)
#yyb 定义自定义信号
#yyb OAuth用户登录信号传递用户id
oauth_user_login_signal = django.dispatch.Signal(['id'])
#yyb 发送邮件信号,传递收件人、标题和内容
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
"""#yyb 发送邮件信号处理器"""
#yyb 从信号参数中获取邮件相关信息
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
#yyb 创建邮件消息对象
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html" #yyb 设置邮件内容类型为HTML
#yyb 记录邮件发送日志
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
try:
#yyb 尝试发送邮件
result = msg.send()
log.send_result = result > 0 #yyb 发送成功结果为True
except Exception as e:
#yyb 记录发送失败日志
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save() #yyb 保存邮件发送日志
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""#yyb OAuth用户登录信号处理器"""
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
#yyb 如果用户头像不在当前站点域名下,则保存头像到本地
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
#yyb 删除侧边栏缓存
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
"""#yyb 模型保存后的通用回调函数"""
clearcache = False
#yyb 如果是LogEntryDjango管理员日志直接返回
if isinstance(instance, LogEntry):
return
#yyb 检查实例是否有get_full_url方法通常是有URL的模型
if 'get_full_url' in dir(instance):
#yyb 判断是否只是更新浏览量
is_update_views = update_fields == {'views'}
#yyb 如果不是测试环境且不是更新浏览量,则通知搜索引擎
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url]) #yyb 通知百度搜索引擎
except Exception as ex:
logger.error("notify sipder", ex)
#yyb 如果不是更新浏览量,设置清除缓存标志
if not is_update_views:
clearcache = True
#yyb 如果是评论模型
if isinstance(instance, Comment):
#yyb 如果评论是启用的
if instance.is_enable:
path = instance.article.get_absolute_url()
site = get_current_site().domain
#yyb 处理站点域名(移除端口号)
if site.find(':') > 0:
site = site[0:site.find(':')]
#yyb 使文章详情页缓存过期
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
#yyb 删除SEO处理器缓存
if cache.get('seo_processor'):
cache.delete('seo_processor')
#yyb 删除文章评论缓存
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
#yyb 删除侧边栏缓存
delete_sidebar_cache()
#yyb 删除文章评论视图缓存
delete_view_cache('article_comments', [str(instance.article.pk)])
#yyb 在新线程中发送评论通知邮件
_thread.start_new_thread(send_comment_email, (instance,))
#yyb 如果需要清除缓存
if clearcache:
cache.clear() #yyb 清除所有缓存
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
"""#yyb 用户登录/登出信号处理器"""
if user and user.username:
logger.info(user) #yyb 记录用户信息
delete_sidebar_cache() #yyb 删除侧边栏缓存
# cache.clear() #yyb 注释掉的清除所有缓存代码

@ -0,0 +1,72 @@
#zjy 评论模块的Django Admin配置
#zjy 自定义评论在Django后台的管理界面
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
#zjy 批量禁用评论(将 is_enable 设为 False
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
#zjy 批量启用评论(将 is_enable 设为 True
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
#zjy 定义动作名称,在 Django Admin 批量操作菜单中显示的文字
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
#zjy 每页显示评论数量
list_per_page = 20
#zjy 在评论列表中显示哪些字段
list_display = (
'id',
'body', #zjy 评论正文
'link_to_userinfo', #zjy 用户信息(带链接)
'link_to_article', #zjy 所属文章(带链接)
'is_enable', #zjy 是否启用
'creation_time' #zjy 创建时间
)
#zjy 可以点击哪些字段进入编辑页面
list_display_links = ('id', 'body', 'is_enable')
#zjy 过滤器(后台右侧筛选功能)
list_filter = ('is_enable',)
#zjy 排除不需要在后台编辑的字段(自动时间字段不应手动修改)
exclude = ('creation_time', 'last_modify_time')
#zjy 批量操作按钮
actions = [disable_commentstatus, enable_commentstatus]
#zjy 显示用户信息,并可点击跳转到用户编辑页面
def link_to_userinfo(self, obj):
#zjy 获取目标 admin change 页面的 URL 路径
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
#zjy 显示用户昵称,若无昵称显示 email
return format_html(
'<a href="{}">{}</a>'.format(
link,
obj.author.nickname if obj.author.nickname else obj.author.email
)
)
#zjy 显示所属文章,并可点击跳转到文章编辑页面
def link_to_article(self, obj):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html('<a href="{}">{}</a>'.format(link, obj.article.title))
#zjy 设置在后台列表中显示的列标题
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -0,0 +1,13 @@
#zjy 评论模块的应用配置
#zjy 定义评论应用的配置信息
from django.apps import AppConfig
class CommentsConfig(AppConfig):
#zjy 指定该 App 在项目中的名称(即所在目录名)
name = 'comments'
#zjy (可选)可以在这里做初始化操作,如引入 signals
# def ready(self):
# import comments.signals

@ -0,0 +1,24 @@
#zjy 评论模块的表单定义
#zjy 定义评论提交的表单验证规则
from django import forms
from django.forms import ModelForm
from .models import Comment
class CommentForm(ModelForm):
#zjy 用于存储父评论的 ID实现评论回复功能
#zjy 该字段不会显示到页面中HiddenInput允许为空一级评论时为空
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput,
required=False
)
class Meta:
#zjy 指定该表单操作的模型为 Comment
model = Comment
#zjy 只允许用户输入评论内容body
#zjy 其他字段(如 author、article、parent_comment将在视图中自动赋值
fields = ['body']

@ -0,0 +1,65 @@
# Generated by Django 4.1.7 on 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
class Migration(migrations.Migration):
# 表示这是该 app 的第一个迁移文件
initial = True
dependencies = [
# 依赖 blog 应用的第一条迁移文件,确保 Article 模型已经被创建
('blog', '0001_initial'),
# 依赖 Django 的用户模型(可自定义 AUTH_USER_MODEL
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
# 创建 Comment 模型
migrations.CreateModel(
name='Comment',
fields=[
# 主键 id自动递增
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 评论内容,最大长度 300 字
('body', models.TextField(max_length=300, 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='修改时间')),
# 评论是否显示,后台可控制隐藏
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 评论所属文章,一个评论只能属于一篇文章
# CASCADE 表示当文章删除时,该评论也会被删除
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
# 评论作者,关联用户模型
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
# 父评论,用于实现评论/回复功能
# 允许为空(空代表这是一级评论)
# 父评论删除时,子评论也删除
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
options={
# 后台显示名
'verbose_name': '评论',
'verbose_name_plural': '评论',
# 查询时默认按 id 倒序排列(新的评论排最前)
'ordering': ['-id'],
# get_latest_by 用于 Django 的 latest() 方法
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
# 当前迁移文件依赖于 comments 应用的 0001 初始迁移文件
dependencies = [
('comments', '0001_initial'),
]
operations = [
# 修改 Comment 模型中 is_enable 字段的属性
migrations.AlterField(
model_name='comment', # 要修改的模型名称
name='is_enable', # 要修改的字段名
field=models.BooleanField(
default=False, # 将默认值改为 False即默认评论不显示
verbose_name='是否显示' # 后台显示名称
),
),
]

@ -0,0 +1,111 @@
# Generated by Django 4.2.5 on 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
class Migration(migrations.Migration):
# 迁移依赖顺序,确保其他相关模型先完成迁移
dependencies = [
# 依赖用户模型(可自定义 AUTH_USER_MODEL
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
# 依赖 blog 应用的某次迁移文件
('blog', '0005_alter_article_options_alter_category_options_and_more'),
# 依赖 comments 应用之前的迁移调整(包括 is_enable 字段的变更)
('comments', '0002_alter_comment_is_enable'),
]
operations = [
# 修改模型的元选项Meta 类中的配置)
migrations.AlterModelOptions(
name='comment',
options={
'get_latest_by': 'id', # 使用 id 作为 latest() 的默认排序依据
'ordering': ['-id'], # 查询结果默认按 id 降序排列(新评论在前)
'verbose_name': 'comment', # 后台显示名称(单数)
'verbose_name_plural': 'comment', # 后台显示名称(复数)
},
),
# 删除原来用于记录创建时间的字段 created_time
migrations.RemoveField(
model_name='comment',
name='created_time',
),
# 删除原来用于记录修改时间的字段 last_mod_time
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
# 新增评论创建时间字段(命名和 verbose_name 英文化)
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='creation time',
),
),
# 新增评论最后修改时间字段(命名和 verbose_name 英文化)
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='last modify time',
),
),
# 修改外键 article 字段的 verbose_name 显示文本
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 文章删除时,本评论也删除
to='blog.article',
verbose_name='article',
),
),
# 修改外键 author 字段的 verbose_name 显示文本
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 用户删除时,评论也删除
to=settings.AUTH_USER_MODEL,
verbose_name='author',
),
),
# 修改评论是否显示字段,将 verbose_name 英文化,默认不显示
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(
default=False,
verbose_name='enable', # 从中文变为英文显示
),
),
# 修改父评论字段(用于实现评论回复结构)
migrations.AlterField(
model_name='comment',
name='parent_comment',
field=models.ForeignKey(
blank=True, # 表单中允许为空
null=True, # 数据库中允许为 null
on_delete=django.db.models.deletion.CASCADE, # 父评论删除时子评论也删除
to='comments.comment',
verbose_name='parent comment',
),
),
]

@ -0,0 +1,74 @@
#zjy 评论模块的数据模型定义
#zjy 定义了Comment模型用于存储博客文章的评论信息支持多级回复功能
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
class Comment(models.Model):
#zjy 评论正文,限制最大输入长度 300使用 TextField 以便输入多行内容
body = models.TextField('正文', max_length=300)
#zjy 评论创建时间,默认使用 timezone.now可自动获取当前时区时间
creation_time = models.DateTimeField(
_('creation time'),
default=now
)
#zjy 评论最后修改时间,通常用于编辑评论功能,但如果不编辑也会保持不变
last_modify_time = models.DateTimeField(
_('last modify time'),
default=now
)
#zjy 评论作者,关联到用户模型,用户删除时,对应评论也一并删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE
)
#zjy 评论所属文章,关联到 Article 模型,文章被删除时,其下所有评论也被删除
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE
)
#zjy 父评论,用于构建"评论回复"树结构
#zjy 若为空 → 表示为一级评论;不为空 → 表示为某条评论的子评论
parent_comment = models.ForeignKey(
'self', #zjy 自关联
verbose_name=_('parent comment'),
blank=True, #zjy 表单中允许为空
null=True, #zjy 数据库允许为 null
on_delete=models.CASCADE #zjy 父评论删除时,子评论也被删除
)
#zjy 是否启用评论(常用于需要审核评论是否展示)
#zjy 默认为 False → 新评论不会立刻显示,需要管理员审核启用
is_enable = models.BooleanField(
_('enable'),
default=False,
blank=False,
null=False
)
class Meta:
#zjy 默认按 id 倒序排列 → 新评论显示在前
ordering = ['-id']
#zjy Django Admin 后台显示的模型名称
verbose_name = _('comment')
verbose_name_plural = verbose_name
#zjy latest() 方法依据 id 获取最新对象
get_latest_by = 'id'
def __str__(self):
#zjy 后台及 shell 打印对象时显示评论内容
return self.body

@ -0,0 +1,55 @@
from django import template
# 注册一个自定义标签库
register = template.Library()
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""
获取指定评论的所有子评论包括多级递归子评论
用法示例在模板中:
{% parse_commenttree article_comments comment as childcomments %}
参数解释
commentlist所有评论的查询集合一般是 article.comments.all()
comment当前评论对象
返回值
datas按层级顺序递归展开的所有子评论列表
"""
datas = [] # 用于存储递归解析得到的子评论
def parse(c):
# 找到当前评论 c 的直接子评论(过滤掉未启用的)
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
datas.append(child) # 保存子评论
parse(child) # 递归查找子评论的子评论
parse(comment)
return datas # 返回递归展开的所有子级评论
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""
渲染评论项组件
用法示例在模板中
{% show_comment_item comment True %}
参数
comment需要渲染的评论对象
ischild是否为子评论用于模板样式控制如缩进/层级
depth 解释
depth = 1 子评论缩进更深
depth = 2 顶级评论缩进较浅
"""
depth = 1 if ischild else 2
return {
'comment_item': comment, # 提供给模板的评论对象
'depth': depth # 让模板根据层级调整样式
}

@ -0,0 +1,131 @@
#zjy 评论模块的测试用例
#zjy 测试评论的提交、审核、显示和邮件通知等功能
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse
from accounts.models import BlogUser
from blog.models import Category, Article
from comments.models import Comment
from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid
class CommentsTest(TransactionTestCase):
#zjy 评论相关功能测试类
#zjy 使用 TransactionTestCase 允许测试包含事务的数据库操作
def setUp(self):
#zjy 测试初始化工作:
#zjy - 创建请求客户端
#zjy - 配置博客系统为"评论需要审核"
#zjy - 创建一个超级管理员用户(用于登录发表评论)
self.client = Client()
self.factory = RequestFactory()
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True #zjy 开启评论审核,提交的评论默认不显示
value.save()
#zjy 创建可登录的超级管理员用户
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1"
)
def update_article_comment_status(self, article):
#zjy 将文章下所有评论改为 is_enable=True
#zjy 模拟管理员审核通过评论(使评论显示)
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
def test_validate_comment(self):
#zjy 测试评论提交、审核、评论树解析等功能流程
#zjy 登录用户
self.client.login(username='liangliangyy1', password='liangliangyy1')
#zjy 创建分类
category = Category()
category.name = "categoryccc"
category.save()
#zjy 创建文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
#zjy 文章评论提交 URL
comment_url = reverse('comments:postcomment', kwargs={'article_id': article.id})
#zjy 用户提交第一条评论
response = self.client.post(comment_url, {'body': '123ffffffffff'})
self.assertEqual(response.status_code, 302) #zjy 正常应重定向(提交成功)
#zjy 因为评论需要审核,未批准前评论数为 0
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
#zjy 模拟管理员审核评论
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
#zjy 提交第二条评论
response = self.client.post(comment_url, {'body': '123ffffffffff'})
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
#zjy 回复第一条评论(测试 parent_comment 功能)
parent_comment_id = article.comment_list()[0].id
response = self.client.post(comment_url, {
'body': '''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
})
self.assertEqual(response.status_code, 302)
#zjy 通过审核
self.update_article_comment_status(article)
#zjy 再次获取文章
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
#zjy 测试评论树解析
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1) #zjy 第一条评论应当有 1 个子评论
#zjy 渲染评论组件标签是否正常返回
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
#zjy 测试工具函数获取最大文章/评论 id
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
#zjy 测试发送评论邮件通知(若配置邮件服务则会成功)
from comments.utils import send_comment_email
send_comment_email(comment)
send_comment_email(comment)

@ -0,0 +1,42 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.views.generic import View
from blog.models import Article
from .models import Comment
from .forms import CommentForm
class CommentPostView(LoginRequiredMixin, View):
"""
负责处理评论提交
"""
def post(self, request, article_id):
# 获取目标文章
article = get_object_or_404(Article, pk=article_id)
form = CommentForm(request.POST)
if form.is_valid():
# 获取评论内容
body = form.cleaned_data['body'].strip()
parent_id = form.cleaned_data.get('parent_comment_id')
comment = Comment()
comment.article = article
comment.author = request.user
comment.body = body
# 判断是否是子评论(回复)
if parent_id:
try:
parent_comment = Comment.objects.get(id=parent_id)
comment.parent_comment = parent_comment
except Comment.DoesNotExist:
pass
comment.save()
# 评论成功后返回文章页面
return HttpResponseRedirect(article.get_absolute_url())

@ -0,0 +1,69 @@
#zjy 评论模块的工具函数
#zjy 提供评论相关的辅助功能,如邮件通知
import logging
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
logger = logging.getLogger(__name__)
def send_comment_email(comment):
#zjy 当用户发表评论后,给评论者发送邮件通知;
#zjy 如果该评论是回复别人的,则同时给被回复的用户发送提醒邮件。
#zjy 获取当前站点域名
site = get_current_site().domain
#zjy 邮件主题
subject = _('Thanks for your comment')
#zjy 构造文章访问 URL以便用户点进查看
article_url = f"https://{site}{comment.article.get_absolute_url()}"
#zjy 给评论者自己发送的邮件内容
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {
'article_url': article_url,
'article_title': comment.article.title
}
#zjy 收件人 = 评论的作者本人
tomail = comment.author.email
#zjy 发送邮件
send_email([tomail], subject, html_content)
try:
#zjy 如果该评论存在父评论(说明是回复行为)
if comment.parent_comment:
#zjy 给被回复的人发送通知邮件
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
go check it out!
<br/>
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s
""") % {
'article_url': article_url,
'article_title': comment.article.title,
'comment_body': comment.parent_comment.body
}
#zjy 父评论的作者邮箱
tomail = comment.parent_comment.author.email
#zjy 向被回复者发送邮件
send_email([tomail], subject, html_content)
except Exception as e:
#zjy 出现错误则记录日志,但不影响评论正常流程
logger.error(e)

@ -0,0 +1,44 @@
#zjy 评论模块的视图处理
#zjy 处理评论的提交和显示逻辑
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.views.generic import View
from blog.models import Article
from .models import Comment
from .forms import CommentForm
class CommentPostView(LoginRequiredMixin, View):
#zjy 负责处理评论提交
#zjy 使用类视图方式处理POST请求
def post(self, request, article_id):
#zjy 获取目标文章
article = get_object_or_404(Article, pk=article_id)
form = CommentForm(request.POST)
if form.is_valid():
#zjy 获取评论内容
body = form.cleaned_data['body'].strip()
parent_id = form.cleaned_data.get('parent_comment_id')
comment = Comment()
comment.article = article
comment.author = request.user
comment.body = body
#zjy 判断是否是子评论(回复)
if parent_id:
try:
parent_comment = Comment.objects.get(id=parent_id)
comment.parent_comment = parent_comment
except Comment.DoesNotExist:
pass
comment.save()
#zjy 评论成功后返回文章页面
return HttpResponseRedirect(article.get_absolute_url())

@ -1,217 +0,0 @@
#yyb 导入必要的模块和类
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
#yyb 导入自定义的Elasticsearch文档和模型
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
#yyb 获取日志记录器
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
"""#yyb Elasticsearch搜索后端实现"""
def __init__(self, connection_alias, **connection_options):
"""#yyb 初始化Elasticsearch后端"""
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager() #yyb 文章文档管理器
self.include_spelling = True #yyb 是否包含拼写建议
def _get_models(self, iterable):
"""#yyb 获取模型实例并转换为文档"""
#yyb 如果有提供模型列表则使用,否则获取所有文章
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models) #yyb 将模型转换为Elasticsearch文档
return docs
def _create(self, models):
"""#yyb 创建索引并重建文档"""
self.manager.create_index() #yyb 创建Elasticsearch索引
docs = self._get_models(models)
self.manager.rebuild(docs) #yyb 重建所有文档
def _delete(self, models):
"""#yyb 删除文档"""
for m in models:
m.delete()
return True
def _rebuild(self, models):
"""#yyb 重建索引文档"""
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs) #yyb 更新文档
def update(self, index, iterable, commit=True):
"""#yyb 更新文档"""
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
"""#yyb 移除指定对象"""
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
"""#yyb 清空索引"""
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""#yyb 获取搜索建议词,如果没有找到建议词则返回原搜索词"""
#yyb 构建搜索建议查询
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
#yyb 处理建议结果
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"]) #yyb 使用建议词
else:
keywords.append(suggest["text"]) #yyb 使用原词
return ' '.join(keywords)
@log_query #yyb 记录查询日志的装饰器
def search(self, query_string, **kwargs):
"""#yyb 执行搜索查询"""
logger.info('search query_string:' + query_string)
#yyb 获取分页参数
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
#yyb 推荐词搜索:如果启用建议,则获取建议词
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
#yyb 构建搜索查询在标题和正文中匹配设置最小匹配度70%
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
#yyb 执行搜索过滤已发布的状态为p且类型为a的文章
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset] #yyb 不返回源文档内容,只返回元数据
results = search.execute()
hits = results['hits'].total #yyb 总命中数
raw_results = []
#yyb 处理搜索结果
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
#yyb 创建搜索结果对象
result = result_class(
app_label,
model_name,
raw_result['_id'], #yyb 文档ID
raw_result['_score'], #yyb 相关性分数
**additional_fields)
raw_results.append(result)
facets = {}
#yyb 如果查询词与建议词不同,则设置拼写建议
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results, #yyb 搜索结果列表
'hits': hits, #yyb 总命中数
'facets': facets, #yyb 分面搜索数据
'spelling_suggestion': spelling_suggestion, #yyb 拼写建议
}
class ElasticSearchQuery(BaseSearchQuery):
"""#yyb Elasticsearch查询构建器"""
def _convert_datetime(self, date):
"""#yyb 转换日期时间格式"""
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S')) #yyb 包含时间的格式
else:
return force_str(date.strftime('%Y%m%d000000')) #yyb 只包含日期的格式
def clean(self, query_fragment):
"""
#yyb 清理用户输入的查询片段,转义保留字符
#yyb Whoosh 1.X与此不同不再使用反斜杠转义保留字符
#yyb 而是应该引用整个单词。
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
#yyb 处理保留字
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
#yyb 处理保留字符
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word #yyb 用引号包围包含保留字符的单词
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
"""#yyb 构建查询片段"""
return value.query_string
def get_count(self):
"""#yyb 获取搜索结果数量"""
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
"""#yyb 获取拼写建议"""
return self._spelling_suggestion
def build_params(self, spelling_query=None):
"""#yyb 构建查询参数"""
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
"""#yyb Elasticsearch模型搜索表单"""
def search(self):
"""#yyb 执行搜索,根据参数决定是否使用建议搜索"""
#yyb 是否建议搜索从请求数据中获取is_suggest参数
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search() #yyb 调用父类搜索方法
return sqs
class ElasticSearchEngine(BaseEngine):
"""#yyb Elasticsearch搜索引擎"""
backend = ElasticSearchBackend #yyb 指定后端类
query = ElasticSearchQuery #yyb 指定查询类

@ -1,59 +0,0 @@
#yyb 导入Django RSS订阅相关模块
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
#yyb 导入自定义模型和工具
from blog.models import Article
from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
"""#yyb DjangoBlog的RSS订阅源类"""
#yyb 指定Feed类型为RSS 2.0
feed_type = Rss201rev2Feed
#yyb Feed的描述信息
description = '大巧无工,重剑无锋.'
#yyb Feed的标题
title = "且听风吟 大巧无工,重剑无锋. "
#yyb Feed的链接地址
link = "/feed/"
def author_name(self):
"""#yyb 获取作者名称 - 返回第一个用户的昵称"""
return get_user_model().objects.first().nickname
def author_link(self):
"""#yyb 获取作者链接 - 返回第一个用户的绝对URL"""
return get_user_model().objects.first().get_absolute_url()
def items(self):
"""#yyb 获取要在Feed中显示的项目列表"""
#yyb 返回最近发布的5篇文章过滤条件类型为'article'且状态为'published'
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""#yyb 获取单个项目的标题"""
return item.title
def item_description(self, item):
"""#yyb 获取单个项目的描述 - 将Markdown内容转换为HTML"""
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
"""#yyb 获取Feed的版权信息"""
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""#yyb 获取单个项目的链接"""
return item.get_absolute_url()
def item_guid(self, item):
"""#yyb 获取单个项目的全局唯一标识符(当前未实现)"""
#yyb 注意:这个方法目前没有返回值,可能需要根据需求实现
#yyb 通常应该返回一个唯一标识项目的字符串如文章的ID或永久链接
pass

@ -1,114 +0,0 @@
#yyb 导入Django管理相关模块
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
"""#yyb Django管理员日志条目的自定义管理界面"""
#yyb 列表页过滤器配置:按内容类型过滤
list_filter = [
'content_type'
]
#yyb 搜索字段配置:可按对象表示和变更消息搜索
search_fields = [
'object_repr',
'change_message'
]
#yyb 列表页中可点击的链接字段
list_display_links = [
'action_time',
'get_change_message',
]
#yyb 列表页显示的字段
list_display = [
'action_time', #yyb 操作时间
'user_link', #yyb 用户链接(自定义)
'content_type', #yyb 内容类型
'object_link', #yyb 对象链接(自定义)
'get_change_message', #yyb 变更消息
]
def has_add_permission(self, request):
"""#yyb 禁止添加新的日志条目"""
return False
def has_change_permission(self, request, obj=None):
"""#yyb 修改权限控制只允许超级用户或具有特定权限的用户查看不允许POST修改"""
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
"""#yyb 禁止删除日志条目"""
return False
def object_link(self, obj):
"""#yyb 生成对象链接的显示"""
object_link = escape(obj.object_repr) #yyb 转义对象表示字符串
content_type = obj.content_type
#yyb 如果不是删除操作且内容类型存在,尝试生成可点击的链接
if obj.action_flag != DELETION and content_type is not None:
try:
#yyb 构建管理员修改页面的URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
#yyb 创建HTML链接
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
#yyb 如果无法生成URL保持原样
pass
return mark_safe(object_link) #yyb 标记为安全HTML
#yyb 设置对象链接字段的排序和显示名称
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def user_link(self, obj):
"""#yyb 生成用户链接的显示"""
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user)) #yyb 转义用户字符串
try:
#yyb 构建用户修改页面的URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
#yyb 创建HTML链接
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
#yyb 如果无法生成URL保持原样
pass
return mark_safe(user_link) #yyb 标记为安全HTML
#yyb 设置用户链接字段的排序和显示名称
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
def get_queryset(self, request):
"""#yyb 获取查询集预取content_type关系以提高性能"""
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
def get_actions(self, request):
"""#yyb 获取可用的批量操作,移除删除选中操作"""
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected'] #yyb 移除批量删除选项
return actions

@ -1,32 +1 @@
def is_prime(n):
"""判断一个数是否为质数"""
if n <= 1:
return False
if n == 2:
return True
if n % 2 == 0:
return False
for i in range(3, int(n**0.5) + 1, 2):
if n % i == 0:
return False
return True
def print_first_n_primes(n):
"""打印前n个质数"""
count = 0
num = 2
primes = []
while count < n:
if is_prime(num):
primes.append(num)
count += 1
num += 1
# 打印结果每10个一行
for i in range(0, len(primes), 10):
print(", ".join(map(str, primes[i:i+10])))
# 输出前100位质数
print("前100位质数为:")
print_first_n_primes(100)
print("Hello World")

@ -1,41 +0,0 @@
import logging
logger = logging.getLogger(__name__)
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
self.init_plugin()
self.register_hooks()
def init_plugin(self):
"""
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
pass
def get_plugin_info(self):
"""
获取插件信息
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
}

@ -1,7 +0,0 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -1,44 +0,0 @@
import logging
logger = logging.getLogger(__name__)
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调
"""
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(callback)
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
callback(*args, **kwargs)
except Exception as e:
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
value = callback(value, *args, **kwargs)
except Exception as e:
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
return value

@ -1,19 +0,0 @@
import os
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -1,345 +0,0 @@
"""
#yyb Django settings for djangoblog project.
#yyb Generated by 'django-admin startproject' using Django 1.10.2.
#yyb For more information on this file, see
#yyb https://docs.djangoproject.com/en/1.10/topics/settings/
#yyb For the full list of settings and their values, see
#yyb https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
"""#yyb 将环境变量转换为布尔值"""
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
#yyb 构建项目内部路径BASE_DIR / 'subdir'
BASE_DIR = Path(__file__).resolve().parent.parent
#yyb 快速开发配置 - 不适用于生产环境
#yyb 参见 https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
#yyb 安全警告:在生产环境中保持密钥保密!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
#yyb 安全警告:在生产环境中不要开启调试模式!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
#yyb DEBUG = False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' #yyb 检测是否在测试模式
#yyb 允许的主机名
#yyb ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
#yyb Django 4.0新增配置受信任的CSRF来源
CSRF_TRUSTED_ORIGINS = ['http://example.com']
#yyb 应用定义
INSTALLED_APPS = [
#yyb 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig', #yyb 使用简化的管理员配置
'django.contrib.auth', #yyb 认证系统
'django.contrib.contenttypes', #yyb 内容类型框架
'django.contrib.sessions', #yyb 会话框架
'django.contrib.messages', #yyb 消息框架
'django.contrib.staticfiles', #yyb 静态文件管理
'django.contrib.sites', #yyb 站点框架
'django.contrib.sitemaps', #yyb 站点地图
'mdeditor', #yyb Markdown编辑器
'haystack', #yyb 搜索框架
'blog', #yyb 博客应用
'accounts', #yyb 账户应用
'comments', #yyb 评论应用
'oauth', #yyb OAuth认证
'servermanager', #yyb 服务器管理
'owntracks', #yyb 位置跟踪
'compressor', #yyb 静态文件压缩
'djangoblog' #yyb 主应用
]
#yyb 中间件配置
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', #yyb 安全中间件
'django.contrib.sessions.middleware.SessionMiddleware', #yyb 会话中间件
'django.middleware.locale.LocaleMiddleware', #yyb 国际化中间件
'django.middleware.gzip.GZipMiddleware', #yyb Gzip压缩
#yyb 'django.middleware.cache.UpdateCacheMiddleware', #yyb 缓存更新(注释)
'django.middleware.common.CommonMiddleware', #yyb 通用中间件
#yyb 'django.middleware.cache.FetchFromCacheMiddleware', #yyb 缓存获取(注释)
'django.middleware.csrf.CsrfViewMiddleware', #yyb CSRF保护
'django.contrib.auth.middleware.AuthenticationMiddleware', #yyb 认证中间件
'django.contrib.messages.middleware.MessageMiddleware', #yyb 消息中间件
'django.middleware.clickjacking.XFrameOptionsMiddleware', #yyb 点击劫持保护
'django.middleware.http.ConditionalGetMiddleware', #yyb 条件GET请求
'blog.middleware.OnlineMiddleware' #yyb 自定义在线用户中间件
]
#yyb 根URL配置
ROOT_URLCONF = 'djangoblog.urls'
#yyb 模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates', #yyb Django模板引擎
'DIRS': [os.path.join(BASE_DIR, 'templates')], #yyb 模板目录
'APP_DIRS': True, #yyb 启用应用模板目录
'OPTIONS': {
'context_processors': [ #yyb 上下文处理器
'django.template.context_processors.debug', #yyb 调试信息
'django.template.context_processors.request', #yyb 请求对象
'django.contrib.auth.context_processors.auth', #yyb 认证信息
'django.contrib.messages.context_processors.messages', #yyb 消息框架
'blog.context_processors.seo_processor' #yyb 自定义SEO处理器
],
},
},
]
#yyb WSGI应用配置
WSGI_APPLICATION = 'djangoblog.wsgi.application'
#yyb 数据库配置
#yyb https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', #yyb MySQL数据库引擎
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', #yyb 数据库名
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'django_user', #yyb 用户名
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'wzm216921', #yyb 密码
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', #yyb 主机
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306), #yyb 端口
'OPTIONS': {
'charset': 'utf8mb4'}, #yyb 字符集配置
}}
#yyb 密码验证
#yyb https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', #yyb 用户属性相似性验证
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', #yyb 最小长度验证
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', #yyb 常见密码验证
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', #yyb 数字密码验证
},
]
#yyb 国际化配置
LANGUAGES = (
('en', _('English')), #yyb 英语
('zh-hans', _('Simplified Chinese')), #yyb 简体中文
('zh-hant', _('Traditional Chinese')), #yyb 繁体中文
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'), #yyb 本地化文件路径
)
LANGUAGE_CODE = 'zh-hans' #yyb 默认语言
TIME_ZONE = 'Asia/Shanghai' #yyb 时区
USE_I18N = True #yyb 启用国际化
USE_L10N = True #yyb 启用本地化
USE_TZ = False #yyb 不使用时区支持
#yyb 静态文件 (CSS, JavaScript, Images)
#yyb https://docs.djangoproject.com/en/1.10/howto/static-files/
#yyb Haystack搜索配置
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', #yyb Whoosh搜索引擎
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), #yyb 索引路径
},
}
#yyb 自动更新搜索索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
#yyb 允许用户使用邮箱或用户名登录
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') #yyb 静态文件收集目录
STATIC_URL = '/static/' #yyb 静态文件URL
STATICFILES = os.path.join(BASE_DIR, 'static') #yyb 静态文件目录
AUTH_USER_MODEL = 'accounts.BlogUser' #yyb 自定义用户模型
LOGIN_URL = '/login/' #yyb 登录URL
TIME_FORMAT = '%Y-%m-%d %H:%M:%S' #yyb 时间格式
DATE_TIME_FORMAT = '%Y-%m-%d' #yyb 日期格式
#yyb bootstrap颜色样式
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
#yyb 分页设置
PAGINATE_BY = 10
#yyb HTTP缓存超时时间
CACHE_CONTROL_MAX_AGE = 2592000
#yyb 缓存设置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', #yyb 本地内存缓存
'TIMEOUT': 10800, #yyb 缓存超时时间3小时
'LOCATION': 'unique-snowflake', #yyb 缓存位置标识
}
}
#yyb 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache', #yyb Redis缓存后端
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', #yyb Redis连接URL
}
}
SITE_ID = 1 #yyb 站点ID
#yyb 百度站长平台通知URL
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
#yyb 邮箱配置:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' #yyb SMTP后端
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) #yyb 是否使用TLS
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) #yyb 是否使用SSL
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' #yyb SMTP主机
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) #yyb SMTP端口
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') #yyb 邮箱用户
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') #yyb 邮箱密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER #yyb 默认发件人
SERVER_EMAIL = EMAIL_HOST_USER #yyb 服务器邮箱
#yyb 设置debug=false不会处理异常邮件通知
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] #yyb 管理员邮箱
#yyb 微信管理员密码两次MD5加密
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
#yyb 日志配置
LOG_PATH = os.path.join(BASE_DIR, 'logs') #yyb 日志路径
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True) #yyb 创建日志目录
LOGGING = {
'version': 1, #yyb 日志配置版本
'disable_existing_loggers': False, #yyb 不禁用现有日志记录器
'root': {
'level': 'INFO', #yyb 根日志级别
'handlers': ['console', 'log_file'], #yyb 处理器
},
'formatters': { #yyb 日志格式
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', #yyb 详细格式
}
},
'filters': { #yyb 过滤器
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse', #yyb 要求调试模式为False
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue', #yyb 要求调试模式为True
},
},
'handlers': { #yyb 处理器
'log_file': {
'level': 'INFO', #yyb 日志级别
'class': 'logging.handlers.TimedRotatingFileHandler', #yyb 按时间轮转的文件处理器
'filename': os.path.join(LOG_PATH, 'djangoblog.log'), #yyb 日志文件路径
'when': 'D', #yyb 按天轮转
'formatter': 'verbose', #yyb 使用详细格式
'interval': 1, #yyb 间隔1天
'delay': True, #yyb 延迟创建
'backupCount': 5, #yyb 保留5个备份
'encoding': 'utf-8' #yyb 文件编码
},
'console': { #yyb 控制台处理器
'level': 'DEBUG',
'filters': ['require_debug_true'], #yyb 仅在调试模式下生效
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': { #yyb 空处理器
'class': 'logging.NullHandler',
},
'mail_admins': { #yyb 管理员邮件处理器
'level': 'ERROR',
'filters': ['require_debug_false'], #yyb 仅在非调试模式下生效
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': { #yyb 日志记录器
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True, #yyb 向上传播
},
'django.request': { #yyb Django请求日志
'handlers': ['mail_admins'], #yyb 发送邮件给管理员
'level': 'ERROR',
'propagate': False, #yyb 不向上传播
}
}
}
#yyb 静态文件查找器
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', #yyb 文件系统查找器
'django.contrib.staticfiles.finders.AppDirectoriesFinder', #yyb 应用目录查找器
#yyb other
'compressor.finders.CompressorFinder', #yyb 压缩文件查找器
)
COMPRESS_ENABLED = True #yyb 启用压缩
#yyb COMPRESS_OFFLINE = True #yyb 离线压缩(注释)
#yyb CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
#yyb 从相对URL创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
#yyb CSS压缩器
'compressor.filters.cssmin.CSSMinFilter'
]
#yyb JS压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter' #yyb JS压缩器
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') #yyb 媒体文件根目录
MEDIA_URL = '/media/' #yyb 媒体文件URL
X_FRAME_OPTIONS = 'SAMEORIGIN' #yyb 帧选项:同源策略
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' #yyb 默认自增字段类型
#yyb Elasticsearch配置
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') #yyb Elasticsearch主机
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', #yyb Elasticsearch引擎
},
}
#yyb 插件系统
PLUGINS_DIR = BASE_DIR / 'plugins' #yyb 插件目录
ACTIVE_PLUGINS = [ #yyb 激活的插件列表
'article_copyright', #yyb 文章版权
'reading_time', #yyb 阅读时间
'external_links', #yyb 外部链接
'view_count', #yyb 浏览量统计
'seo_optimizer' #yyb SEO优化
]

@ -1,82 +0,0 @@
#yyb 导入Django站点地图相关模块
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
#yyb 导入博客相关模型
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
"""#yyb 静态页面站点地图"""
priority = 0.5 #yyb 优先级0.0-1.0
changefreq = 'daily' #yyb 更新频率:每天
def items(self):
"""#yyb 返回包含在站点地图中的项目列表"""
return ['blog:index', ] #yyb 博客首页
def location(self, item):
"""#yyb 返回每个项目的绝对URL"""
return reverse(item) #yyb 通过反向解析生成URL
class ArticleSiteMap(Sitemap):
"""#yyb 文章站点地图"""
changefreq = "monthly" #yyb 更新频率:每月
priority = "0.6" #yyb 优先级0.6
def items(self):
"""#yyb 返回所有已发布的文章"""
return Article.objects.filter(status='p') #yyb 状态为'p'(已发布)的文章
def lastmod(self, obj):
"""#yyb 返回文章的最后修改时间"""
return obj.last_modify_time #yyb 文章的最后修改时间
class CategorySiteMap(Sitemap):
"""#yyb 分类站点地图"""
changefreq = "Weekly" #yyb 更新频率:每周
priority = "0.6" #yyb 优先级0.6
def items(self):
"""#yyb 返回所有分类"""
return Category.objects.all() #yyb 所有分类
def lastmod(self, obj):
"""#yyb 返回分类的最后修改时间"""
return obj.last_modify_time #yyb 分类的最后修改时间
class TagSiteMap(Sitemap):
"""#yyb 标签站点地图"""
changefreq = "Weekly" #yyb 更新频率:每周
priority = "0.3" #yyb 优先级0.3(标签页优先级较低)
def items(self):
"""#yyb 返回所有标签"""
return Tag.objects.all() #yyb 所有标签
def lastmod(self, obj):
"""#yyb 返回标签的最后修改时间"""
return obj.last_modify_time #yyb 标签的最后修改时间
class UserSiteMap(Sitemap):
"""#yyb 用户站点地图"""
changefreq = "Weekly" #yyb 更新频率:每周
priority = "0.3" #yyb 优先级0.3(用户页优先级较低)
def items(self):
"""#yyb 返回所有有文章的作者(去重)"""
#yyb 获取所有文章的作者并通过set去重再转换为列表
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
"""#yyb 返回用户的注册时间"""
return obj.date_joined #yyb 用户的注册时间

@ -1,41 +0,0 @@
#yyb 导入必要的模块
import logging
import requests
from django.conf import settings
#yyb 获取日志记录器
logger = logging.getLogger(__name__)
class SpiderNotify():
"""#yyb 搜索引擎爬虫通知类"""
@staticmethod
def baidu_notify(urls):
"""
#yyb 向百度站长平台提交链接,通知百度爬虫抓取更新内容
Args:
urls: 需要通知的URL列表
"""
try:
#yyb 将URL列表转换为换行分隔的字符串格式
data = '\n'.join(urls)
#yyb 向百度站长平台API提交URL数据
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
#yyb 记录API返回结果
logger.info(result.text)
except Exception as e:
#yyb 记录通知过程中的错误
logger.error(e)
@staticmethod
def notify(url):
"""
#yyb 通用的爬虫通知方法(目前仅支持百度)
Args:
url: 需要通知的URL
"""
SpiderNotify.baidu_notify(url)

@ -1,44 +0,0 @@
#yyb 导入Django测试框架
from django.test import TestCase
#yyb 导入工具函数
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
"""#yyb DjangoBlog应用测试类"""
def setUp(self):
"""#yyb 测试初始化方法"""
#yyb 可以在这里设置测试数据,当前为空
pass
def test_utils(self):
"""#yyb 测试工具函数"""
#yyb 测试SHA256加密函数
md5 = get_sha256('test')
self.assertIsNotNone(md5) #yyb 断言加密结果不为空
#yyb 测试Markdown转换函数
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c) #yyb 断言Markdown转换结果不为空
#yyb 测试字典转URL参数字符串函数
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data) #yyb 断言转换结果不为空

@ -1,73 +0,0 @@
"""#yyb djangoblog URL 配置
#yyb `urlpatterns` 列表将 URL 路由到视图。更多信息请参阅:
#yyb https://docs.djangoproject.com/en/1.10/topics/http/urls/
#yyb 示例:
#yyb 函数视图
#yyb 1. 导入: from my_app import views
#yyb 2. 添加 URL 到 urlpatterns: url(r'^$', views.home, name='home')
#yyb 基于类的视图
#yyb 1. 导入: from other_app.views import Home
#yyb 2. 添加 URL 到 urlpatterns: url(r'^$', Home.as_view(), name='home')
#yyb 包含其他 URLconf
#yyb 1. 导入 include() 函数: from django.conf.urls import url, include
#yyb 2. 添加 URL 到 urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns #yyb 国际化URL模式
from django.conf.urls.static import static #yyb 静态文件服务
from django.contrib.sitemaps.views import sitemap #yyb 站点地图视图
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory #yyb Haystack搜索视图工厂
#yyb 导入自定义视图和组件
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site #yyb 自定义管理员站点
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm #yyb Elasticsearch搜索表单
from djangoblog.feeds import DjangoBlogFeed #yyb RSS订阅源
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
#yyb 站点地图配置字典
sitemaps = {
'blog': ArticleSiteMap, #yyb 文章站点地图
'Category': CategorySiteMap, #yyb 分类站点地图
'Tag': TagSiteMap, #yyb 标签站点地图
'User': UserSiteMap, #yyb 用户站点地图
'static': StaticViewSitemap #yyb 静态页面站点地图
}
#yyb 自定义错误处理视图
handler404 = 'blog.views.page_not_found_view' #yyb 404页面未找到
handler500 = 'blog.views.server_error_view' #yyb 500服务器错误
handle403 = 'blog.views.permission_denied_view' #yyb 403权限拒绝
#yyb 基本URL模式
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), #yyb 国际化URL
]
#yyb 添加国际化URL模式
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls), #yyb 自定义管理员后台URL
re_path(r'', include('blog.urls', namespace='blog')), #yyb 博客应用URL
re_path(r'mdeditor/', include('mdeditor.urls')), #yyb Markdown编辑器URL
re_path(r'', include('comments.urls', namespace='comment')), #yyb 评论应用URL
re_path(r'', include('accounts.urls', namespace='account')), #yyb 账户应用URL
re_path(r'', include('oauth.urls', namespace='oauth')), #yyb OAuth认证URL
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, #yyb 站点地图XML
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()), #yyb RSS订阅源URL
re_path(r'^rss/$', DjangoBlogFeed()), #yyb RSS订阅源别名
#yyb 搜索URL使用自定义的Elasticsearch视图和表单
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')), #yyb 服务器管理URL
re_path(r'', include('owntracks.urls', namespace='owntracks')), #yyb OwnTracks位置跟踪URL
prefix_default_language=False #yyb 不在默认语言的URL前添加语言前缀
) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) #yyb 静态文件服务
#yyb 调试模式下启用媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -1,262 +0,0 @@
#!/usr/bin/env python
#yyb encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
#yyb 获取日志记录器
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""#yyb 获取最新的文章ID和评论ID"""
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""#yyb 计算字符串的SHA256哈希值"""
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
"""#yyb 缓存装饰器,用于缓存函数结果
Args:
expiration: 缓存过期时间默认3分钟
"""
def wrapper(func):
def news(*args, **kwargs):
try:
#yyb 尝试从视图获取缓存键
view = args[0]
key = view.get_cache_key()
except:
key = None
if not key:
#yyb 如果没有缓存键,根据函数和参数生成唯一键
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
if value is not None:
#yyb logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
if value is None:
#yyb 如果函数返回None设置默认缓存值
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
#yyb 刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
#yyb 创建模拟请求对象
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
#yyb 获取缓存键并删除缓存
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
"""#yyb 获取当前站点(带缓存)"""
site = Site.objects.get_current()
return site
class CommonMarkdown:
"""#yyb Markdown处理工具类"""
@staticmethod
def _convert_markdown(value):
"""#yyb 内部方法转换Markdown文本为HTML"""
md = markdown.Markdown(
extensions=[
'extra', #yyb 额外扩展
'codehilite', #yyb 代码高亮
'toc', #yyb 目录生成
'tables', #yyb 表格支持
]
)
body = md.convert(value) #yyb 转换Markdown为HTML
toc = md.toc #yyb 获取目录
return body, toc
@staticmethod
def get_markdown_with_toc(value):
"""#yyb 获取带目录的Markdown转换结果"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
"""#yyb 获取Markdown转换结果不带目录"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""#yyb 发送邮件(通过信号机制)"""
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""#yyb 生成6位随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
"""#yyb 将字典转换为URL参数字符串"""
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
"""#yyb 获取博客设置(带缓存)"""
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
#yyb 如果数据库中没有博客设置,创建默认设置
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value) #yyb 设置缓存
return value
def save_user_avatar(url):
'''
#yyb 保存用户头像到本地
:param url:头像url
:return: 本地路径
'''
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2) #yyb 下载头像
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir) #yyb 创建头像目录
#yyb 检查文件是否为图片格式
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext #yyb 生成唯一文件名
logger.info('保存用户头像:' + basedir + save_filename)
#yyb 保存头像文件
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename) #yyb 返回静态文件URL
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png') #yyb 返回默认头像
def delete_sidebar_cache():
"""#yyb 删除侧边栏相关缓存"""
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
def delete_view_cache(prefix, keys):
"""#yyb 删除视图缓存"""
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
"""#yyb 获取资源URL静态文件URL"""
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
#yyb HTML清理允许的标签
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
#yyb HTML清理允许的属性
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
"""#yyb 清理HTML移除不安全的标签和属性"""
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -1,585 +0,0 @@
#yyb encoding: utf-8
#yyb 文件编码声明
from __future__ import absolute_import, division, print_function, unicode_literals
#yyb 兼容Python 2和3的导入
import json
import os
import re
import shutil
import threading
import warnings
import six
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from datetime import datetime
from django.utils.encoding import force_str
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
from haystack.constants import DJANGO_CT, DJANGO_ID, ID
from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
from haystack.inputs import Clean, Exact, PythonData, Raw
from haystack.models import SearchResult
from haystack.utils import get_identifier, get_model_ct
from haystack.utils import log as logging
from haystack.utils.app_loading import haystack_get_model
from jieba.analyse import ChineseAnalyzer #yyb 中文分词器
from whoosh import index
from whoosh.analysis import StemmingAnalyzer
from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
from whoosh.fields import ID as WHOOSH_ID
from whoosh.filedb.filestore import FileStorage, RamStorage
from whoosh.highlight import ContextFragmenter, HtmlFormatter
from whoosh.highlight import highlight as whoosh_highlight
from whoosh.qparser import QueryParser
from whoosh.searching import ResultsPage
from whoosh.writing import AsyncWriter
try:
import whoosh
except ImportError:
#yyb 如果Whoosh没有安装抛出缺失依赖异常
raise MissingDependency(
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
#yyb 处理最低版本要求
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
raise MissingDependency(
"The 'whoosh' backend requires version 2.5.0 or greater.")
#yyb 日期时间正则表达式,用于解析日期时间字符串
DATETIME_REGEX = re.compile(
'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$')
LOCALS = threading.local() #yyb 线程本地存储
LOCALS.RAM_STORE = None
class WhooshHtmlFormatter(HtmlFormatter):
"""
#yyb 简化的Whoosh HTML格式化器
#yyb 我们使用它来在不同后端之间获得一致的结果。
#yyb 具体来说Solr、Xapian和Elasticsearch都使用这种格式化。
"""
template = '<%(tag)s>%(t)s</%(tag)s>'
class WhooshSearchBackend(BaseSearchBackend):
"""#yyb Whoosh搜索引擎后端实现"""
#yyb Whoosh保留的特殊用途单词
RESERVED_WORDS = (
'AND',
'NOT',
'OR',
'TO',
)
#yyb Whoosh保留的特殊用途字符
#yyb '\\'必须放在前面,以免覆盖其他斜杠替换
RESERVED_CHARACTERS = (
'\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
'[', ']', '^', '"', '~', '*', '?', ':', '.',
)
def __init__(self, connection_alias, **connection_options):
super(
WhooshSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.setup_complete = False #yyb 设置完成标志
self.use_file_storage = True #yyb 是否使用文件存储
self.post_limit = getattr(
connection_options,
'POST_LIMIT',
128 * 1024 * 1024) #yyb 帖子大小限制默认128MB
self.path = connection_options.get('PATH') #yyb 索引存储路径
if connection_options.get('STORAGE', 'file') != 'file':
self.use_file_storage = False #yyb 不使用文件存储,使用内存存储
if self.use_file_storage and not self.path:
raise ImproperlyConfigured(
"You must specify a 'PATH' in your settings for connection '%s'." %
connection_alias)
self.log = logging.getLogger('haystack') #yyb 日志记录器
def setup(self):
"""
#yyb 延迟加载,直到需要时才设置
"""
from haystack import connections
new_index = False #yyb 是否创建新索引
#yyb 确保索引目录存在
if self.use_file_storage and not os.path.exists(self.path):
os.makedirs(self.path)
new_index = True
if self.use_file_storage and not os.access(self.path, os.W_OK):
raise IOError(
"The path to your Whoosh index '%s' is not writable for the current user/group." %
self.path)
#yyb 选择存储类型:文件存储或内存存储
if self.use_file_storage:
self.storage = FileStorage(self.path)
else:
global LOCALS
if getattr(LOCALS, 'RAM_STORE', None) is None:
LOCALS.RAM_STORE = RamStorage()
self.storage = LOCALS.RAM_STORE
#yyb 构建schema和内容字段名
self.content_field_name, self.schema = self.build_schema(
connections[self.connection_alias].get_unified_index().all_searchfields())
self.parser = QueryParser(self.content_field_name, schema=self.schema) #yyb 查询解析器
#yyb 创建或打开索引
if new_index is True:
self.index = self.storage.create_index(self.schema)
else:
try:
self.index = self.storage.open_index(schema=self.schema)
except index.EmptyIndexError:
self.index = self.storage.create_index(self.schema)
self.setup_complete = True #yyb 标记设置完成
def build_schema(self, fields):
"""#yyb 构建Whoosh索引schema"""
schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True), #yyb 唯一标识符
DJANGO_CT: WHOOSH_ID(stored=True), #yyb Django内容类型
DJANGO_ID: WHOOSH_ID(stored=True), #yyb Django对象ID
}
#yyb 获取Haystack硬编码的键数量
initial_key_count = len(schema_fields)
content_field_name = '' #yyb 内容字段名
for field_name, field_class in fields.items():
if field_class.is_multivalued: #yyb 多值字段
if field_class.indexed is False:
schema_fields[field_class.index_fieldname] = IDLIST(
stored=True, field_boost=field_class.boost)
else:
schema_fields[field_class.index_fieldname] = KEYWORD(
stored=True, commas=True, scorable=True, field_boost=field_class.boost)
elif field_class.field_type in ['date', 'datetime']: #yyb 日期时间字段
schema_fields[field_class.index_fieldname] = DATETIME(
stored=field_class.stored, sortable=True)
elif field_class.field_type == 'integer': #yyb 整数字段
schema_fields[field_class.index_fieldname] = NUMERIC(
stored=field_class.stored, numtype=int, field_boost=field_class.boost)
elif field_class.field_type == 'float': #yyb 浮点数字段
schema_fields[field_class.index_fieldname] = NUMERIC(
stored=field_class.stored, numtype=float, field_boost=field_class.boost)
elif field_class.field_type == 'boolean': #yyb 布尔字段
#yyb Field boost在1.8.2版本中不支持BOOLEAN
schema_fields[field_class.index_fieldname] = BOOLEAN(
stored=field_class.stored)
elif field_class.field_type == 'ngram': #yyb N-gram字段
schema_fields[field_class.index_fieldname] = NGRAM(
minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
elif field_class.field_type == 'edge_ngram': #yyb 边缘N-gram字段
schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start',
stored=field_class.stored,
field_boost=field_class.boost)
else:
#yyb 默认使用中文分析器的文本字段
#yyb 原代码使用StemmingAnalyzer现改为ChineseAnalyzer以支持中文分词
schema_fields[field_class.index_fieldname] = TEXT(
stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
if field_class.document is True: #yyb 主文档字段
content_field_name = field_class.index_fieldname
schema_fields[field_class.index_fieldname].spelling = True #yyb 启用拼写建议
#yyb 如果没有找到字段,优雅地失败
if len(schema_fields) <= initial_key_count:
raise SearchBackendError(
"No fields were found in any search_indexes. Please correct this before attempting to search.")
return (content_field_name, Schema(**schema_fields))
def update(self, index, iterable, commit=True):
"""#yyb 更新索引文档"""
if not self.setup_complete:
self.setup()
self.index = self.index.refresh() #yyb 刷新索引
writer = AsyncWriter(self.index) #yyb 异步写入器
for obj in iterable:
try:
doc = index.full_prepare(obj) #yyb 准备文档
except SkipDocument:
self.log.debug(u"Indexing for object `%s` skipped", obj)
else:
#yyb 确保所有值都是Unicode因为Whoosh只接受Unicode
for key in doc:
doc[key] = self._from_python(doc[key])
#yyb Whoosh 2.5.0+不支持文档boost
if 'boost' in doc:
del doc['boost']
try:
writer.update_document(**doc) #yyb 更新文档
except Exception as e:
if not self.silently_fail:
raise
#yyb 记录对象标识符但不包含实际对象,避免处理日志消息时产生编码错误
self.log.error(
u"%s while preparing object for update" %
e.__class__.__name__,
exc_info=True,
extra={
"data": {
"index": index,
"object": get_identifier(obj)}})
if len(iterable) > 0:
#yyb 目前无论如何都要提交,否则会遇到锁定问题
writer.commit()
def remove(self, obj_or_string, commit=True):
"""#yyb 从索引中移除文档"""
if not self.setup_complete:
self.setup()
self.index = self.index.refresh()
whoosh_id = get_identifier(obj_or_string)
try:
#yyb 通过查询删除文档
self.index.delete_by_query(
q=self.parser.parse(
u'%s:"%s"' %
(ID, whoosh_id)))
except Exception as e:
if not self.silently_fail:
raise
self.log.error(
"Failed to remove document '%s' from Whoosh: %s",
whoosh_id,
e,
exc_info=True)
def clear(self, models=None, commit=True):
"""#yyb 清空索引"""
if not self.setup_complete:
self.setup()
self.index = self.index.refresh()
if models is not None:
assert isinstance(models, (list, tuple))
try:
if models is None:
self.delete_index() #yyb 完全删除索引
else:
models_to_delete = []
for model in models:
models_to_delete.append(
u"%s:%s" %
(DJANGO_CT, get_model_ct(model)))
#yyb 通过查询删除指定模型的文档
self.index.delete_by_query(
q=self.parser.parse(
u" OR ".join(models_to_delete)))
except Exception as e:
if not self.silently_fail:
raise
if models is not None:
self.log.error(
"Failed to clear Whoosh index of models '%s': %s",
','.join(models_to_delete),
e,
exc_info=True)
else:
self.log.error(
"Failed to clear Whoosh index: %s", e, exc_info=True)
def delete_index(self):
"""#yyb 删除整个索引"""
#yyb 根据Whoosh邮件列表如果要清除索引中的所有内容直接删除索引文件更高效
if self.use_file_storage and os.path.exists(self.path):
shutil.rmtree(self.path)
elif not self.use_file_storage:
self.storage.clean()
#yyb 重新创建所有内容
self.setup()
def optimize(self):
"""#yyb 优化索引"""
if not self.setup_complete:
self.setup()
self.index = self.index.refresh()
self.index.optimize()
def calculate_page(self, start_offset=0, end_offset=None):
"""#yyb 计算分页信息"""
#yyb 防止Whoosh抛出错误。需要end_offset大于0
if end_offset is not None and end_offset <= 0:
end_offset = 1
#yyb 确定页码
page_num = 0
if end_offset is None:
end_offset = 1000000 #yyb 默认大数
if start_offset is None:
start_offset = 0
page_length = end_offset - start_offset #yyb 页面长度
if page_length and page_length > 0:
page_num = int(start_offset / page_length)
#yyb 递增因为Whoosh使用基于1的页码
page_num += 1
return page_num, page_length
@log_query #yyb 记录查询日志的装饰器
def search(
self,
query_string,
sort_by=None,
start_offset=0,
end_offset=None,
fields='',
highlight=False,
facets=None,
date_facets=None,
query_facets=None,
narrow_queries=None,
spelling_query=None,
within=None,
dwithin=None,
distance_point=None,
models=None,
limit_to_registered_models=None,
result_class=None,
**kwargs):
"""#yyb 执行搜索查询"""
if not self.setup_complete:
self.setup()
#yyb 零长度查询应该返回无结果
if len(query_string) == 0:
return {
'results': [],
'hits': 0,
}
query_string = force_str(query_string) #yyb 确保查询字符串是字符串
#yyb 单字符查询(非通配符)会被停用词过滤器捕获,应该返回零结果
if len(query_string) <= 1 and query_string != u'*':
return {
'results': [],
'hits': 0,
}
reverse = False #yyb 是否反转排序
if sort_by is not None:
#yyb 确定是否需要反转结果以及Whoosh是否可以处理被要求排序的字段
#yyb 反转是一个全有或全无的操作
sort_by_list = []
reverse_counter = 0
for order_by in sort_by:
if order_by.startswith('-'): #yyb 降序排序
reverse_counter += 1
if reverse_counter and reverse_counter != len(sort_by):
raise SearchBackendError("Whoosh requires all order_by fields"
" to use the same sort direction")
for order_by in sort_by:
if order_by.startswith('-'):
sort_by_list.append(order_by[1:]) #yyb 移除负号
if len(sort_by_list) == 1:
reverse = True
else:
sort_by_list.append(order_by)
if len(sort_by_list) == 1:
reverse = False
sort_by = sort_by_list[0] #yyb Whoosh只支持单字段排序
#yyb Whoosh不支持分面搜索发出警告
if facets is not None:
warnings.warn(
"Whoosh does not handle faceting.",
Warning,
stacklevel=2)
if date_facets is not None:
warnings.warn(
"Whoosh does not handle date faceting.",
Warning,
stacklevel=2)
if query_facets is not None:
warnings.warn(
"Whoosh does not handle query faceting.",
Warning,
stacklevel=2)
narrowed_results = None
self.index = self.index.refresh()
#yyb 限制到注册的模型
if limit_to_registered_models is None:
limit_to_registered_models = getattr(
settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
#yyb 构建模型选择列表
if models and len(models):
model_choices = sorted(get_model_ct(model) for model in models)
elif limit_to_registered_models:
#yyb 使用窄查询,将结果限制为当前路由器处理的模型
model_choices = self.build_models_list()
else:
model_choices = []
#yyb 如果有模型选择,添加到窄查询中
if len(model_choices) > 0:
if narrow_queries is None:
narrow_queries = set()
narrow_queries.add(' OR '.join(
['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
narrow_searcher = None
#yyb 处理窄查询
if narrow_queries is not None:
#yyb 可能很昂贵在Whoosh中没有看到其他方法...
narrow_searcher = self.index.searcher()
for nq in narrow_queries:
recent_narrowed_results = narrow_searcher.search(
self.parser.parse(force_str(nq)), limit=None)
if len(recent_narrowed_results) <= 0:
return {
'results': [],
'hits': 0,
}
if narrowed_results:
narrowed_results.filter(recent_narrowed_results)
else:
narrowed_results = recent_narrowed_results
self.index = self.index.refresh()
#yyb 如果索引中有文档,执行搜索
if self.index.doc_count():
searcher = self.index.searcher() #yyb 创建搜索器
parsed_query = self.parser.parse(query_string) #yyb 解析查询
#yyb 如果查询无效/包含停用词,优雅地恢复
if parsed_query is None:
return {
'results': [],
'hits': 0,
}
page_num, page_length = self.calculate_page(
start_offset, end_offset)
search_kwargs = {
'pagelen': page_length, #yyb 页面长度
'sortedby': sort_by, #yyb 排序字段
'reverse': reverse, #yyb 是否反转
}
#yyb 处理结果已被窄化的情况
if narrowed_results is not None:
search_kwargs['filter'] = narrowed_results
try:
#yyb 执行分页搜索
raw_page = searcher.search_page(
parsed_query,
page_num,
**search_kwargs
)
except ValueError:
if not self.silently_fail:
raise
return {
'results': [],
'hits': 0,
'spelling_suggestion': None,
}
#yyb 由于Whoosh 2.5.1的问题,如果请求的页码过高,它会返回错误的页面
if raw_page.pagenum < page_num:
return {
'results': [],
'hits': 0,
'spelling_suggestion': None,
}
#yyb 处理搜索结果
results = self._process_results(
raw_page,
highlight=highlight,
query_string=query_string,
spelling_query=spelling_query,
result_class=result_class)
searcher.close() #yyb 关闭搜索器
if hasattr(narrow_searcher, 'close'):
narrow_searcher.close()
return results
else:
#yyb 如果没有文档,处理拼写建议
if self.include_spelling:
if spelling_query:
spelling_suggestion = self.create_spelling_suggestion(
spelling_query)
else:
spelling_suggestion = self.create_spelling_suggestion(
query_string)
else:
spelling_suggestion = None
return {
'results': [],
'hits': 0,
'spelling_suggestion': spelling_suggestion,
}
def more_like_this(
self,
model_instance,
additional_query_string=None,
start_offset=0,
end_offset=None,
models=None,
limit_to_registered_models=None,
result_class=None,
**kwargs

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save