From ee8428a382832e2969d90cd28c02cb0eb008fedc Mon Sep 17 00:00:00 2001 From: zyy <1339568841@qq.com> Date: Fri, 7 Nov 2025 22:26:58 +0800 Subject: [PATCH] =?UTF-8?q?ZYY=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/djangoblog/admin_site.py | 81 ++- src/django-master/djangoblog/apps.py | 22 +- src/django-master/djangoblog/blog_signals.py | 82 ++- .../djangoblog/elasticsearch_backend.py | 100 ++- src/django-master/djangoblog/feeds.py | 35 +- src/django-master/djangoblog/logentryadmin.py | 48 +- .../djangoblog/plugin_manage/base_plugin.py | 33 +- .../plugin_manage/hook_constants.py | 17 +- .../djangoblog/plugin_manage/hooks.py | 45 +- .../djangoblog/plugin_manage/loader.py | 19 +- src/django-master/djangoblog/settings.py | 468 +++++++------- src/django-master/djangoblog/sitemap.py | 40 +- src/django-master/djangoblog/spider_notify.py | 15 +- src/django-master/djangoblog/tests.py | 12 +- src/django-master/djangoblog/urls.py | 74 ++- src/django-master/djangoblog/utils.py | 69 +- .../djangoblog/whoosh_cn_backend.py | 595 +++++++++--------- src/django-master/djangoblog/wsgi.py | 10 +- 18 files changed, 1034 insertions(+), 731 deletions(-) diff --git a/src/django-master/djangoblog/admin_site.py b/src/django-master/djangoblog/admin_site.py index f120405..9a3bbbb 100644 --- a/src/django-master/djangoblog/admin_site.py +++ b/src/django-master/djangoblog/admin_site.py @@ -1,22 +1,25 @@ +# ZYY 导入 Django 内置的 AdminSite 和 LogEntry 模型 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 - -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 * - - +from django.contrib.admin.models import LogEntry # ZYY操作日志模型 +from django.contrib.sites.admin import SiteAdmin # ZYYDjango 内置站点管理 +from django.contrib.sites.models import Site # ZYY多站点支持模型 + +# ZYY 导入自定义应用的 admin 和 models +from accounts.admin import * #ZYY 用户账户管理 +from blog.admin import *# ZYY博客核心管理 +from blog.models import * # ZYY博客数据模型 +from comments.admin import *#ZYY 评论管理 +from comments.models import * # ZYY评论数据模型 +# ZYY 导入自定义的 LogEntryAdmin +from djangoblog.logentryadmin import LogEntryAdmin # ZYY自定义日志管理 +from oauth.admin import * # ZYY第三方登录管理 +from oauth.models import * # ZYY第三方登录模型 +from owntracks.admin import * #ZYY 位置跟踪管理 +from owntracks.models import *# ZYY位置跟踪模型 +from servermanager.admin import * #ZYY 服务器管理 +from servermanager.models import *# ZYY服务器模型 + +# ZYY 自定义 AdminSite 类 class DjangoBlogAdminSite(AdminSite): site_header = 'djangoblog administration' site_title = 'djangoblog site admin' @@ -27,6 +30,7 @@ class DjangoBlogAdminSite(AdminSite): def has_permission(self, request): return request.user.is_superuser + # ZYY 自定义 URL 的示例(已注释) # def get_urls(self): # urls = super().get_urls() # from django.urls import path @@ -37,28 +41,37 @@ class DjangoBlogAdminSite(AdminSite): # ] # return urls + my_urls - +# ZYY 实例化自定义 AdminSite admin_site = DjangoBlogAdminSite(name='admin') -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) +# ZYY 注册 blog 应用的模型和管理类 +admin_site.register(Article, ArticlelAdmin)# ZYY文章管理 +admin_site.register(Category, CategoryAdmin) # ZYY分类管理 +admin_site.register(Tag, TagAdmin) #ZYY 标签管理 +admin_site.register(Links, LinksAdmin) # ZYY友情链接 +admin_site.register(SideBar, SideBarAdmin)# ZYY侧边栏配置 +admin_site.register(BlogSettings, BlogSettingsAdmin)# ZYY博客全局设置 + + #ZYY 注册 servermanager 应用的模型和管理类 +admin_site.register(commands, CommandsAdmin) #ZYY 命令记录 +admin_site.register(EmailSendLog, EmailSendLogAdmin)# ZYY邮件日志 -admin_site.register(commands, CommandsAdmin) -admin_site.register(EmailSendLog, EmailSendLogAdmin) +# ZYY 注册 accounts 应用的模型和管理类 +admin_site.register(BlogUser, BlogUserAdmin) # ZYY博客用户 -admin_site.register(BlogUser, BlogUserAdmin) +# ZYY 注册 comments 应用的模型和管理类 +admin_site.register(Comment, CommentAdmin)#ZYY 评论内容 -admin_site.register(Comment, CommentAdmin) -admin_site.register(OAuthUser, OAuthUserAdmin) -admin_site.register(OAuthConfig, OAuthConfigAdmin) +# ZYY 注册 oauth 应用的模型和管理类 +admin_site.register(OAuthUser, OAuthUserAdmin) #ZYY绑定账号 +admin_site.register(OAuthConfig, OAuthConfigAdmin) #ZYY 平台配置 -admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) +# ZYY 注册 owntracks 应用的模型和管理类 +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # ZYY移动轨迹 -admin_site.register(Site, SiteAdmin) +# ZYY 注册 Django 内置的 Site 模型和管理类 +admin_site.register(Site, SiteAdmin) # ZYY多站点配置 -admin_site.register(LogEntry, LogEntryAdmin) +# ZYY 注册 Django 内置的 LogEntry 模型和自定义 LogEntryAdmin +admin_site.register(LogEntry, LogEntryAdmin) # ZYY管理操作日志 diff --git a/src/django-master/djangoblog/apps.py b/src/django-master/djangoblog/apps.py index d29e318..51c7f9f 100644 --- a/src/django-master/djangoblog/apps.py +++ b/src/django-master/djangoblog/apps.py @@ -1,11 +1,27 @@ +# ZYYDjango 应用配置类 from django.apps import AppConfig class DjangoblogAppConfig(AppConfig): + # ZYY: 指定默认主键字段类型为BigAutoField(64位自增ID) + # ZYY替代旧版AutoField(32位),适合数据量大的应用 default_auto_field = 'django.db.models.BigAutoField' + + # ZYY: 应用唯一标识,需与项目目录名一致 + # 用于Django内部识别应用(如管理后台、迁移等) name = 'djangoblog' def ready(self): - super().ready() - # Import and load plugins here + """ZYY: 应用启动时的初始化钩子 + - Django在完成应用注册后会自动调用 + - 适合执行启动时加载的任务(如插件系统、信号注册等) + - 注意:此方法可能被多次调用(特别是在开发服务器热重载时) + """ + super().ready() # 确保父类初始化逻辑执行 + + # ZYY: 插件系统加载入口 + # ZYY设计说明: + # ZYY1. 延迟导入避免循环依赖(AppConfig初始化阶段不宜大量导入) + # ZYY2. 插件系统应实现幂等性(应对ready()多次调用) + # ZYY3. 建议添加异常处理防止插件加载失败影响应用启动 from .plugin_manage.loader import load_plugins - load_plugins() \ No newline at end of file + load_plugins() \ No newline at end of file diff --git a/src/django-master/djangoblog/blog_signals.py b/src/django-master/djangoblog/blog_signals.py index 393f441..ab597cb 100644 --- a/src/django-master/djangoblog/blog_signals.py +++ b/src/django-master/djangoblog/blog_signals.py @@ -1,34 +1,41 @@ -import _thread +# ZYY信号处理与系统通知模块 +import _thread # ZYY: 使用底层线程处理耗时操作(如邮件发送),避免阻塞主请求 import logging import django.dispatch from django.conf import settings -from django.contrib.admin.models import LogEntry +from django.contrib.admin.models import LogEntry # ZYY: 排除管理后台操作日志的缓存清理 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 from comments.models import Comment -from comments.utils import send_comment_email -from djangoblog.spider_notify import SpiderNotify +from comments.utils import send_comment_email # ZYY: 异步发送评论通知邮件 +from djangoblog.spider_notify import SpiderNotify # ZYY: 搜索引擎推送接口 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 logger = logging.getLogger(__name__) -oauth_user_login_signal = django.dispatch.Signal(['id']) -send_email_signal = django.dispatch.Signal( - ['emailto', 'title', 'content']) +# ZYY: 自定义信号定义 +oauth_user_login_signal = django.dispatch.Signal(['id']) # ZYY: OAuth用户登录后处理信号 +send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content']) # ZYY: 邮件发送信号 @receiver(send_email_signal) def send_email_signal_handler(sender, **kwargs): + """ZYY: 邮件发送信号处理器 + - 使用信号机制解耦邮件发送逻辑 + - 自动记录发送日志到数据库 + - 捕获异常避免影响主流程 + """ emailto = kwargs['emailto'] title = kwargs['title'] content = kwargs['content'] + # ZYY: 构造多部分邮件(支持HTML内容) msg = EmailMultiAlternatives( title, content, @@ -36,6 +43,7 @@ def send_email_signal_handler(sender, **kwargs): to=emailto) msg.content_subtype = "html" + # ZYY: 记录邮件发送日志 from servermanager.models import EmailSendLog log = EmailSendLog() log.title = title @@ -44,7 +52,7 @@ def send_email_signal_handler(sender, **kwargs): try: result = msg.send() - log.send_result = result > 0 + log.send_result = result > 0 # ZYY: 根据返回值判断是否发送成功 except Exception as e: logger.error(f"失败邮箱号: {emailto}, {e}") log.send_result = False @@ -53,62 +61,78 @@ def send_email_signal_handler(sender, **kwargs): @receiver(oauth_user_login_signal) def oauth_user_login_signal_handler(sender, **kwargs): + """ZYY: OAuth用户登录后处理 + - 自动处理头像域名适配 + - 清理侧边栏缓存 + """ id = kwargs['id'] oauthuser = OAuthUser.objects.get(id=id) site = get_current_site().domain + + # ZYY: 处理头像URL域名适配(避免混合内容警告) 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() - delete_sidebar_cache() + delete_sidebar_cache() # ZYY: 用户信息变更后清理相关缓存 @receiver(post_save) -def model_post_save_callback( - sender, - instance, - created, - raw, - using, - update_fields, - **kwargs): +def model_post_save_callback(sender, instance, created, raw, using, update_fields, **kwargs): + """ZYY: 模型保存后通用处理器 + - 处理内容更新后的缓存清理 + - 搜索引擎URL提交 + - 评论通知的异步处理 + """ clearcache = False + + # ZYY: 排除管理后台日志对象 if isinstance(instance, LogEntry): return + + # ZYY: 处理支持URL获取的模型(如文章、页面等) if 'get_full_url' in dir(instance): - is_update_views = update_fields == {'views'} + is_update_views = update_fields == {'views'} # ZYY: 仅浏览量更新时不触发完整处理 + + # ZYY: 非测试环境且非浏览量更新时推送搜索引擎 if not settings.TESTING and not is_update_views: try: notify_url = instance.get_full_url() - SpiderNotify.baidu_notify([notify_url]) + SpiderNotify.baidu_notify([notify_url]) # ZYY: 百度站长推送 except Exception as ex: logger.error("notify sipder", ex) + if not is_update_views: - clearcache = True + clearcache = True # ZYY: 标记需要清理缓存 + # ZYY: 评论处理特别逻辑 if isinstance(instance, Comment): - if instance.is_enable: + if instance.is_enable: # ZYY: 仅处理已启用的评论 path = instance.article.get_absolute_url() site = get_current_site().domain - if site.find(':') > 0: + if site.find(':') > 0: # ZYY: 去除端口号 site = site[0:site.find(':')] + # ZYY: 清理多级缓存(文章详情页、SEO数据、评论列表等) expire_view_cache( path, servername=site, serverport=80, key_prefix='blogdetail') + if cache.get('seo_processor'): cache.delete('seo_processor') - comment_cache_key = 'article_comments_{id}'.format( - id=instance.article.id) + + comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id) cache.delete(comment_cache_key) delete_sidebar_cache() delete_view_cache('article_comments', [str(instance.article.pk)]) + # ZYY: 使用独立线程发送评论通知邮件 _thread.start_new_thread(send_comment_email, (instance,)) + # ZYY: 通用缓存清理(当内容更新时) if clearcache: cache.clear() @@ -116,7 +140,11 @@ def model_post_save_callback( @receiver(user_logged_in) @receiver(user_logged_out) def user_auth_callback(sender, request, user, **kwargs): + """ZYY: 用户认证状态变化处理 + - 记录用户登录日志 + - 清理用户相关缓存 + """ if user and user.username: - logger.info(user) - delete_sidebar_cache() - # cache.clear() + logger.info(user) # ZYY: 记录用户认证行为 + delete_sidebar_cache() # ZYY: 用户状态变化后清理缓存 + # cache.clear() # ZYY: 注释掉的激进缓存清理策略 \ No newline at end of file diff --git a/src/django-master/djangoblog/elasticsearch_backend.py b/src/django-master/djangoblog/elasticsearch_backend.py index 4afe498..052b035 100644 --- a/src/django-master/djangoblog/elasticsearch_backend.py +++ b/src/django-master/djangoblog/elasticsearch_backend.py @@ -13,49 +13,83 @@ logger = logging.getLogger(__name__) class ElasticSearchBackend(BaseSearchBackend): def __init__(self, connection_alias, **connection_options): + # ZYY 初始化Elasticsearch搜索后端 + # ZYY参数: + # ZYY connection_alias: 连接别名 + # ZYY connection_options: 连接选项 super( ElasticSearchBackend, self).__init__( connection_alias, **connection_options) - self.manager = ArticleDocumentManager() - self.include_spelling = True + self.manager = ArticleDocumentManager()# ZYY 文章文档管理器实例 + self.include_spelling = True # ZYY 是否包含拼写建议 def _get_models(self, iterable): + # ZYY 获取模型列表并转换为文档格式 + # ZYY参数: + # ZYY iterable: 可迭代对象,包含要处理的模型实例 + # ZYY返回: + # ZYY 转换后的文档列表 models = iterable if iterable and iterable[0] else Article.objects.all() docs = self.manager.convert_to_doc(models) return docs def _create(self, models): + # ZYY 创建索引并重建文档 + # 参数: + # models: 要处理的模型列表 self.manager.create_index() docs = self._get_models(models) self.manager.rebuild(docs) def _delete(self, models): + # ZYY 删除模型对应的文档 + # 参数: + # models: 要删除的模型列表 for m in models: m.delete() return True def _rebuild(self, models): + # ZYY 重建索引 + # 参数: + # models: 要重建的模型列表,为空则处理所有文章 models = models if models else Article.objects.all() docs = self.manager.convert_to_doc(models) self.manager.update_docs(docs) def update(self, index, iterable, commit=True): + # ZYY 更新索引 + # 参数: + # index: 索引名称 + # iterable: 可迭代对象,包含要更新的模型实例 + # commit: 是否立即提交 models = self._get_models(iterable) self.manager.update_docs(models) def remove(self, obj_or_string): + # ZYY 从索引中移除对象 + # 参数: + # obj_or_string: 要移除的对象或字符串 models = self._get_models([obj_or_string]) self._delete(models) def clear(self, models=None, commit=True): + # ZYY 清空索引 + # 参数: + # models: 要清空的模型列表(未使用,始终清空所有) + # commit: 是否立即提交 self.remove(None) @staticmethod def get_suggestion(query: str) -> str: - """获取推荐词, 如果没有找到添加原搜索词""" + """ZYY获取推荐词, 如果没有找到添加原搜索词 + 参数: + query: 原始查询字符串 + 返回: + 推荐的搜索词字符串""" search = ArticleDocument.search() \ .query("match", body=query) \ @@ -73,17 +107,27 @@ class ElasticSearchBackend(BaseSearchBackend): @log_query def search(self, query_string, **kwargs): + # ZYY 执行搜索 + # 参数: + # query_string: 查询字符串 + # **kwargs: 其他搜索参数,包括: + # start_offset: 起始偏移量 + # end_offset: 结束偏移量 logger.info('search query_string:' + query_string) start_offset = kwargs.get('start_offset') end_offset = kwargs.get('end_offset') - # 推荐词搜索 + # ZYY推荐词搜索 if getattr(self, "is_suggest", None): suggestion = self.get_suggestion(query_string) else: suggestion = query_string + # ZYY构建复合查询: + # ZYY1. 应该匹配body或title字段 + # ZYY2. 最小匹配度为70% + # ZYY3. 过滤状态为'p'(已发布)和类型为'a'(文章) q = Q('bool', should=[Q('match', body=suggestion), Q('match', title=suggestion)], minimum_should_match="70%") @@ -95,15 +139,16 @@ class ElasticSearchBackend(BaseSearchBackend): .source(False)[start_offset: end_offset] results = search.execute() - hits = results['hits'].total + hits = results['hits'].total # ZYY 总命中数 raw_results = [] for raw_result in results['hits']['hits']: - app_label = 'blog' - model_name = 'Article' - additional_fields = {} + app_label = 'blog'# ZYY 应用标签 + model_name = 'Article'# ZYY 模型名称 + additional_fields = {}# ZYY 额外字段(当前未使用) result_class = SearchResult + # ZYY: 构建搜索结果对象并收集到列表中 result = result_class( app_label, model_name, @@ -111,25 +156,32 @@ class ElasticSearchBackend(BaseSearchBackend): raw_result['_score'], **additional_fields) raw_results.append(result) + # ZYY: 初始化分面数据和拼写建议 facets = {} spelling_suggestion = None if query_string == suggestion else suggestion - + # ZYY: 返回标准化的搜索结果结构 return { - 'results': raw_results, - 'hits': hits, - 'facets': facets, - 'spelling_suggestion': spelling_suggestion, + 'results': raw_results, # ZYY: 实际搜索结果对象列表 + 'hits': hits,# ZYY: 总命中数 + 'facets': facets,# ZYY: 分面统计数据(当前为空) + 'spelling_suggestion': spelling_suggestion,# ZYY: 拼写纠正建议 } class ElasticSearchQuery(BaseSearchQuery): + # ZYY: 将datetime对象转换为ES兼容的字符串格式 def _convert_datetime(self, date): if hasattr(date, 'hour'): - return force_str(date.strftime('%Y%m%d%H%M%S')) + return force_str(date.strftime('%Y%m%d%H%M%S')) # ZYY: 包含时间的完整格式 else: - return force_str(date.strftime('%Y%m%d000000')) + return force_str(date.strftime('%Y%m%d000000')) # ZYY: 仅日期的格式 + # ZYY: 清理用户输入的查询片段 def clean(self, query_fragment): + """ + ZYY: 提供在将用户输入呈现给后端之前进行净化的机制 + Whoosh 1.X在此有所不同,不能再使用反斜杠转义保留字符,而是应该引用整个单词 + """ """ Provides a mechanism for sanitizing user input before presenting the value to the backend. @@ -142,9 +194,11 @@ class ElasticSearchQuery(BaseSearchQuery): cleaned_words = [] for word in words: + # ZYY: 处理保留字(转为小写) if word in self.backend.RESERVED_WORDS: word = word.replace(word, word.lower()) + # ZYY: 处理保留字符(用单引号包裹整个词) for char in self.backend.RESERVED_CHARACTERS: if char in word: word = "'%s'" % word @@ -153,31 +207,35 @@ class ElasticSearchQuery(BaseSearchQuery): cleaned_words.append(word) return ' '.join(cleaned_words) - + # ZYY: 构建查询片段(这里直接返回原始查询字符串) def build_query_fragment(self, field, filter_type, value): return value.query_string +# ZYY: 获取结果总数 def get_count(self): results = self.get_results() return len(results) if results else 0 + # ZYY: 获取拼写建议 def get_spelling_suggestion(self, preferred_query=None): return self._spelling_suggestion + # ZYY: 构建查询参数 def build_params(self, spelling_query=None): kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) return kwargs - +# ZYY: 扩展ModelSearchForm以支持Elasticsearch特定功能 class ElasticSearchModelSearchForm(ModelSearchForm): def search(self): # 是否建议搜索 + # ZYY: 根据请求参数设置是否启用搜索建议 self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" - sqs = super().search() + sqs = super().search()# ZYY: 调用父类搜索方法 return sqs - +# ZYY: Elasticsearch搜索引擎实现 class ElasticSearchEngine(BaseEngine): - backend = ElasticSearchBackend - query = ElasticSearchQuery + backend = ElasticSearchBackend # ZYY: 指定使用的后端 + query = ElasticSearchQuery # ZYY: 指定使用的查询类 diff --git a/src/django-master/djangoblog/feeds.py b/src/django-master/djangoblog/feeds.py index 8c4e851..62907c1 100644 --- a/src/django-master/djangoblog/feeds.py +++ b/src/django-master/djangoblog/feeds.py @@ -6,35 +6,60 @@ from django.utils.feedgenerator import Rss201rev2Feed from blog.models import Article from djangoblog.utils import CommonMarkdown - +# ZYY: Django内置Feed类,用于生成RSS/Atom订阅源 class DjangoBlogFeed(Feed): + # ZYY: 指定使用RSS 2.0规范(支持命名空间扩展) feed_type = Rss201rev2Feed - description = '大巧无工,重剑无锋.' - title = "且听风吟 大巧无工,重剑无锋. " - link = "/feed/" + # ZYY: ================ 订阅源元数据配置 ================ + description = '大巧无工,重剑无锋.'# ZYY: 订阅源副标题/描述 + title = "且听风吟 大巧无工,重剑无锋. "# ZYY: 订阅源主标题 + link = "/feed/" # ZYY: 订阅源自引用URL(实际应为网站根URL) + # ZYY: ================ 作者信息方法 ================ + # ZYY: 注意:这些方法在每次生成feed时都会查询数据库 def author_name(self): + # ZYY: 获取站点作者昵称(潜在问题:未处理无用户情况) return get_user_model().objects.first().nickname def author_link(self): + # ZYY: 获取作者个人页面URL(假设用户模型有get_absolute_url方法) return get_user_model().objects.first().get_absolute_url() + # ZYY: ================ 订阅内容核心方法 ================ def items(self): + # ZYY: 筛选条件: + # ZYYtype='a' - 只包含文章类型(可能区分文章/页面等) + # ZYYstatus='p' - 只包含已发布状态(避免草稿泄露) + # ZYYorder_by('-pub_time') - 按发布时间降序 + # ZYY[:5] - 限制最近5篇 return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] + # ZYY: ================ 单个条目字段映射 ================ def item_title(self, item): + # ZYY: 直接使用文章标题作为条目标题 return item.title def item_description(self, item): + # ZYY: 将文章内容通过Markdown渲染后作为描述 + # 注意:可能产生XSS风险,需确保CommonMarkdown有净化处理 return CommonMarkdown.get_markdown(item.body) + # ZYY: ================ 订阅源附加信息 ================ def feed_copyright(self): + # ZYY: 动态生成版权声明(自动更新年份) now = timezone.now() return "Copyright© {year} 且听风吟".format(year=now.year) + def item_link(self, item): + # ZYY: 使用文章自身的绝对URL作为条目链接 return item.get_absolute_url() + # ZYY: ================ 条目唯一标识 ================ def item_guid(self, item): - return + # ZYY: 原代码不完整,通常应实现为: + # ZYYreturn item.get_absolute_url() # 使用URL作为唯一标识 + #ZYY 或 return str(item.id) # 使用数据库ID + # ZYY当前实现缺失会导致某些阅读器无法识别条目更新 + return # ZYY: 注意这里缺少返回值,实际使用会报错 diff --git a/src/django-master/djangoblog/logentryadmin.py b/src/django-master/djangoblog/logentryadmin.py index 2f6a535..109b7d7 100644 --- a/src/django-master/djangoblog/logentryadmin.py +++ b/src/django-master/djangoblog/logentryadmin.py @@ -9,83 +9,107 @@ from django.utils.translation import gettext_lazy as _ class LogEntryAdmin(admin.ModelAdmin): + # ZYY: 列表过滤器配置 - 允许按内容类型筛选日志 list_filter = [ 'content_type' ] + # ZYY: 搜索字段配置 - 允许按对象表示和变更消息搜索 search_fields = [ 'object_repr', 'change_message' ] + # ZYY: 可点击的列表显示字段 - 指定哪些字段可以点击进入详情页 list_display_links = [ 'action_time', 'get_change_message', ] + + # ZYY: 列表显示字段配置 - 定义在日志列表中显示的字段 list_display = [ - 'action_time', - 'user_link', - 'content_type', - 'object_link', - 'get_change_message', + 'action_time', #ZYY 操作时间 + 'user_link', # ZYY用户链接(自定义方法) + 'content_type', # ZYY内容类型 + 'object_link', # ZYY对象链接(自定义方法) + 'get_change_message', #ZYY 变更消息 ] + # ZYY: 权限控制 - 禁止通过admin添加日志条目 def has_add_permission(self, request): return False + # ZYY: 权限控制 - 允许超级用户或有特定权限的用户修改日志(但禁止POST请求) def has_change_permission(self, request, obj=None): return ( request.user.is_superuser or request.user.has_perm('admin.change_logentry') ) and request.method != 'POST' + # ZYY: 权限控制 - 禁止通过admin删除日志条目 def has_delete_permission(self, request, obj=None): return False + # ZYY: 自定义方法 - 生成对象链接(如果是删除操作则显示纯文本) def object_link(self, obj): + # ZYY: 初始化为转义后的对象表示字符串(防XSS) object_link = escape(obj.object_repr) content_type = obj.content_type + # ZYY: 如果不是删除操作且有内容类型,尝试生成可点击链接 if obj.action_flag != DELETION and content_type is not None: - # try returning an actual link instead of object repr string try: + # ZYY: 构建admin修改页面的URL url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), args=[obj.object_id] ) + # ZYY: 生成HTML链接(后续会标记为安全) object_link = '{}'.format(url, object_link) except NoReverseMatch: + # ZYY: 如果URL反转失败,保持纯文本显示 pass + # ZYY: 标记字符串为安全HTML(因为我们已经正确转义和构建) return mark_safe(object_link) - object_link.admin_order_field = 'object_repr' - object_link.short_description = _('object') + # ZYY: 配置列表排序和描述信息 + object_link.admin_order_field = 'object_repr' # 按object_repr字段排序 + object_link.short_description = _('object') # 列标题显示为"object" + # ZYY: 自定义方法 - 生成用户链接 def user_link(self, obj): + # ZYY: 获取用户模型的内容类型 content_type = ContentType.objects.get_for_model(type(obj.user)) + # ZYY: 初始化为转义后的用户字符串表示 user_link = escape(force_str(obj.user)) try: - # try returning an actual link instead of object repr string + # ZYY: 尝试构建用户admin修改页面的URL url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), args=[obj.user.pk] ) + # ZYY: 生成HTML链接 user_link = '{}'.format(url, user_link) except NoReverseMatch: + # ZYY: URL反转失败时保持纯文本 pass + # ZYY: 标记为安全HTML return mark_safe(user_link) - user_link.admin_order_field = 'user' - user_link.short_description = _('user') + # ZYY: 配置列表排序和描述信息 + user_link.admin_order_field = 'user' # ZYY按user字段排序 + user_link.short_description = _('user') # ZYY列标题显示为"user" + # ZYY: 优化查询集 - 预取关联的内容类型减少查询次数 def get_queryset(self, request): queryset = super(LogEntryAdmin, self).get_queryset(request) return queryset.prefetch_related('content_type') + # ZYY: 操作控制 - 移除默认的批量删除操作 def get_actions(self, request): actions = super(LogEntryAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] - return actions + return actions \ No newline at end of file diff --git a/src/django-master/djangoblog/plugin_manage/base_plugin.py b/src/django-master/djangoblog/plugin_manage/base_plugin.py index 2b4be5c..571e392 100644 --- a/src/django-master/djangoblog/plugin_manage/base_plugin.py +++ b/src/django-master/djangoblog/plugin_manage/base_plugin.py @@ -1,41 +1,60 @@ +# ZYY: 导入Python标准库的logging模块,用于记录日志 import logging +# ZYY: 获取当前模块的日志记录器 +# ZYY: __name__表示当前模块名,logging会根据模块名自动组织日志层次 logger = logging.getLogger(__name__) +# ZYY: 定义插件基类,所有插件应继承此类 class BasePlugin: - # 插件元数据 + # ZYY: 插件元数据字段声明(类属性) + # ZYY: PLUGIN_NAME: 插件名称(必须) PLUGIN_NAME = None + # ZYY: PLUGIN_DESCRIPTION: 插件描述(必须) PLUGIN_DESCRIPTION = None + # ZYY: PLUGIN_VERSION: 插件版本(必须) PLUGIN_VERSION = None + # ZYY: 构造函数,在实例化插件时自动调用 def __init__(self): + # ZYY: 检查必需的元数据是否已定义 + # ZYY: 如果任何元数据为None,则抛出ValueError异常 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.") + + # ZYY: 调用插件初始化方法 self.init_plugin() + # ZYY: 调用钩子注册方法 self.register_hooks() + # ZYY: 插件初始化方法(可被子类重写) def init_plugin(self): """ - 插件初始化逻辑 - 子类可以重写此方法来实现特定的初始化操作 + ZYY: 插件初始化逻辑 + ZYY: 子类可以重写此方法来实现特定的初始化操作 """ + # ZYY: 记录插件初始化日志 logger.info(f'{self.PLUGIN_NAME} initialized.') + # ZYY: 插件钩子注册方法(可被子类重写) def register_hooks(self): """ - 注册插件钩子 - 子类可以重写此方法来注册特定的钩子 + ZYY: 注册插件钩子 + ZYY: 子类可以重写此方法来注册特定的钩子 """ + # ZYY: 基类不实现具体钩子注册逻辑 pass + # ZYY: 获取插件信息的方法 def get_plugin_info(self): """ - 获取插件信息 + ZYY: 获取插件信息 :return: 包含插件元数据的字典 """ + # ZYY: 返回包含插件名称、描述和版本的字典 return { 'name': self.PLUGIN_NAME, 'description': self.PLUGIN_DESCRIPTION, 'version': self.PLUGIN_VERSION - } + } \ No newline at end of file diff --git a/src/django-master/djangoblog/plugin_manage/hook_constants.py b/src/django-master/djangoblog/plugin_manage/hook_constants.py index 6685b7c..789fe82 100644 --- a/src/django-master/djangoblog/plugin_manage/hook_constants.py +++ b/src/django-master/djangoblog/plugin_manage/hook_constants.py @@ -1,7 +1,20 @@ +# ZYY: 定义文章详情页加载的钩子名称常量 +# ZYY: 用于标识文章详情页加载事件,插件可监听此事件执行自定义逻辑 ARTICLE_DETAIL_LOAD = 'article_detail_load' + +# ZYY: 定义文章创建的钩子名称常量 +# ZYY: 用于标识文章创建事件,通常在文章保存前触发 ARTICLE_CREATE = 'article_create' + +# ZYY: 定义文章更新的钩子名称常量 +# ZYY: 用于标识文章更新事件,通常在文章修改后触发 ARTICLE_UPDATE = 'article_update' -ARTICLE_DELETE = 'article_delete' -ARTICLE_CONTENT_HOOK_NAME = "the_content" +# ZYY: 定义文章删除的钩子名称常量 +# ZYY: 用于标识文章删除事件,通常在文章被删除前触发 +ARTICLE_DELETE = 'article_delete' +# ZYY: 定义文章内容处理的钩子名称常量 +# ZYY: 参考WordPress的the_content过滤器概念,用于在文章内容渲染时进行处理 +# ZYY: 插件可以通过此钩子修改文章最终显示的内容 +ARTICLE_CONTENT_HOOK_NAME = "the_content" \ No newline at end of file diff --git a/src/django-master/djangoblog/plugin_manage/hooks.py b/src/django-master/djangoblog/plugin_manage/hooks.py index d712540..1108eea 100644 --- a/src/django-master/djangoblog/plugin_manage/hooks.py +++ b/src/django-master/djangoblog/plugin_manage/hooks.py @@ -1,44 +1,77 @@ +# ZYY: 导入Python标准库logging模块,用于记录钩子系统的运行日志 import logging +# ZYY: 获取当前模块的日志记录器,用于记录调试和错误信息 logger = logging.getLogger(__name__) +# ZYY: 定义全局钩子存储字典,键为钩子名称,值为回调函数列表 _hooks = {} +# ZYY: 钩子注册函数,用于将回调函数注册到指定钩子 def register(hook_name: str, callback: callable): """ - 注册一个钩子回调。 + ZYY: 注册一个钩子回调函数到指定钩子名称。 + ZYY: 如果钩子不存在则创建新列表,然后将回调添加到对应钩子的回调列表中。 + ZYY: @param hook_name: 要注册的钩子名称 + ZYY: @param callback: 要注册的回调函数(必须是可调用对象) """ + # ZYY: 检查钩子是否已存在,不存在则初始化空列表 if hook_name not in _hooks: _hooks[hook_name] = [] + + # ZYY: 将回调函数添加到对应钩子的回调列表 _hooks[hook_name].append(callback) + + # ZYY: 记录调试日志,显示成功注册的钩子和回调函数名 logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") +# ZYY: Action钩子执行函数,用于触发指定名称的所有Action钩子回调 def run_action(hook_name: str, *args, **kwargs): """ - 执行一个 Action Hook。 - 它会按顺序执行所有注册到该钩子上的回调函数。 + ZYY: 执行一个Action Hook(无返回值的钩子)。 + ZYY: 会按注册顺序依次执行所有回调函数,不处理返回值。 + ZYY: @param hook_name: 要触发的钩子名称 + ZYY: @param args: 传递给回调函数的位置参数 + ZYY: @param kwargs: 传递给回调函数的关键字参数 """ + # ZYY: 检查是否有回调注册到该钩子 if hook_name in _hooks: logger.debug(f"Running action hook '{hook_name}'") + + # ZYY: 遍历并执行所有注册的回调函数 for callback in _hooks[hook_name]: try: + # ZYY: 执行回调函数,可能抛出异常 callback(*args, **kwargs) except Exception as e: + # ZYY: 捕获并记录回调执行中的异常,避免影响其他回调 logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) +# ZYY: Filter钩子执行函数,用于触发指定名称的所有Filter钩子回调并处理返回值 def apply_filters(hook_name: str, value, *args, **kwargs): """ - 执行一个 Filter Hook。 - 它会把 value 依次传递给所有注册的回调函数进行处理。 + ZYY: 执行一个Filter Hook(有返回值的钩子)。 + ZYY: 会将初始值依次传递给所有回调函数处理,最终返回处理后的值。 + ZYY: @param hook_name: 要触发的钩子名称 + ZYY: @param value: 要处理的初始值 + ZYY: @param args: 传递给回调函数的位置参数 + ZYY: @param kwargs: 传递给回调函数的关键字参数 + ZYY: @return: 经过所有回调处理后的最终值 """ + # ZYY: 检查是否有回调注册到该钩子 if hook_name in _hooks: logger.debug(f"Applying filter hook '{hook_name}'") + + # ZYY: 遍历并执行所有注册的回调函数,每次用返回值更新value for callback in _hooks[hook_name]: try: value = callback(value, *args, **kwargs) except Exception as e: + # ZYY: 捕获并记录回调执行中的异常,避免影响其他回调 logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) - return value + + # ZYY: 返回最终处理后的值 + return value \ No newline at end of file diff --git a/src/django-master/djangoblog/plugin_manage/loader.py b/src/django-master/djangoblog/plugin_manage/loader.py index 12e824b..8def52d 100644 --- a/src/django-master/djangoblog/plugin_manage/loader.py +++ b/src/django-master/djangoblog/plugin_manage/loader.py @@ -1,19 +1,32 @@ +# ZYY: 导入Python标准库os模块,用于处理文件和目录路径 import os +# ZYY: 导入Python标准库logging模块,用于记录插件加载过程中的日志信息 import logging +# ZYY: 导入Django配置模块,用于访问Django项目的设置参数 from django.conf import settings +# ZYY: 获取当前模块的日志记录器,用于记录插件加载的调试和错误信息 logger = logging.getLogger(__name__) +# ZYY: 定义插件加载函数,用于动态加载和初始化Django插件 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. + ZYY: 动态加载并初始化位于'plugins'目录中的插件。 + ZYY: 该函数应在Django应用注册表就绪后调用,以确保所有依赖项可用。 + ZYY: 加载过程会读取settings.ACTIVE_PLUGINS配置,并尝试加载每个指定的插件。 """ + # ZYY: 遍历settings.ACTIVE_PLUGINS中配置的所有活跃插件名称 for plugin_name in settings.ACTIVE_PLUGINS: + # ZYY: 构建插件的完整路径(目录路径) plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) + + # ZYY: 检查插件目录是否存在且包含plugin.py文件(插件主模块) if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): try: + # ZYY: 动态导入插件模块(格式:plugins..plugin) __import__(f'plugins.{plugin_name}.plugin') + # ZYY: 记录成功加载插件的日志信息 logger.info(f"Successfully loaded plugin: {plugin_name}") except ImportError as e: - logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file + # ZYY: 捕获并记录插件导入失败的错误信息(包含异常堆栈) + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file diff --git a/src/django-master/djangoblog/settings.py b/src/django-master/djangoblog/settings.py index d076bb6..a226a6f 100644 --- a/src/django-master/djangoblog/settings.py +++ b/src/django-master/djangoblog/settings.py @@ -12,332 +12,344 @@ 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 _ - +# ZYY 将环境变量字符串转换为布尔值 def env_to_bool(env, default): - str_val = os.environ.get(env) - return default if str_val is None else str_val == 'True' - + str_val = os.environ.get(env) + return default if str_val is None else str_val == 'True' -# Build paths inside the project like this: BASE_DIR / 'subdir'. +# ZYY 在项目内部构建路径,例如:BASE_DIR / 'subdir' BASE_DIR = Path(__file__).resolve().parent.parent -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! +# ZYY 快速启动开发设置 - 不适用于生产环境 +# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ +# ZYY 安全警告: 请保密生产环境中使用的密钥! SECRET_KEY = os.environ.get( - 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' -# SECURITY WARNING: don't run with debug turned on in production! + 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@ (mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' + +# ZYY 安全警告: 不要在生产环境中开启调试模式! DEBUG = env_to_bool('DJANGO_DEBUG', True) # DEBUG = False + +# ZYY 检测是否为测试环境 TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' +# ZYY 允许的主机列表 # ALLOWED_HOSTS = [] ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] -# django 4.0新增配置 + +# ZYY Django 4.0新增配置: CSRF信任来源 CSRF_TRUSTED_ORIGINS = ['http://example.com'] -# Application definition - +# ZYY 应用定义 INSTALLED_APPS = [ - # 'django.contrib.admin', - 'django.contrib.admin.apps.SimpleAdminConfig', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'mdeditor', - 'haystack', - 'blog', - 'accounts', - 'comments', - 'oauth', - 'servermanager', - 'owntracks', - 'compressor', - 'djangoblog' + # 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', + 'django.contrib.auth', + 'django. contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django.contrib.sitemaps', + 'mdeditor', + 'haystack', + 'blog', + 'accounts', + 'comments', + 'oauth', + ' servermanager', + 'owntracks', + 'compressor', + 'djangoblog' ] +# ZYY 中间件配置 MIDDLEWARE = [ - - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.gzip.GZipMiddleware', - # 'django.middleware.cache.UpdateCacheMiddleware', - 'django.middleware.common.CommonMiddleware', - # 'django.middleware.cache.FetchFromCacheMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', - 'blog.middleware.OnlineMiddleware' + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.gzip.GZipMiddleware', + # 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.cache.FetchFromCacheMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib. messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.http.ConditionalGetMiddleware', + 'blog.middleware.OnlineMiddleware' ] +# ZYY 根URL配置 ROOT_URLCONF = 'djangoblog.urls' +# ZYY 模板配置 TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'blog.context_processors.seo_processor' - ], - }, - }, + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR , 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'blog.context_processors.seo_processor' + ], + }, + }, ] +# ZYY WSGI应用配置 WSGI_APPLICATION = 'djangoblog.wsgi.application' -# Database -# https://docs.djangoproject.com/en/1.10/ref/settings/#databases - - +# ZYY 数据库配置 +# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', - 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', - 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', - 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', - 'PORT': int( - os.environ.get('DJANGO_MYSQL_PORT') or 3306), - 'OPTIONS': { - 'charset': 'utf8mb4'}, - }} - -# Password validation -# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators - + 'default': { + 'ENGINE': ' django.db.backends.mysql', + 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', + 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', + 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', + 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', + 'PORT': int( + os.environ.get('DJANGO_MYSQL_PORT') or 3306), + 'OPTIONS': { + 'charset': 'utf8mb4'}, + }} + +# ZYY 密码验证配置 +# ZYY 参考链接: https://docs.djang oproject.com/en/1.10/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, ] +# ZYY 语言配置 LANGUAGES = ( - ('en', _('English')), - ('zh-hans', _('Simplified Chinese')), - ('zh-hant', _('Traditional Chinese')), + ('en', _('English')), + ('zh-hans', _('Simplified Chinese')), + ('zh-hant', _('Traditional Chinese')), ) LOCALE_PATHS = ( - os.path.join(BASE_DIR, 'locale'), + os.path.join(BASE_DIR, 'locale'), ) - LANGUAGE_CODE = 'zh-hans' - TIME_ZONE = 'Asia/Shanghai' - USE_I18N = True - USE_L10N = True - USE_TZ = False -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - - +# ZYY 静态文件配置(CSS, JavaScript, Images) +# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/howto /static-files/ HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', - 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), - }, + 'default': { + 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', + 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), + }, } -# Automatically update searching index + +# ZYY 自动更新搜索索引 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' -# Allow user login with username and password + +# ZYY 允许用户使用用户名和密码登录 AUTHENTICATION_BACKENDS = [ - 'accounts.user_login_backend.EmailOrUsernameModelBackend'] + 'accounts.user_login_backend.EmailOrUsernameModelBackend'] +# ZYY 静态文件根目录 STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') - STATIC_URL = '/static/' STATICFILES = os.path.join(BASE_DIR, 'static') +# ZYY 自定义用户模型 AUTH_USER_MODEL = 'accounts.BlogUser' LOGIN_URL = '/login/' - TIME_FORMAT = '%Y-%m-%d %H:%M:%S' DATE_TIME_FORMAT = '%Y-%m-%d' - -# bootstrap color styles + +# ZYY Bootstrap颜色样式 BOOTSTRAP_COLOR_TYPES = [ - 'default', 'primary', 'success', 'info', 'warning', 'danger' + 'default', 'primary', 'success', 'info', 'warning', 'danger' ] -# paginate +# ZYY 分页配置 PAGINATE_BY = 10 -# http cache timeout + +# ZYY HTTP缓存超时时间 CACHE_CONTROL_MAX_AGE = 2592000 -# cache setting + +# ZYY 缓存配置 CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'TIMEOUT': 10800, - 'LOCATION': 'unique-snowflake', - } + 'default': { + 'BACKEND': 'django.core .cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } } -# 使用redis作为缓存 + +# ZYY 使用redis作为缓存 if os.environ.get("DJANGO_REDIS_URL"): - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.redis.RedisCache', - 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', - } - } + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', + } + } SITE_ID = 1 -BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ - or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' -# Email: +# ZYY 百度推送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' + +# ZYY 邮件配置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) -EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' +EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com ' EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = EMAIL_HOST_USER -# Setting debug=false did NOT handle except email notifications + +# ZYY 设置debug=false时不会处理异常邮件通知 ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] -# WX ADMIN password(Two times md5) + +# ZYY 微信管理员密码(两次md5 加密) WXADMIN = os.environ.get( - 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' + 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' +# ZYY 日志目录配置 LOG_PATH = os.path.join(BASE_DIR, 'logs') if not os.path.exists(LOG_PATH): - os.makedirs(LOG_PATH, exist_ok=True) + os.makedirs(LOG_PATH, exist_ok=True) +# ZYY 日志配置 LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'root': { - 'level': 'INFO', - 'handlers': ['console', 'log_file'], - }, - 'formatters': { - 'verbose': { - 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', - } - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, - }, - 'handlers': { - 'log_file': { - 'level': 'INFO', - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), - 'when': 'D', - 'formatter': 'verbose', - 'interval': 1, - 'delay': True, - 'backupCount': 5, - 'encoding': 'utf-8' - }, - 'console': { - 'level': 'DEBUG', - 'filters': ['require_debug_true'], - 'class': 'logging.StreamHandler', - 'formatter': 'verbose' - }, - 'null': { - 'class': 'logging.NullHandler', - }, - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'djangoblog': { - 'handlers': ['log_file', 'console'], - 'level': 'INFO', - 'propagate': True, - }, - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': False, - } - } + 'version': 1, + 'disable_existing_loggers': False, + 'root': { + 'level': 'INFO', + 'handlers': ['console', 'log_file'], + }, + 'formatters': { + 'verbose': { + 'format': '[%( asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', + } + }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'log_file': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), + 'when': 'D', + 'formatter': 'verbose', + 'interval': 1, + 'delay': True , + 'backupCount': 5, + 'encoding': 'utf-8' + }, + 'console': { + 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + }, + 'null': { + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'djangoblog': { + 'handlers': ['log_file', 'console'], + 'level': 'INFO', + 'propagate': True, + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + } + } } +# ZYY 静态文件查找器配置 STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - # other - 'compressor.finders.CompressorFinder', -) + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # other + 'compressor.finders.CompressorFinder', +) + +# ZYY 压缩配置 COMPRESS_ENABLED = True # COMPRESS_OFFLINE = True - - COMPRESS_CSS_FILTERS = [ - # creates absolute urls from relative ones - 'compressor.filters.css_default.CssAbsoluteFilter', - # css minimizer - 'compressor.filters.cssmin.CSSMinFilter' + # 创建绝对URL + 'compressor.filters.css_default.CssAbsoluteFilter', + # CSS压缩 + 'compressor.filters.cssmin.CSSMinFilter' ] COMPRESS_JS_FILTERS = [ - 'compressor.filters.jsmin.JSMinFilter' + 'compressor.filters.jsmin.JSMinFilter' ] +# ZYY 媒体文件配置 MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') MEDIA_URL = '/media/' + +# ZYY X-Frame-Options配置 X_FRAME_OPTIONS = 'SAMEORIGIN' +# ZYY 默认自增字段类型 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# ZYY Elasticsearch配置 if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): - ELASTICSEARCH_DSL = { - 'default': { - 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') - }, - } - HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', - }, - } - -# Plugin System + ELASTICSEARCH_DSL = { + 'default': { + 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_ HOST') + }, + } + HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', + }, + } + +# ZYY 插件系统配置 PLUGINS_DIR = BASE_DIR / 'plugins' ACTIVE_PLUGINS = [ - 'article_copyright', - 'reading_time', - 'external_links', - 'view_count', - 'seo_optimizer' + 'article_copyright', + 'reading_time', + ' external_links', + 'view_count', + 'seo_optimizer' ] \ No newline at end of file diff --git a/src/django-master/djangoblog/sitemap.py b/src/django-master/djangoblog/sitemap.py index 8b7d446..5c0e70b 100644 --- a/src/django-master/djangoblog/sitemap.py +++ b/src/django-master/djangoblog/sitemap.py @@ -1,59 +1,73 @@ +# ZYY 导入必要的模块和类 from django.contrib.sitemaps import Sitemap from django.urls import reverse - from blog.models import Article, Category, Tag - +# ZYY 定义静态视图站点地图类,用于生成博客首页等静态页面的站点地图 class StaticViewSitemap(Sitemap): - priority = 0.5 - changefreq = 'daily' + priority = 0.5 # ZYY 优先级设置为0.5 + changefreq = 'daily' # ZYY 更新频率设置为每天 def items(self): + # ZYY 返回需要生成站点地图的URL名称列表,这里只有博客首页 return ['blog:index', ] def location(self, item): + # ZYY 根据URL名称生成具体的URL地址 return reverse(item) +# ZYY 定义文章站点地图类,用于生成文章页面的站点地图 class ArticleSiteMap(Sitemap): - changefreq = "monthly" - priority = "0.6" + changefreq = "monthly" # ZYY 更新频率设置为每月 + priority = "0.6" # ZYY 优先级设置为0.6 def items(self): + # ZYY 返回所有状态为'p'(已发布)的文章对象 return Article.objects.filter(status='p') def lastmod(self, obj): + # ZYY 返回文章的最后修改时间 return obj.last_modify_time +# ZYY 定义分类站点地图类,用于生成分类页面的站点地图 class CategorySiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.6" + changefreq = "Weekly" # ZYY 更新频率设置为每周 + priority = "0.6" # ZYY 优先级设置为0.6 def items(self): + # ZYY 返回所有的分类对象 return Category.objects.all() def lastmod(self, obj): + # ZYY 返回分类的最后修改时间 return obj.last_modify_time +# ZYY 定义标签站点地图类,用于生成标签页面的站点地图 class TagSiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.3" + changefreq = "Weekly" # ZYY 更新频率设置为每周 + priority = "0.3" # ZYY 优先级设置为0.3 def items(self): + # ZYY 返回所有的标签对象 return Tag.objects.all() def lastmod(self, obj): + # ZYY 返回标签的最后修改时间 return obj.last_modify_time +# ZYY 定义用户站点地图类,用于生成用户页面的站点地图 class UserSiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.3" + changefreq = "Weekly" # ZYY 更新频率设置为每周 + priority = "0.3" # ZYY 优先级设置为0.3 def items(self): + # ZYY 返回所有文章的作者集合(去重) return list(set(map(lambda x: x.author, Article.objects.all()))) def lastmod(self, obj): - return obj.date_joined + # ZYY 返回用户的注册时间作为最后修改时间 + return obj.date_joined \ No newline at end of file diff --git a/src/django-master/djangoblog/spider_notify.py b/src/django-master/djangoblog/spider_notify.py index 7b909e9..12723a1 100644 --- a/src/django-master/djangoblog/spider_notify.py +++ b/src/django-master/djangoblog/spider_notify.py @@ -1,21 +1,32 @@ +# ZYY 导入日志模块,用于记录程序运行状态 import logging - +# ZYY 导入requests库,用于发送HTTP请求 import requests +# ZYY 导入Django配置,用于获取项目设置 from django.conf import settings +# ZYY 获取当前模块的日志记录器 logger = logging.getLogger(__name__) +# ZYY 定义蜘蛛通知类,用于向搜索引擎提交URL通知 class SpiderNotify(): @staticmethod + # ZYY 静态方法,用于通知百度蜘蛛抓取指定URL列表 def baidu_notify(urls): try: + # ZYY 将URL列表拼接成字符串,每个URL占一行 data = '\n'.join(urls) + # ZYY 向百度推送接口发送POST请求,提交URL数据 result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + # ZYY 记录百度接口返回的结果(成功响应) logger.info(result.text) except Exception as e: + # ZYY 捕获并记录所有异常(如网络错误、接口错误等) logger.error(e) @staticmethod + # ZYY 静态方法,提供统一的通知接口(目前仅调用百度通知) def notify(url): - SpiderNotify.baidu_notify(url) + # ZYY 调用百度通知方法(注意:这里参数名是url,但实际传入的是单个URL,可能存在命名歧义) + SpiderNotify.baidu_notify(url) \ No newline at end of file diff --git a/src/django-master/djangoblog/tests.py b/src/django-master/djangoblog/tests.py index 01237d9..4efac59 100644 --- a/src/django-master/djangoblog/tests.py +++ b/src/django-master/djangoblog/tests.py @@ -5,11 +5,18 @@ from djangoblog.utils import * class DjangoBlogTest(TestCase): def setUp(self): + # ZYY: 测试用例初始化方法,当前为空实现 pass def test_utils(self): + # ZYY: 测试utils模块中的工具函数 + + # ZYY: 测试get_sha256函数,验证字符串'test'的sha256哈希值不为空 md5 = get_sha256('test') self.assertIsNotNone(md5) + + # ZYY: 测试CommonMarkdown类的get_markdown方法 + # ZYY: 输入包含标题、代码块和链接的Markdown文本,验证输出不为空 c = CommonMarkdown.get_markdown(''' # Title1 @@ -24,9 +31,12 @@ class DjangoBlogTest(TestCase): ''') self.assertIsNotNone(c) + + # ZYY: 测试parse_dict_to_url函数 + # ZYY: 创建测试字典,验证字典转URL参数字符串的功能 d = { 'd': 'key1', 'd2': 'key2' } data = parse_dict_to_url(d) - self.assertIsNotNone(data) + self.assertIsNotNone(data)# ZYY: 验证转换结果不为空 diff --git a/src/django-master/djangoblog/urls.py b/src/django-master/djangoblog/urls.py index 4aae58a..523478e 100644 --- a/src/django-master/djangoblog/urls.py +++ b/src/django-master/djangoblog/urls.py @@ -13,52 +13,92 @@ Including another URLconf 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ +# ZYY: 导入Django基础配置模块 from django.conf import settings +# ZYY: 导入国际化URL模式工具 from django.conf.urls.i18n import i18n_patterns +# ZYY: 导入静态文件URL处理工具 from django.conf.urls.static import static +# ZYY: 导入站点地图视图 from django.contrib.sitemaps.views import sitemap +# ZYY: 导入URL路径处理函数 from django.urls import path, include +# ZYY: 导入正则表达式URL模式 from django.urls import re_path +# ZYY: 导入Haystack搜索视图工厂 from haystack.views import search_view_factory +# ZYY: 导入自定义视图和模块 from blog.views import EsSearchView from djangoblog.admin_site import admin_site from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm from djangoblog.feeds import DjangoBlogFeed from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap +# ZYY: 定义站点地图配置,包含各类内容的站点地图 sitemaps = { - - 'blog': ArticleSiteMap, - 'Category': CategorySiteMap, - 'Tag': TagSiteMap, - 'User': UserSiteMap, - 'static': StaticViewSitemap + 'blog': ArticleSiteMap, # ZYY: 文章站点地图 + 'Category': CategorySiteMap, # ZYY: 分类站点地图 + 'Tag': TagSiteMap, # ZYY: 标签站点地图 + 'User': UserSiteMap, # ZYY: 用户站点地图 + 'static': StaticViewSitemap # ZYY: 静态页面站点地图 } -handler404 = 'blog.views.page_not_found_view' -handler500 = 'blog.views.server_error_view' -handle403 = 'blog.views.permission_denied_view' +# ZYY: 定义自定义错误处理视图 +handler404 = 'blog.views.page_not_found_view' # ZYY: 404错误处理 +handler500 = 'blog.views.server_error_view' # ZYY: 500错误处理 +handle403 = 'blog.views.permission_denied_view' # ZYY: 403错误处理 +# ZYY: 基础URL模式配置 urlpatterns = [ + # ZYY: 国际化URL前缀处理 path('i18n/', include('django.conf.urls.i18n')), ] + +# ZYY: 添加国际化URL模式 urlpatterns += i18n_patterns( - re_path(r'^admin/', admin_site.urls), + # ZYY: 管理后台URL(使用自定义admin_site) + re_path(r'admin/', admin_site.urls), + + # ZYY: 博客应用URL(命名空间为blog) re_path(r'', include('blog.urls', namespace='blog')), + + # ZYY: Markdown编辑器URL re_path(r'mdeditor/', include('mdeditor.urls')), + + # ZYY: 评论系统URL(命名空间为comment) re_path(r'', include('comments.urls', namespace='comment')), + + # ZYY: 账户系统URL(命名空间为account) re_path(r'', include('accounts.urls', namespace='account')), + + # ZYY: 第三方登录URL(命名空间为oauth) re_path(r'', include('oauth.urls', namespace='oauth')), - re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, + + # ZYY: 站点地图URL(生成sitemap.xml) + re_path(r'sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'), - re_path(r'^feed/$', DjangoBlogFeed()), - re_path(r'^rss/$', DjangoBlogFeed()), - re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), + + # ZYY: RSS订阅URL(两种访问路径) + re_path(r'feed/$', DjangoBlogFeed()), + re_path(r'rss/$', DjangoBlogFeed()), + + # ZYY: 搜索功能URL(使用Elasticsearch搜索视图) + re_path('search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), name='search'), + + # ZYY: 服务器管理URL(命名空间为servermanager) re_path(r'', include('servermanager.urls', namespace='servermanager')), - re_path(r'', include('owntracks.urls', namespace='owntracks')) - , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + # ZYY: OwnTracks应用URL(命名空间为owntracks) + re_path(r'', include('owntracks.urls', namespace='owntracks')), + + # ZYY: 禁用默认语言前缀(prefix_default_language=False) + prefix_default_language=False +) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # ZYY: 添加静态文件URL处理 + +# ZYY: 开发环境专用配置(DEBUG模式下) if settings.DEBUG: + # ZYY: 添加媒体文件URL处理(仅开发环境) urlpatterns += static(settings.MEDIA_URL, - document_root=settings.MEDIA_ROOT) + document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/src/django-master/djangoblog/utils.py b/src/django-master/djangoblog/utils.py index 57f63dc..7749382 100644 --- a/src/django-master/djangoblog/utils.py +++ b/src/django-master/djangoblog/utils.py @@ -8,7 +8,6 @@ import random import string import uuid from hashlib import sha256 - import bleach import markdown import requests @@ -16,21 +15,20 @@ from django.conf import settings from django.contrib.sites.models import Site from django.core.cache import cache from django.templatetags.static import static - logger = logging.getLogger(__name__) - +# ZYY: 获取最新的文章ID和评论ID def get_max_articleid_commentid(): - from blog.models import Article - from comments.models import Comment - return (Article.objects.latest().pk, Comment.objects.latest().pk) - + from blog.models import Article + from comments.models import Comment + return (Article.objects.latest().pk, Comment.objects.latest().pk) +# ZYY: 获取字符串的SHA256哈希值 def get_sha256(str): m = sha256(str.encode('utf-8')) return m.hexdigest() - +# ZYY: 缓存装饰器 ,用于缓存函数结果 def cache_decorator(expiration=3 * 60): def wrapper(func): def news(*args, **kwargs): @@ -41,7 +39,6 @@ def cache_decorator(expiration=3 * 60): key = None if not key: unique_str = repr((func, args, kwargs)) - m = sha256(unique_str.encode('utf-8')) key = m.hexdigest() value = cache.get(key) @@ -53,7 +50,7 @@ def cache_decorator(expiration=3 * 60): return value else: logger.debug( - 'cache_decorator set cache:%s key:%s' % + 'cache_decorator set cache:%s key:%s' % (func.__name__, key)) value = func(*args, **kwargs) if value is None: @@ -61,44 +58,40 @@ def cache_decorator(expiration=3 * 60): else: cache.set(key, value, expiration) return value - return news - return wrapper - +# ZYY: 刷新视图缓存 def expire_view_cache(path, servername, serverport, key_prefix=None): ''' 刷新视图缓存 :param path:url路径 :param servername:host :param serverport:端口 - :param key_prefix:前缀 + : param key_prefix:前缀 :return:是否成功 ''' from django.http import HttpRequest from django.utils.cache import get_cache_key - request = HttpRequest() - request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} + request.META = {'SERVER_NAME': servername , 'SERVER_PORT': serverport} request.path = path - 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 True return False - +# ZYY: 使用缓存装饰器获取当前站点信息 @cache_decorator() def get_current_site(): - site = Site.objects.get_current() + site = Site.objects.get_current () return site - class CommonMarkdown: + # ZYY: 将Markdown文本转换为HTML,并生成目录 @staticmethod def _convert_markdown(value): md = markdown.Markdown( @@ -113,17 +106,19 @@ class CommonMarkdown: toc = md.toc return body, toc + # ZYY: 获取带有目录的Markdown转换结果 @staticmethod def get_markdown_with_toc(value): - body, toc = CommonMarkdown._convert_markdown(value) + body, toc = CommonMarkdown._convert_markdown(value) return body, toc + # ZYY: 获取Markdown转换结果(不带目录) @staticmethod def get_markdown(value): body, toc = CommonMarkdown._convert_markdown(value) return body - +# ZYY: 发送邮件 def send_email(emailto, title, content): from djangoblog.blog_signals import send_email_signal send_email_signal.send( @@ -132,19 +127,19 @@ def send_email(emailto, title, content): title=title, content=content) - +# ZYY: 生成随机数验证码 def generate_code() -> str: """生成随机数验证码""" - return ''.join(random.sample(string.digits, 6)) - + return ''.join(random .sample(string.digits, 6)) +# ZYY: 将字典解析为URL字符串 def parse_dict_to_url(dict): from urllib.parse import quote url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) for k, v in dict.items()]) return url - +# ZYY: 获取博客设置,如果缓存中没有则从数据库加载并设置缓存 def get_blog_setting(): value = cache.get('get_blog_setting') if value: @@ -163,7 +158,7 @@ def get_blog_setting(): setting.show_google_adsense = False setting.open_site_comment = True setting.analytics_code = '' - setting.beian_code = '' + setting.beian_code = '' setting.show_gongan_code = False setting.comment_need_review = False setting.save() @@ -172,7 +167,7 @@ def get_blog_setting(): cache.set('get_blog_setting', value) return value - +# ZYY: 保存用户头像到本地 def save_user_avatar(url): ''' 保存用户头像 @@ -180,14 +175,12 @@ def save_user_avatar(url): :return: 本地路径 ''' logger.info(url) - try: basedir = os.path.join(settings.STATICFILES, 'avatar') rsp = requests.get(url, timeout=2) if rsp.status_code == 200: if not os.path.exists(basedir): os.makedirs(basedir) - 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' @@ -195,12 +188,12 @@ def save_user_avatar(url): logger.info('保存用户头像:' + basedir + save_filename) with open(os.path.join(basedir, save_filename), 'wb+') as file: file.write(rsp.content) - return static('avatar/' + save_filename) + return static('avatar /' + save_filename) except Exception as e: logger.error(e) return static('blog/img/avatar.png') - +# ZYY: 删除侧边栏缓存 def delete_sidebar_cache(): from blog.models import LinkShowType keys = ["sidebar" + x for x in LinkShowType.values] @@ -208,13 +201,13 @@ def delete_sidebar_cache(): logger.info('delete sidebar key:' + k) cache.delete(k) - +# ZYY: 删除视图缓存 def delete_view_cache(prefix, keys): from django.core.cache.utils import make_template_fragment_key key = make_template_fragment_key(prefix, keys) cache.delete(key) - +# ZYY: 获取静态资源URL def get_resource_url(): if settings.STATIC_URL: return settings.STATIC_URL @@ -222,11 +215,11 @@ def get_resource_url(): site = get_current_site() return 'http://' + site.domain + '/static/' - +# ZYY: 允许的HTML标签和属性 ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', 'h2', 'p'] ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} - +# ZYY: 清理HTML,只保留允许的标签和属性 def sanitize_html(html): - return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) \ No newline at end of file diff --git a/src/django-master/djangoblog/whoosh_cn_backend.py b/src/django-master/djangoblog/whoosh_cn_backend.py index 04e3f7f..09ec789 100644 --- a/src/django-master/djangoblog/whoosh_cn_backend.py +++ b/src/django-master/djangoblog/whoosh_cn_backend.py @@ -1,7 +1,7 @@ # encoding: utf-8 from __future__ import absolute_import, division, print_function, unicode_literals - +# ZYY: 导入必要的模块和函数 import json import os import re @@ -34,12 +34,14 @@ from whoosh.qparser import QueryParser from whoosh.searching import ResultsPage from whoosh.writing import AsyncWriter +# ZYY: 尝试导入whoosh模块,如果失败则抛出异常 try: import whoosh except ImportError: raise MissingDependency( "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") +# ZYY: 检查whoosh版本是否满足要求 # Handle minimum requirement. if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): raise MissingDependency( @@ -52,16 +54,20 @@ DATETIME_REGEX = re.compile( LOCALS = threading.local() LOCALS.RAM_STORE = None - +# ZYY: 自定义的HtmlFormatter类,用于在whoosh中统一结果格式 class WhooshHtmlFormatter(HtmlFormatter): """ This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. We use it to have consistent results across backends. Specifically, Solr, Xapian and Elasticsearch are using this formatting. """ + """ + ZYY: 这是一个比whoosh.HtmlFormatter更简单的HtmlFormatter。 + 我们使用它来确保不同后端(如Solr, Xapian和Elasticsearch)之间的结果一致性。 + """ template = '<%(tag)s>%(t)s' - +# ZYY: Whoosh搜索后端类 class WhooshSearchBackend(BaseSearchBackend): # Word reserved by Whoosh for special use. RESERVED_WORDS = ( @@ -70,7 +76,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'OR', 'TO', ) - + # ZYY: Whoosh保留的特殊用途字符 # Characters reserved by Whoosh for special use. # The '\\' must come first, so as not to overwrite the other slash # replacements. @@ -80,6 +86,7 @@ class WhooshSearchBackend(BaseSearchBackend): ) def __init__(self, connection_alias, **connection_options): + # ZYY: 初始化Whoosh搜索后端 super( WhooshSearchBackend, self).__init__( @@ -107,9 +114,13 @@ class WhooshSearchBackend(BaseSearchBackend): """ Defers loading until needed. """ + """ + ZYY: 延迟加载直到需要时才加载 + """ from haystack import connections new_index = False + # ZYY: 确保索引存在 # Make sure the index is there. if self.use_file_storage and not os.path.exists(self.path): os.makedirs(self.path) @@ -144,12 +155,14 @@ class WhooshSearchBackend(BaseSearchBackend): self.setup_complete = True +# ZYY: 构建索引模式 def build_schema(self, fields): schema_fields = { ID: WHOOSH_ID(stored=True, unique=True), DJANGO_CT: WHOOSH_ID(stored=True), DJANGO_ID: WHOOSH_ID(stored=True), } + # ZYY: 获取Haystack中硬编码的键数量,用于后续的错误处理 # Grab the number of keys that are hard-coded into Haystack. # We'll use this to (possibly) fail slightly more gracefully later. initial_key_count = len(schema_fields) @@ -173,6 +186,7 @@ class WhooshSearchBackend(BaseSearchBackend): schema_fields[field_class.index_fieldname] = NUMERIC( stored=field_class.stored, numtype=float, field_boost=field_class.boost) elif field_class.field_type == 'boolean': + # ZYY: BOOLEAN字段不支持field_boost # Field boost isn't supported on BOOLEAN as of 1.8.2. schema_fields[field_class.index_fieldname] = BOOLEAN( stored=field_class.stored) @@ -184,13 +198,14 @@ class WhooshSearchBackend(BaseSearchBackend): stored=field_class.stored, field_boost=field_class.boost) else: + # ZYY: 使用中文分析器 # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True) schema_fields[field_class.index_fieldname] = TEXT( stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True) if field_class.document is True: content_field_name = field_class.index_fieldname schema_fields[field_class.index_fieldname].spelling = True - + # ZYY: 如果没有找到任何字段,则抛出错误 # Fail more gracefully than relying on the backend to die if no fields # are found. if len(schema_fields) <= initial_key_count: @@ -198,7 +213,7 @@ class WhooshSearchBackend(BaseSearchBackend): "No fields were found in any search_indexes. Please correct this before attempting to search.") return (content_field_name, Schema(**schema_fields)) - + # ZYY: 更新索引 def update(self, index, iterable, commit=True): if not self.setup_complete: self.setup() @@ -212,11 +227,13 @@ class WhooshSearchBackend(BaseSearchBackend): except SkipDocument: self.log.debug(u"Indexing for object `%s` skipped", obj) else: + # ZYY: 确保所有数据都是unicode格式 # Really make sure it's unicode, because Whoosh won't have it any # other way. for key in doc: doc[key] = self._from_python(doc[key]) + # ZYY: Whoosh 2.5.0+不支持文档boost # Document boosts aren't supported in Whoosh 2.5.0+. if 'boost' in doc: del doc['boost'] @@ -227,6 +244,7 @@ class WhooshSearchBackend(BaseSearchBackend): if not self.silently_fail: raise + # ZYY: 记录错误信息 # We'll log the object identifier but won't include the actual object # to avoid the possibility of that generating encoding errors while # processing the log message: @@ -240,10 +258,12 @@ class WhooshSearchBackend(BaseSearchBackend): "object": get_identifier(obj)}}) if len(iterable) > 0: + # ZYY: 无论如何都提交更改,以避免锁定问题 # For now, commit no matter what, as we run into locking issues # otherwise. writer.commit() + # ZYY: 从索引中删除文档 def remove(self, obj_or_string, commit=True): if not self.setup_complete: self.setup() @@ -266,6 +286,7 @@ class WhooshSearchBackend(BaseSearchBackend): e, exc_info=True) + # ZYY: 清空索引 def clear(self, models=None, commit=True): if not self.setup_complete: self.setup() @@ -302,8 +323,9 @@ class WhooshSearchBackend(BaseSearchBackend): else: self.log.error( "Failed to clear Whoosh index: %s", e, exc_info=True) - + # ZYY: 删除索引文件 def delete_index(self): + # ZYY: 根据Whoosh邮件列表的建议,如果要从索引中删除所有内容,更高效的方法是直接删除索引文件 # Per the Whoosh mailing list, if wiping out everything from the index, # it's much more efficient to simply delete the index files. if self.use_file_storage and os.path.exists(self.path): @@ -311,9 +333,11 @@ class WhooshSearchBackend(BaseSearchBackend): elif not self.use_file_storage: self.storage.clean() + # ZYY: 重新创建索引 # Recreate everything. self.setup() - + + # ZYY: 优化索引 def optimize(self): if not self.setup_complete: self.setup() @@ -321,12 +345,15 @@ class WhooshSearchBackend(BaseSearchBackend): self.index = self.index.refresh() self.index.optimize() + # ZYY: 计算分页 def calculate_page(self, start_offset=0, end_offset=None): # Prevent against Whoosh throwing an error. Requires an end_offset # greater than 0. + # ZYY: 防止Whoosh抛出错误,要求end_offset大于0 if end_offset is not None and end_offset <= 0: end_offset = 1 - + + # ZYY: 确定页码 # Determine the page. page_num = 0 @@ -341,6 +368,7 @@ class WhooshSearchBackend(BaseSearchBackend): if page_length and page_length > 0: page_num = int(start_offset / page_length) + # ZYY: Whoosh使用1-based的页码 # Increment because Whoosh uses 1-based page numbers. page_num += 1 return page_num, page_length @@ -368,7 +396,8 @@ class WhooshSearchBackend(BaseSearchBackend): **kwargs): if not self.setup_complete: self.setup() - + + # ZYY: 空查询应该返回无结果 # A zero length query should return no results. if len(query_string) == 0: return { @@ -377,7 +406,7 @@ class WhooshSearchBackend(BaseSearchBackend): } query_string = force_str(query_string) - + # ZYY: 单字符查询(非通配符)会被停用词过滤器过滤掉,应该返回无结果 # A one-character query (non-wildcard) gets nabbed by a stopwords # filter and should yield zero results. if len(query_string) <= 1 and query_string != u'*': @@ -389,6 +418,7 @@ class WhooshSearchBackend(BaseSearchBackend): reverse = False if sort_by is not None: + # ZYY: 确定是否需要反转结果,以及Whoosh是否能处理排序要求 # Determine if we need to reverse the results and if Whoosh can # handle what it's being asked to sort by. Reversing is an # all-or-nothing action, unfortunately. @@ -445,6 +475,7 @@ class WhooshSearchBackend(BaseSearchBackend): if models and len(models): model_choices = sorted(get_model_ct(model) for model in models) elif limit_to_registered_models: + # ZYY: 使用窄查询限制结果到当前路由器处理的模型 # Using narrow queries, limit the results to only models handled # with the current routers. model_choices = self.build_models_list() @@ -461,6 +492,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrow_searcher = None if narrow_queries is not None: + # ZYY: 潜在的高开销操作 # Potentially expensive? I don't see another way to do it in # Whoosh... narrow_searcher = self.index.searcher() @@ -486,6 +518,7 @@ class WhooshSearchBackend(BaseSearchBackend): searcher = self.index.searcher() parsed_query = self.parser.parse(query_string) + # ZYY: 处理无效/停用词查询,优雅恢复 # In the event of an invalid/stopworded query, recover gracefully. if parsed_query is None: return { @@ -502,6 +535,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'reverse': reverse, } + # ZYY: 处理已缩小范围的查询结果 # Handle the case where the results have been narrowed. if narrowed_results is not None: search_kwargs['filter'] = narrowed_results @@ -521,7 +555,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'hits': 0, 'spelling_suggestion': None, } - + # ZYY: Whoosh 2.5.1如果请求的页码过高,会返回错误的页码 # Because as of Whoosh 2.5.1, it will return the wrong page of # results if you request something too high. :( if raw_page.pagenum < page_num: @@ -559,7 +593,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'hits': 0, 'spelling_suggestion': spelling_suggestion, } - + # ZYY: 查找类似文档 def more_like_this( self, model_instance, @@ -572,7 +606,7 @@ class WhooshSearchBackend(BaseSearchBackend): **kwargs): if not self.setup_complete: self.setup() - +# ZYY: 延迟模型将具有不同的类名("RealClass_Deferred_fieldname"),该类名不会在我们的注册表中 # Deferred models will have a different class ("RealClass_Deferred_fieldname") # which won't be in our registry: model_klass = model_instance._meta.concrete_model @@ -589,6 +623,7 @@ class WhooshSearchBackend(BaseSearchBackend): if models and len(models): model_choices = sorted(get_model_ct(model) for model in models) elif limit_to_registered_models: + # ZYY: 使用窄查询限制结果到当前路由器处理的模型 # Using narrow queries, limit the results to only models handled # with the current routers. model_choices = self.build_models_list() @@ -608,6 +643,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrow_searcher = None if narrow_queries is not None: + # ZYY: 潜在的高开销操作 # Potentially expensive? I don't see another way to do it in # Whoosh... narrow_searcher = self.index.searcher() @@ -642,10 +678,11 @@ class WhooshSearchBackend(BaseSearchBackend): raw_results = results[0].more_like_this( field_name, top=end_offset) + # ZYY: 处理已缩小范围的查询结果 # Handle the case where the results have been narrowed. if narrowed_results is not None and hasattr(raw_results, 'filter'): raw_results.filter(narrowed_results) - + # ZYY: Whoosh 2.5.1如果请求的页码过高,会返回错误的页码 try: raw_page = ResultsPage(raw_results, page_num, page_length) except ValueError: @@ -674,7 +711,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrow_searcher.close() return results - +# ZYY: 处理查询结果 def _process_results( self, raw_page, @@ -684,174 +721,153 @@ class WhooshSearchBackend(BaseSearchBackend): result_class=None): from haystack import connections results = [] - + # ZYY: 获取命中数 # It's important to grab the hits first before slicing. Otherwise, this # can cause pagination failures. hits = len(raw_page) - if result_class is None: - result_class = SearchResult - - facets = {} - spelling_suggestion = None - unified_index = connections[self.connection_alias].get_unified_index() - indexed_models = unified_index.get_indexed_models() - - for doc_offset, raw_result in enumerate(raw_page): - score = raw_page.score(doc_offset) or 0 - app_label, model_name = raw_result[DJANGO_CT].split('.') - additional_fields = {} - model = haystack_get_model(app_label, model_name) - - if model and model in indexed_models: - for key, value in raw_result.items(): - index = unified_index.get_index(model) - string_key = str(key) - - if string_key in index.fields and hasattr( - index.fields[string_key], 'convert'): - # Special-cased due to the nature of KEYWORD fields. - if index.fields[string_key].is_multivalued: - if value is None or len(value) == 0: - additional_fields[string_key] = [] - else: - additional_fields[string_key] = value.split( - ',') - else: - additional_fields[string_key] = index.fields[string_key].convert( - value) + # ZYY: 如果result_class为None,则将其设置为SearchResult +if result_class is None: + result_class = SearchResult +facets = {} +spelling_suggestion = None +# ZYY: 获取统一索引 +unified_index = connections[self.connection_alias].get_unified_index() +# ZYY: 获取已索引的模型列表 +indexed_models = unified_index.get_indexed_models() +for doc_offset, raw_result in enumerate(raw_page): + # ZYY: 获取文档得分,如果没有则为0 + score = raw_page.score(doc_offset) or 0 + app_label, model_name = raw_result[DJANGO_CT].split('.') + additional_fields = {} + # ZYY: 根据app_label和model_name获取模型 + model = haystack_get_model(app_label, model_name) + if model and model in indexed_models: + for key, value in raw_result.items(): + index = unified_index.get_index(model) + string_key = str(key) + # ZYY : 检查字段是否在索引中且具有convert方法,特殊处理KEYWORD字段 + if string_key in index.fields and hasattr( + index.fields[string_key], 'convert'): + # ZYY: 处理多值字段 + if index.fields[string_key].is_multivalued: + if value is None or len(value) == 0: + additional_fields[string_key] = [] else: - additional_fields[string_key] = self._to_python(value) - - del (additional_fields[DJANGO_CT]) - del (additional_fields[DJANGO_ID]) - - if highlight: - sa = StemmingAnalyzer() - formatter = WhooshHtmlFormatter('em') - terms = [token.text for token in sa(query_string)] - - whoosh_result = whoosh_highlight( - additional_fields.get(self.content_field_name), - terms, - sa, - ContextFragmenter(), - formatter - ) - additional_fields['highlighted'] = { - self.content_field_name: [whoosh_result], - } - - result = result_class( - app_label, - model_name, - raw_result[DJANGO_ID], - score, - **additional_fields) - results.append(result) - else: - hits -= 1 - - if self.include_spelling: - if spelling_query: - spelling_suggestion = self.create_spelling_suggestion( - spelling_query) + additional_fields[string_key] = value.split( + ',') + else: + additional_fields[string_key] = index.fields[string_key].convert( + value) else: - spelling_suggestion = self.create_spelling_suggestion( - query_string) - - return { - 'results': results, - 'hits': hits, - 'facets': facets, - 'spelling_suggestion': spelling_suggestion, - } - - def create_spelling_suggestion(self, query_string): - spelling_suggestion = None - reader = self.index.reader() - corrector = reader.corrector(self.content_field_name) - cleaned_query = force_str(query_string) - - if not query_string: - return spelling_suggestion - - # Clean the string. - for rev_word in self.RESERVED_WORDS: - cleaned_query = cleaned_query.replace(rev_word, '') - - for rev_char in self.RESERVED_CHARACTERS: - cleaned_query = cleaned_query.replace(rev_char, '') - - # Break it down. - query_words = cleaned_query.split() - suggested_words = [] - - for word in query_words: - suggestions = corrector.suggest(word, limit=1) - - if len(suggestions) > 0: - suggested_words.append(suggestions[0]) - - spelling_suggestion = ' '.join(suggested_words) + additional_fields[string_key] = self._to_python(value) + del (additional_fields[DJANGO_CT]) + del (additional_fields[DJANGO_ID]) + if highlight: + sa = StemmingAnalyzer() + formatter = WhooshHtmlFormatter('em') + terms = [token .text for token in sa(query_string)] + # ZYY: 使用Whoosh进行高亮显示 + whoosh_result = whoosh_highlight( + additional_fields.get(self.content_field_name), + terms, + sa, + ContextFragmenter(), + formatter + ) + additional_fields['highlighted'] = { + self.content_field_name: [whoosh_result], + } + result = result_class( + app_label, + model_name, + raw_result[DJANGO_ID], + score, + **additional_fields) + results.append(result) + else: + hits -= 1 +if self.include_spelling: + if spelling_query: + # ZYY: 根据spelling_query创建拼写建议 + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + # ZYY: 根据query_string创建拼写建议 + spelling_suggestion = self.create_spelling_suggestion( + query_string) +return { + 'results': results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, +} + +# ZYY: 创建拼写建议的方法 +def create_spelling_suggestion(self, query_string): + spelling_suggestion = None + reader = self.index.reader() + corrector = reader.corrector(self.content_field_name) + cleaned_query = force_str(query_string) + if not query_string: return spelling_suggestion - - def _from_python(self, value): - """ - Converts Python values to a string for Whoosh. - - Code courtesy of pysolr. - """ - if hasattr(value, 'strftime'): - if not hasattr(value, 'hour'): - value = datetime(value.year, value.month, value.day, 0, 0, 0) - elif isinstance(value, bool): - if value: - value = 'true' - else: - value = 'false' - elif isinstance(value, (list, tuple)): - value = u','.join([force_str(v) for v in value]) - elif isinstance(value, (six.integer_types, float)): - # Leave it alone. - pass + # ZYY: 清理字符串,移除保留字和保留字符 + for rev_word in self.RESERVED_WORDS: + cleaned_query = cleaned_query.replace(rev_word, '') + for rev_char in self.RESERVED_CHARACTERS: + cleaned_query = cleaned_query.replace(rev_char, '') + # ZYY: 分解查询词 + query_words = cleaned_query.split() + suggested_words = [] + for word in query_words: + suggestions = corrector.suggest(word, limit=1) + if len(suggestions) > 0: + suggested_words.append(suggestions[0]) + spelling_suggestion = ' '.join(suggested_words) + return spelling_suggestion + +# ZYY: 将Python值转换为Whoosh使用的字符串 +def _from_python(self, value): + if hasattr(value, 'strftime'): + if not hasattr(value, 'hour'): + value = datetime(value.year, value.month, value.day, 0, 0, 0) + elif isinstance(value, bool): + if value: + value = ' true' else: - value = force_str(value) - return value - - def _to_python(self, value): - """ - Converts values from Whoosh to native Python values. - - A port of the same method in pysolr, as they deal with data the same way. - """ - if value == 'true': - return True - elif value == 'false': - return False - - if value and isinstance(value, six.string_types): - possible_datetime = DATETIME_REGEX.search(value) - - if possible_datetime: - date_values = possible_datetime.groupdict() - - for dk, dv in date_values.items(): - date_values[dk] = int(dv) - - return datetime( - date_values['year'], - date_values['month'], - date_values['day'], - date_values['hour'], - date_values['minute'], - date_values['second']) - + value = 'false' + elif isinstance(value, (list, tuple)): + value = u','.join([force_str(v) for v in value]) + elif isinstance(value, (six.integer_types, float)): + # ZYY: 整数和浮点数保持不变 + pass + else: + value = force_str(value) + return value + +# ZYY: 将Whoosh的值转换为Python原生值 +def _to_python(self, value): + if value == 'true ': + return True + elif value == 'false': + return False + if value and isinstance(value, six.string_types): + possible_datetime = DATETIME_REGEX.search(value) + if possible_datetime: + date_values = possible_datetime.groupdict() + for dk, dv in date_values.items(): + date_values[dk] = int(dv) + return datetime( + date_values['year'], + date_values['month'], + date_values ['day'], + date_values['hour'], + date_values['minute'], + date_values['second']) try: - # Attempt to use json to load the values. + # ZYY: 尝试使用json加载值 converted_value = json.loads(value) - - # Try to handle most built-in types. + # ZYY: 尝试处理大多数内置类型 if isinstance( converted_value, (list, @@ -863,182 +879,139 @@ class WhooshSearchBackend(BaseSearchBackend): complex)): return converted_value except BaseException: - # If it fails (SyntaxError or its ilk) or we don't trust it, - # continue on. - pass - - return value + # ZYY: 如果失败或不信任,则继续 + pass + return value class WhooshSearchQuery(BaseSearchQuery): + # ZYY: 将datetime对象转换为Whoosh使用的字符串 def _convert_datetime(self, date): if hasattr(date, 'hour'): return force_str(date.strftime('%Y%m%d%H%M%S')) else: - return force_str(date.strftime('%Y%m%d000000')) + return force_str(date.strftime('%Y%m%d000000')) + # ZYY: 清理查询片段,处理保留字和保留字符 def clean(self, query_fragment): - """ - Provides a mechanism for sanitizing user input before presenting the - value to the backend. - - Whoosh 1.X differs here in that you can no longer use a backslash - to escape reserved characters. Instead, the whole word should be - quoted. - """ words = query_fragment.split() cleaned_words = [] - for word in words: if word in self.backend.RESERVED_WORDS: word = word.replace(word, word.lower()) - for char in self.backend.RESERVED_CHARACTERS: if char in word: word = "'%s'" % word break - - cleaned_words.append(word) - + cleaned_words. append(word) return ' '.join(cleaned_words) + # ZYY: 构建查询片段 def build_query_fragment(self, field, filter_type, value): from haystack import connections query_frag = '' is_datetime = False - if not hasattr(value, 'input_type_name'): - # Handle when we've got a ``ValuesListQuerySet``... + # ZYY: 处理ValuesListQuerySet的情况 if hasattr(value, 'values_list'): value = list(value) - - if hasattr(value, 'strftime'): + if hasattr(value, ' strftime'): is_datetime = True - if isinstance(value, six.string_types) and value != ' ': - # It's not an ``InputType``. Assume ``Clean``. + # ZYY: 假设为Clean类型 value = Clean(value) else: value = PythonData(value) - - # Prepare the query using the InputType. - prepared_value = value.prepare(self) - - if not isinstance(prepared_value, (set, list, tuple)): - # Then convert whatever we get back to what pysolr wants if needed. - prepared_value = self.backend._from_python(prepared_value) - - # 'content' is a special reserved word, much like 'pk' in - # Django's ORM layer. It indicates 'no special field'. - if field == 'content': - index_fieldname = '' - else: - index_fieldname = u'%s:' % connections[self._using].get_unified_index( - ).get_index_fieldname(field) - - filter_types = { - 'content': '%s', - 'contains': '*%s*', - 'endswith': "*%s", - 'startswith': "%s*", - 'exact': '%s', - 'gt': "{%s to}", - 'gte': "[%s to]", - 'lt': "{to %s}", - 'lte': "[to %s]", - 'fuzzy': u'%s~', - } - - if value.post_process is False: - query_frag = prepared_value - else: - if filter_type in [ - 'content', - 'contains', - 'startswith', - 'endswith', - 'fuzzy']: - if value.input_type_name == 'exact': - query_frag = prepared_value - else: - # Iterate over terms & incorportate the converted form of - # each into the query. - terms = [] - - if isinstance(prepared_value, six.string_types): - possible_values = prepared_value.split(' ') + # ZYY: 使用InputType准备查询 + prepared_value = value.prepare(self) + if not isinstance(prepared_value, (set, list, tuple )): + # ZYY: 转换为pysolr需要的格式 + prepared_value = self.backend._from_python(prepared_value) + # ZYY: 'content'是特殊保留字,表示无特殊字段 + if field == 'content': + index_fieldname = '' + else: + index_fieldname = u'%s:' % connections[self._using].get_unified_index( + ).get_index_fieldname(field) + filter_types = { + 'content': '%s', + 'contains': '*%s*', + ' endswith': "*%s", + 'startswith': "%s*", + 'exact': '%s', + 'gt': "{%s to}", + 'gte': "[%s to]", + 'lt': "{to %s}", + 'lte': "[to %s]", + 'fuzzy': u'%s~', + } + if value.post_process is False: + query_frag = prepared_value + else: + if filter_type in [ + 'content', + 'contains', + 'startswith ', + 'endswith', + 'fuzzy']: + if value.input_type_name == 'exact': + query_frag = prepared_value else: + # ZYY: 遍历词项并合并转换后的形式 + terms = [] + if isinstance(prepared_value, six.string_types): + possible_values = prepared_value.split(' ') + else: + if is_datetime is True: + prepared_value = self._convert_datetime( + prepared_value) + possible_values = [prepared_value] + for possible_value in possible_values: + terms.append( + filter_types[filter_type] % + self.backend._from_python(possible_value)) + if len(terms) == 1: + query_frag = terms[0] + else: + query_frag = u"(%s)" % " AND ".join(terms) + elif filter_type == 'in': + in_options = [] + for possible_value in prepared_value: + is_datetime = False + if hasattr(possible_value, 'strftime'): + is_datetime = True + pv = self.backend._from_python (possible_value) if is_datetime is True: - prepared_value = self._convert_datetime( - prepared_value) - - possible_values = [prepared_value] - - for possible_value in possible_values: - terms.append( - filter_types[filter_type] % - self.backend._from_python(possible_value)) - - if len(terms) == 1: - query_frag = terms[0] - else: - query_frag = u"(%s)" % " AND ".join(terms) - elif filter_type == 'in': - in_options = [] - - for possible_value in prepared_value: - is_datetime = False - - if hasattr(possible_value, 'strftime'): - is_datetime = True - - pv = self.backend._from_python(possible_value) - - if is_datetime is True: - pv = self._convert_datetime(pv) - - if isinstance(pv, six.string_types) and not is_datetime: - in_options.append('"%s"' % pv) + pv = self._convert_datetime(pv) + if isinstance(pv, six.string_types) and not is_datetime: + in_options.append('"%s"' % pv) + else: + in_options.append('%s' % pv) + query_frag = "(%s)" % " OR ".join(in_options) + elif filter_type == 'range': + start = self.backend._from_python(prepared_value[0]) + end = self.backend._from_python(prepared_value[1]) + if hasattr(prepared_value [0], 'strftime'): + start = self._convert_datetime(start) + if hasattr(prepared_value[1], 'strftime'): + end = self._convert_datetime(end) + query_frag = u"[%s to %s]" % (start, end) + elif filter_type == 'exact': + if value.input_type_name == 'exact': + query_frag = prepared_value else: - in_options.append('%s' % pv) - - query_frag = "(%s)" % " OR ".join(in_options) - elif filter_type == 'range': - start = self.backend._from_python(prepared_value[0]) - end = self.backend._from_python(prepared_value[1]) - - if hasattr(prepared_value[0], 'strftime'): - start = self._convert_datetime(start) - - if hasattr(prepared_value[1], 'strftime'): - end = self._convert_datetime(end) - - query_frag = u"[%s to %s]" % (start, end) - elif filter_type == 'exact': - if value.input_type_name == 'exact': - query_frag = prepared_value + prepared_value = Exact(prepared_value).prepare(self) + query_frag = filter_types[filter_type] % prepared_value else: - prepared_value = Exact(prepared_value).prepare(self) + if is_datetime is True: + prepared_value = self._convert_datetime(prepared_value) query_frag = filter_types[filter_type] % prepared_value - else: - if is_datetime is True: - prepared_value = self._convert_datetime(prepared_value) - - query_frag = filter_types[filter_type] % prepared_value - - if len(query_frag) and not isinstance(value, Raw): - if not query_frag.startswith('(') and not query_frag.endswith(')'): - query_frag = "(%s)" % query_frag - + if len(query_frag) and not isinstance(value, Raw): + if not query_frag.startswith('(') and not query_frag.endswith(')'): + query_frag = "(%s)" % query_frag return u"%s%s" % (index_fieldname, query_frag) - # if not filter_type in ('in', 'range'): - # # 'in' is a bit of a special case, as we don't want to - # # convert a valid list/tuple to string. Defer handling it - # # until later... - # value = self.backend._from_python(value) - class WhooshEngine(BaseEngine): backend = WhooshSearchBackend - query = WhooshSearchQuery + query = WhooshSearchQuery \ No newline at end of file diff --git a/src/django-master/djangoblog/wsgi.py b/src/django-master/djangoblog/wsgi.py index 2295efd..8ee9ba4 100644 --- a/src/django-master/djangoblog/wsgi.py +++ b/src/django-master/djangoblog/wsgi.py @@ -6,11 +6,19 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ """ - +# ZYY: 导入操作系统模块,用于与操作系统交互(如环境变量设置) import os +# ZYY: 从Django核心模块导入WSGI应用获取函数 +# ZYY: WSGI (Web Server Gateway Interface) 是Python web应用与服务器之间的标准接口 from django.core.wsgi import get_wsgi_application +# ZYY: 设置默认环境变量DJANGO_SETTINGS_MODULE +# ZYY: 该变量指定Django项目的设置模块路径(格式为"项目名.settings") +# ZYY: os.environ.setdefault()表示如果环境变量已存在则不覆盖 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") +# ZYY: 获取WSGI应用实例并赋值给application变量 +# ZYY: 这是WSGI服务器的入口点,服务器将通过这个变量调用Django应用 +# ZYY: get_wsgi_application()会加载Django设置并初始化应用 application = get_wsgi_application()