commit fba1ac0efffa3b493a91c155c6e923237fc096f4 Author: ZXY <1908008916@qq.com> Date: Sat Nov 8 18:23:15 2025 +0800 Initial commit: djangoblog core diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3279070 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +# Zxy指定默认的应用配置类 +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..5853089 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/admin_site.cpython-312.pyc b/__pycache__/admin_site.cpython-312.pyc new file mode 100644 index 0000000..0fa2546 Binary files /dev/null and b/__pycache__/admin_site.cpython-312.pyc differ diff --git a/__pycache__/apps.cpython-312.pyc b/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..46ec159 Binary files /dev/null and b/__pycache__/apps.cpython-312.pyc differ diff --git a/__pycache__/blog_signals.cpython-312.pyc b/__pycache__/blog_signals.cpython-312.pyc new file mode 100644 index 0000000..3632114 Binary files /dev/null and b/__pycache__/blog_signals.cpython-312.pyc differ diff --git a/__pycache__/elasticsearch_backend.cpython-312.pyc b/__pycache__/elasticsearch_backend.cpython-312.pyc new file mode 100644 index 0000000..87a8e07 Binary files /dev/null and b/__pycache__/elasticsearch_backend.cpython-312.pyc differ diff --git a/__pycache__/feeds.cpython-312.pyc b/__pycache__/feeds.cpython-312.pyc new file mode 100644 index 0000000..3da3d84 Binary files /dev/null and b/__pycache__/feeds.cpython-312.pyc differ diff --git a/__pycache__/logentryadmin.cpython-312.pyc b/__pycache__/logentryadmin.cpython-312.pyc new file mode 100644 index 0000000..468536b Binary files /dev/null and b/__pycache__/logentryadmin.cpython-312.pyc differ diff --git a/__pycache__/settings.cpython-312.pyc b/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000..2e2c752 Binary files /dev/null and b/__pycache__/settings.cpython-312.pyc differ diff --git a/__pycache__/sitemap.cpython-312.pyc b/__pycache__/sitemap.cpython-312.pyc new file mode 100644 index 0000000..36414fc Binary files /dev/null and b/__pycache__/sitemap.cpython-312.pyc differ diff --git a/__pycache__/spider_notify.cpython-312.pyc b/__pycache__/spider_notify.cpython-312.pyc new file mode 100644 index 0000000..200e720 Binary files /dev/null and b/__pycache__/spider_notify.cpython-312.pyc differ diff --git a/__pycache__/urls.cpython-312.pyc b/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..513c117 Binary files /dev/null and b/__pycache__/urls.cpython-312.pyc differ diff --git a/__pycache__/utils.cpython-312.pyc b/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..8205f44 Binary files /dev/null and b/__pycache__/utils.cpython-312.pyc differ diff --git a/__pycache__/whoosh_cn_backend.cpython-312.pyc b/__pycache__/whoosh_cn_backend.cpython-312.pyc new file mode 100644 index 0000000..f9c8676 Binary files /dev/null and b/__pycache__/whoosh_cn_backend.cpython-312.pyc differ diff --git a/__pycache__/wsgi.cpython-312.pyc b/__pycache__/wsgi.cpython-312.pyc new file mode 100644 index 0000000..0eff43b Binary files /dev/null and b/__pycache__/wsgi.cpython-312.pyc differ diff --git a/admin_site.py b/admin_site.py new file mode 100644 index 0000000..a0cb357 --- /dev/null +++ b/admin_site.py @@ -0,0 +1,60 @@ +# Zxy导入 Django 的 AdminSite 类和其他相关模块 +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 + +# Zxy导入自定义模块和模型 +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 * + +# Zxy定义自定义 AdminSite 类 +class DjangoBlogAdminSite(AdminSite): + # 自定义站点标题和头部 + site_header = 'djangoblog administration' + site_title = 'djangoblog site admin' + + # 初始化方法 + def __init__(self, name='admin'): + super().__init__(name) + + # 定义权限检查方法,仅允许超级用户访问 + def has_permission(self, request): + return request.user.is_superuser + +# Zxy创建自定义 AdminSite 实例 +admin_site = DjangoBlogAdminSite(name='admin') + +# Zxy注册模型到自定义 AdminSite +admin_site.register(Article, ArticleAdmin) +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) + +admin_site.register(commands, CommandsAdmin) +admin_site.register(EmailSendLog, EmailSendLogAdmin) + +admin_site.register(BlogUser, BlogUserAdmin) + +admin_site.register(Comment, CommentAdmin) + +admin_site.register(OAuthUser, OAuthUserAdmin) +admin_site.register(OAuthConfig, OAuthConfigAdmin) + +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) + +admin_site.register(Site, SiteAdmin) + +admin_site.register(LogEntry, LogEntryAdmin) \ No newline at end of file diff --git a/apps.py b/apps.py new file mode 100644 index 0000000..f902735 --- /dev/null +++ b/apps.py @@ -0,0 +1,16 @@ +# Zxy导入Django的AppConfig模块 +from django.apps import AppConfig + +# Zxy定义Djangoblog应用的配置类 +class DjangoblogAppConfig(AppConfig): + # Zxy设置默认的自动字段类型 + default_auto_field = 'django.db.models.BigAutoField' + # Zxy应用名称 + name = 'djangoblog' + + # Zxy重写ready方法,用于初始化应用 + def ready(self): + super().ready() # Zxy调用父类的ready方法 + # Zxy导入并加载插件 + from .plugin_manage.loader import load_plugins + load_plugins() # Zxy加载插件 \ No newline at end of file diff --git a/blog_signals.py b/blog_signals.py new file mode 100644 index 0000000..9ba3376 --- /dev/null +++ b/blog_signals.py @@ -0,0 +1,133 @@ +# Zxy导入线程模块 +import _thread +# Zxy导入日志模块 +import logging + +# Zxy导入Django的信号模块 +import django.dispatch +# Zxy导入Django的配置模块 +from django.conf import settings +# Zxy导入Django的LogEntry模型 +from django.contrib.admin.models import LogEntry +# Zxy导入用户登录和登出信号 +from django.contrib.auth.signals import user_logged_in, user_logged_out +# Zxy导入邮件发送模块 +from django.core.mail import EmailMultiAlternatives +# Zxy导入模型保存信号 +from django.db.models.signals import post_save +# Zxy导入信号接收器 +from django.dispatch import receiver + +# Zxy导入评论模型 +from comments.models import Comment +# Zxy导入发送评论邮件的工具函数 +from comments.utils import send_comment_email +# Zxy导入爬虫通知工具 +from djangoblog.spider_notify import SpiderNotify +# Zxy导入缓存和缓存清理工具 +from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache +# Zxy导入获取当前站点的工具函数 +from djangoblog.utils import get_current_site +# Zxy导入OAuth用户模型 +from oauth.models import OAuthUser + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy定义OAuth用户登录信号 +oauth_user_login_signal = django.dispatch.Signal(['id']) +# Zxy定义发送邮件信号 +send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content']) + +# Zxy定义发送邮件信号的处理函数 +@receiver(send_email_signal) +def send_email_signal_handler(sender, **kwargs): + emailto = kwargs['emailto'] # Zxy收件人 + title = kwargs['title'] # Zxy邮件标题 + content = kwargs['content'] # Zxy邮件内容 + + msg = EmailMultiAlternatives( # Zxy创建邮件对象 + title, + content, + from_email=settings.DEFAULT_FROM_EMAIL, # Zxy发件人 + to=emailto # Zxy收件人 + ) + msg.content_subtype = "html" # Zxy设置邮件内容类型为HTML + + from servermanager.models import EmailSendLog # Zxy导入邮件发送日志模型 + log = EmailSendLog() # Zxy创建日志记录 + log.title = title + log.content = content + log.emailto = ','.join(emailto) # Zxy记录收件人 + + try: + result = msg.send() # Zxy发送邮件 + log.send_result = result > 0 # Zxy记录发送结果 + except Exception as e: + logger.error(f"失败邮箱号: {emailto}, {e}") # Zxy记录错误信息 + log.send_result = False + log.save() # Zxy保存日志 + +# Zxy定义OAuth用户登录信号的处理函数 +@receiver(oauth_user_login_signal) +def oauth_user_login_signal_handler(sender, **kwargs): + id = kwargs['id'] # Zxy获取用户ID + oauthuser = OAuthUser.objects.get(id=id) # Zxy获取OAuth用户 + site = get_current_site().domain # Zxy获取当前站点域名 + if oauthuser.picture and not oauthuser.picture.find(site) >= 0: # Zxy检查头像URL是否包含当前站点域名 + from djangoblog.utils import save_user_avatar # Zxy导入保存头像的工具函数 + oauthuser.picture = save_user_avatar(oauthuser.picture) # Zxy保存用户头像 + oauthuser.save() # Zxy保存用户信息 + + delete_sidebar_cache() # Zxy删除侧边栏缓存 + +# Zxy定义模型保存后的回调函数 +@receiver(post_save) +def model_post_save_callback(sender, instance, created, raw, using, update_fields, **kwargs): + clearcache = False # Zxy标记是否需要清理缓存 + if isinstance(instance, LogEntry): # Zxy如果是LogEntry模型,直接返回 + return + if 'get_full_url' in dir(instance): # Zxy检查是否有获取完整URL的方法 + is_update_views = update_fields == {'views'} # Zxy检查是否是更新浏览次数 + if not settings.TESTING and not is_update_views: # Zxy如果不是测试环境且不是更新浏览次数 + try: + notify_url = instance.get_full_url() # Zxy获取完整URL + SpiderNotify.baidu_notify([notify_url]) # Zxy通知百度爬虫 + except Exception as ex: + logger.error("notify spider", ex) # Zxy记录错误信息 + if not is_update_views: + clearcache = True # Zxy标记清理缓存 + + if isinstance(instance, Comment): # Zxy如果是评论模型 + if instance.is_enable: # Zxy检查评论是否启用 + path = instance.article.get_absolute_url() # Zxy获取文章的绝对URL + site = get_current_site().domain # Zxy获取当前站点域名 + if site.find(':') > 0: # Zxy去除端口号 + site = site[0:site.find(':')] + + expire_view_cache( # Zxy清理视图缓存 + path, + servername=site, + serverport=80, + key_prefix='blogdetail' + ) + if cache.get('seo_processor'): # Zxy清理SEO处理器缓存 + cache.delete('seo_processor') + comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id) # Zxy生成评论缓存键 + cache.delete(comment_cache_key) # Zxy清理评论缓存 + delete_sidebar_cache() # Zxy清理侧边栏缓存 + delete_view_cache('article_comments', [str(instance.article.pk)]) # Zxy清理文章评论缓存 + + _thread.start_new_thread(send_comment_email, (instance,)) # Zxy异步发送评论邮件 + + if clearcache: # Zxy清理缓存 + cache.clear() + +# Zxy定义用户登录和登出的回调函数 +@receiver(user_logged_in) +@receiver(user_logged_out) +def user_auth_callback(sender, request, user, **kwargs): + if user and user.username: # Zxy检查用户是否登录 + logger.info(user) # Zxy记录用户信息 + delete_sidebar_cache() # Zxy清理侧边栏缓存 + # cache.clear() # Zxy清理所有缓存(暂时注释) \ No newline at end of file diff --git a/elasticsearch_backend.py b/elasticsearch_backend.py new file mode 100644 index 0000000..55a9b89 --- /dev/null +++ b/elasticsearch_backend.py @@ -0,0 +1,196 @@ +# Zxy导入Django的编码工具 +from django.utils.encoding import force_str +# Zxy导入Elasticsearch DSL的查询构造器 +from elasticsearch_dsl import Q +# Zxy导入Haystack的搜索后端模块 +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query +# Zxy导入Haystack的表单模块 +from haystack.forms import ModelSearchForm +# Zxy导入Haystack的搜索结果模型 +from haystack.models import SearchResult +# Zxy导入Haystack的日志工具 +from haystack.utils import log as logging + +# Zxy导入项目中的Elasticsearch文档和管理器 +from blog.documents import ArticleDocument, ArticleDocumentManager +# Zxy导入项目中的文章模型 +from blog.models import Article + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy定义Elasticsearch搜索后端类 +class ElasticSearchBackend(BaseSearchBackend): + # Zxy初始化方法 + def __init__(self, connection_alias, **connection_options): + super(ElasticSearchBackend, self).__init__(connection_alias, **connection_options) + self.manager = ArticleDocumentManager() # Zxy初始化文档管理器 + self.include_spelling = True # Zxy启用拼写建议 + + # Zxy获取模型数据并转换为文档 + def _get_models(self, iterable): + models = iterable if iterable and iterable[0] else Article.objects.all() # Zxy获取模型数据 + docs = self.manager.convert_to_doc(models) # Zxy转换为Elasticsearch文档 + return docs + + # Zxy创建索引并重建数据 + def _create(self, models): + self.manager.create_index() # Zxy创建索引 + docs = self._get_models(models) # Zxy获取文档 + self.manager.rebuild(docs) # Zxy重建索引数据 + + # Zxy删除模型数据 + def _delete(self, models): + for m in models: + m.delete() # Zxy删除模型实例 + return True + + # Zxy重建索引数据 + def _rebuild(self, models): + models = models if models else Article.objects.all() # Zxy获取模型数据 + docs = self.manager.convert_to_doc(models) # Zxy转换为文档 + self.manager.update_docs(docs) # Zxy更新索引数据 + + # Zxy更新索引 + def update(self, index, iterable, commit=True): + models = self._get_models(iterable) # Zxy获取文档 + self.manager.update_docs(models) # Zxy更新索引 + + # Zxy删除文档 + def remove(self, obj_or_string): + models = self._get_models([obj_or_string]) # Zxy获取文档 + self._delete(models) # Zxy删除文档 + + # Zxy清空索引 + def clear(self, models=None, commit=True): + self.remove(None) # Zxy删除所有文档 + + # Zxy获取拼写建议 + @staticmethod + def get_suggestion(query: str) -> str: + """获取推荐词,如果没有找到则返回原搜索词""" + search = ArticleDocument.search() \ + .query("match", body=query) \ + .suggest('suggest_search', query, term={'field': 'body'}) \ + .execute() + + keywords = [] + for suggest in search.suggest.suggest_search: + if suggest["options"]: # Zxy检查是否有建议选项 + keywords.append(suggest["options"][0]["text"]) # Zxy添加建议词 + else: + keywords.append(suggest["text"]) # Zxy添加原搜索词 + + return ' '.join(keywords) # Zxy返回拼写建议 + + # Zxy执行搜索查询 + @log_query + def search(self, query_string, **kwargs): + logger.info('search query_string:' + query_string) # Zxy记录搜索查询 + + start_offset = kwargs.get('start_offset') # Zxy获取起始偏移量 + end_offset = kwargs.get('end_offset') # Zxy获取结束偏移量 + + # Zxy检查是否启用拼写建议 + if getattr(self, "is_suggest", None): + suggestion = self.get_suggestion(query_string) # Zxy获取拼写建议 + else: + suggestion = query_string # Zxy使用原搜索词 + + q = Q('bool', # Zxy构造布尔查询 + should=[Q('match', body=suggestion), Q('match', title=suggestion)], # Zxy匹配标题或正文 + minimum_should_match="70%") # Zxy至少匹配70% + + search = ArticleDocument.search() \ + .query('bool', filter=[q]) \ + .filter('term', status='p') \ + .filter('term', type='a') \ + .source(False)[start_offset: end_offset] # Zxy执行搜索 + + results = search.execute() # Zxy执行搜索查询 + hits = results['hits'].total # Zxy获取总匹配数 + raw_results = [] # Zxy初始化结果列表 + + for raw_result in results['hits']['hits']: # Zxy遍历搜索结果 + app_label = 'blog' # Zxy应用标签 + model_name = 'Article' # Zxy模型名称 + additional_fields = {} # Zxy额外字段 + + result_class = SearchResult # Zxy搜索结果类 + + result = result_class( # Zxy创建搜索结果对象 + app_label, + model_name, + raw_result['_id'], + raw_result['_score'], + **additional_fields + ) + raw_results.append(result) # Zxy添加到结果列表 + + facets = {} # Zxy初始化分面信息 + spelling_suggestion = None if query_string == suggestion else suggestion # Zxy拼写建议 + + return { + 'results': raw_results, # Zxy返回搜索结果 + 'hits': hits, # Zxy返回匹配数 + 'facets': facets, # Zxy返回分面信息 + 'spelling_suggestion': spelling_suggestion, # Zxy返回拼写建议 + } + +# Zxy定义Elasticsearch搜索查询类 +class ElasticSearchQuery(BaseSearchQuery): + # Zxy转换日期时间格式 + 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')) + + # Zxy清理用户输入 + def clean(self, query_fragment): + 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) + + return ' '.join(cleaned_words) + + # Zxy构建查询片段 + def build_query_fragment(self, field, filter_type, value): + return value.query_string + + # Zxy获取结果数量 + def get_count(self): + results = self.get_results() + return len(results) if results else 0 + + # Zxy获取拼写建议 + def get_spelling_suggestion(self, preferred_query=None): + return self._spelling_suggestion + + # Zxy构建查询参数 + def build_params(self, spelling_query=None): + kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) + return kwargs + +# Zxy定义Elasticsearch模型搜索表单 +class ElasticSearchModelSearchForm(ModelSearchForm): + # Zxy执行搜索 + def search(self): + self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" # Zxy检查是否启用拼写建议 + sqs = super().search() # Zxy调用父类搜索方法 + return sqs + +# Zxy定义Elasticsearch搜索引擎 +class ElasticSearchEngine(BaseEngine): + backend = ElasticSearchBackend # Zxy后端类 + query = ElasticSearchQuery # Zxy查询类 \ No newline at end of file diff --git a/feeds.py b/feeds.py new file mode 100644 index 0000000..c7afbe3 --- /dev/null +++ b/feeds.py @@ -0,0 +1,54 @@ +# Zxy导入Django的用户模型 +from django.contrib.auth import get_user_model +# Zxy导入Django的Feed视图 +from django.contrib.syndication.views import Feed +# Zxy导入Django的时区工具 +from django.utils import timezone +# Zxy导入RSS 2.0 Feed生成器 +from django.utils.feedgenerator import Rss201rev2Feed + +# Zxy导入项目中的文章模型 +from blog.models import Article +# Zxy导入Markdown工具 +from djangoblog.utils import CommonMarkdown + +# Zxy定义Django博客Feed +class DjangoBlogFeed(Feed): + feed_type = Rss201rev2Feed # Zxy使用RSS 2.0格式 + + description = '大巧无工,重剑无锋.' # ZxyFeed描述 + title = "且听风吟 大巧无工,重剑无锋." # ZxyFeed标题 + link = "/feed/" # ZxyFeed链接 + + # Zxy获取作者名称 + def author_name(self): + return get_user_model().objects.first().nickname + + # Zxy获取作者链接 + def author_link(self): + return get_user_model().objects.first().get_absolute_url() + + # Zxy获取Feed项 + def items(self): + return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] # Zxy获取最近5篇已发布的文章 + + # Zxy获取Feed项标题 + def item_title(self, item): + return item.title + + # Zxy获取Feed项描述 + def item_description(self, item): + return CommonMarkdown.get_markdown(item.body) # Zxy将文章内容转换为Markdown格式 + + # Zxy获取Feed版权信息 + def feed_copyright(self): + now = timezone.now() + return "Copyright© {year} 且听风吟".format(year=now.year) # Zxy动态生成版权年份 + + # Zxy获取Feed项链接 + def item_link(self, item): + return item.get_absolute_url() + + # Zxy获取Feed项GUID + def item_guid(self, item): + return \ No newline at end of file diff --git a/logentryadmin.py b/logentryadmin.py new file mode 100644 index 0000000..e1b4bd3 --- /dev/null +++ b/logentryadmin.py @@ -0,0 +1,88 @@ +# Zxy导入Django的admin模块 +from django.contrib import admin +# Zxy导入Django的LogEntry模型 +from django.contrib.admin.models import DELETION +# Zxy导入Django的内容类型模型 +from django.contrib.contenttypes.models import ContentType +# Zxy导入Django的URL工具 +from django.urls import reverse, NoReverseMatch +# Zxy导入Django的编码工具 +from django.utils.encoding import force_str +# Zxy导入Django的HTML工具 +from django.utils.html import escape +# Zxy导入Django的安全字符串工具 +from django.utils.safestring import mark_safe +# Zxy导入Django的翻译工具 +from django.utils.translation import gettext_lazy as _ + +# Zxy定义LogEntryAdmin类 +class LogEntryAdmin(admin.ModelAdmin): + list_filter = ['content_type'] # Zxy按内容类型过滤 + search_fields = ['object_repr', 'change_message'] # Zxy搜索字段 + + list_display_links = ['action_time', 'get_change_message'] # Zxy显示链接的字段 + list_display = ['action_time', 'user_link', 'content_type', 'object_link', 'get_change_message'] # Zxy显示字段 + + # Zxy检查是否有添加权限 + def has_add_permission(self, request): + return False + + # Zxy检查是否有修改权限 + 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' + + # Zxy检查是否有删除权限 + def has_delete_permission(self, request, obj=None): + return False + + # Zxy获取对象链接 + def object_link(self, obj): + object_link = escape(obj.object_repr) # Zxy转义对象表示 + content_type = obj.content_type + + if obj.action_flag != DELETION and content_type is not None: # Zxy检查是否为删除操作 + try: + url = reverse( # Zxy生成反向URL + 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), + args=[obj.object_id] + ) + object_link = '{}'.format(url, object_link) # Zxy生成链接 + except NoReverseMatch: + pass + return mark_safe(object_link) # Zxy标记为安全字符串 + + object_link.admin_order_field = 'object_repr' # Zxy排序字段 + object_link.short_description = _('object') # Zxy字段描述 + + # Zxy获取用户链接 + def user_link(self, obj): + content_type = ContentType.objects.get_for_model(type(obj.user)) # Zxy获取用户的内容类型 + user_link = escape(force_str(obj.user)) # Zxy转义用户表示 + + try: + url = reverse( # Zxy生成反向URL + 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), + args=[obj.user.pk] + ) + user_link = '{}'.format(url, user_link) # Zxy生成链接 + except NoReverseMatch: + pass + return mark_safe(user_link) # Zxy标记为安全字符串 + + user_link.admin_order_field = 'user' # Zxy排序字段 + user_link.short_description = _('user') # Zxy字段描述 + + # Zxy获取查询集 + def get_queryset(self, request): + queryset = super(LogEntryAdmin, self).get_queryset(request) + return queryset.prefetch_related('content_type') # Zxy预加载内容类型 + + # Zxy获取操作 + def get_actions(self, request): + actions = super(LogEntryAdmin, self).get_actions(request) + if 'delete_selected' in actions: # Zxy移除删除操作 + del actions['delete_selected'] + return actions \ No newline at end of file diff --git a/plugin_manage/__pycache__/base_plugin.cpython-312.pyc b/plugin_manage/__pycache__/base_plugin.cpython-312.pyc new file mode 100644 index 0000000..8039e14 Binary files /dev/null and b/plugin_manage/__pycache__/base_plugin.cpython-312.pyc differ diff --git a/plugin_manage/__pycache__/hook_constants.cpython-312.pyc b/plugin_manage/__pycache__/hook_constants.cpython-312.pyc new file mode 100644 index 0000000..fac9507 Binary files /dev/null and b/plugin_manage/__pycache__/hook_constants.cpython-312.pyc differ diff --git a/plugin_manage/__pycache__/hooks.cpython-312.pyc b/plugin_manage/__pycache__/hooks.cpython-312.pyc new file mode 100644 index 0000000..4dbba8f Binary files /dev/null and b/plugin_manage/__pycache__/hooks.cpython-312.pyc differ diff --git a/plugin_manage/__pycache__/loader.cpython-312.pyc b/plugin_manage/__pycache__/loader.cpython-312.pyc new file mode 100644 index 0000000..9bbb7c2 Binary files /dev/null and b/plugin_manage/__pycache__/loader.cpython-312.pyc differ diff --git a/plugin_manage/base_plugin.py b/plugin_manage/base_plugin.py new file mode 100644 index 0000000..e87d7df --- /dev/null +++ b/plugin_manage/base_plugin.py @@ -0,0 +1,48 @@ +# Zxy导入日志模块 +import logging + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy定义基础插件类 +class BasePlugin: + # Zxy插件元数据 + PLUGIN_NAME = None # Zxy插件名称 + PLUGIN_DESCRIPTION = None # Zxy插件描述 + PLUGIN_VERSION = None # Zxy插件版本 + + # Zxy初始化插件 + def __init__(self): + # Zxy检查插件元数据是否已定义 + 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() # Zxy调用初始化方法 + self.register_hooks() # Zxy注册钩子 + + # Zxy插件初始化逻辑 + def init_plugin(self): + """ + 插件初始化逻辑 + 子类可以重写此方法来实现特定的初始化操作 + """ + logger.info(f'{self.PLUGIN_NAME} initialized.') # Zxy记录插件初始化日志 + + # Zxy注册插件钩子 + def register_hooks(self): + """ + 注册插件钩子 + 子类可以重写此方法来注册特定的钩子 + """ + pass + + # Zxy获取插件信息 + def get_plugin_info(self): + """ + 获取插件信息 + :return: 包含插件元数据的字典 + """ + return { + 'name': self.PLUGIN_NAME, # Zxy插件名称 + 'description': self.PLUGIN_DESCRIPTION, # Zxy插件描述 + 'version': self.PLUGIN_VERSION # Zxy插件版本 + } \ No newline at end of file diff --git a/plugin_manage/hook_constants.py b/plugin_manage/hook_constants.py new file mode 100644 index 0000000..cabaed1 --- /dev/null +++ b/plugin_manage/hook_constants.py @@ -0,0 +1,8 @@ +# Zxy定义文章相关的钩子常量 +ARTICLE_DETAIL_LOAD = 'article_detail_load' # Zxy文章详情加载时触发的钩子 +ARTICLE_CREATE = 'article_create' # Zxy文章创建时触发的钩子 +ARTICLE_UPDATE = 'article_update' # Zxy文章更新时触发的钩子 +ARTICLE_DELETE = 'article_delete' # Zxy文章删除时触发的钩子 + +# Zxy定义文章内容处理的钩子名称 +ARTICLE_CONTENT_HOOK_NAME = "the_content" # Zxy文章内容处理的钩子名称 \ No newline at end of file diff --git a/plugin_manage/hooks.py b/plugin_manage/hooks.py new file mode 100644 index 0000000..75317ab --- /dev/null +++ b/plugin_manage/hooks.py @@ -0,0 +1,47 @@ +# Zxy导入日志模块 +import logging + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy定义全局钩子字典 +_hooks = {} + +# Zxy注册钩子回调 +def register(hook_name: str, callback: callable): + """ + 注册一个钩子回调。 + """ + if hook_name not in _hooks: # Zxy检查钩子是否已存在 + _hooks[hook_name] = [] # Zxy初始化钩子列表 + _hooks[hook_name].append(callback) # Zxy添加回调到钩子列表 + logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") # Zxy记录注册日志 + +# Zxy执行Action Hook +def run_action(hook_name: str, *args, **kwargs): + """ + 执行一个 Action Hook。 + 它会按顺序执行所有注册到该钩子上的回调函数。 + """ + if hook_name in _hooks: # Zxy检查钩子是否存在 + logger.debug(f"Running action hook '{hook_name}'") # Zxy记录执行日志 + for callback in _hooks[hook_name]: # Zxy遍历钩子回调 + try: + callback(*args, **kwargs) # Zxy执行回调 + except Exception as e: + logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) # Zxy记录错误日志 + +# Zxy执行Filter Hook +def apply_filters(hook_name: str, value, *args, **kwargs): + """ + 执行一个 Filter Hook。 + 它会把 value 依次传递给所有注册的回调函数进行处理。 + """ + if hook_name in _hooks: # Zxy检查钩子是否存在 + logger.debug(f"Applying filter hook '{hook_name}'") # Zxy记录执行日志 + for callback in _hooks[hook_name]: # Zxy遍历钩子回调 + try: + value = callback(value, *args, **kwargs) # Zxy调用回调处理值 + except Exception as e: + logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) # Zxy记录错误日志 + return value # Zxy返回处理后的值 \ No newline at end of file diff --git a/plugin_manage/loader.py b/plugin_manage/loader.py new file mode 100644 index 0000000..9ee7372 --- /dev/null +++ b/plugin_manage/loader.py @@ -0,0 +1,24 @@ +# Zxy导入os模块,用于操作路径 +import os +# Zxy导入日志模块 +import logging +# Zxy导入Django的配置模块 +from django.conf import settings + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy动态加载插件 +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: # Zxy遍历激活的插件 + plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) # Zxy获取插件路径 + if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): # Zxy检查插件目录和文件 + try: + __import__(f'plugins.{plugin_name}.plugin') # Zxy动态导入插件模块 + logger.info(f"Successfully loaded plugin: {plugin_name}") # Zxy记录加载成功日志 + except ImportError as e: + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) # Zxy记录加载失败日志 \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..943c243 --- /dev/null +++ b/settings.py @@ -0,0 +1,344 @@ +""" +Django settings for djangoblog project. + +Generated by 'django-admin startproject' using Django 1.10.2. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +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 _ + + +# Zxy环境变量布尔值转换函数:将环境变量转换为布尔值 +def env_to_bool(env, default): + str_val = os.environ.get(env) + return default if str_val is None else str_val == 'True' + + +# Zxy项目根目录:获取项目的绝对路径 +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! +SECRET_KEY = os.environ.get( + 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' +# Zxy调试模式:从环境变量获取或默认为 True +DEBUG = env_to_bool('DJANGO_DEBUG', True) +# Zxy测试模式检测:检查是否运行测试 +TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' + +# Zxy允许的主机名:允许访问的域名列表 +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] +# ZxyCSRF 可信来源:允许的 CSRF 来源 +CSRF_TRUSTED_ORIGINS = ['http://example.com'] +# Application definition + +# Zxy应用程序定义:定义项目中使用的 Django 应用程序 +INSTALLED_APPS = [ + # 默认的 Django 应用程序 + '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' +] +# Zxy中间件配置:定义请求处理的中间件 +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' +] +# ZxyURL 配置:定义项目的 URL 配置文件 +ROOT_URLCONF = 'djangoblog.urls' +# Zxy模板配置:配置 Django 模板引擎 +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' + ], + }, + }, +] +# ZxyWSGI 应用程序:定义 WSGI 应用程序入口 +WSGI_APPLICATION = 'djangoblog.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +# Zxy数据库配置:配置数据库连接 +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'djangoblog', + 'USER': 'root', + 'PASSWORD': '123456', + 'HOST': '127.0.0.1', + 'PORT': 3306, + } +} + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators +# Zxy密码验证器:配置密码验证规则 +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', + }, +] +# Zxy语言和时区配置:配置语言和时区 +LANGUAGES = ( + ('en', _('English')), + ('zh-hans', _('Simplified Chinese')), + ('zh-hant', _('Traditional Chinese')), +) +LOCALE_PATHS = ( + 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/ + +# ZxyHaystack 搜索配置:配置 Haystack 搜索引擎 +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', + 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), + }, +} +# Automatically update searching index +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' +# Allow user login with username and password +AUTHENTICATION_BACKENDS = [ + 'accounts.user_login_backend.EmailOrUsernameModelBackend'] +# Zxy静态文件配置:配置静态文件路径 +STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') + +STATIC_URL = '/static/' +STATICFILES = os.path.join(BASE_DIR, 'static') +# Zxy用户模型配置:定义自定义用户模型和登录 URL +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 +BOOTSTRAP_COLOR_TYPES = [ + 'default', 'primary', 'success', 'info', 'warning', 'danger' +] + +# paginate +PAGINATE_BY = 10 +# http cache timeout +CACHE_CONTROL_MAX_AGE = 2592000 +# cache setting +# Zxy缓存配置:配置缓存后端 +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } +} +# 使用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")}', + } + } +# Zxy站点 ID:定义当前站点的 ID +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: +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_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 +ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] +# WX ADMIN password(Two times md5) +WXADMIN = os.environ.get( + 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' + +LOG_PATH = os.path.join(BASE_DIR, 'logs') +if not os.path.exists(LOG_PATH): + os.makedirs(LOG_PATH, exist_ok=True) +# Zxy日志配置:配置日志记录 +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, + } + } +} +# Zxy压缩配置:配置静态文件压缩 +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # other + 'compressor.finders.CompressorFinder', +) +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' +] +COMPRESS_JS_FILTERS = [ + 'compressor.filters.jsmin.JSMinFilter' +] +# Zxy媒体文件配置:配置媒体文件路径 +MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') +MEDIA_URL = '/media/' +X_FRAME_OPTIONS = 'SAMEORIGIN' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# ZxyElasticsearch 配置:配置 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', + }, + } + +# Zxy插件系统配置:定义插件目录和激活的插件 +PLUGINS_DIR = BASE_DIR / 'plugins' +ACTIVE_PLUGINS = [ + 'article_copyright', + 'reading_time', + 'external_links', + 'view_count', + 'seo_optimizer' +] \ No newline at end of file diff --git a/sitemap.py b/sitemap.py new file mode 100644 index 0000000..986f1ef --- /dev/null +++ b/sitemap.py @@ -0,0 +1,70 @@ +# Zxy导入 Django 的 Sitemap 模块和其他相关模块 +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + +from blog.models import Article, Category, Tag + +# Zxy定义静态页面的 Sitemap +class StaticViewSitemap(Sitemap): + priority = 0.5 # Zxy设置优先级 + changefreq = 'daily' # Zxy设置更新频率 + + def items(self): + # Zxy返回静态页面的 URL 名称 + return ['blog:index', ] + + def location(self, item): + # Zxy通过 URL 名称生成完整的 URL + return reverse(item) + +# Zxy定义文章的 Sitemap +class ArticleSiteMap(Sitemap): + changefreq = "monthly" # Zxy文章更新频率为每月 + priority = "0.6" # Zxy文章优先级 + + def items(self): + # Zxy返回所有已发布的文章 + return Article.objects.filter(status='p') + + def lastmod(self, obj): + # Zxy返回文章的最后修改时间 + return obj.last_modify_time + +# Zxy定义分类的 Sitemap +class CategorySiteMap(Sitemap): + changefreq = "Weekly" # Zxy分类更新频率为每周 + priority = "0.6" # Zxy分类优先级 + + def items(self): + # Zxy返回所有分类 + return Category.objects.all() + + def lastmod(self, obj): + # Zxy返回分类的最后修改时间 + return obj.last_modify_time + +# Zxy定义标签的 Sitemap +class TagSiteMap(Sitemap): + changefreq = "Weekly" # Zxy标签更新频率为每周 + priority = "0.3" # Zxy标签优先级 + + def items(self): + # Zxy返回所有标签 + return Tag.objects.all() + + def lastmod(self, obj): + # Zxy返回标签的最后修改时间 + return obj.last_modify_time + +# Zxy定义用户的 Sitemap +class UserSiteMap(Sitemap): + changefreq = "Weekly" # Zxy用户更新频率为每周 + priority = "0.3" # Zxy用户优先级 + + def items(self): + # Zxy返回所有用户的作者列表(去重) + return list(set(map(lambda x: x.author, Article.objects.all()))) + + def lastmod(self, obj): + # Zxy返回用户的注册时间 + return obj.date_joined \ No newline at end of file diff --git a/spider_notify.py b/spider_notify.py new file mode 100644 index 0000000..20fed14 --- /dev/null +++ b/spider_notify.py @@ -0,0 +1,26 @@ +# Zxy导入日志模块 +import logging + +# Zxy导入 requests 模块用于发送 HTTP 请求 +import requests +from django.conf import settings + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy定义爬虫通知类 +class SpiderNotify(): + @staticmethod + def baidu_notify(urls): + # Zxy向百度站长平台发送 URL 提交请求 + try: + data = '\n'.join(urls) # Zxy将 URL 列表拼接为字符串 + result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) # Zxy发送 POST 请求 + logger.info(result.text) # Zxy记录响应内容 + except Exception as e: + logger.error(e) # Zxy记录异常信息 + + @staticmethod + def notify(url): + # Zxy调用百度通知方法 + SpiderNotify.baidu_notify(url) \ No newline at end of file diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..83af4c1 --- /dev/null +++ b/tests.py @@ -0,0 +1,38 @@ +# Zxy导入 Django 的测试模块 +from django.test import TestCase + +# Zxy导入项目中的工具函数 +from djangoblog.utils import * + +# Zxy定义测试类 +class DjangoBlogTest(TestCase): + def setUp(self): + # Zxy测试初始化方法(暂无内容) + pass + + def test_utils(self): + # Zxy测试工具函数 + md5 = get_sha256('test') # Zxy测试 SHA256 函数 + self.assertIsNotNone(md5) # Zxy断言返回值不为空 + + c = CommonMarkdown.get_markdown(''' # Zxy测试 Markdown 转换 + # Title1 + + ```python + import os + ``` + + [url](https://www.lylinux.net/) + + [ddd](http://www.baidu.com) + + + ''') + self.assertIsNotNone(c) # Zxy断言返回值不为空 + + d = { + 'd': 'key1', + 'd2': 'key2' + } + data = parse_dict_to_url(d) # Zxy测试字典转 URL 函数 + self.assertIsNotNone(data) # Zxy断言返回值不为空 \ No newline at end of file diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..f158317 --- /dev/null +++ b/urls.py @@ -0,0 +1,68 @@ +# Zxy定义项目的 URL 配置 +"""djangoblog URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +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')) +""" +from django.conf import settings +from django.conf.urls.i18n import i18n_patterns +from django.conf.urls.static import static +from django.contrib.sitemaps.views import sitemap +from django.urls import path, include +from django.urls import re_path +from haystack.views import search_view_factory + +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 + +# Zxy定义站点地图 +sitemaps = { + 'blog': ArticleSiteMap, + 'Category': CategorySiteMap, + 'Tag': TagSiteMap, + 'User': UserSiteMap, + 'static': StaticViewSitemap +} + +# Zxy定义 404、500 和 403 错误页面的视图 +handler404 = 'blog.views.page_not_found_view' +handler500 = 'blog.views.server_error_view' +handle403 = 'blog.views.permission_denied_view' + +# Zxy定义 URL 模式 +urlpatterns = [ + path('i18n/', include('django.conf.urls.i18n')), # Zxy国际化语言切换 +] +urlpatterns += i18n_patterns( + re_path(r'^admin/', admin_site.urls), # Zxy自定义 Admin 站点 + re_path(r'', include('blog.urls', namespace='blog')), # Zxy博客应用的 URL + re_path(r'mdeditor/', include('mdeditor.urls')), # Zxy Markdown 编辑器的 URL + re_path(r'', include('comments.urls', namespace='comment')), # Zxy评论应用的 URL + re_path(r'', include('accounts.urls', namespace='account')), # Zxy用户账户的 URL + re_path(r'', include('oauth.urls', namespace='oauth')), # Zxy OAuth 应用的 URL + re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, # Zxy站点地图 + name='django.contrib.sitemaps.views.sitemap'), + re_path(r'^feed/$', DjangoBlogFeed()), # Zxy RSS 订阅 + re_path(r'^rss/$', DjangoBlogFeed()), # Zxy RSS 订阅 + re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), # Zxy搜索功能 + name='search'), + re_path(r'', include('servermanager.urls', namespace='servermanager')), # Zxy服务器管理应用的 URL + re_path(r'', include('owntracks.urls', namespace='owntracks')), # Zxy位置跟踪应用的 URL + prefix_default_language=False +) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # Zxy静态文件的 URL +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, # Zxy媒体文件的 URL + document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..90cfa42 --- /dev/null +++ b/utils.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Zxy导入日志模块 +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 + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy获取文章和评论的最大 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) + +# Zxy计算字符串的 SHA256 值 +def get_sha256(str): + m = sha256(str.encode('utf-8')) + return m.hexdigest() + +# Zxy缓存装饰器,用于缓存函数的返回值 +def cache_decorator(expiration=3 * 60): + def wrapper(func): + def news(*args, **kwargs): + try: + view = args[0] + key = view.get_cache_key() + except: + 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) + if value is not None: + 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: + cache.set(key, '__default_cache_value__', expiration) + else: + cache.set(key, value, expiration) + return value + return news + return wrapper + +# Zxy刷新视图缓存 +def expire_view_cache(path, servername, serverport, key_prefix=None): + from django.http import HttpRequest + from django.utils.cache import get_cache_key + + request = HttpRequest() + 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 False + +# Zxy获取当前站点 +@cache_decorator() +def get_current_site(): + site = Site.objects.get_current() + return site + +# Zxy定义 CommonMarkdown 类,用于处理 Markdown 转换 +class CommonMarkdown: + @staticmethod + def _convert_markdown(value): + md = markdown.Markdown(extensions=['extra', 'codehilite', 'toc', 'tables']) + body = md.convert(value) + toc = md.toc + return body, toc + + @staticmethod + def get_markdown_with_toc(value): + body, toc = CommonMarkdown._convert_markdown(value) + return body, toc + + @staticmethod + def get_markdown(value): + body, toc = CommonMarkdown._convert_markdown(value) + return body + +# Zxy发送邮件 +def send_email(emailto, title, content): + from djangoblog.blog_signals import send_email_signal + send_email_signal.send(send_email.__class__, emailto=emailto, title=title, content=content) + +# Zxy生成随机验证码 +def generate_code() -> str: + return ''.join(random.sample(string.digits, 6)) + +# Zxy将字典转换为 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 + +# Zxy获取博客设置 +def get_blog_setting(): + value = cache.get('get_blog_setting') + if value: + return value + else: + from blog.models import BlogSettings + 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) + return value + +# Zxy保存用户头像 +def save_user_avatar(url): + 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' + save_filename = str(uuid.uuid4().hex) + ext + 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) + except Exception as e: + logger.error(e) + return static('blog/img/avatar.png') + +# Zxy删除侧边栏缓存 +def delete_sidebar_cache(): + 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) + +# Zxy删除视图缓存 +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) + +# Zxy获取资源 URL +def get_resource_url(): + if settings.STATIC_URL: + return settings.STATIC_URL + else: + site = get_current_site() + return 'http://' + site.domain + '/static/' + +# Zxy定义允许的 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']} + +# Zxy清理 HTML 内容 +def sanitize_html(html): + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) \ No newline at end of file diff --git a/whoosh_cn_backend.py b/whoosh_cn_backend.py new file mode 100644 index 0000000..994247c --- /dev/null +++ b/whoosh_cn_backend.py @@ -0,0 +1,1256 @@ +# Zxy 导入未来支持模块,确保代码在 Python 2 和 3 中的兼容性 +# encoding: utf-8 +from __future__ import absolute_import, division, print_function, unicode_literals + +# Zxy 导入标准库模块 +import json +import os +import re +import shutil +import threading +import warnings + +# Zxy 导入第三方库 +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 +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 + +# Zxy 尝试导入 whoosh 库,如果失败则抛出依赖缺失异常 +try: + import whoosh +except ImportError: + raise MissingDependency( + "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") + +# Zxy 检查 whoosh 版本是否满足最低要求 +if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): + raise MissingDependency( + "The 'whoosh' backend requires version 2.5.0 or greater.") + +# Zxy 定义一个正则表达式,用于匹配 ISO 8601 格式的日期时间字符串 +DATETIME_REGEX = re.compile( + '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$') +# Zxy 创建一个线程局部存储对象,用于在每个线程中存储独立的数据 +LOCALS = threading.local() +# Zxy 初始化线程局部存储中的 RAM_STORE 为 None,用于存储内存索引 +LOCALS.RAM_STORE = None + + +# Zxy 定义一个自定义的 HTML 格式化器,继承自 Whoosh 的 HtmlFormatter +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. + """ + # Zxy 定义高亮显示的 HTML 模板 + template = '<%(tag)s>%(t)s' + + +# Zxy 定义 Whoosh 搜索后端类,继承自 Haystack 的 BaseSearchBackend +class WhooshSearchBackend(BaseSearchBackend): + # Zxy 定义 Whoosh 保留的关键字列表,这些关键字在查询中有特殊含义 + RESERVED_WORDS = ( + 'AND', + 'NOT', + 'OR', + 'TO', + ) + + # Zxy 定义 Whoosh 保留的字符列表,这些字符在查询中有特殊含义 + # The '\\' must come first, so as not to overwrite the other slash + # replacements. + RESERVED_CHARACTERS = ( + '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', + '[', ']', '^', '"', '~', '*', '?', ':', '.', + ) + + # Zxy 初始化方法,设置连接别名和连接选项 + def __init__(self, connection_alias, **connection_options): + # Zxy 调用父类的初始化方法 + super( + WhooshSearchBackend, + self).__init__( + connection_alias, + **connection_options) + # Zxy 标记后端设置尚未完成 + self.setup_complete = False + # Zxy 默认使用文件存储 + self.use_file_storage = True + # Zxy 设置 POST 请求的大小限制,默认为 128MB + self.post_limit = getattr( + connection_options, + 'POST_LIMIT', + 128 * 1024 * 1024) + # Zxy 从连接选项中获取索引文件存储路径 + self.path = connection_options.get('PATH') + + # Zxy 如果配置的存储类型不是 'file',则使用内存存储 + if connection_options.get('STORAGE', 'file') != 'file': + self.use_file_storage = False + + # Zxy 如果使用文件存储但未提供路径,则抛出配置错误异常 + if self.use_file_storage and not self.path: + raise ImproperlyConfigured( + "You must specify a 'PATH' in your settings for connection '%s'." % + connection_alias) + + # Zxy 获取 Haystack 的日志记录器 + self.log = logging.getLogger('haystack') + + + # Zxy 设置搜索引擎,延迟加载直到需要时才执行 + def setup(self): + """ + Defers loading until needed. + """ + # Zxy 导入 haystack 的连接管理器 + from haystack import connections + # Zxy 标记是否为新创建的索引 + new_index = False + + # Zxy 确保索引目录存在 + if self.use_file_storage and not os.path.exists(self.path): + # Zxy 如果目录不存在,则创建它 + os.makedirs(self.path) + # Zxy 标记这是一个新索引 + new_index = True + + # Zxy 检查索引目录是否可写 + if self.use_file_storage and not os.access(self.path, os.W_OK): + # Zxy 如果不可写,则抛出 IO 错误 + raise IOError( + "The path to your Whoosh index '%s' is not writable for the current user/group." % + self.path) + + # Zxy 根据配置选择存储方式:文件存储或内存存储 + if self.use_file_storage: + # Zxy 使用文件存储 + self.storage = FileStorage(self.path) + else: + # Zxy 声明使用全局的线程局部存储 + global LOCALS + + # Zxy 如果内存存储尚未初始化,则创建一个 + if getattr(LOCALS, 'RAM_STORE', None) is None: + LOCALS.RAM_STORE = RamStorage() + + # Zxy 使用内存存储 + self.storage = LOCALS.RAM_STORE + + # Zxy 构建索引模式,并获取主内容字段名 + self.content_field_name, self.schema = self.build_schema( + connections[self.connection_alias].get_unified_index().all_searchfields()) + # Zxy 创建查询解析器,用于解析用户查询 + self.parser = QueryParser(self.content_field_name, schema=self.schema) + + # Zxy 根据是否为新索引来创建或打开索引 + if new_index is True: + # Zxy 如果是新索引,则根据 schema 创建 + self.index = self.storage.create_index(self.schema) + else: + # Zxy 否则尝试打开现有索引 + try: + self.index = self.storage.open_index(schema=self.schema) + except index.EmptyIndexError: + # Zxy 如果索引为空(可能刚创建目录但无文件),则创建新索引 + self.index = self.storage.create_index(self.schema) + + # Zxy 标记后端设置已完成 + self.setup_complete = True + + # Zxy 根据 Haystack 的搜索字段构建 Whoosh 的索引模式 + def build_schema(self, fields): + # Zxy 初始化模式字段,包含 Haystack 内置的 ID、类型和模型 ID + schema_fields = { + ID: WHOOSH_ID(stored=True, unique=True), + DJANGO_CT: WHOOSH_ID(stored=True), + DJANGO_ID: WHOOSH_ID(stored=True), + } + # Zxy 获取 Haystack 内置字段的数量,用于后续检查 + initial_key_count = len(schema_fields) + # Zxy 初始化主内容字段名 + content_field_name = '' + + # Zxy 遍历所有搜索字段,根据字段类型转换为 Whoosh 字段 + for field_name, field_class in fields.items(): + # Zxy 如果字段是多值字段 + if field_class.is_multivalued: + if field_class.indexed is False: + # Zxy 如果多值字段不被索引,使用 IDLIST 类型 + schema_fields[field_class.index_fieldname] = IDLIST( + stored=True, field_boost=field_class.boost) + else: + # Zxy 如果多值字段被索引,使用 KEYWORD 类型 + schema_fields[field_class.index_fieldname] = KEYWORD( + stored=True, commas=True, scorable=True, field_boost=field_class.boost) + # Zxy 如果字段类型是日期或日期时间 + elif field_class.field_type in ['date', 'datetime']: + # Zxy 使用 DATETIME 类型,并设置为可排序 + schema_fields[field_class.index_fieldname] = DATETIME( + stored=field_class.stored, sortable=True) + # Zxy 如果字段类型是整数 + elif field_class.field_type == 'integer': + # Zxy 使用 NUMERIC 类型,并指定数字类型为整数 + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=int, field_boost=field_class.boost) + # Zxy 如果字段类型是浮点数 + elif field_class.field_type == 'float': + # Zxy 使用 NUMERIC 类型,并指定数字类型为浮点数 + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=float, field_boost=field_class.boost) + # Zxy 如果字段类型是布尔值 + elif field_class.field_type == 'boolean': + # Zxy 使用 BOOLEAN 类型 + # Field boost isn't supported on BOOLEAN as of 1.8.2. + schema_fields[field_class.index_fieldname] = BOOLEAN( + stored=field_class.stored) + # Zxy 如果字段类型是 N-gram + elif field_class.field_type == 'ngram': + # Zxy 使用 NGRAM 类型 + schema_fields[field_class.index_fieldname] = NGRAM( + minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost) + # Zxy 如果字段类型是边 N-gram + elif field_class.field_type == 'edge_ngram': + # Zxy 使用 NGRAMWORDS 类型,并设置为从词首开始 + schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', + stored=field_class.stored, + field_boost=field_class.boost) + else: + # Zxy 默认情况下,使用 TEXT 类型,并配置中文分词器 + # 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) + # Zxy 如果字段被标记为文档主内容 + if field_class.document is True: + # Zxy 记录主内容字段名 + content_field_name = field_class.index_fieldname + # Zxy 启用拼写建议功能 + schema_fields[field_class.index_fieldname].spelling = True + + # Zxy 如果除了内置字段外没有找到任何其他字段,则优雅地报错 + if len(schema_fields) <= initial_key_count: + raise SearchBackendError( + "No fields were found in any search_indexes. Please correct this before attempting to search.") + + # Zxy 返回主内容字段名和构建好的 Schema 对象 + return (content_field_name, Schema(**schema_fields)) + + # Zxy 更新索引,将可迭代对象中的每个对象添加或更新到 Whoosh 索引中 + def update(self, index, iterable, commit=True): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + # Zxy 创建一个异步写入器,用于高效地写入索引 + writer = AsyncWriter(self.index) + + # Zxy 遍历所有需要更新的对象 + for obj in iterable: + try: + # Zxy 准备要索引的文档数据 + doc = index.full_prepare(obj) + except SkipDocument: + # Zxy 如果对象被标记为跳过,则记录调试信息 + self.log.debug(u"Indexing for object `%s` skipped", obj) + else: + # Zxy 确保所有字段值都是 Whoosh 可以处理的格式 + for key in doc: + doc[key] = self._from_python(doc[key]) + + # Zxy 文档级别的权重提升在 Whoosh 2.5.0+ 版本中不再支持 + if 'boost' in doc: + del doc['boost'] + + try: + # Zxy 使用写入器更新文档(如果存在则更新,否则创建) + writer.update_document(**doc) + except Exception as e: + # Zxy 如果未配置为静默失败,则重新抛出异常 + if not self.silently_fail: + raise + + # Zxy 记录错误信息,包含对象标识符,但不包含对象本身以避免编码问题 + self.log.error( + u"%s while preparing object for update" % + e.__class__.__name__, + exc_info=True, + extra={ + "data": { + "index": index, + "object": get_identifier(obj)}}) + + # Zxy 如果可迭代对象不为空,则提交更改 + if len(iterable) > 0: + # For now, commit no matter what, as we run into locking issues + # otherwise. + writer.commit() + + # Zxy 从索引中移除一个对象 + def remove(self, obj_or_string, commit=True): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + # Zxy 获取对象的唯一标识符 + whoosh_id = get_identifier(obj_or_string) + + try: + # Zxy 构建一个查询,根据 ID 查找文档并删除 + self.index.delete_by_query( + q=self.parser.parse( + u'%s:"%s"' % + (ID, whoosh_id))) + except Exception as e: + # Zxy 如果未配置为静默失败,则重新抛出异常 + if not self.silently_fail: + raise + + # Zxy 记录删除失败的错误 + self.log.error( + "Failed to remove document '%s' from Whoosh: %s", + whoosh_id, + e, + exc_info=True) + + # Zxy 清空索引,可以清空所有内容或指定模型的内容 + def clear(self, models=None, commit=True): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + + # Zxy 如果提供了模型列表,则检查其类型 + if models is not None: + assert isinstance(models, (list, tuple)) + + try: + if models is None: + # Zxy 如果没有指定模型,则删除整个索引 + self.delete_index() + else: + # Zxy 准备要删除的模型列表 + models_to_delete = [] + + for model in models: + # Zxy 为每个模型构建查询字符串 + models_to_delete.append( + u"%s:%s" % + (DJANGO_CT, get_model_ct(model))) + + # Zxy 使用 OR 连接多个模型查询,并删除匹配的文档 + self.index.delete_by_query( + q=self.parser.parse( + u" OR ".join(models_to_delete))) + except Exception as e: + # Zxy 如果未配置为静默失败,则重新抛出异常 + if not self.silently_fail: + raise + + # Zxy 根据是否指定模型,记录不同的错误信息 + 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) + + # Zxy 物理删除整个索引 + def delete_index(self): + # Per the Whoosh mailing list, if wiping out everything from the index, + # it's much more efficient to simply delete the index files. + # Zxy 如果使用文件存储,则直接删除索引目录 + if self.use_file_storage and os.path.exists(self.path): + shutil.rmtree(self.path) + # Zxy 如果使用内存存储,则清空存储 + elif not self.use_file_storage: + self.storage.clean() + + # Zxy 重新初始化后端,创建新的空索引 + self.setup() + + # Zxy 优化索引,合并索引段以提高搜索性能 + def optimize(self): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + # Zxy 执行 Whoosh 的优化操作 + self.index.optimize() + + # Zxy 根据偏移量计算 Whoosh 分页所需的页码和每页大小 + def calculate_page(self, start_offset=0, end_offset=None): + # Zxy 防止 Whoosh 因 end_offset 小于等于 0 而抛出错误 + if end_offset is not None and end_offset <= 0: + end_offset = 1 + + # Zxy 初始化页码 + page_num = 0 + + # Zxy 如果未指定结束偏移量,则设置一个很大的默认值 + if end_offset is None: + end_offset = 1000000 + + # Zxy 如果未指定起始偏移量,则默认为 0 + if start_offset is None: + start_offset = 0 + + # Zxy 计算每页的长度 + page_length = end_offset - start_offset + + # Zxy 如果页长度有效,则计算页码 + if page_length and page_length > 0: + page_num = int(start_offset / page_length) + + # Zxy Whoosh 使用 1-based 页码,所以需要加 1 + page_num += 1 + return page_num, page_length + + # Zxy 执行搜索查询的核心方法 + @log_query + 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): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 空查询字符串应返回无结果 + if len(query_string) == 0: + return { + 'results': [], + 'hits': 0, + } + + # Zxy 确保查询字符串为正确的字符串类型 + query_string = force_str(query_string) + + # Zxy 单个字符(非通配符)查询会被停用词过滤器拦截,应返回无结果 + if len(query_string) <= 1 and query_string != u'*': + return { + 'results': [], + 'hits': 0, + } + + # Zxy 初始化排序方向为非逆序 + reverse = False + + # Zxy 如果提供了排序字段 + if sort_by is not None: + # Zxy 确定是否需要反转结果,以及 Whoosh 是否能处理排序 + sort_by_list = [] + reverse_counter = 0 + + # Zxy 统计逆序排序字段的数量 + for order_by in sort_by: + if order_by.startswith('-'): + reverse_counter += 1 + + # Zxy Whoosh 要求所有排序字段的排序方向必须一致 + if reverse_counter and reverse_counter != len(sort_by): + raise SearchBackendError("Whoosh requires all order_by fields" + " to use the same sort direction") + + # Zxy 处理排序字段列表,去除 '-' 前缀并确定最终排序方向 + for order_by in sort_by: + if order_by.startswith('-'): + sort_by_list.append(order_by[1:]) + if len(sort_by_list) == 1: + reverse = True + else: + sort_by_list.append(order_by) + if len(sort_by_list) == 1: + reverse = False + + # Zxy Whoosh 的 search_page 方法只接受单个排序字段 + sort_by = sort_by_list[0] + + # Zxy Whoosh 后端不支持分面搜索,发出警告 + if facets is not None: + warnings.warn( + "Whoosh does not handle faceting.", + Warning, + stacklevel=2) + + # Zxy Whoosh 后端不支持日期分面,发出警告 + if date_facets is not None: + warnings.warn( + "Whoosh does not handle date faceting.", + Warning, + stacklevel=2) + + # Zxy Whoosh 后端不支持查询分面,发出警告 + if query_facets is not None: + warnings.warn( + "Whoosh does not handle query faceting.", + Warning, + stacklevel=2) + + # Zxy 初始化用于存储缩小范围后的结果 + narrowed_results = None + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + + # Zxy 确定是否限制搜索到已注册的模型 + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + # Zxy 根据传入的模型或配置构建模型选择列表 + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # Zxy 使用缩小查询的方式,将结果限制在当前路由器处理的模型中 + model_choices = self.build_models_list() + else: + model_choices = [] + + # Zxy 如果存在模型选择,则将其添加到缩小查询中 + if len(model_choices) > 0: + if narrow_queries is None: + narrow_queries = set() + + # Zxy 构建一个 OR 查询来限制模型类型 + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + # Zxy 初始化缩小查询的搜索器 + narrow_searcher = None + + # Zxy 如果存在缩小查询,则执行它们以获取一个结果集过滤器 + if narrow_queries is not None: + # Zxy 这个操作可能很昂贵,但 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) + + # Zxy 如果任何一个缩小查询返回空结果,则直接返回空 + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + # Zxy 将多个缩小查询的结果集进行交集过滤 + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + # Zxy 再次刷新索引以确保所有写入都可见 + self.index = self.index.refresh() + + # Zxy 如果索引中有文档,则执行搜索 + if self.index.doc_count(): + searcher = self.index.searcher() + # Zxy 解析查询字符串 + parsed_query = self.parser.parse(query_string) + + # Zxy 如果查询无效或被停用词过滤,则优雅地恢复 + if parsed_query is None: + return { + 'results': [], + 'hits': 0, + } + + # Zxy 计算分页参数 + page_num, page_length = self.calculate_page( + start_offset, end_offset) + + # Zxy 准备搜索参数 + search_kwargs = { + 'pagelen': page_length, + 'sortedby': sort_by, + 'reverse': reverse, + } + + # Zxy 如果存在缩小范围的结果,则将其作为过滤器 + if narrowed_results is not None: + search_kwargs['filter'] = narrowed_results + + try: + # Zxy 执行分页搜索 + raw_page = searcher.search_page( + parsed_query, + page_num, + **search_kwargs + ) + except ValueError: + # Zxy 如果页码无效,则返回空结果 + if not self.silently_fail: + raise + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Zxy 兼容 Whoosh 2.5.1 的 bug:请求过高的页码会返回错误的页 + if raw_page.pagenum < page_num: + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Zxy 处理原始搜索结果,转换为 Haystack 的 SearchResult 对象 + results = self._process_results( + raw_page, + highlight=highlight, + query_string=query_string, + spelling_query=spelling_query, + result_class=result_class) + # Zxy 关闭主搜索器 + searcher.close() + + # Zxy 关闭缩小查询的搜索器 + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + else: + # Zxy 如果索引为空,但仍需处理拼写建议 + spelling_suggestion = None + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': spelling_suggestion, + } + + # Zxy 实现“更多类似于此”功能,根据给定模型实例查找相似文档 + 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): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 获取模型的真实类,避免使用延迟加载的模型类 + # Deferred models will have a different class ("RealClass_Deferred_fieldname") + # which won't be in our registry: + model_klass = model_instance._meta.concrete_model + + # Zxy 获取主内容字段名,用于相似性分析 + field_name = self.content_field_name + # Zxy 初始化缩小查询集合 + narrow_queries = set() + # Zxy 初始化缩小范围后的结果集 + narrowed_results = None + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + + # Zxy 确定是否限制搜索到已注册的模型 + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + # Zxy 根据传入的模型或配置构建模型选择列表 + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # Zxy 使用缩小查询的方式,将结果限制在当前路由器处理的模型中 + model_choices = self.build_models_list() + else: + model_choices = [] + + # Zxy 如果存在模型选择,则将其添加到缩小查询中 + if len(model_choices) > 0: + if narrow_queries is None: + narrow_queries = set() + + # Zxy 构建一个 OR 查询来限制模型类型 + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + # Zxy 如果提供了额外的查询字符串,则也添加到缩小查询中 + if additional_query_string and additional_query_string != '*': + narrow_queries.add(additional_query_string) + + # Zxy 初始化缩小查询的搜索器 + narrow_searcher = None + + # Zxy 如果存在缩小查询,则执行它们以获取一个结果集过滤器 + if narrow_queries is not None: + # Zxy 这个操作可能很昂贵,但 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) + + # Zxy 如果任何一个缩小查询返回空结果,则直接返回空 + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + # Zxy 将多个缩小查询的结果集进行交集过滤 + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + # Zxy 计算分页参数 + page_num, page_length = self.calculate_page(start_offset, end_offset) + + # Zxy 再次刷新索引以确保所有写入都可见 + self.index = self.index.refresh() + # Zxy 初始化原始结果为空 + raw_results = EmptyResults() + + # Zxy 如果索引中有文档,则执行“更多类似于此”查询 + if self.index.doc_count(): + # Zxy 构建一个查询以找到当前模型实例对应的索引文档 + query = "%s:%s" % (ID, get_identifier(model_instance)) + searcher = self.index.searcher() + parsed_query = self.parser.parse(query) + # Zxy 搜索当前文档 + results = searcher.search(parsed_query) + + # Zxy 如果找到了当前文档,则调用其 more_like_this 方法 + if len(results): + # Zxy 获取与当前文档相似的其他文档 + raw_results = results[0].more_like_this( + field_name, top=end_offset) + + # Zxy 如果存在缩小范围的结果,则将其作为过滤器应用于相似结果 + if narrowed_results is not None and hasattr(raw_results, 'filter'): + raw_results.filter(narrowed_results) + + try: + # Zxy 将原始结果集包装成分页对象 + raw_page = ResultsPage(raw_results, page_num, page_length) + except ValueError: + # Zxy 如果页码无效,则返回空结果 + if not self.silently_fail: + raise + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Zxy 兼容 Whoosh 2.5.1 的 bug:请求过高的页码会返回错误的页 + if raw_page.pagenum < page_num: + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Zxy 处理原始搜索结果,转换为 Haystack 的 SearchResult 对象 + results = self._process_results(raw_page, result_class=result_class) + # Zxy 关闭主搜索器 + searcher.close() + + # Zxy 关闭缩小查询的搜索器 + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + + + # Zxy 处理 Whoosh 返回的原始搜索结果,转换为 Haystack 标准格式 + def _process_results( + self, + raw_page, + highlight=False, + query_string='', + spelling_query=None, + result_class=None): + # Zxy 导入 haystack 连接管理器 + from haystack import connections + # Zxy 初始化结果列表 + results = [] + + # Zxy 在切片之前获取总命中数,这对于分页至关重要 + hits = len(raw_page) + + # Zxy 如果未指定结果类,则使用默认的 SearchResult + if result_class is None: + result_class = SearchResult + + # Zxy 初始化分面和拼写建议 + facets = {} + spelling_suggestion = None + # Zxy 获取统一索引对象和已注册的模型列表 + unified_index = connections[self.connection_alias].get_unified_index() + indexed_models = unified_index.get_indexed_models() + + # Zxy 遍历原始结果页中的每个文档 + for doc_offset, raw_result in enumerate(raw_page): + # Zxy 获取文档的得分 + score = raw_page.score(doc_offset) or 0 + # Zxy 从文档中解析出应用标签和模型名 + app_label, model_name = raw_result[DJANGO_CT].split('.') + # Zxy 初始化额外字段字典 + additional_fields = {} + # Zxy 根据应用标签和模型名获取模型类 + model = haystack_get_model(app_label, model_name) + + # Zxy 确保模型存在且已注册到索引 + if model and model in indexed_models: + # Zxy 遍历文档中的所有字段 + for key, value in raw_result.items(): + # Zxy 获取该模型对应的索引 + index = unified_index.get_index(model) + string_key = str(key) + + # Zxy 如果字段在索引定义中,并且有转换方法 + if string_key in index.fields and hasattr( + index.fields[string_key], 'convert'): + # Zxy 特殊处理多值字段 + 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: + # Zxy 使用索引字段定义的转换方法 + additional_fields[string_key] = index.fields[string_key].convert( + value) + else: + # Zxy 否则使用通用的 Python 类型转换 + additional_fields[string_key] = self._to_python(value) + + # Zxy 删除 Haystack 内部字段,不返回给用户 + del (additional_fields[DJANGO_CT]) + del (additional_fields[DJANGO_ID]) + + # Zxy 如果需要高亮显示 + if highlight: + # Zxy 创建词干分析器和 HTML 格式化器 + sa = StemmingAnalyzer() + formatter = WhooshHtmlFormatter('em') + # Zxy 从查询字符串中提取词条 + terms = [token.text for token in sa(query_string)] + + # Zxy 调用 Whoosh 的高亮方法 + whoosh_result = whoosh_highlight( + additional_fields.get(self.content_field_name), + terms, + sa, + ContextFragmenter(), + formatter + ) + # Zxy 将高亮结果添加到额外字段中 + additional_fields['highlighted'] = { + self.content_field_name: [whoosh_result], + } + + # Zxy 创建 SearchResult 对象并添加到结果列表 + result = result_class( + app_label, + model_name, + raw_result[DJANGO_ID], + score, + **additional_fields) + results.append(result) + else: + # Zxy 如果模型未注册,则减少总命中数 + hits -= 1 + + # Zxy 如果启用了拼写建议 + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + + # Zxy 返回包含结果、命中数、分面和拼写建议的字典 + return { + 'results': results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + # Zxy 根据查询字符串创建拼写建议 + def create_spelling_suggestion(self, query_string): + # Zxy 初始化拼写建议 + spelling_suggestion = None + # Zxy 获取索引的读取器和校正器 + reader = self.index.reader() + corrector = reader.corrector(self.content_field_name) + # Zxy 清理查询字符串 + cleaned_query = force_str(query_string) + + # Zxy 如果查询字符串为空,直接返回 + if not query_string: + return spelling_suggestion + + # Zxy 移除查询中的保留字 + for rev_word in self.RESERVED_WORDS: + cleaned_query = cleaned_query.replace(rev_word, '') + + # Zxy 移除查询中的保留字符 + for rev_char in self.RESERVED_CHARACTERS: + cleaned_query = cleaned_query.replace(rev_char, '') + + # Zxy 将清理后的查询拆分为单词列表 + query_words = cleaned_query.split() + suggested_words = [] + + # Zxy 为每个单词查找拼写建议 + for word in query_words: + suggestions = corrector.suggest(word, limit=1) + + if len(suggestions) > 0: + suggested_words.append(suggestions[0]) + + # Zxy 将建议的单词重新组合成字符串 + spelling_suggestion = ' '.join(suggested_words) + return spelling_suggestion + + # Zxy 将 Python 值转换为 Whoosh 可用的字符串格式 + def _from_python(self, value): + """ + Converts Python values to a string for Whoosh. + + Code courtesy of pysolr. + """ + # Zxy 处理日期时间对象 + if hasattr(value, 'strftime'): + # Zxy 如果只有日期没有时间,则将时间部分设为 0 + if not hasattr(value, 'hour'): + value = datetime(value.year, value.month, value.day, 0, 0, 0) + # Zxy 处理布尔值 + elif isinstance(value, bool): + if value: + value = 'true' + else: + value = 'false' + # Zxy 处理列表或元组,用逗号连接 + elif isinstance(value, (list, tuple)): + value = u','.join([force_str(v) for v in value]) + # Zxy 处理数字,保持原样 + elif isinstance(value, (six.integer_types, float)): + # Leave it alone. + pass + else: + # Zxy 其他类型强制转换为字符串 + value = force_str(value) + return value + + # Zxy 将 Whoosh 的值转换为原生 Python 值 + 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. + """ + # Zxy 处理布尔字符串 + if value == 'true': + return True + elif value == 'false': + return False + + # Zxy 尝试解析日期时间字符串 + 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']) + + # Zxy 尝试使用 json 解复杂数据类型 + try: + # Attempt to use json to load the values. + converted_value = json.loads(value) + + # Try to handle most built-in types. + if isinstance( + converted_value, + (list, + tuple, + set, + dict, + six.integer_types, + float, + complex)): + return converted_value + except BaseException: + # If it fails (SyntaxError or its ilk) or we don't trust it, + # continue on. + pass + + # Zxy 如果都无法转换,则返回原始值 + return value + + # Zxy 定义 Whoosh 搜索查询类,继承自 Haystack 的 BaseSearchQuery +class WhooshSearchQuery(BaseSearchQuery): + # Zxy 将日期时间对象转换为 Whoosh 查询所需的字符串格式 + def _convert_datetime(self, date): + # Zxy 如果包含时间,则转换为完整格式 + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + # Zxy 如果只有日期,则补充零时间 + return force_str(date.strftime('%Y%m%d000000')) + + # Zxy 清理查询片段,转义 Whoosh 的保留字符 + 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. + """ + # Zxy 将查询片段按空格分割成单词 + words = query_fragment.split() + cleaned_words = [] + + # Zxy 遍历每个单词进行清理 + for word in words: + # Zxy 如果是保留字,则转为小写 + if word in self.backend.RESERVED_WORDS: + word = word.replace(word, word.lower()) + + # Zxy 如果包含保留字符,则用单引号将整个单词括起来 + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + # Zxy 将清理后的单词重新组合 + return ' '.join(cleaned_words) + + # Zxy 构建查询片段,根据字段、过滤类型和值生成 Whoosh 查询语法 + def build_query_fragment(self, field, filter_type, value): + # Zxy 导入 haystack 连接管理器 + from haystack import connections + query_frag = '' + is_datetime = False + + # Zxy 如果值没有 input_type_name 属性,则进行类型推断 + if not hasattr(value, 'input_type_name'): + # Handle when we've got a ``ValuesListQuerySet``... + if hasattr(value, 'values_list'): + value = list(value) + + if hasattr(value, 'strftime'): + is_datetime = True + + if isinstance(value, six.string_types) and value != ' ': + # It's not an ``InputType``. Assume ``Clean``. + value = Clean(value) + else: + value = PythonData(value) + + # Zxy 使用 InputType 准备查询值 + prepared_value = value.prepare(self) + + # Zxy 如果准备好的值不是集合类型,则转换为 Whoosh 可用的格式 + 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'. + # Zxy 'content' 是特殊字段,代表所有可搜索内容 + if field == 'content': + index_fieldname = '' + else: + # Zxy 获取字段在索引中的真实名称 + index_fieldname = u'%s:' % connections[self._using].get_unified_index( + ).get_index_fieldname(field) + + # Zxy 定义不同过滤类型对应的 Whoosh 查询模板 + 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~', + } + # Zxy 如果值不需要后处理,则直接使用 + if value.post_process is False: + query_frag = prepared_value + else: + # Zxy 根据不同的过滤类型构建查询片段 + if filter_type in [ + 'content', + 'contains', + 'startswith', + 'endswith', + 'fuzzy']: + # Zxy 如果输入类型是精确匹配,则直接使用值 + 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(' ') + 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) + # Zxy 处理 'in' 过滤类型 + 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) + else: + in_options.append('%s' % pv) + + query_frag = "(%s)" % " OR ".join(in_options) + # Zxy 处理 'range' 过滤类型 + 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) + # Zxy 处理 'exact' 过滤类型 + elif filter_type == 'exact': + if value.input_type_name == 'exact': + query_frag = prepared_value + else: + prepared_value = Exact(prepared_value).prepare(self) + query_frag = filter_types[filter_type] % prepared_value + else: + # Zxy 处理其他类型(如 gt, gte, lt, lte) + if is_datetime is True: + prepared_value = self._convert_datetime(prepared_value) + + query_frag = filter_types[filter_type] % prepared_value + + # Zxy 如果查询片段不为空且不是原始查询,则用括号括起来 + 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) + +# Zxy 定义 Whoosh 引擎类,继承自 Haystack 的 BaseEngine +class WhooshEngine(BaseEngine): + # Zxy 指定后端和查询类 + backend = WhooshSearchBackend + query = WhooshSearchQuery diff --git a/whoosh_index/MAIN_5bjud9prw1dxe6io.seg b/whoosh_index/MAIN_5bjud9prw1dxe6io.seg new file mode 100644 index 0000000..d37c524 Binary files /dev/null and b/whoosh_index/MAIN_5bjud9prw1dxe6io.seg differ diff --git a/whoosh_index/MAIN_7s7twfilcbt07zs1.seg b/whoosh_index/MAIN_7s7twfilcbt07zs1.seg new file mode 100644 index 0000000..36d4412 Binary files /dev/null and b/whoosh_index/MAIN_7s7twfilcbt07zs1.seg differ diff --git a/whoosh_index/MAIN_WRITELOCK b/whoosh_index/MAIN_WRITELOCK new file mode 100644 index 0000000..e69de29 diff --git a/whoosh_index/MAIN_hnkxp6bdzv6onzg5.seg b/whoosh_index/MAIN_hnkxp6bdzv6onzg5.seg new file mode 100644 index 0000000..8bb7a70 Binary files /dev/null and b/whoosh_index/MAIN_hnkxp6bdzv6onzg5.seg differ diff --git a/whoosh_index/MAIN_hqwvke8n7syrdbus.seg b/whoosh_index/MAIN_hqwvke8n7syrdbus.seg new file mode 100644 index 0000000..14ffa8b Binary files /dev/null and b/whoosh_index/MAIN_hqwvke8n7syrdbus.seg differ diff --git a/whoosh_index/_MAIN_39.toc b/whoosh_index/_MAIN_39.toc new file mode 100644 index 0000000..a71fc80 Binary files /dev/null and b/whoosh_index/_MAIN_39.toc differ diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..730430e --- /dev/null +++ b/wsgi.py @@ -0,0 +1,20 @@ +""" +WSGI config for djangoblog project. + +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/ +""" + +# Zxy导入os模块,用于操作环境变量 +import os + +# Zxy从Django中导入get_wsgi_application函数,用于获取WSGI应用 +from django.core.wsgi import get_wsgi_application + +# Zxy设置环境变量DJANGO_SETTINGS_MODULE,指定Django项目的配置文件 +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") + +# Zxy获取WSGI应用实例,用于部署 +application = get_wsgi_application() \ No newline at end of file