当前位置:   article > 正文

基于Python+Djingo实现个人博客系统_基于python的个人博客系统

基于python的个人博客系统

作者主页:编程指南针

作者简介:Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师

主要内容:Java项目、简历模板、学习资料、面试题库、技术互助

收藏点赞不迷路  关注作者有好处

文末获取源码 

项目编号:BS-Python-007

一,环境介绍

语言环境:Python3.8  Django4.2.1

数据库:Mysql: mysql5.7

开发工具:IDEA

前端技术:HTML+CSS+JS

二,项目简介

主要功能:

- 文章,页面,分类目录,标签的添加,删除,编辑等。文章、评论及页面支持`Markdown`,支持代码高亮。

- 支持文章全文搜索。

- 完整的评论功能,包括发表回复评论,以及评论的邮件提醒,支持`Markdown`。

- 侧边栏功能,最新文章,最多阅读,标签云等。

- 支持Oauth登陆,现已有Google,GitHub,facebook,微博,QQ登录。

- 支持`Redis`缓存,支持缓存自动刷新。

- 简单的SEO功能,新建文章等会自动通知Google和百度。

- 集成了简单的图床功能。

- 集成`django-compressor`,自动压缩`css`,`js`。

- 网站异常邮件提醒,若有未捕捉到的异常会自动发送提醒邮件。

- 集成了微信公众号功能,现在可以使用微信公众号来管理你的vps了。

系统的用户可以分为两类,前端用户和后台管理用户,用户的权限可以在后台由管理员进行管理设定。系统功能相对比较完整,包含了用户管理、博文分类管理、博文管理、标签管理、评论管理、友情连接管理、侧边栏管理、第三方授权登录管理等等

三,系统展示

系统首页

前端用户登录

博客详情

文档归类

后台管理

用户管理

分类管理

文章管理

标签管理

网站配置

评论管理

