Просмотр исходного кода

Further work to replace Flask-Plugins with pluggy

Peter Justin 7 лет назад
Родитель
Сommit
cae9525557

+ 9 - 6
flaskbb/app.py

@@ -31,7 +31,7 @@ from flaskbb.user.models import User, Guest
 # extensions
 # extensions
 from flaskbb.extensions import (alembic, allows, babel, cache, celery, csrf,
 from flaskbb.extensions import (alembic, allows, babel, cache, celery, csrf,
                                 db, debugtoolbar, limiter, login_manager, mail,
                                 db, debugtoolbar, limiter, login_manager, mail,
-                                plugin_manager, redis_store, themes, whooshee)
+                                redis_store, themes, whooshee)
 
 
 # various helpers
 # various helpers
 from flaskbb.utils.helpers import (time_utcnow, format_date, time_since,
 from flaskbb.utils.helpers import (time_utcnow, format_date, time_since,
@@ -143,9 +143,6 @@ def configure_extensions(app):
     # Flask-WTF CSRF
     # Flask-WTF CSRF
     csrf.init_app(app)
     csrf.init_app(app)
 
 
-    # Flask-Plugins
-    plugin_manager.init_app(app)
-
     # Flask-SQLAlchemy
     # Flask-SQLAlchemy
     db.init_app(app)
     db.init_app(app)
 
 
@@ -381,13 +378,19 @@ def load_plugins(app):
         if not plugin.enabled:
         if not plugin.enabled:
             app.pluggy.set_blocked(plugin.name)
             app.pluggy.set_blocked(plugin.name)
 
 
-    app.pluggy.load_setuptools_entrypoints('flaskbb_plugin')
+    app.pluggy.load_setuptools_entrypoints('flaskbb_plugins')
     app.pluggy.hook.flaskbb_extensions(app=app)
     app.pluggy.hook.flaskbb_extensions(app=app)
 
 
     loaded_names = set([p[0] for p in app.pluggy.list_name_plugin()])
     loaded_names = set([p[0] for p in app.pluggy.list_name_plugin()])
     registered_names = set([p.name for p in plugins])
     registered_names = set([p.name for p in plugins])
-    unregistered = [PluginRegistry(name=name) for name in loaded_names - registered_names]
+    unregistered = [
+        PluginRegistry(name=name) for name in loaded_names - registered_names
+    ]
 
 
     with app.app_context():
     with app.app_context():
         db.session.add_all(unregistered)
         db.session.add_all(unregistered)
         db.session.commit()
         db.session.commit()
+
+    app.logger.debug(
+        "Enabled Plugins: {}".format(app.pluggy.list_name_plugin())
+    )

+ 13 - 93
flaskbb/cli/plugins.py

@@ -8,23 +8,11 @@
     :copyright: (c) 2016 by the FlaskBB Team.
     :copyright: (c) 2016 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
-import sys
-import os
-import shutil
-
+import pluggy
 import click
 import click
 from flask import current_app
 from flask import current_app
-from flask_plugins import (get_all_plugins, get_enabled_plugins,
-                           get_plugin_from_all)
 
 
 from flaskbb.cli.main import flaskbb
 from flaskbb.cli.main import flaskbb
-from flaskbb.cli.utils import check_cookiecutter, validate_plugin
-from flaskbb.extensions import plugin_manager
-
-try:
-    from cookiecutter.main import cookiecutter
-except ImportError:
-    pass
 
 
 
 
 @flaskbb.group()
 @flaskbb.group()
@@ -33,98 +21,30 @@ def plugins():
     pass
     pass
 
 
 
 
-@plugins.command("new")
-@click.argument("plugin_identifier", callback=check_cookiecutter)
-@click.option("--template", "-t", type=click.STRING,
-              default="https://github.com/sh4nks/cookiecutter-flaskbb-plugin",
-              help="Path to a cookiecutter template or to a valid git repo.")
-def new_plugin(plugin_identifier, template):
-    """Creates a new plugin based on the cookiecutter plugin
-    template. Defaults to this template:
-    https://github.com/sh4nks/cookiecutter-flaskbb-plugin.
-    It will either accept a valid path on the filesystem
-    or a URL to a Git repository which contains the cookiecutter template.
-    """
-    out_dir = os.path.join(current_app.root_path, "plugins")
-    click.secho("[+] Creating new plugin {}".format(plugin_identifier),
-                fg="cyan")
-    cookiecutter(template, output_dir=out_dir)
-    click.secho("[+] Done. Created in {}".format(out_dir),
-                fg="green", bold=True)
-
-
-@plugins.command("install")
-@click.argument("plugin_identifier")
-def install_plugin(plugin_identifier):
-    """Installs a new plugin."""
-    validate_plugin(plugin_identifier)
-    plugin = get_plugin_from_all(plugin_identifier)
-    click.secho("[+] Installing plugin {}...".format(plugin.name), fg="cyan")
-    try:
-        plugin_manager.install_plugins([plugin])
-    except Exception as e:
-        click.secho("[-] Couldn't install plugin because of following "
-                    "exception: \n{}".format(e), fg="red")
-
-
-@plugins.command("uninstall")
-@click.argument("plugin_identifier")
-def uninstall_plugin(plugin_identifier):
-    """Uninstalls a plugin from FlaskBB."""
-    validate_plugin(plugin_identifier)
-    plugin = get_plugin_from_all(plugin_identifier)
-    click.secho("[+] Uninstalling plugin {}...".format(plugin.name), fg="cyan")
-    try:
-        plugin_manager.uninstall_plugins([plugin])
-    except AttributeError:
-        pass
-
-
-@plugins.command("remove")
-@click.argument("plugin_identifier")
-@click.option("--force", "-f", default=False, is_flag=True,
-              help="Removes the plugin without asking for confirmation.")
-def remove_plugin(plugin_identifier, force):
-    """Removes a plugin from the filesystem."""
-    validate_plugin(plugin_identifier)
-    if not force and not \
-            click.confirm(click.style("Are you sure?", fg="magenta")):
-        sys.exit(0)
-
-    plugin = get_plugin_from_all(plugin_identifier)
-    click.secho("[+] Uninstalling plugin {}...".format(plugin.name), fg="cyan")
-    try:
-        plugin_manager.uninstall_plugins([plugin])
-    except Exception as e:
-        click.secho("[-] Couldn't uninstall plugin because of following "
-                    "exception: \n{}".format(e), fg="red")
-        if not click.confirm(click.style(
-            "Do you want to continue anyway?", fg="magenta")
-        ):
-            sys.exit(0)
-
-    click.secho("[+] Removing plugin from filesystem...", fg="cyan")
-    shutil.rmtree(plugin.path, ignore_errors=False, onerror=None)
-
-
 @plugins.command("list")
 @plugins.command("list")
 def list_plugins():
 def list_plugins():
     """Lists all installed plugins."""
     """Lists all installed plugins."""
     click.secho("[+] Listing all installed plugins...", fg="cyan")
     click.secho("[+] Listing all installed plugins...", fg="cyan")
 
 
-    # This is subject to change as I am not happy with the current
-    # plugin system
-    enabled_plugins = get_enabled_plugins()
-    disabled_plugins = set(get_all_plugins()) - set(enabled_plugins)
+    enabled_plugins = current_app.pluggy.list_plugin_distinfo()
     if len(enabled_plugins) > 0:
     if len(enabled_plugins) > 0:
         click.secho("[+] Enabled Plugins:", fg="blue", bold=True)
         click.secho("[+] Enabled Plugins:", fg="blue", bold=True)
         for plugin in enabled_plugins:
         for plugin in enabled_plugins:
+            # plugin[0] is the module
+            plugin = plugin[1]
             click.secho("    - {} (version {})".format(
             click.secho("    - {} (version {})".format(
-                plugin.name, plugin.version), bold=True
+                plugin.key, plugin.version), bold=True
             )
             )
+
+    # TODO: is there a better way for doing this?
+    pm = pluggy.PluginManager('flaskbb', implprefix='flaskbb_')
+    pm.load_setuptools_entrypoints('flaskbb_plugins')
+    all_plugins = pm.list_plugin_distinfo()
+    disabled_plugins = set(all_plugins) - set(enabled_plugins)
     if len(disabled_plugins) > 0:
     if len(disabled_plugins) > 0:
         click.secho("[+] Disabled Plugins:", fg="yellow", bold=True)
         click.secho("[+] Disabled Plugins:", fg="yellow", bold=True)
         for plugin in disabled_plugins:
         for plugin in disabled_plugins:
+            plugin = plugin[1]
             click.secho("    - {} (version {})".format(
             click.secho("    - {} (version {})".format(
-                plugin.name, plugin.version), bold=True
+                plugin.key, plugin.version), bold=True
             )
             )

+ 2 - 3
flaskbb/cli/utils.py

@@ -15,11 +15,10 @@ import re
 
 
 import click
 import click
 
 
-from flask import __version__ as flask_version
+from flask import current_app, __version__ as flask_version
 from flask_themes2 import get_theme
 from flask_themes2 import get_theme
 
 
 from flaskbb import __version__
 from flaskbb import __version__
-from flaskbb.extensions import plugin_manager
 from flaskbb.utils.populate import create_user, update_user
 from flaskbb.utils.populate import create_user, update_user
 
 
 
 
@@ -75,7 +74,7 @@ def validate_plugin(plugin):
           the appcontext can't be found and using with_appcontext doesn't
           the appcontext can't be found and using with_appcontext doesn't
           help either.
           help either.
     """
     """
-    if plugin not in plugin_manager.all_plugins.keys():
+    if plugin not in current_app.pluggy.get_plugins():
         raise FlaskBBCLIError("Plugin {} not found.".format(plugin), fg="red")
         raise FlaskBBCLIError("Plugin {} not found.".format(plugin), fg="red")
     return True
     return True
 
 

+ 0 - 4
flaskbb/extensions.py

@@ -19,7 +19,6 @@ from flask_debugtoolbar import DebugToolbarExtension
 from flask_redis import FlaskRedis
 from flask_redis import FlaskRedis
 from flask_alembic import Alembic
 from flask_alembic import Alembic
 from flask_themes2 import Themes
 from flask_themes2 import Themes
-from flask_plugins import PluginManager
 from flask_babelplus import Babel
 from flask_babelplus import Babel
 from flask_wtf.csrf import CSRFProtect
 from flask_wtf.csrf import CSRFProtect
 from flask_limiter import Limiter
 from flask_limiter import Limiter
@@ -57,9 +56,6 @@ alembic = Alembic()
 # Themes
 # Themes
 themes = Themes()
 themes = Themes()
 
 
-# PluginManager
-plugin_manager = PluginManager()
-
 # Babel
 # Babel
 babel = Babel()
 babel = Babel()
 
 

+ 3 - 98
flaskbb/management/models.py

@@ -8,13 +8,10 @@
     :copyright: (c) 2014 by the FlaskBB Team.
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
-from wtforms import (TextField, IntegerField, FloatField, BooleanField,
-                     SelectField, SelectMultipleField, validators)
-from flask_wtf import FlaskForm
-
-from flaskbb._compat import text_type, iteritems
+from flaskbb._compat import iteritems
 from flaskbb.extensions import db, cache
 from flaskbb.extensions import db, cache
 from flaskbb.utils.database import CRUDMixin
 from flaskbb.utils.database import CRUDMixin
+from flaskbb.utils.forms import generate_settings_form
 
 
 
 
 class SettingsGroup(db.Model, CRUDMixin):
 class SettingsGroup(db.Model, CRUDMixin):
@@ -61,99 +58,7 @@ class Setting(db.Model, CRUDMixin):
         :param group: The settingsgroup name. It is used to get the settings
         :param group: The settingsgroup name. It is used to get the settings
                       which are in the specified group.
                       which are in the specified group.
         """
         """
-
-        class SettingsForm(FlaskForm):
-            pass
-
-        # now parse the settings in this group
-        for setting in group.settings:
-            field_validators = []
-
-            if setting.value_type in ("integer", "float"):
-                validator_class = validators.NumberRange
-            elif setting.value_type == "string":
-                validator_class = validators.Length
-
-            # generate the validators
-            if "min" in setting.extra:
-                # Min number validator
-                field_validators.append(
-                    validator_class(min=setting.extra["min"])
-                )
-
-            if "max" in setting.extra:
-                # Max number validator
-                field_validators.append(
-                    validator_class(max=setting.extra["max"])
-                )
-
-            # Generate the fields based on value_type
-            # IntegerField
-            if setting.value_type == "integer":
-                setattr(
-                    SettingsForm, setting.key,
-                    IntegerField(setting.name, validators=field_validators,
-                                 description=setting.description)
-                )
-            # FloatField
-            elif setting.value_type == "float":
-                setattr(
-                    SettingsForm, setting.key,
-                    FloatField(setting.name, validators=field_validators,
-                               description=setting.description)
-                )
-
-            # TextField
-            elif setting.value_type == "string":
-                setattr(
-                    SettingsForm, setting.key,
-                    TextField(setting.name, validators=field_validators,
-                              description=setting.description)
-                )
-
-            # SelectMultipleField
-            elif setting.value_type == "selectmultiple":
-                # if no coerce is found, it will fallback to unicode
-                if "coerce" in setting.extra:
-                    coerce_to = setting.extra['coerce']
-                else:
-                    coerce_to = text_type
-
-                setattr(
-                    SettingsForm, setting.key,
-                    SelectMultipleField(
-                        setting.name,
-                        choices=setting.extra['choices'](),
-                        coerce=coerce_to,
-                        description=setting.description
-                    )
-                )
-
-            # SelectField
-            elif setting.value_type == "select":
-                # if no coerce is found, it will fallback to unicode
-                if "coerce" in setting.extra:
-                    coerce_to = setting.extra['coerce']
-                else:
-                    coerce_to = text_type
-
-                setattr(
-                    SettingsForm, setting.key,
-                    SelectField(
-                        setting.name,
-                        coerce=coerce_to,
-                        choices=setting.extra['choices'](),
-                        description=setting.description)
-                )
-
-            # BooleanField
-            elif setting.value_type == "boolean":
-                setattr(
-                    SettingsForm, setting.key,
-                    BooleanField(setting.name, description=setting.description)
-                )
-
-        return SettingsForm
+        return generate_settings_form(settings=group.settings)
 
 
     @classmethod
     @classmethod
     def get_all(cls):
     def get_all(cls):

+ 63 - 48
flaskbb/management/views.py

@@ -14,7 +14,6 @@ from celery import __version__ as celery_version
 from flask import (Blueprint, current_app, request, redirect, url_for, flash,
 from flask import (Blueprint, current_app, request, redirect, url_for, flash,
                    jsonify, __version__ as flask_version)
                    jsonify, __version__ as flask_version)
 from flask_login import current_user, login_fresh
 from flask_login import current_user, login_fresh
-from flask_plugins import get_all_plugins, get_plugin, get_plugin_from_all
 from flask_babelplus import gettext as _
 from flask_babelplus import gettext as _
 from flask_allows import Permission, Not
 from flask_allows import Permission, Not
 
 
@@ -28,6 +27,7 @@ from flaskbb.utils.requirements import (IsAtleastModerator, IsAdmin,
 from flaskbb.extensions import db, allows, celery
 from flaskbb.extensions import db, allows, celery
 from flaskbb.utils.helpers import (render_template, time_diff, time_utcnow,
 from flaskbb.utils.helpers import (render_template, time_diff, time_utcnow,
                                    get_online_users)
                                    get_online_users)
+from flaskbb.plugins.models import PluginRegistry, PluginStore
 from flaskbb.user.models import Guest, User, Group
 from flaskbb.user.models import Guest, User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.management.models import Setting, SettingsGroup
@@ -96,7 +96,7 @@ def overview():
         "flask_version": flask_version,
         "flask_version": flask_version,
         "flaskbb_version": flaskbb_version,
         "flaskbb_version": flaskbb_version,
         # plugins
         # plugins
-        "plugins": get_all_plugins()
+        "plugins": []
     }
     }
 
 
     return render_template("management/overview.html", **stats)
     return render_template("management/overview.html", **stats)
@@ -703,78 +703,93 @@ def delete_category(category_id):
 @management.route("/plugins")
 @management.route("/plugins")
 @allows.requires(IsAdmin)
 @allows.requires(IsAdmin)
 def plugins():
 def plugins():
-    plugins = get_all_plugins()
+    from flaskbb.utils.helpers import parse_pkginfo
+    plugins_distinfo = current_app.pluggy.list_plugin_distinfo()
+
+    plugins = {}
+    # XXX: Mapping from PKG-INFO to something more readable?
+    for plugin in plugins_distinfo:
+        name = current_app.pluggy.get_name(plugin[0])
+        plugins[name] = parse_pkginfo(plugin[1].key)
+
+    return render_template("management/plugins.html", plugins=plugins)
+
+
+@management.route("/plugins/<path:pluginname>/settings")
+@allows.requires(IsAdmin)
+def plugin_settings():
+    plugins = current_app.pluggy.get_plugins()
     return render_template("management/plugins.html", plugins=plugins)
     return render_template("management/plugins.html", plugins=plugins)
 
 
 
 
-@management.route("/plugins/<path:plugin>/enable", methods=["POST"])
+@management.route("/plugins/<path:pluginname>/enable", methods=["POST"])
 @allows.requires(IsAdmin)
 @allows.requires(IsAdmin)
-def enable_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
+def enable_plugin(pluginname):
+    plugin_module = current_app.pluggy.get_plugin(pluginname)
+    if plugin_module is None:
+        flash(_("Plugin %(plugin)s not found.", plugin=pluginname), "error")
+        return redirect(url_for("management.plugins"))
+
+    plugin = PluginRegistry.query.filter_by(name=pluginname).first_or_404()
 
 
     if plugin.enabled:
     if plugin.enabled:
         flash(_("Plugin %(plugin)s is already enabled.", plugin=plugin.name),
         flash(_("Plugin %(plugin)s is already enabled.", plugin=plugin.name),
               "info")
               "info")
         return redirect(url_for("management.plugins"))
         return redirect(url_for("management.plugins"))
 
 
-    try:
-        plugin.enable()
-        flash(_("Plugin %(plugin)s enabled. Please restart FlaskBB now.",
-                plugin=plugin.name), "success")
-    except OSError:
-        flash(_("It seems that FlaskBB does not have enough filesystem "
-                "permissions. Try removing the 'DISABLED' file by "
-                "yourself instead."), "danger")
+    plugin.enabled = True
+    plugin.save()
 
 
+    flash(_("Plugin %(plugin)s enabled. Please restart FlaskBB now.",
+            plugin=plugin.name), "success")
     return redirect(url_for("management.plugins"))
     return redirect(url_for("management.plugins"))
 
 
 
 
-@management.route("/plugins/<path:plugin>/disable", methods=["POST"])
+@management.route("/plugins/<path:pluginname>/disable", methods=["POST"])
 @allows.requires(IsAdmin)
 @allows.requires(IsAdmin)
-def disable_plugin(plugin):
-    try:
-        plugin = get_plugin(plugin)
-    except KeyError:
-        flash(_("Plugin %(plugin)s not found.", plugin=plugin.name), "danger")
+def disable_plugin(pluginname):
+    plugin_module = current_app.pluggy.get_plugin(pluginname)
+    if plugin_module is None:
+        flash(_("Plugin %(plugin)s not found.", plugin=pluginname), "danger")
         return redirect(url_for("management.plugins"))
         return redirect(url_for("management.plugins"))
+    plugin = PluginRegistry.query.filter_by(name=pluginname).first_or_404()
 
 
-    try:
-        plugin.disable()
-        flash(_("Plugin %(plugin)s disabled. Please restart FlaskBB now.",
-                plugin=plugin.name), "success")
-    except OSError:
-        flash(_("It seems that FlaskBB does not have enough filesystem "
-                "permissions. Try creating the 'DISABLED' file by "
-                "yourself instead."), "danger")
+    if not plugin.enabled:
+        flash(_("Plugin %(plugin)s is already disabled.", plugin=plugin.name),
+              "info")
+        return redirect(url_for("management.plugins"))
+
+    plugin.enabled = False
+    plugin.save()
+    flash(_("Plugin %(plugin)s disabled. Please restart FlaskBB now.",
+            plugin=plugin.name), "success")
 
 
     return redirect(url_for("management.plugins"))
     return redirect(url_for("management.plugins"))
 
 
 
 
-@management.route("/plugins/<path:plugin>/uninstall", methods=["POST"])
+@management.route("/plugins/<path:pluginname>/uninstall", methods=["POST"])
 @allows.requires(IsAdmin)
 @allows.requires(IsAdmin)
-def uninstall_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
-    if plugin.installed:
-        plugin.uninstall()
-        Setting.invalidate_cache()
-
-        flash(_("Plugin has been uninstalled."), "success")
-    else:
-        flash(_("Cannot uninstall plugin."), "danger")
+def uninstall_plugin(pluginname):
+    plugin_module = current_app.pluggy.get_plugin(pluginname)
+    if plugin_module is None:
+        flash(_("Plugin %(plugin)s not found.", plugin=pluginname), "danger")
+        return redirect(url_for("management.plugins"))
+    plugin = PluginRegistry.query.filter_by(name=pluginname).first_or_404()
 
 
+    plugin.delete()
+    flash(_("Plugin has been uninstalled."), "success")
     return redirect(url_for("management.plugins"))
     return redirect(url_for("management.plugins"))
 
 
 
 
-@management.route("/plugins/<path:plugin>/install", methods=["POST"])
+@management.route("/plugins/<path:pluginname>/install", methods=["POST"])
 @allows.requires(IsAdmin)
 @allows.requires(IsAdmin)
-def install_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
-    if not plugin.installed:
-        plugin.install()
-        Setting.invalidate_cache()
-
-        flash(_("Plugin has been installed."), "success")
-    else:
-        flash(_("Cannot install plugin."), "danger")
+def install_plugin(pluginname):
+    plugin_module = current_app.pluggy.get_plugin(pluginname)
+    if plugin_module is None:
+        flash(_("Plugin %(plugin)s not found.", plugin=pluginname), "danger")
+        return redirect(url_for("management.plugins"))
+    plugin = PluginRegistry.query.filter_by(name=pluginname).first_or_404()
 
 
+    plugin.add_settings(plugin_module.SETTINGS)
+    flash(_("Plugin has been installed."), "success")
     return redirect(url_for("management.plugins"))
     return redirect(url_for("management.plugins"))

+ 0 - 61
flaskbb/plugins/__init__.py

@@ -8,64 +8,3 @@
     :copyright: (c) 2014 by the FlaskBB Team.
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
-import warnings
-from flask import current_app
-from flask_plugins import Plugin
-
-from flaskbb.management.models import SettingsGroup
-
-
-class FlaskBBPluginDeprecationWarning(DeprecationWarning):
-    pass
-
-
-warnings.simplefilter("always", FlaskBBPluginDeprecationWarning)
-
-
-class FlaskBBPlugin(Plugin):
-    #: This is the :class:`SettingsGroup` key - if your the plugin needs to
-    #: install additional things you must set it, else it won't install
-    #: anything.
-    settings_key = None
-
-    @property
-    def has_settings(self):
-        """Is ``True`` if the Plugin **can** be installed."""
-        if self.settings_key is not None:
-            return True
-        return False
-
-    @property
-    def installed(self):
-        is_installed = False
-        if self.has_settings:
-            group = SettingsGroup.query.\
-                filter_by(key=self.settings_key).\
-                first()
-            is_installed = group and len(group.settings.all()) > 0
-        return is_installed
-
-    @property
-    def uninstallable(self):
-        """Is ``True`` if the Plugin **can** be uninstalled."""
-        warnings.warn(
-            "self.uninstallable is deprecated. Use self.installed instead.",
-            FlaskBBPluginDeprecationWarning
-        )
-        return self.installed
-
-    @property
-    def installable(self):
-        warnings.warn(
-            "self.installable is deprecated. Use self.has_settings instead.",
-            FlaskBBPluginDeprecationWarning
-        )
-        return self.has_settings
-
-    # Some helpers
-    def register_blueprint(self, blueprint, **kwargs):
-        """Registers a blueprint.
-
-        :param blueprint: The blueprint which should be registered.
-        """
-        current_app.register_blueprint(blueprint, **kwargs)

+ 39 - 11
flaskbb/plugins/models.py

@@ -3,7 +3,8 @@
     flaskbb.plugins.models
     flaskbb.plugins.models
     ~~~~~~~~~~~~~~~~~~~~~~~
     ~~~~~~~~~~~~~~~~~~~~~~~
 
 
-    This module provides registration and a basic DB backed key-value store for plugins.
+    This module provides registration and a basic DB backed key-value
+    store for plugins.
 
 
     :copyright: (c) 2017 by the FlaskBB Team.
     :copyright: (c) 2017 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
@@ -13,35 +14,62 @@ from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.orm.collections import attribute_mapped_collection
 from sqlalchemy.orm.collections import attribute_mapped_collection
 
 
 from flaskbb.extensions import db
 from flaskbb.extensions import db
+from flaskbb.utils.database import CRUDMixin
 
 
 
 
-class PluginStore(db.Model):
+class PluginStore(CRUDMixin, db.Model):
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
-    name = db.Column(db.Unicode(255), nullable=False)
-    value = db.Column(db.Unicode(255))
+    key = db.Column(db.Unicode(255), nullable=False)
+    value = db.Column(db.PickleType, nullable=False)
+    # Enum?
+    # Available types: string, integer, float, boolean, select, selectmultiple
+    value_type = db.Column(db.Unicode(20), nullable=False)
+    # Extra attributes like, validation things (min, max length...)
+    # For Select*Fields required: choices
+    extra = db.Column(db.PickleType, nullable=True)
     plugin_id = db.Column(db.Integer, db.ForeignKey('plugin_registry.id'))
     plugin_id = db.Column(db.Integer, db.ForeignKey('plugin_registry.id'))
 
 
-    __table_args__ = (UniqueConstraint('name', 'plugin_id', name='plugin_kv_uniq'), )
+    # Display stuff
+    name = db.Column(db.Unicode(255), nullable=False)
+    description = db.Column(db.Text, nullable=True)
+
+    __table_args__ = (UniqueConstraint('key', 'plugin_id', name='plugin_kv_uniq'), )
 
 
     def __repr__(self):
     def __repr__(self):
-        return '<FlaskBBPluginSetting plugin={} name={} value={}>'.format(
-            self.plugin.name, self.name, self.value
+        return '<PluginSetting plugin={} key={} value={}>'.format(
+            self.plugin.name, self.key, self.value
         )
         )
 
 
 
 
-class PluginRegistry(db.Model):
+class PluginRegistry(CRUDMixin, db.Model):
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
     name = db.Column(db.Unicode(255), unique=True, nullable=False)
     name = db.Column(db.Unicode(255), unique=True, nullable=False)
     enabled = db.Column(db.Boolean, default=True)
     enabled = db.Column(db.Boolean, default=True)
     values = db.relationship(
     values = db.relationship(
         'PluginStore',
         'PluginStore',
-        collection_class=attribute_mapped_collection('name'),
+        collection_class=attribute_mapped_collection('key'),
         cascade='all, delete-orphan',
         cascade='all, delete-orphan',
         backref='plugin'
         backref='plugin'
     )
     )
     settings = association_proxy(
     settings = association_proxy(
-        'values', 'value', creator=lambda k, v: PluginStore(name=k, value=v)
+        'values', 'value', creator=lambda k, v: PluginStore(key=k, value=v)
     )
     )
 
 
+    def add_settings(self, settings):
+        plugin_settings = []
+        for key in settings:
+            pluginstore = PluginStore()
+            pluginstore.key = key
+            pluginstore.value = settings[key]['value']
+            pluginstore.value_type = settings[key]['value_type']
+            pluginstore.extra = settings[key]['extra']
+            pluginstore.name = settings[key]['name']
+            pluginstore.description = settings[key]['description']
+            pluginstore.plugin = self
+            plugin_settings.append(pluginstore)
+
+        db.session.add_all(plugin_settings)
+        db.session.commit()
+
     def __repr__(self):
     def __repr__(self):
-        return '<FlaskBBPlugin name={} enabled={}>'.format(self.name, self.enabled)
+        return '<Plugin name={} enabled={}>'.format(self.name, self.enabled)

+ 0 - 2
flaskbb/templates/layout.html

@@ -70,13 +70,11 @@
                         <!-- navbar left -->
                         <!-- navbar left -->
                         <ul class="nav navbar-nav forum-nav">
                         <ul class="nav navbar-nav forum-nav">
                             {%- from theme("macros.html") import is_active, topnav with context -%}
                             {%- from theme("macros.html") import is_active, topnav with context -%}
-                            {{ emit_event("before-first-navigation-element") }}
 
 
                             {{ topnav(endpoint='forum.index', name=_('Forum'), icon='fa fa-comment', active=active_forum_nav) }}
                             {{ topnav(endpoint='forum.index', name=_('Forum'), icon='fa fa-comment', active=active_forum_nav) }}
                             {{ topnav(endpoint='forum.memberlist', name=_('Memberlist'), icon='fa fa-user') }}
                             {{ topnav(endpoint='forum.memberlist', name=_('Memberlist'), icon='fa fa-user') }}
                             {{ topnav(endpoint='forum.search', name=_('Search'), icon='fa fa-search') }}
                             {{ topnav(endpoint='forum.search', name=_('Search'), icon='fa fa-search') }}
 
 
-                            {{ emit_event("after-last-navigation-element") }}
                         </ul>
                         </ul>
 
 
                         <!-- navbar right -->
                         <!-- navbar right -->

+ 14 - 28
flaskbb/templates/management/plugins.html

@@ -27,55 +27,41 @@
                         <div class="col-md-4 col-sm-4 col-xs-4 meta-item">{% trans %}Manage{% endtrans %}</div>
                         <div class="col-md-4 col-sm-4 col-xs-4 meta-item">{% trans %}Manage{% endtrans %}</div>
                     </div>
                     </div>
                 </div>
                 </div>
-                {% for plugin in plugins %}
+                {% for pluginname, plugininfo in plugins.items() %}
                 <div class="row settings-row hover with-border-bottom">
                 <div class="row settings-row hover with-border-bottom">
                     <div class="col-md-4 col-sm-4 col-xs-4">
                     <div class="col-md-4 col-sm-4 col-xs-4">
-                    {% if plugin.website %}
-                      <a href="{{ plugin.website }}">{{ plugin.name }}</a>
+                    {% if plugininfo['Home-page'] %}
+                      <a href="{{ plugininfo['Home-page'] }}">{{ pluginname.title() }}</a>
                     {% else %}
                     {% else %}
-                      {{ plugin.name }}
+                      {{ pluginname.title() }}
                     {% endif %}
                     {% endif %}
-                    <div class="pull-right">
-                    {% if not plugin.enabled %}
-                        <div class="label label-danger">not enabled</div>
-                    {% elif plugin.enabled and plugin.installed %}
-                        <div class="label label-success">enabled &amp; installed</div>
-                    {% elif plugin.enabled and not plugin.installed %}
-                        <div class="label label-warning">not installed</div>
-                    {% endif %}
-                    </div>
                     </div>
                     </div>
                     <div class="col-md-4 col-sm-4 col-xs-4">
                     <div class="col-md-4 col-sm-4 col-xs-4">
-                        <div class="plugin-version">{% trans %}Version{% endtrans %}: {{ plugin.version }}</div>
-                        <div class="plugin-description">{{ plugin.description }}</div>
-                        <div class="plugin-author">{% trans %}by{% endtrans %} {{ plugin.author }}</div>
+                        <div class="plugin-version">{% trans %}Version{% endtrans %}: {{ plugininfo['Version'] }}</div>
+                        <div class="plugin-description">{{ plugininfo['Description'] }}</div>
+                        <div class="plugin-author">{% trans %}by{% endtrans %} {{ plugininfo['Author'] }}</div>
                     </div>
                     </div>
                     <div class="col-md-4 col-sm-4 col-xs-4">
                     <div class="col-md-4 col-sm-4 col-xs-4">
-                        {% if not plugin.enabled %}
-                        <form class="inline-form" method="post" action="{{ url_for('management.enable_plugin', plugin=plugin.identifier) }}">
+                        <form class="inline-form" method="post" action="{{ url_for('management.enable_plugin', pluginname=pluginname) }}">
                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
                             <button class="btn btn-success">{% trans %}Enable{% endtrans %}</button>
                             <button class="btn btn-success">{% trans %}Enable{% endtrans %}</button>
                         </form>
                         </form>
-                        {% else %}
-                        <form class="inline-form" method="post" action="{{ url_for('management.disable_plugin', plugin=plugin.identifier) }}">
+                        <form class="inline-form" method="post" action="{{ url_for('management.disable_plugin', pluginname=pluginname) }}">
                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
                             <button class="btn btn-warning">{% trans %}Disable{% endtrans %}</button>
                             <button class="btn btn-warning">{% trans %}Disable{% endtrans %}</button>
                         </form>
                         </form>
-                        {% endif %}
 
 
-                        {% if not plugin.installed %}
-                        <form class="inline-form" method="post" action="{{ url_for('management.install_plugin', plugin=plugin.identifier) }}">
+                        <form class="inline-form" method="post" action="{{ url_for('management.install_plugin', pluginname=pluginname) }}">
                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
                             <button class="btn btn-info">{% trans %}Install{% endtrans %}</button>
                             <button class="btn btn-info">{% trans %}Install{% endtrans %}</button>
                         </form>
                         </form>
-                        {% endif %}
-                        {% if plugin.installed %}
-                        <form class="inline-form" method="post" action="{{ url_for('management.uninstall_plugin', plugin=plugin.identifier) }}">
+
+                        <form class="inline-form" method="post" action="{{ url_for('management.uninstall_plugin', pluginname=pluginname) }}">
                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
                             <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
                             <button class="btn btn-danger">{% trans %}Uninstall{% endtrans %}</button>
                             <button class="btn btn-danger">{% trans %}Uninstall{% endtrans %}</button>
                         </form>
                         </form>
-                        <a class="btn btn-info" href="{{ url_for('management.settings', slug=plugin.settings_key) }}">Settings</a>
-                        {% endif %}
+                        <a class="btn btn-info" href="{{ url_for('management.plugin_settings', pluginname=pluginname) }}">Settings</a>
+                    </div>
                     </div>
                     </div>
                 </div>
                 </div>
                 {% endfor %}
                 {% endfor %}

+ 111 - 0
flaskbb/utils/forms.py

@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.utils.forms
+    ~~~~~~~~~~~~~~~~~~~
+
+    This module contains stuff for forms.
+
+    :copyright: (c) 2017 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+from wtforms import (TextField, IntegerField, FloatField, BooleanField,
+                     SelectField, SelectMultipleField, validators)
+from flask_wtf import FlaskForm
+from flaskbb._compat import text_type
+
+
+def generate_settings_form(settings):
+    """Generates a settings form which includes field validation
+    based on our Setting Schema."""
+    class SettingsForm(FlaskForm):
+        pass
+
+    # now parse the settings in this group
+    for setting in settings:
+        field_validators = []
+
+        if setting.value_type in ("integer", "float"):
+            validator_class = validators.NumberRange
+        elif setting.value_type == "string":
+            validator_class = validators.Length
+
+        # generate the validators
+        if "min" in setting.extra:
+            # Min number validator
+            field_validators.append(
+                validator_class(min=setting.extra["min"])
+            )
+
+        if "max" in setting.extra:
+            # Max number validator
+            field_validators.append(
+                validator_class(max=setting.extra["max"])
+            )
+
+        # Generate the fields based on value_type
+        # IntegerField
+        if setting.value_type == "integer":
+            setattr(
+                SettingsForm, setting.key,
+                IntegerField(setting.name, validators=field_validators,
+                             description=setting.description)
+            )
+        # FloatField
+        elif setting.value_type == "float":
+            setattr(
+                SettingsForm, setting.key,
+                FloatField(setting.name, validators=field_validators,
+                           description=setting.description)
+            )
+
+        # TextField
+        elif setting.value_type == "string":
+            setattr(
+                SettingsForm, setting.key,
+                TextField(setting.name, validators=field_validators,
+                          description=setting.description)
+            )
+
+        # SelectMultipleField
+        elif setting.value_type == "selectmultiple":
+            # if no coerce is found, it will fallback to unicode
+            if "coerce" in setting.extra:
+                coerce_to = setting.extra['coerce']
+            else:
+                coerce_to = text_type
+
+            setattr(
+                SettingsForm, setting.key,
+                SelectMultipleField(
+                    setting.name,
+                    choices=setting.extra['choices'](),
+                    coerce=coerce_to,
+                    description=setting.description
+                )
+            )
+
+        # SelectField
+        elif setting.value_type == "select":
+            # if no coerce is found, it will fallback to unicode
+            if "coerce" in setting.extra:
+                coerce_to = setting.extra['coerce']
+            else:
+                coerce_to = text_type
+
+            setattr(
+                SettingsForm, setting.key,
+                SelectField(
+                    setting.name,
+                    coerce=coerce_to,
+                    choices=setting.extra['choices'](),
+                    description=setting.description)
+            )
+
+        # BooleanField
+        elif setting.value_type == "boolean":
+            setattr(
+                SettingsForm, setting.key,
+                BooleanField(setting.name, description=setting.description)
+            )
+
+    return SettingsForm

+ 15 - 2
flaskbb/utils/helpers.py

@@ -16,6 +16,8 @@ import operator
 import os
 import os
 import glob
 import glob
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
+from pkg_resources import get_distribution
+from email import message_from_string
 from pytz import UTC
 from pytz import UTC
 from PIL import ImageFile
 from PIL import ImageFile
 
 
@@ -619,9 +621,20 @@ class ReverseProxyPathFix(object):
 
 
 
 
 def real(obj):
 def real(obj):
-    """
-    Unwraps a werkzeug.local.LocalProxy object if given one, else returns the object
+    """Unwraps a werkzeug.local.LocalProxy object if given one,
+    else returns the object.
     """
     """
     if isinstance(obj, LocalProxy):
     if isinstance(obj, LocalProxy):
         return obj._get_current_object()
         return obj._get_current_object()
     return obj
     return obj
+
+
+def parse_pkginfo(plugin_dist_name):
+    raw_metadata = get_distribution(plugin_dist_name).get_metadata('PKG-INFO')
+    metadata = {}
+
+    # lets use the Parser from email to parse our metadata :)
+    for key, value in message_from_string(raw_metadata).items():
+        metadata[key] = value
+
+    return metadata

+ 2 - 5
flaskbb/utils/translations.py

@@ -15,9 +15,6 @@ import babel
 from flask import current_app
 from flask import current_app
 
 
 from flask_babelplus import Domain, get_locale
 from flask_babelplus import Domain, get_locale
-from flask_plugins import get_enabled_plugins
-
-from flaskbb.extensions import plugin_manager
 
 
 
 
 class FlaskBBDomain(Domain):
 class FlaskBBDomain(Domain):
@@ -38,7 +35,7 @@ class FlaskBBDomain(Domain):
         with self.app.app_context():
         with self.app.app_context():
             self.plugin_translations = [
             self.plugin_translations = [
                 os.path.join(plugin.path, "translations")
                 os.path.join(plugin.path, "translations")
-                for plugin in get_enabled_plugins()
+                for plugin in self.app.pluggy.get_plugins()
                 if os.path.exists(os.path.join(plugin.path, "translations"))
                 if os.path.exists(os.path.join(plugin.path, "translations"))
             ]
             ]
 
 
@@ -94,7 +91,7 @@ def update_translations(include_plugins=False):
                      "-d", translations_folder])
                      "-d", translations_folder])
 
 
     if include_plugins:
     if include_plugins:
