Browse Source

Merge pull request #38 from sh4nks/plugin-api

Plugin API
sh4nks 11 years ago
parent
commit
ef88bb86e0

+ 21 - 0
flaskbb/admin/views.py

@@ -14,6 +14,7 @@ from datetime import datetime
 from flask import (Blueprint, current_app, request, redirect, url_for, flash,
                    __version__ as flask_version)
 from flask.ext.login import current_user
+from flask.ext.plugins import get_plugins_list, get_plugin
 
 from flaskbb import __version__ as flaskbb_version
 from flaskbb.forum.forms import UserSearchForm
@@ -94,6 +95,26 @@ def reports():
     return render_template("admin/reports.html", reports=reports)
 
 
+@admin.route("/plugins")
+@admin_required
+def plugins():
+    return render_template("admin/plugins.html", plugins=get_plugins_list())
+
+
+@admin.route("/plugins/enable/<plugin>")
+def enable_plugin(plugin):
+    plugin = get_plugin(plugin)
+    current_app.plugin_manager.enable_plugins([plugin])
+    return redirect(url_for("admin.plugins"))
+
+
+@admin.route("/plugins/disable/<plugin>")
+def disable_plugin(plugin):
+    plugin = get_plugin(plugin)
+    current_app.plugin_manager.disable_plugins([plugin])
+    return redirect(url_for("admin.plugins"))
+
+
 @admin.route("/reports/unread")
 @admin_required
 def unread_reports():

+ 6 - 3
flaskbb/app.py

@@ -25,9 +25,9 @@ from flaskbb.admin.views import admin
 # Import the forum blueprint
 from flaskbb.forum.views import forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
-# extenesions
+# extensions
 from flaskbb.extensions import db, login_manager, mail, cache, redis, \
-    debugtoolbar, migrate, themes
+    debugtoolbar, migrate, themes, plugin_manager
 from flask.ext.whooshalchemy import whoosh_index
 # various helpers
 from flaskbb.utils.helpers import format_date, time_since, crop_title, \
@@ -36,7 +36,7 @@ from flaskbb.utils.helpers import format_date, time_since, crop_title, \
 # permission checks (here they are used for the jinja filters)
 from flaskbb.utils.permissions import can_post_reply, can_post_topic, \
     can_delete_topic, can_delete_post, can_edit_post, can_lock_topic, \
-    can_move_topic, can_moderate
+    can_move_topic
 
 
 def create_app(config=None):
@@ -75,6 +75,9 @@ def configure_extensions(app):
     Configures the extensions
     """
 
+    # Flask-Plugins
+    plugin_manager.init_app(app)
+
     # Flask-SQLAlchemy
     db.init_app(app)
 

+ 5 - 0
flaskbb/extensions.py

@@ -16,6 +16,8 @@ from flask.ext.debugtoolbar import DebugToolbarExtension
 from flask.ext.redis import Redis
 from flask.ext.migrate import Migrate
 from flask.ext.themes2 import Themes
+from flask.ext.plugins import PluginManager
+
 
 # Database
 db = SQLAlchemy()
@@ -40,3 +42,6 @@ migrate = Migrate()
 
 # Themes
 themes = Themes()
+
+# PluginManager
+plugin_manager = PluginManager()

+ 0 - 1
flaskbb/forum/views.py

@@ -28,7 +28,6 @@ from flaskbb.forum.forms import (QuickreplyForm, ReplyForm, NewTopicForm,
                                  ReportForm, UserSearchForm, SearchPageForm)
 from flaskbb.user.models import User
 
-
 forum = Blueprint("forum", __name__)
 
 

+ 20 - 59
flaskbb/plugins/__init__.py

@@ -1,84 +1,45 @@
-# -*- coding: utf-8 -*-
-"""
-    flaskbb.plugins
-    ~~~~~~~~~~~~~~~
+from flask.ext.plugins import Plugin
+from flask import current_app
 
