Browse Source

Further work to replace Flask-Plugins with pluggy

Peter Justin 7 years ago
parent
commit
cae9525557

+ 9 - 6
flaskbb/app.py

@@ -31,7 +31,7 @@ from flaskbb.user.models import User, Guest
 # extensions
 from flaskbb.extensions import (alembic, allows, babel, cache, celery, csrf,
                                 db, debugtoolbar, limiter, login_manager, mail,
-                                plugin_manager, redis_store, themes, whooshee)
+                                redis_store, themes, whooshee)
 
 # various helpers
 from flaskbb.utils.helpers import (time_utcnow, format_date, time_since,
@@ -143,9 +143,6 @@ def configure_extensions(app):
     # Flask-WTF CSRF
     csrf.init_app(app)
 
-    # Flask-Plugins
-    plugin_manager.init_app(app)
-
     # Flask-SQLAlchemy
     db.init_app(app)
 
@@ -381,13 +378,19 @@ def load_plugins(app):
         if not plugin.enabled:
             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)
 
     loaded_names = set([p[0] for p in app.pluggy.list_name_plugin()])
     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():
         db.session.add_all(unregistered)
         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.
     :license: BSD, see LICENSE for more details.
 """
-import sys
-import os
-import shutil
-
+import pluggy
 import click
 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.utils import check_cookiecutter, validate_plugin
-from flaskbb.extensions import plugin_manager
-
-try:
-    from cookiecutter.main import cookiecutter
-except ImportError:
-    pass
 
 
 @flaskbb.group()
@@ -33,98 +21,30 @@ def plugins():
     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")
 def list_plugins():
     """Lists all installed plugins."""
     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:
         click.secho("[+] Enabled Plugins:", fg="blue", bold=True)
         for plugin in enabled_plugins:
+            # plugin[0] is the module
+            plugin = plugin[1]
             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:
         click.secho("[+] Disabled Plugins:", fg="yellow", bold=True)
         for plugin in disabled_plugins:
+            plugin = plugin[1]
             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
 
-from flask import __version__ as flask_version
+from flask import current_app, __version__ as flask_version
 from flask_themes2 import get_theme
 
 from flaskbb import __version__
-from flaskbb.extensions import plugin_manager
 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
           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")
     return True
 

+ 0 - 4
flaskbb/extensions.py

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

+ 3 - 98
flaskbb/management/models.py

@@ -8,13 +8,10 @@
     :copyright: (c) 2014 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, iteritems
+from flaskbb._compat import iteritems
 from flaskbb.extensions import db, cache
 from flaskbb.utils.database import CRUDMixin
+from flaskbb.utils.forms import generate_settings_form
 
 
 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
                       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
     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,
                    jsonify, __version__ as flask_version)
 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_allows import Permission, Not
 
@@ -28,6 +27,7 @@ from flaskbb.utils.requirements import (IsAtleastModerator, IsAdmin,
 from flaskbb.extensions import db, allows, celery
 from flaskbb.utils.helpers import (render_template, time_diff, time_utcnow,
                                    get_online_users)
+from flaskbb.plugins.models import PluginRegistry, PluginStore
 from flaskbb.user.models import Guest, User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 from flaskbb.management.models import Setting, SettingsGroup
@@ -96,7 +96,7 @@ def overview():
         "flask_version": flask_version,
         "flaskbb_version": flaskbb_version,
         # plugins
-        "plugins": get_all_plugins()
+        "plugins": []
     }
 
     return render_template("management/overview.html", **stats)
@@ -703,78 +703,93 @@ def delete_category(category_id):
 @management.route("/plugins")
 @allows.requires(IsAdmin)
 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)
 
 
-@management.route("/plugins/<path:plugin>/enable", methods=["POST"])
+@management.route("/plugins/<path:pluginname>/enable", methods=["POST"])
 @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:
         flash(_("Plugin %(plugin)s is already enabled.", plugin=plugin.name),
               "info")
         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"))
 
 
-@management.route("/plugins/<path:plugin>/disable", methods=["POST"])
+@management.route("/plugins/<path:pluginname>/disable", methods=["POST"])
 @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"))
+    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"))
 
 
-@management.route("/plugins/<path:plugin>/uninstall", methods=["POST"])
+@management.route("/plugins/<path:pluginname>/uninstall", methods=["POST"])
 @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"))
 
 
-@management.route("/plugins/<path:plugin>/install", methods=["POST"])
+@management.route("/plugins/<path:pluginname>/install", methods=["POST"])
 @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"))

+ 0 - 61
flaskbb/plugins/__init__.py

@@ -8,64 +8,3 @@
     :copyright: (c) 2014 by the FlaskBB Team.
     :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
     ~~~~~~~~~~~~~~~~~~~~~~~
 
-    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.
     :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 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)
-    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'))
 
-    __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):
-        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)
     name = db.Column(db.Unicode(255), unique=True, nullable=False)
     enabled = db.Column(db.Boolean, default=True)
     values = db.relationship(
         'PluginStore',
-        collection_class=attribute_mapped_collection('name'),
+        collection_class=attribute_mapped_collection('key'),
         cascade='all, delete-orphan',
         backref='plugin'
     )
     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):
-        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 -->
                         <ul class="nav navbar-nav forum-nav">
                             {%- 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.memberlist', name=_('Memberlist'), icon='fa fa-user') }}
                             {{ topnav(endpoint='forum.search', name=_('Search'), icon='fa fa-search') }}
 
-                            {{ emit_event("after-last-navigation-element") }}
                         </ul>
 
                         <!-- 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>
                 </div>
-                {% for plugin in plugins %}
+                {% for pluginname, plugininfo in plugins.items() %}
                 <div class="row settings-row hover with-border-bottom">
                     <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 %}
-                      {{ plugin.name }}
+                      {{ pluginname.title() }}
                     {% 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 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 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() }}" />
                             <button class="btn btn-success">{% trans %}Enable{% endtrans %}</button>
                         </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() }}" />
                             <button class="btn btn-warning">{% trans %}Disable{% endtrans %}</button>
                         </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() }}" />
                             <button class="btn btn-info">{% trans %}Install{% endtrans %}</button>
                         </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() }}" />
                             <button class="btn btn-danger">{% trans %}Uninstall{% endtrans %}</button>
                         </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>
                 {% 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 glob
 from datetime import datetime, timedelta
+from pkg_resources import get_distribution
+from email import message_from_string
 from pytz import UTC
 from PIL import ImageFile
 
@@ -619,9 +621,20 @@ class ReverseProxyPathFix(object):
 
 
 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):
         return obj._get_current_object()
     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_babelplus import Domain, get_locale
-from flask_plugins import get_enabled_plugins
-
-from flaskbb.extensions import plugin_manager
 
 
 class FlaskBBDomain(Domain):
@@ -38,7 +35,7 @@ class FlaskBBDomain(Domain):
         with self.app.app_context():
             self.plugin_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"))
             ]
 
@@ -94,7 +91,7 @@ def update_translations(include_plugins=False):
                      "-d", translations_folder])
 
     if include_plugins:
-        for plugin in plugin_manager.all_plugins:
+        for plugin in current_app.pluggy.get_plugins():
             update_plugin_translations(plugin)
 
 

+ 9 - 4
migrations/7c3fcf8a3335_add_plugin_tables.py

@@ -1,7 +1,7 @@
 """Add plugin tables
 
 Revision ID: 7c3fcf8a3335
-Revises: 
+Revises:
 Create Date: 2017-08-12 12:41:04.725309
 
 """
@@ -25,14 +25,19 @@ def upgrade():
     sa.PrimaryKeyConstraint('id'),
     sa.UniqueConstraint('name')
     )
+
     op.create_table('plugin_store',
     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('name', sa.Unicode(length=255), nullable=False),
+    sa.Column('description', sa.Text(), nullable=False),
     sa.ForeignKeyConstraint(['plugin_id'], ['plugin_registry.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 ###
 

+ 1 - 2
requirements.txt

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

+ 0 - 1
setup.py

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