-        for plugin in plugin_manager.all_plugins:
+        for plugin in current_app.pluggy.get_plugins():
             update_plugin_translations(plugin)
             update_plugin_translations(plugin)
 
 
 
 

+ 9 - 4
migrations/7c3fcf8a3335_add_plugin_tables.py

@@ -1,7 +1,7 @@
 """Add plugin tables
 """Add plugin tables
 
 
 Revision ID: 7c3fcf8a3335
 Revision ID: 7c3fcf8a3335
-Revises: 
+Revises:
 Create Date: 2017-08-12 12:41:04.725309
 Create Date: 2017-08-12 12:41:04.725309
 
 
 """
 """
@@ -25,14 +25,19 @@ def upgrade():
     sa.PrimaryKeyConstraint('id'),
     sa.PrimaryKeyConstraint('id'),
     sa.UniqueConstraint('name')
     sa.UniqueConstraint('name')
     )
     )
+
     op.create_table('plugin_store',
     op.create_table('plugin_store',
     sa.Column('id', sa.Integer(), nullable=False),
     sa.Column('id', sa.Integer(), nullable=False),
-    sa.Column('name', sa.Unicode(length=255), nullable=False),
-    sa.Column('value', sa.Unicode(length=255), nullable=True),
+    sa.Column('key', sa.Unicode(length=255), nullable=False),
+    sa.Column('value', sa.PickleType(), nullable=True),
+    sa.Column('value_type', sa.Unicode(length=20), nullable=False),
+    sa.Column('extra', sa.PickleType(), nullable=False),
     sa.Column('plugin_id', sa.Integer(), nullable=True),
     sa.Column('plugin_id', sa.Integer(), nullable=True),
+    sa.Column('name', sa.Unicode(length=255), nullable=False),
+    sa.Column('description', sa.Text(), nullable=False),
     sa.ForeignKeyConstraint(['plugin_id'], ['plugin_registry.id'], ),
     sa.ForeignKeyConstraint(['plugin_id'], ['plugin_registry.id'], ),
     sa.PrimaryKeyConstraint('id'),
     sa.PrimaryKeyConstraint('id'),
-    sa.UniqueConstraint('name', 'plugin_id', name='plugin_kv_uniq')
+    sa.UniqueConstraint('key', 'plugin_id', name='plugin_kv_uniq')
     )
     )
     # ### end Alembic commands ###
     # ### end Alembic commands ###
 
 

