Плагины для веб приложения на django

Уже сейчас в byteflow ощущается необходимость плагинов, то ли ещё будет ) Я решил оформить свои мысли документально, чтобы потом было проще обсуждать.

Итак, как мне видятся плагины в byteflow: каждый плагин это поддиректория-модуль каталога plugins. В __init__.py содержится информация об авторе, версии и т.д. Каждый плагин имеет уникальную метку, состоящую из букв латинского алфавита, цифр и знака подчёркивания. Не должно быть двух плагинов с одинаковой меткой. Метку можно использовать, когда нужно привязать какую-то информацию в БД к плагину, например, статус активности. Также, используя метку можно запрашивать обновления с официального сайта byteflow, где будут храниться плагины.

Плагины могут объявлять свои template tags. Имя template tag плагина обязано начинаться с mod_[метка плагина]_ Может быть, имеет смысл автоматически делать глобальными все template tags активных плагинов.

Плагины могут содержать urls, views, models, middlewares, которые должны подлючаться автоматически в settings.py и глобальном urls.py. Однако непонятно, как это делать, если, скажем, информацию об активности плагина мы будем хранить в БД, ведь БД в settings.py недоступна.

Где лучше хранить статические файлы типа стилей и картинок я не знаю. Если в каталогах плагинов, то надо морочаться с симлинками для подключения их в MEDIA_PATH, если же копировать в MEDIA_PATH, то усложняется установка плагина: не просто воткнуть в plugins, но ещё и скопировать куда нужно статические файлы.

Также мне весьма нравится идея хуков. Т.е. базовое приложение объявляет точки подключения, в которые плагины могут подключаться и влиять на процесс. Возьмём для примера такую точку подключения, как сохранение публикации в блоге. Используя систему хуков плагин может подключиться к этой точке и изменить сохраняемые данные заменив, например, известные ему аббревиатуры на HTML-код, показывающий расшифровку акронима, при наведении на него.

В джанго встроен pydispatcher, позволяющий посылать и обрабатывать сигналы, но, как мне кажется, система принудительных хуков, позволит плагинам более гибко и просто изменять алгоритм работы базовой системы. Сигналы - это асинхронная модель - мы посылаем сигнал и продолжаем что-то делать, а сигнал кто-то ловит и тоже что-то делает… Хук же - это передача эстафетной палочки - в один момент времени объект обрабатывается в одном месте.

Некоторым плагинам может понадобиться место на диске, чтобы хранить данные, для этого можно завести отдельную директорию, в которой создавать директорию с правами на запись для каждого активируемого плагина.
Add post to:   Google Slashdot Yahoo Digg Technorati Delicious Bobrdobr.ru Newsland.ru Smi2.ru Rumarkz.ru Vaau.ru Memori.ru Rucity.com Moemesto.ru News2.ru Mister-Wong.ru Yandex.ru Myscoop.ru 100zakladok.ru
Make comment

Comments

Сигналы в pydispatcher’e используют синхронную модель, а не асинхронную.

И тут нашли неточность )) злые юзеры

я не злой юзер, я добрый unix way программист :)

Таки как раз сейчас читаю - мне пираныч её уже два раза кидал )

Интересно, касательно “хуков” сильно перекликается с моими идеями :)

А на счет статических файлов отдельно — не вижу ничего плохого, ведь они реально не входят в django application.

И распространять плагины как питоновские яйца =).

В общем-то зачем придумывать велосипед - уже давно есть, например, Trac. Там система плагинов довольно разумная.

Я вот только не пойму реальный смысл. Для django самого это нафик не нужно. Если кто то в дальнейшем создаст CMS на базе django - вот туда только.

Ай, не заметил сначала, это вы о своём byteflow.. ж)

Я сделал проще, выдрал из Trac (core.py). Помоему более элегантное решение, чем simple-plugin-framework.

В core/interface.py (core - это мой основной application в проекте) определил класс CorePluginManager и функцию get_plugin_manager - полу-фабрибрика/полу-синглетон.

from myinet.libs.contrib.trac.core import Interface, Component, ExtensionPoint, implements, Interface, ComponentManager

import os, glob, imp, copy, sys, re

# Хак, "создаем" модуль myinet.plugins. К нему будем "присоединять"
# код плагинов. Помоему будет работать и без этого, давно не делал 
# ревизии
import myinet.plugins
from myinet.libs.globals import globals

class IURLExtension(Interface):
    """Интерфейс расширения urlconf
    """

    def get_url_list(self, view):
        """Генератор с url'ками
        """

