def _setup_assets(self): url_base_path = urlparse(config.BASE_URL).path output_dir = os.path.join(config.ASSETS_DIR, 'plugin-{}'.format(self.name)) output_url = '{}/static/assets/plugin-{}'.format(url_base_path, self.name) static_dir = os.path.join(self.root_path, 'static') static_url = '{}/static/plugins/{}'.format(url_base_path, self.name) self.assets = LazyCacheEnvironment(output_dir, output_url, debug=config.DEBUG, cache=get_webassets_cache_dir(self.name)) self.assets.append_path(output_dir, output_url) self.assets.append_path(static_dir, static_url) configure_pyscss(self.assets) self.register_assets()
def _setup_assets(self): config = Config.getInstance() url_base_path = urlparse(config.getBaseURL()).path output_dir = os.path.join(config.getAssetsDir(), 'plugin-{}'.format(self.name)) output_url = '{}/static/assets/plugin-{}'.format(url_base_path, self.name) static_dir = os.path.join(self.root_path, 'static') static_url = '{}/static/plugins/{}'.format(url_base_path, self.name) self.assets = LazyCacheEnvironment(output_dir, output_url, debug=config.getDebug(), cache=get_webassets_cache_dir(self.name)) self.assets.append_path(output_dir, output_url) self.assets.append_path(static_dir, static_url) configure_pyscss(self.assets) self.register_assets()
class IndicoPlugin(Plugin): """Base class for an Indico plugin All your plugins need to inherit from this class. It extends the `Plugin` class from Flask-PluginEngine with useful indico-specific functionality that makes it easier to write custom plugins. When creating your plugin, the class-level docstring is used to generate the friendly name and description of a plugin. Its first line becomes the name while everything else goes into the description. This class provides methods for some of the more common hooks Indico provides. Additional signals are defined in :mod:`~indico.core.signals` and can be connected to custom functions using :meth:`connect`. """ #: WTForm for the plugin's settings (requires `configurable=True`). #: All fields must return JSON-serializable types. settings_form = None #: A dictionary which can contain the kwargs for a specific field in the `settings_form`. settings_form_field_opts = {} #: A dictionary containing default values for settings default_settings = {} #: A dictionary containing default values for event-specific settings default_event_settings = {} #: A dictionary containing default values for user-specific settings default_user_settings = {} #: A set containing the names of settings which store ACLs acl_settings = frozenset() #: A set containing the names of event-specific settings which store ACLs acl_event_settings = frozenset() #: A dict containing custom converters for settings settings_converters = {} #: A dict containing custom converters for event-specific settings event_settings_converters = {} #: A dict containing custom converters for user-specific settings user_settings_converters = {} #: If the plugin should link to a details/config page in the admin interface configurable = False #: The group category that the plugin belongs to category = None #: If `settings`, `event_settings` and `user_settings` should use strict #: mode, i.e. only allow keys in `default_settings`, `default_event_settings` #: or `default_user_settings` (or the related `acl_settings` sets). #: This should not be disabled in most cases; if you need to store arbitrary #: keys, consider storing a dict inside a single top-level setting. strict_settings = True def init(self): """Called when the plugin is being loaded/initialized. If you want to run custom initialization code, this is the method to override. Make sure to call the base method or the other overridable methods in this class will not be called anymore. """ assert self.configurable or not self.settings_form, 'Non-configurable plugin cannot have a settings form' self.alembic_versions_path = os.path.join(self.root_path, 'migrations') self.connect(signals.plugin.get_blueprints, lambda app: self.get_blueprints()) self.template_hook('vars-js', self.inject_vars_js) self._setup_assets() self._import_models() def _setup_assets(self): config = Config.getInstance() url_base_path = urlparse(config.getBaseURL()).path output_dir = os.path.join(config.getAssetsDir(), 'plugin-{}'.format(self.name)) output_url = '{}/static/assets/plugin-{}'.format(url_base_path, self.name) static_dir = os.path.join(self.root_path, 'static') static_url = '{}/static/plugins/{}'.format(url_base_path, self.name) self.assets = LazyCacheEnvironment(output_dir, output_url, debug=config.getDebug(), cache=get_webassets_cache_dir(self.name)) self.assets.append_path(output_dir, output_url) self.assets.append_path(static_dir, static_url) configure_pyscss(self.assets) self.register_assets() def _import_models(self): old_models = set(db.Model._decl_class_registry.items()) import_all_models(self.package_name) added_models = set(db.Model._decl_class_registry.items()) - old_models # Ensure that only plugin schemas have been touched. It would be nice if we could actually # restrict a plugin to plugin_PLUGNNAME but since we load all models from the plugin's package # which could contain more than one plugin this is not easily possible. for name, model in added_models: schema = model.__table__.schema if not schema.startswith('plugin_'): raise Exception("Plugin '{}' added a model which is not in a plugin schema ('{}' in '{}')" .format(self.name, name, schema)) def connect(self, signal, receiver, **connect_kwargs): connect_kwargs['weak'] = False func = wrap_in_plugin_context(self, receiver) func.indico_plugin = self signal.connect(func, **connect_kwargs) def get_blueprints(self): """Return blueprints to be registered on the application A single blueprint can be returned directly, for multiple blueprint you need to yield them or return an iterable. """ pass def get_vars_js(self): """Return a dictionary with variables to be added to vars.js file""" return None @cached_property def translation_path(self): """ Return translation files to be used by the plugin. By default, get <root_path>/translations, unless it does not exist """ translations_path = os.path.join(self.root_path, 'translations') return translations_path if os.path.exists(translations_path) else None @cached_property def translation_domain(self): """Return the domain for this plugin's translation_path""" path = self.translation_path return Domain(path) if path else NullDomain() def register_assets(self): """Add assets to the plugin's webassets environment. In most cases the whole method can consist of calls to :meth:`register_js_bundle` and :meth:`register_css_bundle`. """ pass def register_js_bundle(self, name, *files): """Registers a JS bundle in the plugin's webassets environment""" pretty_name = re.sub(r'_js$', '', name) bundle = Bundle(*files, filters='rjsmin', output='js/{}_%(version)s.min.js'.format(pretty_name)) self.assets.register(name, bundle) def register_css_bundle(self, name, *files): """Registers an SCSS bundle in the plugin's webassets environment""" pretty_name = re.sub(r'_css$', '', name) bundle = Bundle(*files, filters=('pyscss', 'indico_cssrewrite', 'csscompressor'), output='css/{}_%(version)s.min.css'.format(pretty_name), depends=SASS_BASE_MODULES) self.assets.register(name, bundle) def inject_css(self, name, view_class=None, subclasses=True, condition=None): """Injects a CSS bundle into Indico's pages :param name: Name of the bundle :param view_class: If a WP class is specified, only inject it into pages using that class :param subclasses: also inject into subclasses of `view_class` :param condition: a callable to determine whether to inject or not. only called, when the view_class criterion matches """ self._inject_asset(signals.plugin.inject_css, name, view_class, subclasses, condition) def inject_js(self, name, view_class=None, subclasses=True, condition=None): """Injects a JS bundle into Indico's pages :param name: Name of the bundle :param view_class: If a WP class is specified, only inject it into pages using that class :param subclasses: also inject into subclasses of `view_class` :param condition: a callable to determine whether to inject or not. only called, when the view_class criterion matches """ self._inject_asset(signals.plugin.inject_js, name, view_class, subclasses, condition) def _inject_asset(self, signal, name, view_class=None, subclasses=True, condition=None): """Injects an asset bundle into Indico's pages :param signal: the signal to use for injection :param name: Name of the bundle :param view_class: If a WP class is specified, only inject it into pages using that class :param subclasses: also inject into subclasses of `view_class` :param condition: a callable to determine whether to inject or not. only called, when the view_class criterion matches """ def _do_inject(sender): if condition is None or condition(): return self.assets[name].urls() if view_class is None: self.connect(signal, _do_inject) elif not subclasses: self.connect(signal, _do_inject, sender=view_class) else: def _func(sender): if issubclass(sender, view_class): return _do_inject(sender) self.connect(signal, _func) def inject_vars_js(self): """Returns a string that will define variables for the plugin in the vars.js file""" vars_js = self.get_vars_js() if vars_js: return 'var {}Plugin = {};'.format(self.name.title(), json.dumps(vars_js)) def template_hook(self, name, receiver, priority=50, markup=True): """Registers a function to be called when a template hook is invoked. For details see :func:~`indico.web.flask.templating.register_template_hook` """ register_template_hook(name, receiver, priority, markup, self) @classproperty @classmethod def logger(cls): return Logger.get('plugin.{}'.format(cls.name)) @cached_classproperty @classmethod def settings(cls): """:class:`SettingsProxy` for the plugin's settings""" if cls.name is None: raise RuntimeError('Plugin has not been loaded yet') instance = cls.instance with instance.plugin_context(): # in case the default settings come from a property return SettingsProxy('plugin_{}'.format(cls.name), instance.default_settings, cls.strict_settings, acls=cls.acl_settings, converters=cls.settings_converters) @cached_classproperty @classmethod def event_settings(cls): """:class:`EventSettingsProxy` for the plugin's event-specific settings""" if cls.name is None: raise RuntimeError('Plugin has not been loaded yet') instance = cls.instance with instance.plugin_context(): # in case the default settings come from a property return EventSettingsProxy('plugin_{}'.format(cls.name), instance.default_event_settings, cls.strict_settings, acls=cls.acl_event_settings, converters=cls.event_settings_converters) @cached_classproperty @classmethod def user_settings(cls): """:class:`UserSettingsProxy` for the plugin's user-specific settings""" if cls.name is None: raise RuntimeError('Plugin has not been loaded yet') instance = cls.instance with instance.plugin_context(): # in case the default settings come from a property return UserSettingsProxy('plugin_{}'.format(cls.name), instance.default_user_settings, cls.strict_settings, converters=cls.user_settings_converters)
class IndicoPlugin(Plugin): """Base class for an Indico plugin All your plugins need to inherit from this class. It extends the `Plugin` class from Flask-PluginEngine with useful indico-specific functionality that makes it easier to write custom plugins. When creating your plugin, the class-level docstring is used to generate the friendly name and description of a plugin. Its first line becomes the name while everything else goes into the description. This class provides methods for some of the more common hooks Indico provides. Additional signals are defined in :mod:`~indico.core.signals` and can be connected to custom functions using :meth:`connect`. """ #: WTForm for the plugin's settings (requires `configurable=True`). #: All fields must return JSON-serializable types. settings_form = None #: A dictionary which can contain the kwargs for a specific field in the `settings_form`. settings_form_field_opts = {} #: A dictionary containing default values for settings default_settings = {} #: A dictionary containing default values for event-specific settings default_event_settings = {} #: A dictionary containing default values for user-specific settings default_user_settings = {} #: A set containing the names of settings which store ACLs acl_settings = frozenset() #: A set containing the names of event-specific settings which store ACLs acl_event_settings = frozenset() #: A dict containing custom converters for settings settings_converters = {} #: A dict containing custom converters for event-specific settings event_settings_converters = {} #: A dict containing custom converters for user-specific settings user_settings_converters = {} #: If the plugin should link to a details/config page in the admin interface configurable = False #: The group category that the plugin belongs to category = None #: If `settings`, `event_settings` and `user_settings` should use strict #: mode, i.e. only allow keys in `default_settings`, `default_event_settings` #: or `default_user_settings` (or the related `acl_settings` sets). #: This should not be disabled in most cases; if you need to store arbitrary #: keys, consider storing a dict inside a single top-level setting. strict_settings = True def init(self): """Called when the plugin is being loaded/initialized. If you want to run custom initialization code, this is the method to override. Make sure to call the base method or the other overridable methods in this class will not be called anymore. """ assert self.configurable or not self.settings_form, 'Non-configurable plugin cannot have a settings form' self.alembic_versions_path = os.path.join(self.root_path, 'migrations') self.connect(signals.plugin.get_blueprints, lambda app: self.get_blueprints()) self.template_hook('vars-js', self.inject_vars_js) self._setup_assets() self._import_models() def _setup_assets(self): url_base_path = urlparse(config.BASE_URL).path output_dir = os.path.join(config.ASSETS_DIR, 'plugin-{}'.format(self.name)) output_url = '{}/static/assets/plugin-{}'.format(url_base_path, self.name) static_dir = os.path.join(self.root_path, 'static') static_url = '{}/static/plugins/{}'.format(url_base_path, self.name) self.assets = LazyCacheEnvironment(output_dir, output_url, debug=config.DEBUG, cache=get_webassets_cache_dir(self.name)) self.assets.append_path(output_dir, output_url) self.assets.append_path(static_dir, static_url) configure_pyscss(self.assets) self.register_assets() def _import_models(self): old_models = set(db.Model._decl_class_registry.items()) import_all_models(self.package_name) added_models = set(db.Model._decl_class_registry.items()) - old_models # Ensure that only plugin schemas have been touched. It would be nice if we could actually # restrict a plugin to plugin_PLUGNNAME but since we load all models from the plugin's package # which could contain more than one plugin this is not easily possible. for name, model in added_models: schema = model.__table__.schema if not schema.startswith('plugin_'): raise Exception("Plugin '{}' added a model which is not in a plugin schema ('{}' in '{}')" .format(self.name, name, schema)) def connect(self, signal, receiver, **connect_kwargs): connect_kwargs['weak'] = False func = wrap_in_plugin_context(self, receiver) func.indico_plugin = self signal.connect(func, **connect_kwargs) def get_blueprints(self): """Return blueprints to be registered on the application A single blueprint can be returned directly, for multiple blueprint you need to yield them or return an iterable. """ pass def get_vars_js(self): """Return a dictionary with variables to be added to vars.js file""" return None @cached_property def translation_path(self): """ Return translation files to be used by the plugin. By default, get <root_path>/translations, unless it does not exist """ translations_path = os.path.join(self.root_path, 'translations') return translations_path if os.path.exists(translations_path) else None @cached_property def translation_domain(self): """Return the domain for this plugin's translation_path""" path = self.translation_path return Domain(path) if path else NullDomain() def register_assets(self): """Add assets to the plugin's webassets environment. In most cases the whole method can consist of calls to :meth:`register_js_bundle` and :meth:`register_css_bundle`. """ pass def register_js_bundle(self, name, *files): """Registers a JS bundle in the plugin's webassets environment""" pretty_name = re.sub(r'_js$', '', name) bundle = Bundle(*files, filters='rjsmin', output='js/{}_%(version)s.min.js'.format(pretty_name)) self.assets.register(name, bundle) def register_css_bundle(self, name, *files): """Registers an SCSS bundle in the plugin's webassets environment""" pretty_name = re.sub(r'_css$', '', name) bundle = Bundle(*files, filters=('pyscss', 'indico_cssrewrite', 'csscompressor'), output='css/{}_%(version)s.min.css'.format(pretty_name), depends=SASS_BASE_MODULES) self.assets.register(name, bundle) def inject_css(self, name, view_class=None, subclasses=True, condition=None): """Injects a CSS bundle into Indico's pages :param name: Name of the bundle :param view_class: If a WP class is specified, only inject it into pages using that class :param subclasses: also inject into subclasses of `view_class` :param condition: a callable to determine whether to inject or not. only called, when the view_class criterion matches """ self._inject_asset(signals.plugin.inject_css, name, view_class, subclasses, condition) def inject_js(self, name, view_class=None, subclasses=True, condition=None): """Injects a JS bundle into Indico's pages :param name: Name of the bundle :param view_class: If a WP class is specified, only inject it into pages using that class :param subclasses: also inject into subclasses of `view_class` :param condition: a callable to determine whether to inject or not. only called, when the view_class criterion matches """ self._inject_asset(signals.plugin.inject_js, name, view_class, subclasses, condition) def _inject_asset(self, signal, name, view_class=None, subclasses=True, condition=None): """Injects an asset bundle into Indico's pages :param signal: the signal to use for injection :param name: Name of the bundle :param view_class: If a WP class is specified, only inject it into pages using that class :param subclasses: also inject into subclasses of `view_class` :param condition: a callable to determine whether to inject or not. only called, when the view_class criterion matches """ def _do_inject(sender): if condition is None or condition(): return self.assets[name].urls() if view_class is None: self.connect(signal, _do_inject) elif not subclasses: self.connect(signal, _do_inject, sender=view_class) else: def _func(sender): if issubclass(sender, view_class): return _do_inject(sender) self.connect(signal, _func) def inject_vars_js(self): """Returns a string that will define variables for the plugin in the vars.js file""" vars_js = self.get_vars_js() if vars_js: return 'var {}Plugin = {};'.format(self.name.title(), json.dumps(vars_js)) def template_hook(self, name, receiver, priority=50, markup=True): """Registers a function to be called when a template hook is invoked. For details see :func:`~indico.web.flask.templating.register_template_hook` """ register_template_hook(name, receiver, priority, markup, self) @classproperty @classmethod def logger(cls): return Logger.get('plugin.{}'.format(cls.name)) @cached_classproperty @classmethod def settings(cls): """:class:`SettingsProxy` for the plugin's settings""" if cls.name is None: raise RuntimeError('Plugin has not been loaded yet') instance = cls.instance with instance.plugin_context(): # in case the default settings come from a property return SettingsProxy('plugin_{}'.format(cls.name), instance.default_settings, cls.strict_settings, acls=cls.acl_settings, converters=cls.settings_converters) @cached_classproperty @classmethod def event_settings(cls): """:class:`EventSettingsProxy` for the plugin's event-specific settings""" if cls.name is None: raise RuntimeError('Plugin has not been loaded yet') instance = cls.instance with instance.plugin_context(): # in case the default settings come from a property return EventSettingsProxy('plugin_{}'.format(cls.name), instance.default_event_settings, cls.strict_settings, acls=cls.acl_event_settings, converters=cls.event_settings_converters) @cached_classproperty @classmethod def user_settings(cls): """:class:`UserSettingsProxy` for the plugin's user-specific settings""" if cls.name is None: raise RuntimeError('Plugin has not been loaded yet') instance = cls.instance with instance.plugin_context(): # in case the default settings come from a property return UserSettingsProxy('plugin_{}'.format(cls.name), instance.default_user_settings, cls.strict_settings, converters=cls.user_settings_converters)