+ 1 - 2
requirements.txt

@@ -14,7 +14,6 @@ Flask-DebugToolbar==0.10.1
 Flask-Limiter==0.9.3
 Flask-Limiter==0.9.3
 Flask-Login==0.4.0
 Flask-Login==0.4.0
 Flask-Mail==0.9.1
 Flask-Mail==0.9.1
-Flask-Plugins==1.6.1
 Flask-Redis==0.3.0
 Flask-Redis==0.3.0
 Flask-Script==2.0.5
 Flask-Script==2.0.5
 Flask-SQLAlchemy==2.2
 Flask-SQLAlchemy==2.2
@@ -30,7 +29,7 @@ MarkupSafe==1.0
 mistune==0.7.4
 mistune==0.7.4
 olefile==0.44
 olefile==0.44
 Pillow==4.0.0
 Pillow==4.0.0
-pluggy==0.4.0
+pluggy==0.5.1
 Pygments==2.2.0
 Pygments==2.2.0
 python-editor==1.0.3
 python-editor==1.0.3
 pytz==2017.2
 pytz==2017.2

+ 0 - 1
setup.py

@@ -77,7 +77,6 @@ setup(
         'Flask-Limiter',
         'Flask-Limiter',
         'Flask-Login',
         'Flask-Login',
         'Flask-Mail',
         'Flask-Mail',
-        'Flask-Plugins',
         'Flask-Redis',
         'Flask-Redis',
         'Flask-SQLAlchemy',
         'Flask-SQLAlchemy',
         'Flask-Themes2',
         'Flask-Themes2',