class CorePluginManager(Component, ComponentManager):
    """ Generic plugin loader
    """
    # Точка расширения. В этом списке будут регистрироваться потомки Componenet,
    # расширяющие IURLExtension 
    _url_list         = ExtensionPoint(IURLExtension)

    @classmethod
    def get_plugins_dir(self):
        """ Список каталогов с плагинами """
        from django.conf import settings

        if not hasattr(settings, 'PLUGIN_DIRS'):
            return ['plugins']
        else:
            return settings.PLUGIN_DIRS

    @classmethod
    def mod_import(self, module_name, file_base, path):
        mparam = imp.find_module(file_base, path)
        return imp.load_module(module_name, *mparam)

    def __init__(self, *pargs, **kwargs):
        from django.conf import settings

        super(CorePluginManager, self).__init__(*pargs, **kwargs)

        self.loaded_plugins = {}

        # Загрузка плагинов
        plugins_dirs = [os.path.normcase(os.path.realpath(a)) for a in self.get_plugins_dir()]

        for plugin_dir in plugins_dirs:
            debug(plugin_dir)

                plugin_files = glob.glob(os.path.join(plugin_dir, '*'))
            plugin_file = None

            for _plugin_file in plugin_files:
                if os.path.isdir(_plugin_file):
                    plugin_file = _plugin_file
                    break
            else:
                # Skip invalid plugin
                continue

            # Приводим имя каталога с плагином к имени модуля 
            # (тут есть грязный хак c myinet.plugins, но 
            # можно обойтись и включенем каталога с плагинами в
            # PYTHON_PATH

            _plugin_file = os.path.abspath(plugin_file)
            if not _plugin_file in self.loaded_plugins:
                plugin_name = 'myinet.plugins.' + plugin_file.replace(plugin_dir +'/', '')
                debug("Importing "+ plugin_name +" from " +plugin_file)

                try:
                    plugin_settings = self.mod_import(plugin_name +'.settings', 'settings', [plugin_file])
                except ImportError, e:
                    self.loaded_plugins[_plugin_file] = False
                    continue

                if not plugin_settings.PLUGIN_ENABLED:
                    self.loaded_plugins[_plugin_file] = False
                    continue

                try:
                    plugin = self.mod_import(plugin_name, os.path.basename(plugin_file), [os.path.dirname(plugin_file)])
                except ImportError, e:
                    self.loaded_plugins[_plugin_file] = False
                    print e
                    continue

                plugin.settings = plugin_settings
                self.loaded_plugins[_plugin_file] = plugin

            # Каждый плагин может содержать свои шаблоны
            pluging_templates = _plugin_file + '/templates/'
            if os.path.isdir(pluging_templates) and not pluging_templates in settings.TEMPLATE_DIRS:
                settings.TEMPLATE_DIRS += [pluging_templates]

    def get_url_list(self, view):
        """ Return generator with SettingAttr objects
        """
        for _url in self._url_list:
            for _u in _url.get_url_list(view):
                yield _u

def get_plugin_manager():
    # Loading plugins
    if not hasattr(globals, 'plugin_manager'):
        globals.plugin_manager = CorePluginManager()
    return globals.plugin_manager

И какой плагин без расширения urls?

Соотвественно в urls/init.py проекта, определяем url_extend, который используется в местах, где мы хотим предоставить возможность расширения функциональности.

from myinet.core.interface import get_plugin_manager
from myinet.settings import ROOT_URL

prefix = copy(ROOT_URL)
prefix = prefix.strip('/')

class RegexURLResolverExtended(RegexURLResolver):
    """ Allow to filling up urlpatterns by classes and objects.
    Class or objects should have urlpatterns property which is 
    standart Django's urlpatterns
    """

    @property
    def urlconf_module(self):
        return self.urlconf_name

def url_extend(regex_str, view, default_args=None):
    """ Like a standart Django's include but allow uses to assign plugin's views to the view
    view    - callable object to their regex_str will appended plugin's regex'es
    """
    class x(object): pass

    manager = get_plugin_manager()

    patterns_block = x()
    patterns_block.urlpatterns = [RegexURLPattern('^$', view, default_args)]

    for v in manager.get_url_list(view):
        patterns_block.urlpatterns.extend(v)
    return [RegexURLResolverExtended(regex_str, patterns_block)]

Пример (в модуле urls проекта):

urlpatterns += url_extend(r'^add/+',  new_product)

В плагине (расширяем new_product):

class TAURLAddService(Component):
    implements(IURLExtension)

    def get_url_list(self, view):
        from myinet.views.services import new_product
        from views import ta_class_add
        from django.conf.urls.defaults import patterns

        if view == new_product:
            rv = patterns('',
                          (r'^ta_costs/{0,}$', ta_class_add))
            yield rv

Ага, спасибо, изучу как будет время ) Мы уже присматривались к trac core при обсуждении системы плагинов в byteflow.

Required. 30 chars of fewer.

Required.

captcha image Please, enter symbols, which you see on the image