diff --git a/blog/views.py b/blog/views.py index 4af9242..e4cc47e 100644 --- a/blog/views.py +++ b/blog/views.py @@ -2,6 +2,7 @@ import logging import os import uuid +import markdown from django.conf import settings from django.core.paginator import Paginator from django.http import HttpResponse, HttpResponseForbidden @@ -17,6 +18,7 @@ from haystack.views import SearchView from blog.models import Article, Category, LinkShowType, Links, Tag from comments.forms import CommentForm +from djangoblog.plugin_manage import hooks from djangoblog.utils import cache, get_blog_setting, get_sha256 logger = logging.getLogger(__name__) @@ -154,7 +156,19 @@ class ArticleDetailView(DetailView): kwargs['next_article'] = self.object.next_article kwargs['prev_article'] = self.object.prev_article - return super(ArticleDetailView, self).get_context_data(**kwargs) + context = super(ArticleDetailView, self).get_context_data(**kwargs) + article = self.object + # Action Hook, 通知插件"文章详情已获取" + hooks.run_action('after_article_body_get', article=article, request=self.request) + # Filter Hook, 允许插件修改文章正文 + article.body = hooks.apply_filters('the_content', article.body, article=article, request=self.request) + # toc = markdown.toc + md = markdown.Markdown(extensions=[ + 'markdown.extensions.extra', + ]) + article.body = md.convert(article.body) + + return context class CategoryDetailView(ArticleListView): diff --git a/djangoblog/plugin_manage/base_plugin.py b/djangoblog/plugin_manage/base_plugin.py new file mode 100644 index 0000000..2b4be5c --- /dev/null +++ b/djangoblog/plugin_manage/base_plugin.py @@ -0,0 +1,41 @@ +import logging + +logger = logging.getLogger(__name__) + + +class BasePlugin: + # 插件元数据 + PLUGIN_NAME = None + PLUGIN_DESCRIPTION = None + PLUGIN_VERSION = None + + def __init__(self): + if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]): + raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.") + self.init_plugin() + self.register_hooks() + + def init_plugin(self): + """ + 插件初始化逻辑 + 子类可以重写此方法来实现特定的初始化操作 + """ + logger.info(f'{self.PLUGIN_NAME} initialized.') + + def register_hooks(self): + """ + 注册插件钩子 + 子类可以重写此方法来注册特定的钩子 + """ + pass + + def get_plugin_info(self): + """ + 获取插件信息 + :return: 包含插件元数据的字典 + """ + return { + 'name': self.PLUGIN_NAME, + 'description': self.PLUGIN_DESCRIPTION, + 'version': self.PLUGIN_VERSION + } diff --git a/djangoblog/plugin_manage/hook_constants.py b/djangoblog/plugin_manage/hook_constants.py new file mode 100644 index 0000000..e1ad4be --- /dev/null +++ b/djangoblog/plugin_manage/hook_constants.py @@ -0,0 +1,4 @@ +ARTICLE_DETAIL_LOAD = 'article_detail_load' +ARTICLE_CREATE = 'article_create' +ARTICLE_UPDATE = 'article_update' +ARTICLE_DELETE = 'article_delete' diff --git a/djangoblog/plugin_manage/hooks.py b/djangoblog/plugin_manage/hooks.py new file mode 100644 index 0000000..d712540 --- /dev/null +++ b/djangoblog/plugin_manage/hooks.py @@ -0,0 +1,44 @@ +import logging + +logger = logging.getLogger(__name__) + +_hooks = {} + + +def register(hook_name: str, callback: callable): + """ + 注册一个钩子回调。 + """ + if hook_name not in _hooks: + _hooks[hook_name] = [] + _hooks[hook_name].append(callback) + logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") + + +def run_action(hook_name: str, *args, **kwargs): + """ + 执行一个 Action Hook。 + 它会按顺序执行所有注册到该钩子上的回调函数。 + """ + if hook_name in _hooks: + logger.debug(f"Running action hook '{hook_name}'") + for callback in _hooks[hook_name]: + try: + callback(*args, **kwargs) + except Exception as e: + logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + + +def apply_filters(hook_name: str, value, *args, **kwargs): + """ + 执行一个 Filter Hook。 + 它会把 value 依次传递给所有注册的回调函数进行处理。 + """ + if hook_name in _hooks: + logger.debug(f"Applying filter hook '{hook_name}'") + for callback in _hooks[hook_name]: + try: + value = callback(value, *args, **kwargs) + except Exception as e: + logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) + return value diff --git a/djangoblog/settings.py b/djangoblog/settings.py index fa970b5..ea7add5 100644 --- a/djangoblog/settings.py +++ b/djangoblog/settings.py @@ -11,6 +11,7 @@ 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 _ @@ -20,9 +21,8 @@ def env_to_bool(env, default): return default if str_val is None else str_val == 'True' -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ @@ -330,3 +330,18 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', }, } + +# Plugin System +PLUGINS_DIR = BASE_DIR / 'plugins' +ACTIVE_PLUGINS = [ + 'article_plugin_test', +] + +# 加载插件 +for plugin_dir in os.listdir(PLUGINS_DIR): + plugin_path = os.path.join(PLUGINS_DIR, plugin_dir) + if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, '__init__.py')): + try: + __import__(f'plugins.{plugin_dir}.plugin') + except ImportError: + pass diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/article_copyright_plugin/__init__.py b/plugins/article_copyright_plugin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/article_copyright_plugin/plugin.py b/plugins/article_copyright_plugin/plugin.py new file mode 100644 index 0000000..2dab2b3 --- /dev/null +++ b/plugins/article_copyright_plugin/plugin.py @@ -0,0 +1,33 @@ +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks + + +class ArticleCopyrightPlugin(BasePlugin): + # 1. 将插件元数据定义为类属性,以匹配 BasePlugin 的要求 + PLUGIN_NAME = '文章结尾版权声明' + PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的测试插件。' + PLUGIN_VERSION = '0.2.0' + # 也可以添加作者等其他自定义元数据 + PLUGIN_AUTHOR = 'liangliangyy' + + # 2. 实现 register_hooks 方法,专门用于注册钩子 + def register_hooks(self): + # 在这里将插件的方法注册到指定的钩子上 + hooks.register('the_content', self.add_copyright_to_content) + + def add_copyright_to_content(self, content, *args, **kwargs): + """ + 这个方法会被注册到 'the_content' 过滤器钩子上。 + 它接收原始内容,并返回添加了版权信息的新内容。 + """ + article = kwargs.get('article') + if not article: + return content + + copyright_info = f"\n
本文由 {article.author.username} 原创,转载请注明出处。
" + return content + copyright_info + + +# 3. 实例化插件。 +# 这会自动调用 BasePlugin.__init__,然后 BasePlugin.__init__ 会调用我们上面定义的 register_hooks 方法。 +plugin = ArticleCopyrightPlugin()