-    The Plugin class that every plugin should implement
 
-    :copyright: (c) 2014 by the FlaskBB Team.
-    :license: BSD, see LICENSE for more details.
-"""
-
-
-class PluginError(Exception):
-    """Error class for all plugin errors."""
-    pass
-
-
-class Plugin(object):
-    """Every plugin should implement this class. It handles the registration
-    for the plugin hooks, creates or modifies additional relations or
-    registers plugin specific thinks"""
-
-    #: If you want create your tables with one command, put your models
-    #: in here.
-    models = []
-
-    #: The name of the plugin
-    #: E.g. "Example Plugin"
-    name = None
-
-    #: A small description of the plugin
-    description = None
-
-    def install(self, app=None):
-        """The plugin should specify here what needs to be installed.
-        For example, create the database and register the hooks."""
-        raise NotImplementedError
-
-    def uninstall(self, app=None):
-        """Uninstalls the plugin and deletes the things that
-        the plugin has installed."""
-        raise NotImplementedError
+class FlaskBBPlugin(Plugin):
+        # Some helpers
+    def register_blueprint(self, blueprint, **kwargs):
+        """Registers a blueprint."""
+        current_app.register_blueprint(blueprint, **kwargs)
 
     def create_table(self, model, db):
-        """Creates the table for the model
+        """Creates the relation for the model
 
         :param model: The Model which should be created
         :param db: The database instance.
         """
-        model.__table__.create(bind=db.engine)
+        if not model.__table__.exists(bind=db.engine):
+            model.__table__.create(bind=db.engine)
 
     def drop_table(self, model, db):
-        """Drops the table for the bounded model.
+        """Drops the relation for the bounded model.
 
         :param model: The model on which the table is bound.
         :param db: The database instance.
         """
         model.__table__.drop(bind=db.engine)
 
-    def create_all_tables(self, db):
+    def create_all_tables(self, models, db):
         """A interface for creating all models specified in ``models``.
-        If no models are specified in that variable it will abort
-        with a exception.
 
+        :param models: A list with models
         :param db: The database instance
         """
-        if len(self.models) > 0:
-            for model in self.models:
-                self.create_table(model, db)
-        else:
-            raise PluginError("No models found in 'models'.")
+        for model in models:
+            self.create_table(model, db)
 
-    def drop_all_tables(self, db):
+    def drop_all_tables(self, models, db):
         """A interface for dropping all models specified in the
-        variable ``models``. If no models are specified in that variable,
-        it will abort with an exception.
+        variable ``models``.
 
+        :param models: A list with models
         :param db: The database instance.
         """
-        if len(self.models) > 0:
-            for model in self.models:
-                self.drop_table(model, db)
-        else:
-            raise PluginError("No models found in 'models'.")
+        for model in models:
+            self.drop_table(model, db)

+ 0 - 60
flaskbb/plugins/loader.py

@@ -1,60 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    flaskbb.plugins.loader
-    ~~~~~~~~~~~~~~~~~~~~~~
-
-    The Plugin loader.
-
-    :copyright: (c) 2014 by the FlaskBB Team.
-    :license: BSD, see LICENSE for more details.
-"""
-import os
-
-
-class PluginLoader(object):
-
-    #: Stores all founded plugins in the plugins folder
-    plugins = list()
-
-    def __init__(self, app):
-        self.app = app
-        self.plugin_folder = os.path.join(self.app.root_path, "plugins")
-        self.base_plugin_package = ".".join([self.app.name, "plugins"])
-
-    def load_plugins(self):
-        """Loads all plugins"""
-        #from flaskbb.plugins.pluginname import PluginName
-        #__import__(plugin, globals(), locals(), [tmp.__plugin__], -1)
-        pass
-
-    def check_plugin(self, plugin):
-        """Checks if a plugin is appropriate.
-
-        :param plugin:
-        """
-        pass
-
-    def find_plugins(self):
-        for item in os.listdir(self.plugin_folder):
-
-            if os.path.isdir(os.path.join(self.plugin_folder, item)) and \
-                    os.path.exists(
-                        os.path.join(self.plugin_folder, item, "__init__.py")):
-
-                plugin = ".".join([self.base_plugin_package, item])
-
-                # Same like from flaskbb.plugins.pluginname import __plugin__
-                tmp = __import__(
-                    plugin, globals(), locals(), ['__plugin__'], -1
-                )
-
-                self.plugins.append(tmp.__plugin__)
-
-"""
-for ipython:
-from flask import current_app
-from flaskbb.plugins.loader import PluginLoader
-loader = PluginLoader(current_app)
-loader.find_plugins()
-loader.plugins
-"""