四,核心代码展示

  1. from django.contrib.admin import AdminSite
  2. from django.contrib.admin.models import LogEntry
  3. from django.contrib.sites.admin import SiteAdmin
  4. from django.contrib.sites.models import Site
  5. from accounts.admin import *
  6. from blog.admin import *
  7. from blog.models import *
  8. from comments.admin import *
  9. from comments.models import *
  10. from djangoblog.logentryadmin import LogEntryAdmin
  11. from oauth.admin import *
  12. from oauth.models import *
  13. from owntracks.admin import *
  14. from owntracks.models import *
  15. from servermanager.admin import *
  16. from servermanager.models import *
  17. class DjangoBlogAdminSite(AdminSite):
  18. site_header = 'Python博客后台管理'
  19. site_title = '后台管理'
  20. def __init__(self, name='admin'):
  21. super().__init__(name)
  22. def has_permission(self, request):
  23. return request.user.is_superuser
  24. # def get_urls(self):
  25. # urls = super().get_urls()
  26. # from django.urls import path
  27. # from blog.views import refresh_memcache
  28. #
  29. # my_urls = [
  30. # path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
  31. # ]
  32. # return urls + my_urls
  33. admin_site = DjangoBlogAdminSite(name='admin')
  34. admin_site.register(Article, ArticlelAdmin)
  35. admin_site.register(Category, CategoryAdmin)
  36. admin_site.register(Tag, TagAdmin)
  37. admin_site.register(Links, LinksAdmin)
  38. admin_site.register(SideBar, SideBarAdmin)
  39. admin_site.register(BlogSettings, BlogSettingsAdmin)
  40. admin_site.register(commands, CommandsAdmin)
  41. admin_site.register(EmailSendLog, EmailSendLogAdmin)
  42. admin_site.register(BlogUser, BlogUserAdmin)
  43. admin_site.register(Comment, CommentAdmin)
  44. admin_site.register(OAuthUser, OAuthUserAdmin)
  45. admin_site.register(OAuthConfig, OAuthConfigAdmin)
  46. admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
  47. admin_site.register(Site, SiteAdmin)
  48. admin_site.register(LogEntry, LogEntryAdmin)

  1. from django.contrib import admin
  2. from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.urls import reverse, NoReverseMatch
  5. from django.utils.encoding import force_str
  6. from django.utils.html import escape
  7. from django.utils.safestring import mark_safe
  8. from django.utils.translation import pgettext_lazy, gettext_lazy as _
  9. action_names = {
  10. ADDITION: pgettext_lazy('logentry_admin:action_type', 'Addition'),
  11. DELETION: pgettext_lazy('logentry_admin:action_type', 'Deletion'),
  12. CHANGE: pgettext_lazy('logentry_admin:action_type', 'Change'),
  13. }
  14. class LogEntryAdmin(admin.ModelAdmin):
  15. date_hierarchy = 'action_time'
  16. readonly_fields = ([f.name for f in LogEntry._meta.fields] +
  17. ['object_link', 'action_description', 'user_link',
  18. 'get_change_message'])
  19. fieldsets = (
  20. (_('Metadata'), {
  21. 'fields': (
  22. 'action_time',
  23. 'user_link',
  24. 'action_description',
  25. 'object_link',
  26. )
  27. }),
  28. (_('Details'), {
  29. 'fields': (
  30. 'get_change_message',
  31. 'content_type',
  32. 'object_id',
  33. 'object_repr',
  34. )
  35. }),
  36. )
  37. list_filter = [
  38. 'content_type'
  39. ]
  40. search_fields = [
  41. 'object_repr',
  42. 'change_message'
  43. ]
  44. list_display_links = [
  45. 'action_time',
  46. 'get_change_message',
  47. ]
  48. list_display = [
  49. 'action_time',
  50. 'user_link',
  51. 'content_type',
  52. 'object_link',
  53. 'action_description',
  54. 'get_change_message',
  55. ]
  56. def has_add_permission(self, request):
  57. return False
  58. def has_change_permission(self, request, obj=None):
  59. return (
  60. request.user.is_superuser or
  61. request.user.has_perm('admin.change_logentry')
  62. ) and request.method != 'POST'
  63. def has_delete_permission(self, request, obj=None):
  64. return False
  65. def object_link(self, obj):
  66. object_link = escape(obj.object_repr)
  67. content_type = obj.content_type
  68. if obj.action_flag != DELETION and content_type is not None:
  69. # try returning an actual link instead of object repr string
  70. try:
  71. url = reverse(
  72. 'admin:{}_{}_change'.format(content_type.app_label,
  73. content_type.model),
  74. args=[obj.object_id]
  75. )
  76. object_link = '<a href="{}">{}</a>'.format(url, object_link)
  77. except NoReverseMatch:
  78. pass
  79. return mark_safe(object_link)
  80. object_link.admin_order_field = 'object_repr'
  81. object_link.short_description = _('object')
  82. def user_link(self, obj):
  83. content_type = ContentType.objects.get_for_model(type(obj.user))
  84. user_link = escape(force_str(obj.user))
  85. try:
  86. # try returning an actual link instead of object repr string
  87. url = reverse(
  88. 'admin:{}_{}_change'.format(content_type.app_label,
  89. content_type.model),
  90. args=[obj.user.pk]
  91. )
  92. user_link = '<a href="{}">{}</a>'.format(url, user_link)
  93. except NoReverseMatch:
  94. pass
  95. return mark_safe(user_link)
  96. user_link.admin_order_field = 'user'
  97. user_link.short_description = _('user')
  98. def get_queryset(self, request):
  99. queryset = super(LogEntryAdmin, self).get_queryset(request)
  100. return queryset.prefetch_related('content_type')
  101. def get_actions(self, request):
  102. actions = super(LogEntryAdmin, self).get_actions(request)
  103. if 'delete_selected' in actions:
  104. del actions['delete_selected']
  105. return actions
  106. def action_description(self, obj):
  107. return action_names[obj.action_flag]
  108. action_description.short_description = _('action')
  109. def get_change_message(self, obj):
  110. return obj.get_change_message()
  111. get_change_message.short_description = _('change message')
  1. # encoding: utf-8
  2. from __future__ import absolute_import, division, print_function, unicode_literals
  3. import json
  4. import os
  5. import re
  6. import shutil
  7. import threading
  8. import warnings
  9. import six
  10. from django.conf import settings
  11. from django.core.exceptions import ImproperlyConfigured
  12. from django.utils.datetime_safe import datetime
  13. from django.utils.encoding import force_str
  14. from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
  15. from haystack.constants import DJANGO_CT, DJANGO_ID, ID
  16. from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
  17. from haystack.inputs import Clean, Exact, PythonData, Raw
  18. from haystack.models import SearchResult
  19. from haystack.utils import get_identifier, get_model_ct
  20. from haystack.utils import log as logging
  21. from haystack.utils.app_loading import haystack_get_model
  22. from jieba.analyse import ChineseAnalyzer
  23. from whoosh import index
  24. from whoosh.analysis import StemmingAnalyzer
  25. from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
  26. from whoosh.fields import ID as WHOOSH_ID
  27. from whoosh.filedb.filestore import FileStorage, RamStorage
  28. from whoosh.highlight import ContextFragmenter, HtmlFormatter
  29. from whoosh.highlight import highlight as whoosh_highlight
  30. from whoosh.qparser import QueryParser
  31. from whoosh.searching import ResultsPage
  32. from whoosh.writing import AsyncWriter
  33. try:
  34. import whoosh
  35. except ImportError:
  36. raise MissingDependency(
  37. "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
  38. # Handle minimum requirement.
  39. if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
  40. raise MissingDependency(
  41. "The 'whoosh' backend requires version 2.5.0 or greater.")
  42. # Bubble up the correct error.
  43. DATETIME_REGEX = re.compile(
  44. '^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$')
  45. LOCALS = threading.local()
  46. LOCALS.RAM_STORE = None
  47. class WhooshHtmlFormatter(HtmlFormatter):
  48. """
  49. This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
  50. We use it to have consistent results across backends. Specifically,
  51. Solr, Xapian and Elasticsearch are using this formatting.
  52. """
  53. template = '<%(tag)s>%(t)s</%(tag)s>'
  54. class WhooshSearchBackend(BaseSearchBackend):
  55. # Word reserved by Whoosh for special use.
  56. RESERVED_WORDS = (
  57. 'AND',
  58. 'NOT',
  59. 'OR',
  60. 'TO',
  61. )
  62. # Characters reserved by Whoosh for special use.
  63. # The '\\' must come first, so as not to overwrite the other slash
  64. # replacements.
  65. RESERVED_CHARACTERS = (
  66. '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
  67. '[', ']', '^', '"', '~', '*', '?', ':', '.',
  68. )
  69. def __init__(self, connection_alias, **connection_options):
  70. super(
  71. WhooshSearchBackend,
  72. self).__init__(
  73. connection_alias,
  74. **connection_options)
  75. self.setup_complete = False
  76. self.use_file_storage = True
  77. self.post_limit = getattr(
  78. connection_options,
  79. 'POST_LIMIT',
  80. 128 * 1024 * 1024)
  81. self.path = connection_options.get('PATH')
  82. if connection_options.get('STORAGE', 'file') != 'file':
  83. self.use_file_storage = False
  84. if self.use_file_storage and not self.path:
  85. raise ImproperlyConfigured(
  86. "You must specify a 'PATH' in your settings for connection '%s'." %
  87. connection_alias)
  88. self.log = logging.getLogger('haystack')
  89. def setup(self):
  90. """
  91. Defers loading until needed.
  92. """
  93. from haystack import connections
  94. new_index = False
  95. # Make sure the index is there.
  96. if self.use_file_storage and not os.path.exists(self.path):
  97. os.makedirs(self.path)
  98. new_index = True
  99. if self.use_file_storage and not os.access(self.path, os.W_OK):
  100. raise IOError(
  101. "The path to your Whoosh index '%s' is not writable for the current user/group." %
  102. self.path)
  103. if self.use_file_storage:
  104. self.storage = FileStorage(self.path)
  105. else:
  106. global LOCALS
  107. if getattr(LOCALS, 'RAM_STORE', None) is None:
  108. LOCALS.RAM_STORE = RamStorage()
  109. self.storage = LOCALS.RAM_STORE
  110. self.content_field_name, self.schema = self.build_schema(
  111. connections[self.connection_alias].get_unified_index().all_searchfields())
  112. self.parser = QueryParser(self.content_field_name, schema=self.schema)
  113. if new_index is True:
  114. self.index = self.storage.create_index(self.schema)
  115. else:
  116. try:
  117. self.index = self.storage.open_index(schema=self.schema)
  118. except index.EmptyIndexError:
  119. self.index = self.storage.create_index(self.schema)
  120. self.setup_complete = True
  121. def build_schema(self, fields):
  122. schema_fields = {
  123. ID: WHOOSH_ID(stored=True, unique=True),
  124. DJANGO_CT: WHOOSH_ID(stored=True),
  125. DJANGO_ID: WHOOSH_ID(stored=True),
  126. }
  127. # Grab the number of keys that are hard-coded into Haystack.
  128. # We'll use this to (possibly) fail slightly more gracefully later.
  129. initial_key_count = len(schema_fields)
  130. content_field_name = ''
  131. for field_name, field_class in fields.items():
  132. if field_class.is_multivalued:
  133. if field_class.indexed is False:
  134. schema_fields[field_class.index_fieldname] = IDLIST(
  135. stored=True, field_boost=field_class.boost)
  136. else:
  137. schema_fields[field_class.index_fieldname] = KEYWORD(
  138. stored=True, commas=True, scorable=True, field_boost=field_class.boost)
  139. elif field_class.field_type in ['date', 'datetime']:
  140. schema_fields[field_class.index_fieldname] = DATETIME(
  141. stored=field_class.stored, sortable=True)
  142. elif field_class.field_type == 'integer':
  143. schema_fields[field_class.index_fieldname] = NUMERIC(
  144. stored=field_class.stored, numtype=int, field_boost=field_class.boost)
  145. elif field_class.field_type == 'float':
  146. schema_fields[field_class.index_fieldname] = NUMERIC(
  147. stored=field_class.stored, numtype=float, field_boost=field_class.boost)
  148. elif field_class.field_type == 'boolean':
  149. # Field boost isn't supported on BOOLEAN as of 1.8.2.
  150. schema_fields[field_class.index_fieldname] = BOOLEAN(
  151. stored=field_class.stored)
  152. elif field_class.field_type == 'ngram':
  153. schema_fields[field_class.index_fieldname] = NGRAM(
  154. minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
  155. elif field_class.field_type == 'edge_ngram':
  156. schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start',
  157. stored=field_class.stored,
  158. field_boost=field_class.boost)
  159. else:
  160. # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True)
  161. schema_fields[field_class.index_fieldname] = TEXT(
  162. stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
  163. if field_class.document is True:
  164. content_field_name = field_class.index_fieldname
  165. schema_fields[field_class.index_fieldname].spelling = True
  166. # Fail more gracefully than relying on the backend to die if no fields
  167. # are found.
  168. if len(schema_fields) <= initial_key_count:
  169. raise SearchBackendError(
  170. "No fields were found in any search_indexes. Please correct this before attempting to search.")
  171. return (content_field_name, Schema(**schema_fields))
  172. def update(self, index, iterable, commit=True):
  173. if not self.setup_complete:
  174. self.setup()
  175. self.index = self.index.refresh()
  176. writer = AsyncWriter(self.index)
  177. for obj in iterable:
  178. try:
  179. doc = index.full_prepare(obj)
  180. except SkipDocument:
  181. self.log.debug(u"Indexing for object `%s` skipped", obj)
  182. else:
  183. # Really make sure it's unicode, because Whoosh won't have it any
  184. # other way.
  185. for key in doc:
  186. doc[key] = self._from_python(doc[key])
  187. # Document boosts aren't supported in Whoosh 2.5.0+.
  188. if 'boost' in doc:
  189. del doc['boost']
  190. try:
  191. writer.update_document(**doc)
  192. except Exception as e:
  193. if not self.silently_fail:
  194. raise
  195. # We'll log the object identifier but won't include the actual object
  196. # to avoid the possibility of that generating encoding errors while
  197. # processing the log message:
  198. self.log.error(
  199. u"%s while preparing object for update" %
  200. e.__class__.__name__,
  201. exc_info=True,
  202. extra={
  203. "data": {
  204. "index": index,
  205. "object": get_identifier(obj)}})
  206. if len(iterable) > 0:
  207. # For now, commit no matter what, as we run into locking issues
  208. # otherwise.
  209. writer.commit()
  210. def remove(self, obj_or_string, commit=True):
  211. if not self.setup_complete:
  212. self.setup()
  213. self.index = self.index.refresh()
  214. whoosh_id = get_identifier(obj_or_string)
  215. try:
  216. self.index.delete_by_query(
  217. q=self.parser.parse(
  218. u'%s:"%s"' %
  219. (ID, whoosh_id)))
  220. except Exception as e:
  221. if not self.silently_fail:
  222. raise
  223. self.log.error(
  224. "Failed to remove document '%s' from Whoosh: %s",
  225. whoosh_id,
  226. e,
  227. exc_info=True)
  228. def clear(self, models=None, commit=True):
  229. if not self.setup_complete:
  230. self.setup()
  231. self.index = self.index.refresh()
  232. if models is not None:
  233. assert isinstance(models, (list, tuple))
  234. try:
  235. if models is None:
  236. self.delete_index()
  237. else:
  238. models_to_delete = []
  239. for model in models:
  240. models_to_delete.append(
  241. u"%s:%s" %
  242. (DJANGO_CT, get_model_ct(model)))
  243. self.index.delete_by_query(
  244. q=self.parser.parse(
  245. u" OR ".join(models_to_delete)))
  246. except Exception as e:
  247. if not self.silently_fail:
  248. raise
  249. if models is not None:
  250. self.log.error(
  251. "Failed to clear Whoosh index of models '%s': %s",
  252. ','.join(models_to_delete),
  253. e,
  254. exc_info=True)
  255. else:
  256. self.log.error(
  257. "Failed to clear Whoosh index: %s", e, exc_info=True)
  258. def delete_index(self):
  259. # Per the Whoosh mailing list, if wiping out everything from the index,
  260. # it's much more efficient to simply delete the index files.
  261. if self.use_file_storage and os.path.exists(self.path):
  262. shutil.rmtree(self.path)
  263. elif not self.use_file_storage:
  264. self.storage.clean()
  265. # Recreate everything.
  266. self.setup()
  267. def optimize(self):
  268. if not self.setup_complete:
  269. self.setup()
  270. self.index = self.index.refresh()
  271. self.index.optimize()
  272. def calculate_page(self, start_offset=0, end_offset=None):
  273. # Prevent against Whoosh throwing an error. Requires an end_offset
  274. # greater than 0.
  275. if end_offset is not None and end_offset <= 0:
  276. end_offset = 1
  277. # Determine the page.
  278. page_num = 0
  279. if end_offset is None:
  280. end_offset = 1000000
  281. if start_offset is None:
  282. start_offset = 0
  283. page_length = end_offset - start_offset
  284. if page_length and page_length > 0:
  285. page_num = int(start_offset / page_length)
  286. # Increment because Whoosh uses 1-based page numbers.
  287. page_num += 1
  288. return page_num, page_length
  289. @log_query
  290. def search(
  291. self,
  292. query_string,
  293. sort_by=None,
  294. start_offset=0,
  295. end_offset=None,
  296. fields='',
  297. highlight=False,
  298. facets=None,
  299. date_facets=None,
  300. query_facets=None,
  301. narrow_queries=None,
  302. spelling_query=None,
  303. within=None,
  304. dwithin=None,
  305. distance_point=None,
  306. models=None,
  307. limit_to_registered_models=None,
  308. result_class=None,
  309. **kwargs):
  310. if not self.setup_complete:
  311. self.setup()
  312. # A zero length query should return no results.
  313. if len(query_string) == 0:
  314. return {
  315. 'results': [],
  316. 'hits': 0,
  317. }
  318. query_string = force_str(query_string)
  319. # A one-character query (non-wildcard) gets nabbed by a stopwords
  320. # filter and should yield zero results.
  321. if len(query_string) <= 1 and query_string != u'*':
  322. return {
  323. 'results': [],
  324. 'hits': 0,
  325. }
  326. reverse = False
  327. if sort_by is not None:
  328. # Determine if we need to reverse the results and if Whoosh can
  329. # handle what it's being asked to sort by. Reversing is an
  330. # all-or-nothing action, unfortunately.
  331. sort_by_list = []
  332. reverse_counter = 0
  333. for order_by in sort_by:
  334. if order_by.startswith('-'):
  335. reverse_counter += 1
  336. if reverse_counter and reverse_counter != len(sort_by):
  337. raise SearchBackendError("Whoosh requires all order_by fields"
  338. " to use the same sort direction")
  339. for order_by in sort_by:
  340. if order_by.startswith('-'):
  341. sort_by_list.append(order_by[1:])
  342. if len(sort_by_list) == 1:
  343. reverse = True
  344. else:
  345. sort_by_list.append(order_by)
  346. if len(sort_by_list) == 1:
  347. reverse = False
  348. sort_by = sort_by_list[0]
  349. if facets is not None:
  350. warnings.warn(
  351. "Whoosh does not handle faceting.",
  352. Warning,
  353. stacklevel=2)
  354. if date_facets is not None:
  355. warnings.warn(
  356. "Whoosh does not handle date faceting.",
  357. Warning,
  358. stacklevel=2)
  359. if query_facets is not None:
  360. warnings.warn(
  361. "Whoosh does not handle query faceting.",
  362. Warning,
  363. stacklevel=2)
  364. narrowed_results = None
  365. self.index = self.index.refresh()
  366. if limit_to_registered_models is None:
  367. limit_to_registered_models = getattr(
  368. settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
  369. if models and len(models):
  370. model_choices = sorted(get_model_ct(model) for model in models)
  371. elif limit_to_registered_models:
  372. # Using narrow queries, limit the results to only models handled
  373. # with the current routers.
  374. model_choices = self.build_models_list()
  375. else:
  376. model_choices = []
  377. if len(model_choices) > 0:
  378. if narrow_queries is None:
  379. narrow_queries = set()
  380. narrow_queries.add(' OR '.join(
  381. ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
  382. narrow_searcher = None
  383. if narrow_queries is not None:
  384. # Potentially expensive? I don't see another way to do it in
  385. # Whoosh...
  386. narrow_searcher = self.index.searcher()
  387. for nq in narrow_queries:
  388. recent_narrowed_results = narrow_searcher.search(
  389. self.parser.parse(force_str(nq)), limit=None)
  390. if len(recent_narrowed_results) <= 0:
  391. return {
  392. 'results': [],
  393. 'hits': 0,
  394. }
  395. if narrowed_results:
  396. narrowed_results.filter(recent_narrowed_results)
  397. else:
  398. narrowed_results = recent_narrowed_results
  399. self.index = self.index.refresh()
  400. if self.index.doc_count():
  401. searcher = self.index.searcher()
  402. parsed_query = self.parser.parse(query_string)
  403. # In the event of an invalid/stopworded query, recover gracefully.
  404. if parsed_query is None:
  405. return {
  406. 'results': [],
  407. 'hits': 0,
  408. }
  409. page_num, page_length = self.calculate_page(
  410. start_offset, end_offset)
  411. search_kwargs = {
  412. 'pagelen': page_length,
  413. 'sortedby': sort_by,
  414. 'reverse': reverse,
  415. }
  416. # Handle the case where the results have been narrowed.
  417. if narrowed_results is not None:
  418. search_kwargs['filter'] = narrowed_results
  419. try:
  420. raw_page = searcher.search_page(
  421. parsed_query,
  422. page_num,
  423. **search_kwargs
  424. )
  425. except ValueError:
  426. if not self.silently_fail:
  427. raise
  428. return {
  429. 'results': [],
  430. 'hits': 0,
  431. 'spelling_suggestion': None,
  432. }
  433. # Because as of Whoosh 2.5.1, it will return the wrong page of
  434. # results if you request something too high. :(
  435. if raw_page.pagenum < page_num:
  436. return {
  437. 'results': [],
  438. 'hits': 0,
  439. 'spelling_suggestion': None,
  440. }
  441. results = self._process_results(
  442. raw_page,
  443. highlight=highlight,
  444. query_string=query_string,
  445. spelling_query=spelling_query,
  446. result_class=result_class)
  447. searcher.close()
  448. if hasattr(narrow_searcher, 'close'):
  449. narrow_searcher.close()
  450. return results
  451. else:
  452. if self.include_spelling:
  453. if spelling_query:
  454. spelling_suggestion = self.create_spelling_suggestion(
  455. spelling_query)
  456. else:
  457. spelling_suggestion = self.create_spelling_suggestion(
  458. query_string)
  459. else:
  460. spelling_suggestion = None
  461. return {
  462. 'results': [],
  463. 'hits': 0,
  464. 'spelling_suggestion': spelling_suggestion,
  465. }
  466. def more_like_this(
  467. self,
  468. model_instance,
  469. additional_query_string=None,
  470. start_offset=0,
  471. end_offset=None,
  472. models=None,
  473. limit_to_registered_models=None,
  474. result_class=None,
  475. **kwargs):
  476. if not self.setup_complete:
  477. self.setup()
  478. # Deferred models will have a different class ("RealClass_Deferred_fieldname")
  479. # which won't be in our registry:
  480. model_klass = model_instance._meta.concrete_model
  481. field_name = self.content_field_name
  482. narrow_queries = set()
  483. narrowed_results = None
  484. self.index = self.index.refresh()
  485. if limit_to_registered_models is None:
  486. limit_to_registered_models = getattr(
  487. settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
  488. if models and len(models):
  489. model_choices = sorted(get_model_ct(model) for model in models)
  490. elif limit_to_registered_models:
  491. # Using narrow queries, limit the results to only models handled
  492. # with the current routers.
  493. model_choices = self.build_models_list()
  494. else:
  495. model_choices = []
  496. if len(model_choices) > 0:
  497. if narrow_queries is None:
  498. narrow_queries = set()
  499. narrow_queries.add(' OR '.join(
  500. ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
  501. if additional_query_string and additional_query_string != '*':
  502. narrow_queries.add(additional_query_string)
  503. narrow_searcher = None
  504. if narrow_queries is not None:
  505. # Potentially expensive? I don't see another way to do it in
  506. # Whoosh...
  507. narrow_searcher = self.index.searcher()
  508. for nq in narrow_queries:
  509. recent_narrowed_results = narrow_searcher.search(
  510. self.parser.parse(force_str(nq)), limit=None)
  511. if len(recent_narrowed_results) <= 0:
  512. return {
  513. 'results': [],
  514. 'hits': 0,
  515. }
  516. if narrowed_results:
  517. narrowed_results.filter(recent_narrowed_results)
  518. else:
  519. narrowed_results = recent_narrowed_results
  520. page_num, page_length = self.calculate_page(start_offset, end_offset)
  521. self.index = self.index.refresh()
  522. raw_results = EmptyResults()
  523. if self.index.doc_count():
  524. query = "%s:%s" % (ID, get_identifier(model_instance))
  525. searcher = self.index.searcher()
  526. parsed_query = self.parser.parse(query)
  527. results = searcher.search(parsed_query)
  528. if len(results):
  529. raw_results = results[0].more_like_this(
  530. field_name, top=end_offset)
  531. # Handle the case where the results have been narrowed.
  532. if narrowed_results is not None and hasattr(raw_results, 'filter'):
  533. raw_results.filter(narrowed_results)
  534. try:
  535. raw_page = ResultsPage(raw_results, page_num, page_length)
  536. except ValueError:
  537. if not self.silently_fail:
  538. raise
  539. return {
  540. 'results': [],
  541. 'hits': 0,
  542. 'spelling_suggestion': None,
  543. }
  544. # Because as of Whoosh 2.5.1, it will return the wrong page of
  545. # results if you request something too high. :(
  546. if raw_page.pagenum < page_num:
  547. return {
  548. 'results': [],
  549. 'hits': 0,
  550. 'spelling_suggestion': None,
  551. }
  552. results = self._process_results(raw_page, result_class=result_class)
  553. searcher.close()
  554. if hasattr(narrow_searcher, 'close'):
  555. narrow_searcher.close()
  556. return results
  557. def _process_results(
  558. self,
  559. raw_page,
  560. highlight=False,
  561. query_string='',
  562. spelling_query=None,
  563. result_class=None):
  564. from haystack import connections
  565. results = []
  566. # It's important to grab the hits first before slicing. Otherwise, this
  567. # can cause pagination failures.
  568. hits = len(raw_page)
  569. if result_class is None:
  570. result_class = SearchResult
  571. facets = {}
  572. spelling_suggestion = None
  573. unified_index = connections[self.connection_alias].get_unified_index()
  574. indexed_models = unified_index.get_indexed_models()
  575. for doc_offset, raw_result in enumerate(raw_page):
  576. score = raw_page.score(doc_offset) or 0
  577. app_label, model_name = raw_result[DJANGO_CT].split('.')
  578. additional_fields = {}
  579. model = haystack_get_model(app_label, model_name)
  580. if model and model in indexed_models:
  581. for key, value in raw_result.items():
  582. index = unified_index.get_index(model)
  583. string_key = str(key)
  584. if string_key in index.fields and hasattr(
  585. index.fields[string_key], 'convert'):
  586. # Special-cased due to the nature of KEYWORD fields.
  587. if index.fields[string_key].is_multivalued:
  588. if value is None or len(value) == 0:
  589. additional_fields[string_key] = []
  590. else:
  591. additional_fields[string_key] = value.split(
  592. ',')
  593. else:
  594. additional_fields[string_key] = index.fields[string_key].convert(
  595. value)
  596. else:
  597. additional_fields[string_key] = self._to_python(value)
  598. del (additional_fields[DJANGO_CT])
  599. del (additional_fields[DJANGO_ID])
  600. if highlight:
  601. sa = StemmingAnalyzer()
  602. formatter = WhooshHtmlFormatter('em')
  603. terms = [token.text for token in sa(query_string)]
  604. whoosh_result = whoosh_highlight(
  605. additional_fields.get(self.content_field_name),
  606. terms,
  607. sa,
  608. ContextFragmenter(),
  609. formatter
  610. )
  611. additional_fields['highlighted'] = {
  612. self.content_field_name: [whoosh_result],
  613. }
  614. result = result_class(
  615. app_label,
  616. model_name,
  617. raw_result[DJANGO_ID],
  618. score,
  619. **additional_fields)
  620. results.append(result)
  621. else:
  622. hits -= 1
  623. if self.include_spelling:
  624. if spelling_query:
  625. spelling_suggestion = self.create_spelling_suggestion(
  626. spelling_query)
  627. else:
  628. spelling_suggestion = self.create_spelling_suggestion(
  629. query_string)
  630. return {
  631. 'results': results,
  632. 'hits': hits,
  633. 'facets': facets,
  634. 'spelling_suggestion': spelling_suggestion,
  635. }
  636. def create_spelling_suggestion(self, query_string):
  637. spelling_suggestion = None
  638. reader = self.index.reader()
  639. corrector = reader.corrector(self.content_field_name)
  640. cleaned_query = force_str(query_string)
  641. if not query_string:
  642. return spelling_suggestion
  643. # Clean the string.
  644. for rev_word in self.RESERVED_WORDS:
  645. cleaned_query = cleaned_query.replace(rev_word, '')
  646. for rev_char in self.RESERVED_CHARACTERS:
  647. cleaned_query = cleaned_query.replace(rev_char, '')
  648. # Break it down.
  649. query_words = cleaned_query.split()
  650. suggested_words = []
  651. for word in query_words:
  652. suggestions = corrector.suggest(word, limit=1)
  653. if len(suggestions) > 0:
  654. suggested_words.append(suggestions[0])
  655. spelling_suggestion = ' '.join(suggested_words)
  656. return spelling_suggestion
  657. def _from_python(self, value):
  658. """
  659. Converts Python values to a string for Whoosh.
  660. Code courtesy of pysolr.
  661. """
  662. if hasattr(value, 'strftime'):
  663. if not hasattr(value, 'hour'):
  664. value = datetime(value.year, value.month, value.day, 0, 0, 0)
  665. elif isinstance(value, bool):
  666. if value:
  667. value = 'true'
  668. else:
  669. value = 'false'
  670. elif isinstance(value, (list, tuple)):
  671. value = u','.join([force_str(v) for v in value])
  672. elif isinstance(value, (six.integer_types, float)):
  673. # Leave it alone.
  674. pass
  675. else:
  676. value = force_str(value)
  677. return value
  678. def _to_python(self, value):
  679. """
  680. Converts values from Whoosh to native Python values.
  681. A port of the same method in pysolr, as they deal with data the same way.
  682. """
  683. if value == 'true':
  684. return True
  685. elif value == 'false':
  686. return False
  687. if value and isinstance(value, six.string_types):
  688. possible_datetime = DATETIME_REGEX.search(value)
  689. if possible_datetime:
  690. date_values = possible_datetime.groupdict()
  691. for dk, dv in date_values.items():
  692. date_values[dk] = int(dv)
  693. return datetime(
  694. date_values['year'],
  695. date_values['month'],
  696. date_values['day'],
  697. date_values['hour'],
  698. date_values['minute'],
  699. date_values['second'])
  700. try:
  701. # Attempt to use json to load the values.
  702. converted_value = json.loads(value)
  703. # Try to handle most built-in types.
  704. if isinstance(
  705. converted_value,
  706. (list,
  707. tuple,
  708. set,
  709. dict,
  710. six.integer_types,
  711. float,
  712. complex)):
  713. return converted_value
  714. except BaseException:
  715. # If it fails (SyntaxError or its ilk) or we don't trust it,
  716. # continue on.
  717. pass
  718. return value
  719. class WhooshSearchQuery(BaseSearchQuery):
  720. def _convert_datetime(self, date):
  721. if hasattr(date, 'hour'):
  722. return force_str(date.strftime('%Y%m%d%H%M%S'))
  723. else:
  724. return force_str(date.strftime('%Y%m%d000000'))
  725. def clean(self, query_fragment):
  726. """
  727. Provides a mechanism for sanitizing user input before presenting the
  728. value to the backend.
  729. Whoosh 1.X differs here in that you can no longer use a backslash
  730. to escape reserved characters. Instead, the whole word should be
  731. quoted.
  732. """
  733. words = query_fragment.split()
  734. cleaned_words = []
  735. for word in words:
  736. if word in self.backend.RESERVED_WORDS:
  737. word = word.replace(word, word.lower())
  738. for char in self.backend.RESERVED_CHARACTERS:
  739. if char in word:
  740. word = "'%s'" % word
  741. break
  742. cleaned_words.append(word)
  743. return ' '.join(cleaned_words)
  744. def build_query_fragment(self, field, filter_type, value):
  745. from haystack import connections
  746. query_frag = ''
  747. is_datetime = False
  748. if not hasattr(value, 'input_type_name'):
  749. # Handle when we've got a ``ValuesListQuerySet``...
  750. if hasattr(value, 'values_list'):
  751. value = list(value)
  752. if hasattr(value, 'strftime'):
  753. is_datetime = True
  754. if isinstance(value, six.string_types) and value != ' ':
  755. # It's not an ``InputType``. Assume ``Clean``.
  756. value = Clean(value)
  757. else:
  758. value = PythonData(value)
  759. # Prepare the query using the InputType.
  760. prepared_value = value.prepare(self)
  761. if not isinstance(prepared_value, (set, list, tuple)):
  762. # Then convert whatever we get back to what pysolr wants if needed.
  763. prepared_value = self.backend._from_python(prepared_value)
  764. # 'content' is a special reserved word, much like 'pk' in
  765. # Django's ORM layer. It indicates 'no special field'.
  766. if field == 'content':
  767. index_fieldname = ''
  768. else:
  769. index_fieldname = u'%s:' % connections[self._using].get_unified_index(
  770. ).get_index_fieldname(field)
  771. filter_types = {
  772. 'content': '%s',
  773. 'contains': '*%s*',
  774. 'endswith': "*%s",
  775. 'startswith': "%s*",
  776. 'exact': '%s',
  777. 'gt': "{%s to}",
  778. 'gte': "[%s to]",
  779. 'lt': "{to %s}",
  780. 'lte': "[to %s]",
  781. 'fuzzy': u'%s~',
  782. }
  783. if value.post_process is False:
  784. query_frag = prepared_value
  785. else:
  786. if filter_type in [
  787. 'content',
  788. 'contains',
  789. 'startswith',
  790. 'endswith',
  791. 'fuzzy']:
  792. if value.input_type_name == 'exact':
  793. query_frag = prepared_value
  794. else:
  795. # Iterate over terms & incorportate the converted form of
  796. # each into the query.
  797. terms = []
  798. if isinstance(prepared_value, six.string_types):
  799. possible_values = prepared_value.split(' ')
  800. else:
  801. if is_datetime is True:
  802. prepared_value = self._convert_datetime(
  803. prepared_value)
  804. possible_values = [prepared_value]
  805. for possible_value in possible_values:
  806. terms.append(
  807. filter_types[filter_type] %
  808. self.backend._from_python(possible_value))
  809. if len(terms) == 1:
  810. query_frag = terms[0]
  811. else:
  812. query_frag = u"(%s)" % " AND ".join(terms)
  813. elif filter_type == 'in':
  814. in_options = []
  815. for possible_value in prepared_value:
  816. is_datetime = False
  817. if hasattr(possible_value, 'strftime'):
  818. is_datetime = True
  819. pv = self.backend._from_python(possible_value)
  820. if is_datetime is True:
  821. pv = self._convert_datetime(pv)
  822. if isinstance(pv, six.string_types) and not is_datetime:
  823. in_options.append('"%s"' % pv)
  824. else:
  825. in_options.append('%s' % pv)
  826. query_frag = "(%s)" % " OR ".join(in_options)
  827. elif filter_type == 'range':
  828. start = self.backend._from_python(prepared_value[0])
  829. end = self.backend._from_python(prepared_value[1])
  830. if hasattr(prepared_value[0], 'strftime'):
  831. start = self._convert_datetime(start)
  832. if hasattr(prepared_value[1], 'strftime'):
  833. end = self._convert_datetime(end)
  834. query_frag = u"[%s to %s]" % (start, end)
  835. elif filter_type == 'exact':
  836. if value.input_type_name == 'exact':
  837. query_frag = prepared_value
  838. else:
  839. prepared_value = Exact(prepared_value).prepare(self)
  840. query_frag = filter_types[filter_type] % prepared_value
  841. else:
  842. if is_datetime is True:
  843. prepared_value = self._convert_datetime(prepared_value)
  844. query_frag = filter_types[filter_type] % prepared_value
  845. if len(query_frag) and not isinstance(value, Raw):
  846. if not query_frag.startswith('(') and not query_frag.endswith(')'):
  847. query_frag = "(%s)" % query_frag
  848. return u"%s%s" % (index_fieldname, query_frag)
  849. # if not filter_type in ('in', 'range'):
  850. # # 'in' is a bit of a special case, as we don't want to
  851. # # convert a valid list/tuple to string. Defer handling it
  852. # # until later...
  853. # value = self.backend._from_python(value)
  854. class WhooshEngine(BaseEngine):
  855. backend = WhooshSearchBackend
  856. query = WhooshSearchQuery

五,项目总结

对于博客系统来讲,用JAVA开发的较多,Python开发的相对较少,功能完整又比较全面的更是不多,这个系统做的整体功能完整,界面简洁大方,使用了较新的组件和技术框架,相对比较优秀。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/746431
推荐阅读
相关标签
  

闽ICP备14008679号