Compare commits

...

78 Commits

Author SHA1 Message Date
pup2vei6r 621f5efc7a ADD file via upload
3 months ago
pup2vei6r 3e3b97d347 ADD file via upload
3 months ago
pup2vei6r 509f9fd6bf ADD file via upload
3 months ago
pup2vei6r 3db7dda8ab ADD file via upload
4 months ago
pup2vei6r 41e8916574 ADD file via upload
4 months ago
pup2vei6r 359bef50ab ADD file via upload
4 months ago
pup2vei6r bf1dcccf95 ADD file via upload
4 months ago
pup2vei6r 167c298a47 ADD file via upload
4 months ago
pup2vei6r f8d9747738 ADD file via upload
4 months ago
pup2vei6r 33ab6bf7b7 ADD file via upload
4 months ago
pup2vei6r 13b30b08f5 ADD file via upload
4 months ago
pup2vei6r 28dc937b21 ADD file via upload
4 months ago
pup2vei6r 24f66856a9 ADD file via upload
4 months ago
pup2vei6r f89650d0b2 ADD file via upload
4 months ago
pup2vei6r b8fc9258d5 ADD file via upload
4 months ago
pup2vei6r 9ed05ffc0d ADD file via upload
4 months ago
pup2vei6r 1428d1df64 ADD file via upload
4 months ago
pup2vei6r a26d13d457 ADD file via upload
4 months ago
pup2vei6r 82234782c5 ADD file via upload
4 months ago
pup2vei6r 65df2f559b ADD file via upload
4 months ago
pup2vei6r 69bd84e16c ADD file via upload
4 months ago
pup2vei6r 54981df7e6 ADD file via upload
4 months ago
pup2vei6r b38abf9019 ADD file via upload
4 months ago
pup2vei6r fa6da37e1c ADD file via upload
4 months ago
pup2vei6r 4bac9a4de4 ADD file via upload
4 months ago
pup2vei6r 2397ab8b7e ADD file via upload
4 months ago
pup2vei6r 8e23e872a1 ADD file via upload
4 months ago
pup2vei6r 283942cca0 ADD file via upload
4 months ago
pup2vei6r f38c38d5b5 ADD file via upload
4 months ago
pup2vei6r ff11b0afcf ADD file via upload
4 months ago
pup2vei6r c000cdcb1a ADD file via upload
4 months ago
pup2vei6r 7de45ddffa ADD file via upload
4 months ago
pup2vei6r 53563903ec ADD file via upload
4 months ago
pup2vei6r 3092c828db ADD file via upload
4 months ago
pup2vei6r b6a045969c ADD file via upload
4 months ago
pup2vei6r 9a53163a38 ADD file via upload
4 months ago
pup2vei6r 2b833099ff ADD file via upload
4 months ago
pup2vei6r b7a1d78ce2 ADD file via upload
4 months ago
pup2vei6r ed395a15ad ADD file via upload
4 months ago
pup2vei6r c138204236 ADD file via upload
4 months ago
pup2vei6r 11b19ec75b ADD file via upload
4 months ago
pup2vei6r 81583a650d ADD file via upload
4 months ago
pup2vei6r e80d20862f ADD file via upload
4 months ago
pup2vei6r aae9a0d7d5 ADD file via upload
4 months ago
pup2vei6r 5d3c9b037f ADD file via upload
4 months ago
pup2vei6r a1cf593ac8 ADD file via upload
4 months ago
pup2vei6r 733cf01d72 ADD file via upload
4 months ago
pup2vei6r 47a48dec61 ADD file via upload
4 months ago
pup2vei6r 133d1fa2b3 ADD file via upload
4 months ago
pup2vei6r 67a1f81e6c ADD file via upload
4 months ago
pup2vei6r bdd01e0640 ADD file via upload
4 months ago
pup2vei6r 51de2474ba ADD file via upload
4 months ago
pup2vei6r ab1cb87369 ADD file via upload
4 months ago
pup2vei6r 23e6eff0eb ADD file via upload
4 months ago
pup2vei6r 7b57183122 ADD file via upload
4 months ago
pup2vei6r 6918a3468b ADD file via upload
4 months ago
pup2vei6r 7ad4a7203d ADD file via upload
4 months ago
pup2vei6r 98b6764542 ADD file via upload
4 months ago
pup2vei6r 577ab1808a ADD file via upload
4 months ago
pup2vei6r e415eba80f ADD file via upload
4 months ago
pup2vei6r 7c3d3da5b8 ADD file via upload
4 months ago
pup2vei6r 7a906aef9b ADD file via upload
4 months ago
pup2vei6r 980fd9ea1e ADD file via upload
4 months ago
pup2vei6r 1022a18d9a ADD file via upload
4 months ago
pup2vei6r 1d549453cc ADD file via upload
4 months ago
pup2vei6r 906099fa80 ADD file via upload
4 months ago
pup2vei6r 949d5f15ef ADD file via upload
4 months ago
pup2vei6r c47ceb2ab0 ADD file via upload
4 months ago
pup2vei6r 1d67d6af8f ADD file via upload
4 months ago
pup2vei6r d46ceb3995 Delete 'doc/第六周大作业(个人想法).docx'
4 months ago
pup2vei6r c192063ff8 Delete 'doc/第五周大作业(个人想法).docx'
4 months ago
pup2vei6r b6375072db ADD file via upload
4 months ago
pup2vei6r 46af711ff3 ADD file via upload
4 months ago
pup2vei6r 102ed61014 Update README.md
5 months ago
pup2vei6r 1ed36f7564 Merge pull request '1' (#1) from lxy_branch into master
5 months ago
刘夏渝 e544cc5289 TTTTest
5 months ago
刘夏渝 4aaff977e2 更新文档
5 months ago
刘夏渝 0cccd9607e 更新README和main.py文件
5 months ago

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

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,262 @@
#!/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)

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

@ -0,0 +1,82 @@
#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)

@ -0,0 +1,21 @@
#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()

@ -0,0 +1,163 @@
#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,217 @@
#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 指定查询类

@ -0,0 +1,59 @@
#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

@ -0,0 +1,114 @@
#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

@ -0,0 +1,345 @@
"""
#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优化
]

@ -0,0 +1,82 @@
#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 用户的注册时间

@ -0,0 +1,41 @@
#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)

@ -0,0 +1,44 @@
#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 断言转换结果不为空

@ -0,0 +1,73 @@
"""#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)

@ -0,0 +1,262 @@
#!/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)

@ -0,0 +1,585 @@
#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

@ -0,0 +1,20 @@
"""
#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()

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

Binary file not shown.

@ -0,0 +1,21 @@
#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()

@ -0,0 +1,163 @@
#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,217 @@
#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 指定查询类

@ -0,0 +1,59 @@
#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

@ -0,0 +1,114 @@
#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 +1,32 @@
print("Hello World")
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)

@ -0,0 +1,41 @@
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
}

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

@ -0,0 +1,44 @@
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

@ -0,0 +1,19 @@
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)

@ -0,0 +1,345 @@
"""
#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优化
]

@ -0,0 +1,82 @@
#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 用户的注册时间

@ -0,0 +1,41 @@
#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)

@ -0,0 +1,44 @@
#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 断言转换结果不为空

@ -0,0 +1,73 @@
"""#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)

@ -0,0 +1,262 @@
#!/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)

@ -0,0 +1,585 @@
#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

@ -0,0 +1,20 @@
"""
#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()

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