+ 15 - 14
flaskbb/plugins/portal/__init__.py

@@ -1,23 +1,24 @@
-from flaskbb.extensions import db
-from flaskbb.plugins import Plugin
+from flask.ext.plugins import connect_event
 
-from .portal import PortalModel
+from flaskbb.plugins import FlaskBBPlugin
+from .views import portal, inject_portal_link
 
-
-#: The name of your plugin class
+__version__ = "0.1"
 __plugin__ = "PortalPlugin"
 
 
-class PortalPlugin(Plugin):
-    models = [PortalModel]
+class PortalPlugin(FlaskBBPlugin):
 
     name = "Portal Plugin"
-    description = "A simple Portal"
 
-    def install(self):
-        self.create_all_tables(db)
-        #
-        # register hooks and blueprints/routes here
+    description = ("This Plugin provides a simple portal for FlaskBB.")
+
+    author = "sh4nks"
+
+    license = "BSD"
+
+    version = __version__
 
-    def uninstall(self):
-        self.drop_all_tables(db)
+    def setup(self):
+        self.register_blueprint(portal, url_prefix="/portal")
+        connect_event("before-first-navigation-element", inject_portal_link)

+ 8 - 0
flaskbb/plugins/portal/info.json

@@ -0,0 +1,8 @@
+{
+    "identifier": "portal",
+    "name": "Portal",
+    "author": "sh4nks",
+    "license": "BSD",
+    "description": "A Portal Plugin for FlaskBB",
+    "version": "0.1"
+}

+ 0 - 0
flaskbb/plugins/portal/models.py


+ 0 - 17
flaskbb/plugins/portal/portal.py

