You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DjangoBlog/djangoblog/plugin_manage/base_plugin.py

247 lines
9.8 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import logging # 用于日志记录
from pathlib import Path # 用于文件路径处理
from django.template import TemplateDoesNotExist # Django模板不存在异常
from django.template.loader import render_to_string # 用于渲染模板为字符串
# 创建日志记录器,用于记录插件相关日志
logger = logging.getLogger(__name__)
class BasePlugin:
"""
插件基类,所有自定义插件需继承此类并实现特定方法。
提供插件元数据管理、位置渲染、模板渲染、静态资源处理等基础功能。
"""
# 插件元数据(子类必须重写这些属性)
PLUGIN_NAME = None # 插件名称(如"天气插件"
PLUGIN_DESCRIPTION = None # 插件描述(功能说明)
PLUGIN_VERSION = None # 插件版本(如"1.0.0"
PLUGIN_AUTHOR = None # 插件作者
# 插件配置(子类可根据需求重写)
SUPPORTED_POSITIONS = [] # 支持的显示位置(如['sidebar', 'footer']表示支持侧边栏和页脚)
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高,用于多个插件在同一位置排序)
POSITION_PRIORITIES = {} # 各位置的优先级(覆盖默认值,如{'sidebar': 50}表示侧边栏优先级为50
def __init__(self):
"""初始化插件,验证元数据并设置基础属性"""
# 校验必须的元数据是否完整,不完整则抛出异常
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("插件元数据PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION必须定义。")
# 设置插件路径和唯一标识
self.plugin_dir = self._get_plugin_directory() # 插件所在目录路径
self.plugin_slug = self._get_plugin_slug() # 插件唯一标识(默认使用目录名)
# 初始化插件并注册钩子
self.init_plugin()
self.register_hooks()
def _get_plugin_directory(self):
"""获取插件所在的目录路径(内部方法)"""
import inspect
# 通过inspect模块获取当前类的定义文件路径再获取其所在目录
plugin_file = inspect.getfile(self.__class__)
return Path(plugin_file).parent
def _get_plugin_slug(self):
"""获取插件的唯一标识符(默认使用插件目录名,内部方法)"""
return self.plugin_dir.name
def init_plugin(self):
"""
插件初始化逻辑(钩子方法)
子类可重写此方法实现自定义初始化操作(如加载配置、连接数据库等)
"""
logger.info(f'{self.PLUGIN_NAME} 初始化完成。')
def register_hooks(self):
"""
注册插件钩子(钩子方法)
子类可重写此方法注册自定义钩子如响应Django信号、注册URL路由等
"""
pass
# === 位置渲染系统 ===
def render_position_widget(self, position, context, **kwargs):
"""
根据指定位置渲染插件组件(核心方法)
Args:
position: 位置标识(如'sidebar'表示侧边栏)
context: 模板上下文(包含当前请求、用户等信息)
**kwargs: 额外参数如文章ID、页面类型等
Returns:
dict: 包含渲染结果的字典,格式为:
{'html': 'HTML内容', 'priority': 优先级, 'plugin_name': 插件名}
若不支持该位置或不满足显示条件返回None
"""
# 检查当前位置是否在插件支持的位置列表中
if position not in self.SUPPORTED_POSITIONS:
return None
# 检查是否满足显示条件调用should_display方法
if not self.should_display(position, context, **kwargs):
return None
# 动态调用对应位置的渲染方法如position为'sidebar'则调用render_sidebar_widget
method_name = f'render_{position}_widget'
if hasattr(self, method_name):
# 调用具体位置的渲染方法获取HTML内容
html = getattr(self, method_name)(context, **kwargs)
if html: # 若渲染成功有HTML内容
# 确定当前位置的优先级优先使用POSITION_PRIORITIES否则用默认值
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
return {
'html': html,
'priority': priority,
'plugin_name': self.PLUGIN_NAME
}
return None
def should_display(self, position, context, **kwargs):
"""
判断插件是否应该在指定位置显示(钩子方法)
子类可重写此方法实现条件显示逻辑(如只在特定页面/用户组显示)
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
bool: True表示显示False表示不显示
"""
return True # 默认始终显示
# === 各位置渲染方法 - 子类需根据支持的位置重写 ===
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏组件(钩子方法),子类重写此方法实现侧边栏内容"""
return None
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部组件(钩子方法),子类重写此方法实现文章底部内容"""
return None
def render_article_top_widget(self, context, **kwargs):
"""渲染文章顶部组件(钩子方法),子类重写此方法实现文章顶部内容"""
return None
def render_header_widget(self, context, **kwargs):
"""渲染页头组件(钩子方法),子类重写此方法实现页头内容"""
return None
def render_footer_widget(self, context, **kwargs):
"""渲染页脚组件(钩子方法),子类重写此方法实现页脚内容"""
return None
def render_comment_before_widget(self, context, **kwargs):
"""渲染评论前组件(钩子方法),子类重写此方法实现评论区前内容"""
return None
def render_comment_after_widget(self, context, **kwargs):
"""渲染评论后组件(钩子方法),子类重写此方法实现评论区后内容"""
return None
# === 模板系统 ===
def render_template(self, template_name, context=None):
"""
渲染插件自带的模板文件
Args:
template_name: 模板文件名(如"sidebar.html"
context: 模板上下文(传递给模板的变量)
Returns:
str: 渲染后的HTML字符串模板不存在则返回空字符串
"""
if context is None:
context = {} # 默认为空上下文
# 构建模板路径plugins/插件标识/模板名遵循Django模板查找规则
template_path = f"plugins/{self.plugin_slug}/{template_name}"
try:
# 调用Django的render_to_string渲染模板
return render_to_string(template_path, context)
except TemplateDoesNotExist:
# 模板不存在时记录警告日志
logger.warning(f"插件模板不存在:{template_path}")
return ""
# === 静态资源系统 ===
def get_static_url(self, static_file):
"""
获取插件静态文件的URL如CSS、JS、图片等
Args:
static_file: 静态文件相对路径(如"css/style.css"
Returns:
str: 静态文件的完整URL"/static/myplugin/static/myplugin/css/style.css"
"""
from django.templatetags.static import static # 导入Django的static标签
# 构建静态文件路径:插件标识/static/插件标识/文件路径遵循Django静态文件规则
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
def get_css_files(self):
"""
获取插件需要加载的CSS文件列表钩子方法
子类重写此方法返回CSS文件路径列表框架会自动在页面加载这些CSS
Returns:
list: CSS文件路径列表如["css/style.css"]
"""
return []
def get_js_files(self):
"""
获取插件需要加载的JavaScript文件列表钩子方法
子类重写此方法返回JS文件路径列表框架会自动在页面加载这些JS
Returns:
list: JS文件路径列表如["js/script.js"]
"""
return []
def get_head_html(self, context=None):
"""
获取需要插入到HTML头部<head>标签内)的内容(钩子方法)
子类重写此方法返回自定义HTML如额外的CSS链接、meta标签等
Returns:
str: 要插入<head>的HTML字符串
"""
return ""
def get_body_html(self, context=None):
"""
获取需要插入到HTML body底部的内容钩子方法
子类重写此方法返回自定义HTML如额外的JS脚本
Returns:
str: 要插入<body>底部的HTML字符串
"""
return ""
def get_plugin_info(self):
"""
获取插件的详细信息(用于插件管理、展示等)
Returns:
dict: 包含插件元数据和配置的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION,
'author': self.PLUGIN_AUTHOR,
'slug': self.plugin_slug,
'directory': str(self.plugin_dir),
'supported_positions': self.SUPPORTED_POSITIONS,
'priorities': self.POSITION_PRIORITIES
}