|
|
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
|
|
|
} |