@@ -1,17 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    flaskbb.plugins.portal
-    ~~~~~~~~~~~~~~~~~~~~~~
-
-    This plugin implements a portal. You can choose which topics and posts
-    from forums are displayed.
-
-    :copyright: (c) 2014 by the FlaskBB Team.
-    :license: BSD, see LICENSE for more details.
-"""
-from flaskbb.extensions import db
-
-
-class PortalModel(db.Model):
-    id = db.Column(db.Integer, primary_key=True)
-    forum_id = db.Column(db.Integer, db.ForeignKey("forums.id"))

+ 209 - 0
flaskbb/plugins/portal/templates/index.html

@@ -0,0 +1,209 @@
+{% extends theme("layout.html") %}
+
+{% block css %}
+  {{  super() }}
+
+  <style>
+  /* news posts */
+  .portal-info {
+      font-size:15px;
+      color:#999999;
+      padding: 0;
+      margin-top:5px;
+      margin-bottom:10px;
+      margin-right: 0px;
+      margin-left: 0px;
+  }
+  .portal-info ul {
+      list-style-type:none;
+  }
+  .portal-info li {
+      display:inline;
+      padding-right:10px;
+  }
+
+  .portal-info a {
+      color:#999999;
+  }
+  .portal-content h1,
+  .portal-content h2,
+  .portal-content h3,
+  .portal-content h4,
+  .portal-content h5 {
+      font-weight:500;
+  }
+
+  /* recent topics */
+  .portal-topic:not(:first-child) {
+    padding-top: 5px;
+    clear: both;
+
+    border-top: 1px solid #ddd;
+  }
+
+  .portal-topic-name {
+    float: left;
+  }
+
+  .portal-topic-updated-by {
+    float: right;
+  }
+
+  .portal-topic-updated {
+    color:#999999;
+    clear: both;
+    float: right;
+
+  }
+
+  /* stats */
+  .portal-stats {
+    color:#999999;
+  }
+  .portal-stats:not(:first-child) {
+    padding-top: 5px;
+    clear: both;
+  }
+
+  .portal-stats-left {
+    float: left;
+  }
+
+  .portal-stats-right {
+    float: right;
+  }
+  </style>
+{% endblock %}
+
+{% block content %}
+  <div class="container main-content">
+    <div class="row">
+
+      <!-- Left -->
+      <div class="col-md-8">
+        <div class="panel panel-default">
+          <div class="panel-heading">
+            <h3 class="panel-title">News</h3>
+          </div>
+          <div class="panel-body">
+          {% for topic in news %}
+            <h1><a href="{{ topic.url }}">{{ topic.title }}</a></h1>
+            <ul class="portal-info">
+                <li><i class="fa fa-calendar"></i> {{ topic.date_created|format_date('%b %d %Y') }}</li>
+                <li><i class="fa fa-user"></i> <a href="{{ url_for('user.profile', username=topic.user.username) }}">{{ topic.user.username }}</a></li>
+                <li><i class="fa fa-comment"></i> <a href="{{ topic.url }}">Comments ({{ topic.post_count }})</a></li>
+            </ul>
+            <div class="portal-content">
+                {{ topic.first_post.content | markup | safe }}<br />
+            </div>
+
+            {% if not loop.last %}<hr>{% endif %}
+          {% endfor %}
+
+          </div> <!-- /.panel-body -->
+        </div>
+
+      </div>
+
+      <!-- Right -->
+      <div class="col-md-4">
+        <div class="panel panel-default">
+          <div class="panel-heading">
+            <h3 class="panel-title">Recent Topics</h3>
+          </div>
+          <div class="panel-body">
+          {% for topic in recent_topics %}
+
+              <div class="portal-topic">
+                <div class="portal-topic-name">
+                  <a href="{{ topic.url }}">{{ topic.title }}</a>
+                </div>
+                <div class="portal-topic-updated-by">
+                  <a href="{{ url_for('user.profile', username=topic.user.username) }}">{{ topic.user.username }}</a>
+                </div>
+                <div class="portal-topic-updated">
+                  {{ topic.date_created | time_since }}
+                </div>
+              </div> <!-- /.topic -->
+
+          {% endfor %}
+          </div>
+        </div>
+
+        <div class="panel panel-default">
+          <div class="panel-heading">
+            <h3 class="panel-title">Statistics</h3>
+          </div>
+          <div class="panel-body">
+
+                <div class="portal-stats">
+                  <div class="portal-stats-left">
+                    Topics
+                  </div>
+                  <div class="portal-stats-right">
+                    {{ topic_count }}
+                  </div>
+                </div>
+
+                <div class="portal-stats">
+                  <div class="portal-stats-left">
+                    Posts
+                  </div>
+                  <div class="portal-stats-right">
+                    {{ post_count }}
+                  </div>
+                </div>
+
+                <div class="portal-stats">
+                  <div class="portal-stats-left">
+                    Registered Users
+                  </div>
+                  <div class="portal-stats-right">
+                    {{ user_count }}
+                  </div>
+                </div>
+
+                {% if newest_user %}
+                <div class="portal-stats">
+                  <div class="portal-stats-left">
+                    Newest User
+                  </div>
+                  <div class="portal-stats-right">
+                    <a href="{{ newest_user.url }}">{{ newest_user.username }}</a>
+                  </div>
+                </div>
+                {% endif %}
+
+                <div class="portal-stats">
+                  <div class="portal-stats-left">
+                    Online Users
+                  </div>
+
+                  <div class="portal-stats-right">
+                    {{ online_users }}
+                  </div>
+                </div>
+
+                {% if config["REDIS_ENABLED"] %}
+                <div class="portal-stats">
+                  <div class="portal-stats-left">
+                    Guests online
+                  </div>
+
+                  <div class="portal-stats-right">
+                    {{ online_guests }}
+                  </div>
+                </div>
+                {% endif %}
+            </td>
+        </tr>
+          </div>
+        </div>
+      </div>
+
+    </div>
+  </div>
+
+
+
+{% endblock %}

+ 5 - 0
flaskbb/plugins/portal/templates/navigation_snippet.html

@@ -0,0 +1,5 @@
+<li {% if 'portal.index' == request.endpoint %}class="active"{% endif %}>
+    <a href={{ url_for('portal.index') }}>
+        <i class="fa fa-home"></i> Portal
+    </a>
+</li>

+ 42 - 0
flaskbb/plugins/portal/views.py

@@ -0,0 +1,42 @@
+from flask import Blueprint, current_app
+from flaskbb.utils.helpers import render_template
+from flaskbb.forum.models import Topic, Post
+from flaskbb.user.models import User
+from flaskbb.utils.helpers import time_diff, get_online_users
+
+
+FORUM_IDS = [1, 2]
+
+portal = Blueprint("portal", __name__, template_folder="templates")
+
+
+def inject_portal_link():
+    return render_template("navigation_snippet.html")
+
+
+@portal.route("/")
+def index():
+    news = Topic.query.filter(Topic.forum_id.in_(FORUM_IDS)).all()
+    recent_topics = Topic.query.order_by(Topic.date_created).limit(5).offset(0)
+
+    user_count = User.query.count()
+    topic_count = Topic.query.count()
+    post_count = Post.query.count()
+    newest_user = User.query.order_by(User.id.desc()).first()
+
+    # Check if we use redis or not
+    if not current_app.config["REDIS_ENABLED"]:
+        online_users = User.query.filter(User.lastseen >= time_diff()).count()
+
+        # Because we do not have server side sessions, we cannot check if there
+        # are online guests
+        online_guests = None
+    else:
+        online_users = len(get_online_users())
+        online_guests = len(get_online_users(guest=True))
+
+    return render_template("index.html", news=news, recent_topics=recent_topics,
+                           user_count=user_count, topic_count=topic_count,
+                           post_count=post_count, newest_user=newest_user,
+                           online_guests=online_guests,
+                           online_users=online_users)

+ 0 - 29
flaskbb/static/css/flaskbb.css

@@ -188,32 +188,3 @@ margin-bottom: 0px;
   border-top-right-radius: 0;
 }
 /* End sidebar */
-
-
-.blog-info {
-    font-size:15px;
-    color:#999999;
-    padding: 0;
-    margin-top:5px;
-    margin-bottom:10px;
-    margin-right: 0px;
-    margin-left: 0px;
-}
-.blog-info ul {
-    list-style-type:none;
-}
-.blog-info li {
-    display:inline;
-    padding-right:10px;
-}
-
-.blog-info a {
-    color:#999999;
-}
-.blog-content h1,
-.blog-content h2,
-.blog-content h3,
-.blog-content h4,
-.blog-content h5 {
-    font-weight:500;
-}

+ 1 - 0
flaskbb/templates/admin/admin_layout.html

@@ -12,6 +12,7 @@
                 {{ navlink('admin.groups', 'Groups') }}
                 {{ navlink('admin.forums', 'Forums') }}
                 {{ navlink('admin.unread_reports', 'Reports') }}
+                {{ navlink('admin.plugins', 'Plugins') }}
             </ul>
         </div><!--/.sidebar -->
     </div><!--/.col-sm-3 -->

+ 32 - 0
flaskbb/templates/admin/plugins.html

@@ -0,0 +1,32 @@
+{% extends theme("admin/admin_layout.html") %}
+{% block admin_content %}
+{% from theme('macros.html') import render_pagination %}
+
+<legend>Manage Plugins</legend>
+
+<table class="table table-bordered">
+    <thead>
+        <tr>
+            <th>Plugin</th>
+            <th>Information</th>
+            <th>Manage</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for plugin in plugins %}
+        <tr>
+            <td><a href="#">{{ plugin.name }}</a></td>
+            <td>
+                {{ plugin.version }} <br />
+                {{ plugin.description }} <br />
+                {{ plugin.author }}
+            </td>
+            <td>
+                <a href="{{ url_for('admin.enable_plugin', plugin=plugin.name) }}">Enable</a> |
+                <a href="{{ url_for('admin.disable_plugin', plugin=plugin.name) }}">Disable</a>
+            </td>
+        </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% endblock %}

+ 4 - 0
flaskbb/templates/layout.html

@@ -43,10 +43,14 @@
                     </div>
                     <div class="collapse navbar-collapse navbar-ex1-collapse">
                         <ul class="nav navbar-nav">
+                            {{ emit_event("before-first-navigation-element") }}
+
                             {# active_forum_nav is set in {forum, category, topic}.html and new_{topic, post}.html #}
                             {{ 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>
 
                     {% if current_user and current_user.is_authenticated() %}

+ 4 - 0
flaskbb/themes/bootstrap2/templates/layout.html

@@ -44,10 +44,14 @@
                     </div>
                     <div class="collapse navbar-collapse navbar-ex1-collapse">
                         <ul class="nav navbar-nav">
+                            {{ emit_event("before-first-navigation-element") }}
+
                             {# active_forum_nav is set in {forum, category, topic}.html and new_{topic, post}.html #}
                             {{ 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>
 
                     {% if current_user and current_user.is_authenticated() %}

+ 4 - 0
flaskbb/themes/bootstrap3/templates/layout.html

@@ -43,10 +43,14 @@
                     </div>
                     <div class="collapse navbar-collapse navbar-ex1-collapse">
                         <ul class="nav navbar-nav">
+                            {{ emit_event("before-first-navigation-element") }}
+
                             {# active_forum_nav is set in {forum, category, topic}.html and new_{topic, post}.html #}
                             {{ 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>
 
                     {% if current_user and current_user.is_authenticated() %}

+ 6 - 5
requirements.txt

@@ -1,30 +1,31 @@
 Flask==0.10.1
 Flask-And-Redis==0.5
-Flask-Cache==0.12
+Flask-Cache==0.13.1
 Flask-DebugToolbar==0.9.0
 Flask-Login==0.2.10
 Flask-Mail==0.9.0
 Flask-Migrate==1.2.0
+Flask-Plugins==1.1
 Flask-SQLAlchemy==1.0
-Flask-Script==0.6.7
+Flask-Script==2.0.5
 Flask-Themes2==0.1.3
 Flask-WTF==0.9.5
 Jinja2==2.7.2
 Mako==0.9.1
-MarkupSafe==0.19
+MarkupSafe==0.23
 Pygments==1.6
 SQLAlchemy==0.9.4
 WTForms==1.0.5
 Werkzeug==0.9.4
 Whoosh==2.6.0
-alembic==0.6.4
+alembic==0.6.5
 blinker==1.3
 itsdangerous==0.24
 py==1.4.20
 pytest==2.5.2
 pytest-random==0.02
 redis==2.9.1
-simplejson==3.4.0
+simplejson==3.4.1
 wsgiref==0.1.2
 
 https://github.com/miguelgrinberg/Flask-WhooshAlchemy/tarball/master#egg=Flask-WhooshAlchemy