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

Merge pull request #419 from justanr/load-blueprints-via-hooks

Load internal blueprints via hooks
Peter Justin 7 лет назад
Родитель
Сommit
97cf6ad62c
5 измененных файлов с 833 добавлено и 512 удалено
  1. 18 21
      flaskbb/app.py
  2. 87 74
      flaskbb/auth/views.py
  3. 331 157
      flaskbb/forum/views.py
  4. 346 238
      flaskbb/management/views.py
  5. 51 22
      flaskbb/user/views.py

+ 18 - 21
flaskbb/app.py

@@ -23,21 +23,16 @@ from sqlalchemy.engine import Engine
 from sqlalchemy.exc import OperationalError, ProgrammingError
 from sqlalchemy.exc import OperationalError, ProgrammingError
 
 
 from flaskbb._compat import iteritems, string_types
 from flaskbb._compat import iteritems, string_types
-from flaskbb.auth.views import auth
 # 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,
                                 redis_store, themes, whooshee)
                                 redis_store, themes, whooshee)
-from flaskbb.forum.views import forum
-from flaskbb.management.views import management
 from flaskbb.plugins import spec
 from flaskbb.plugins import spec
 from flaskbb.plugins.manager import FlaskBBPluginManager
 from flaskbb.plugins.manager import FlaskBBPluginManager
 from flaskbb.plugins.models import PluginRegistry
 from flaskbb.plugins.models import PluginRegistry
 from flaskbb.plugins.utils import remove_zombie_plugins_from_db, template_hook
 from flaskbb.plugins.utils import remove_zombie_plugins_from_db, template_hook
 # models
 # models
 from flaskbb.user.models import Guest, User
 from flaskbb.user.models import Guest, User
-# views
-from flaskbb.user.views import user
 # various helpers
 # various helpers
 from flaskbb.utils.helpers import (app_config_from_env, crop_title,
 from flaskbb.utils.helpers import (app_config_from_env, crop_title,
                                    format_date, forum_is_unread,
                                    format_date, forum_is_unread,
@@ -58,6 +53,10 @@ from flaskbb.utils.search import (ForumWhoosheer, PostWhoosheer,
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.translations import FlaskBBDomain
 from flaskbb.utils.translations import FlaskBBDomain
 
 
+from .auth import views as auth_views
+from .forum import views as forum_views
+from .management import views as management_views
+from .user import views as user_views
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -78,9 +77,7 @@ def create_app(config=None, instance_path=None):
     """
     """
 
 
     app = Flask(
     app = Flask(
-        "flaskbb",
-        instance_path=instance_path,
-        instance_relative_config=True
+        "flaskbb", instance_path=instance_path, instance_relative_config=True
     )
     )
 
 
     # instance folders are not automatically created by flask
     # instance folders are not automatically created by flask
@@ -149,20 +146,15 @@ def configure_celery_app(app, celery):
     TaskBase = celery.Task
     TaskBase = celery.Task
 
 
     class ContextTask(TaskBase):
     class ContextTask(TaskBase):
+
         def __call__(self, *args, **kwargs):
         def __call__(self, *args, **kwargs):
             with app.app_context():
             with app.app_context():
                 return TaskBase.__call__(self, *args, **kwargs)
                 return TaskBase.__call__(self, *args, **kwargs)
+
     celery.Task = ContextTask
     celery.Task = ContextTask
 
 
 
 
 def configure_blueprints(app):
 def configure_blueprints(app):
-    app.register_blueprint(forum, url_prefix=app.config["FORUM_URL_PREFIX"])
-    app.register_blueprint(user, url_prefix=app.config["USER_URL_PREFIX"])
-    app.register_blueprint(auth, url_prefix=app.config["AUTH_URL_PREFIX"])
-    app.register_blueprint(
-        management, url_prefix=app.config["ADMIN_URL_PREFIX"]
-    )
-
     app.pluggy.hook.flaskbb_load_blueprints(app=app)
     app.pluggy.hook.flaskbb_load_blueprints(app=app)
 
 
 
 
@@ -249,9 +241,9 @@ def configure_template_filters(app):
         ('can_ban_user', CanBanUser),
         ('can_ban_user', CanBanUser),
     ]
     ]
 
 
-    filters.update([
-        (name, partial(perm, request=request)) for name, perm in permissions
-    ])
+    filters.update(
+        [(name, partial(perm, request=request)) for name, perm in permissions]
+    )
 
 
     # these create closures
     # these create closures
     filters['can_moderate'] = TplCanModerate(request)
     filters['can_moderate'] = TplCanModerate(request)
@@ -297,6 +289,7 @@ def configure_before_handlers(app):
             db.session.commit()
             db.session.commit()
 
 
     if app.config["REDIS_ENABLED"]:
     if app.config["REDIS_ENABLED"]:
+
         @app.before_request
         @app.before_request
         def mark_current_user_online():
         def mark_current_user_online():
             if current_user.is_authenticated:
             if current_user.is_authenticated:
@@ -423,8 +416,11 @@ def load_plugins(app):
             plugins = PluginRegistry.query.all()
             plugins = PluginRegistry.query.all()
 
 
     except (OperationalError, ProgrammingError) as exc:
     except (OperationalError, ProgrammingError) as exc:
-        logger.debug("Database is not setup correctly or has not been "
-                     "setup yet.", exc_info=exc)
+        logger.debug(
+            "Database is not setup correctly or has not been "
+            "setup yet.",
+            exc_info=exc
+        )
         # load plugins even though the database isn't setup correctly
         # load plugins even though the database isn't setup correctly
         # i.e. when creating the initial database and wanting to install
         # i.e. when creating the initial database and wanting to install
         # the plugins migration as well
         # the plugins migration as well
@@ -441,7 +437,8 @@ def load_plugins(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 = [
     unregistered = [
-        PluginRegistry(name=name) for name in loaded_names - registered_names
+        PluginRegistry(name=name)
+        for name in loaded_names - registered_names
         # ignore internal FlaskBB modules
         # ignore internal FlaskBB modules
         if not name.startswith('flaskbb.') and name != 'flaskbb'
         if not name.startswith('flaskbb.') and name != 'flaskbb'
     ]
     ]

+ 87 - 74
flaskbb/auth/views.py

@@ -9,15 +9,14 @@
     :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 datetime import datetime
 import logging
 import logging
+from datetime import datetime
 
 
 from flask import Blueprint, flash, g, redirect, request, url_for
 from flask import Blueprint, flash, g, redirect, request, url_for
 from flask.views import MethodView
 from flask.views import MethodView
 from flask_babelplus import gettext as _
 from flask_babelplus import gettext as _
 from flask_login import (confirm_login, current_user, login_fresh,
 from flask_login import (confirm_login, current_user, login_fresh,
                          login_required, login_user, logout_user)
                          login_required, login_user, logout_user)
-
 from flaskbb.auth.forms import (AccountActivationForm, ForgotPasswordForm,
 from flaskbb.auth.forms import (AccountActivationForm, ForgotPasswordForm,
                                 LoginForm, LoginRecaptchaForm, ReauthForm,
                                 LoginForm, LoginRecaptchaForm, ReauthForm,
                                 RegisterForm, RequestActivationForm,
                                 RegisterForm, RequestActivationForm,
@@ -33,30 +32,13 @@ from flaskbb.utils.helpers import (anonymous_required, enforce_recaptcha,
                                    requires_unactivated)
                                    requires_unactivated)
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.tokens import get_token_status
 from flaskbb.utils.tokens import get_token_status
+from pluggy import HookimplMarker
 
 
+impl = HookimplMarker('flaskbb')
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-auth = Blueprint("auth", __name__)
-
-
-@auth.before_request
-def check_rate_limiting():
-    """Check the the rate limits for each request for this blueprint."""
-    if not flaskbb_config["AUTH_RATELIMIT_ENABLED"]:
-        return None
-    return limiter.check()
-
-
-@auth.errorhandler(429)
-def login_rate_limit_error(error):
-    """Register a custom error handler for a 'Too Many Requests'
-    (HTTP CODE 429) error."""
-    return render_template("errors/too_many_logins.html",
-                           timeout=error.description)
-
-
 def login_rate_limit():
 def login_rate_limit():
     """Dynamically load the rate limiting config from the database."""
     """Dynamically load the rate limiting config from the database."""
     # [count] [per|/] [n (optional)] [second|minute|hour|day|month|year]
     # [count] [per|/] [n (optional)] [second|minute|hour|day|month|year]
@@ -77,10 +59,6 @@ def login_rate_limit_message():
     return "{timeout}".format(timeout=format_timedelta(timeout))
     return "{timeout}".format(timeout=format_timedelta(timeout))
 
 
 
 
-# Activate rate limiting on the whole blueprint
-limiter.limit(login_rate_limit, error_message=login_rate_limit_message)(auth)
-
-
 class Logout(MethodView):
 class Logout(MethodView):
     decorators = [limiter.exempt, login_required]
     decorators = [limiter.exempt, login_required]
 
 
@@ -107,9 +85,13 @@ class Login(MethodView):
             try:
             try:
                 user = User.authenticate(form.login.data, form.password.data)
                 user = User.authenticate(form.login.data, form.password.data)
                 if not login_user(user, remember=form.remember_me.data):
                 if not login_user(user, remember=form.remember_me.data):
-                    flash(_("In order to use your account you have to "
+                    flash(
+                        _(
+                            "In order to use your account you have to "
                             "activate it through the link we have sent to "
                             "activate it through the link we have sent to "
-                            "your email address."), "danger")
+                            "your email address."
+                        ), "danger"
+                    )
                 return redirect_or_next(url_for("forum.index"))
                 return redirect_or_next(url_for("forum.index"))
             except AuthenticationError:
             except AuthenticationError:
                 flash(_("Wrong username or password."), "danger")
                 flash(_("Wrong username or password."), "danger")
@@ -168,8 +150,13 @@ class Register(MethodView):
                 send_activation_token.delay(
                 send_activation_token.delay(
                     user_id=user.id, username=user.username, email=user.email
                     user_id=user.id, username=user.username, email=user.email
                 )
                 )
-                flash(_("An account activation email has been sent to "
-                        "%(email)s", email=user.email), "success")
+                flash(
+                    _(
+                        "An account activation email has been sent to "
+                        "%(email)s",
+                        email=user.email
+                    ), "success"
+                )
             else:
             else:
                 login_user(user)
                 login_user(user)
                 flash(_("Thanks for registering."), "success")
                 flash(_("Thanks for registering."), "success")
@@ -198,8 +185,12 @@ class ForgotPassword(MethodView):
                 flash(_("Email sent! Please check your inbox."), "info")
                 flash(_("Email sent! Please check your inbox."), "info")
                 return redirect(url_for("auth.forgot_password"))
                 return redirect(url_for("auth.forgot_password"))
             else:
             else:
-                flash(_("You have entered an username or email address that "
-                        "is not linked with your account."), "danger")
+                flash(
+                    _(
+                        "You have entered an username or email address that "
+                        "is not linked with your account."
+                    ), "danger"
+                )
         return render_template("auth/forgot_password.html", form=form)
         return render_template("auth/forgot_password.html", form=form)
 
 
 
 
@@ -242,8 +233,9 @@ class RequestActivationToken(MethodView):
     form = RequestActivationForm
     form = RequestActivationForm
 
 
     def get(self):
     def get(self):
-        return render_template("auth/request_account_activation.html",
-                               form=self.form())
+        return render_template(
+            "auth/request_account_activation.html", form=self.form()
+        )
 
 
     def post(self):
     def post(self):
         form = self.form()
         form = self.form()
@@ -253,13 +245,16 @@ class RequestActivationToken(MethodView):
                 user_id=user.id, username=user.username, email=user.email
                 user_id=user.id, username=user.username, email=user.email
             )
             )
             flash(
             flash(
-                _("A new account activation token has been sent to "
-                  "your email address."), "success"
+                _(
+                    "A new account activation token has been sent to "
+                    "your email address."
+                ), "success"
             )
             )
             return redirect(url_for("auth.activate_account"))
             return redirect(url_for("auth.activate_account"))
 
 
-        return render_template("auth/request_account_activation.html",
-                               form=form)
+        return render_template(
+            "auth/request_account_activation.html", form=form
+        )
 
 
 
 
 class ActivateAccount(MethodView):
 class ActivateAccount(MethodView):
@@ -269,9 +264,7 @@ class ActivateAccount(MethodView):
     def get(self, token=None):
     def get(self, token=None):
         expired = invalid = user = None
         expired = invalid = user = None
         if token is not None:
         if token is not None:
-            expired, invalid, user = get_token_status(
-                token, "activate_account"
-            )
+            expired, invalid, user = get_token_status(token, "activate_account")
 
 
         if invalid:
         if invalid:
             flash(_("Your account activation token is invalid."), "danger")
             flash(_("Your account activation token is invalid."), "danger")
@@ -292,18 +285,14 @@ class ActivateAccount(MethodView):
             flash(_("Your account has been activated."), "success")
             flash(_("Your account has been activated."), "success")
             return redirect(url_for("forum.index"))
             return redirect(url_for("forum.index"))
 
 
-        return render_template(
-            "auth/account_activation.html", form=self.form()
-        )
+        return render_template("auth/account_activation.html", form=self.form())
 
 
     def post(self, token=None):
     def post(self, token=None):
         expired = invalid = user = None
         expired = invalid = user = None
         form = self.form()
         form = self.form()
 
 
         if token is not None:
         if token is not None:
-            expired, invalid, user = get_token_status(
-                token, "activate_account"
-            )
+            expired, invalid, user = get_token_status(token, "activate_account")
 
 
         elif form.validate_on_submit():
         elif form.validate_on_submit():
             expired, invalid, user = get_token_status(
             expired, invalid, user = get_token_status(
@@ -332,31 +321,55 @@ class ActivateAccount(MethodView):
         return render_template("auth/account_activation.html", form=form)
         return render_template("auth/account_activation.html", form=form)
 
 
 
 
-register_view(auth, routes=['/logout'], view_func=Logout.as_view('logout'))
-register_view(auth, routes=['/login'], view_func=Login.as_view('login'))
-register_view(auth, routes=['/reauth'], view_func=Reauth.as_view('reauth'))
-register_view(
-    auth,
-    routes=['/register'],
-    view_func=Register.as_view('register')
-)
-register_view(
-    auth,
-    routes=['/reset-password'],
-    view_func=ForgotPassword.as_view('forgot_password')
-)
-register_view(
-    auth,
-    routes=['/reset-password/<token>'],
-    view_func=ResetPassword.as_view('reset_password')
-)
-register_view(
-    auth,
-    routes=['/activate'],
-    view_func=RequestActivationToken.as_view('request_activation_token')
-)
-register_view(
-    auth,
-    routes=['/activate/confirm', '/activate/confirm/<token>'],
-    view_func=ActivateAccount.as_view('activate_account')
-)
+@impl(tryfirst=True)
+def flaskbb_load_blueprints(app):
+    auth = Blueprint("auth", __name__)
+
+    @auth.before_request
+    def check_rate_limiting():
+        """Check the the rate limits for each request for this blueprint."""
+        if not flaskbb_config["AUTH_RATELIMIT_ENABLED"]:
+            return None
+        return limiter.check()
+
+    @auth.errorhandler(429)
+    def login_rate_limit_error(error):
+        """Register a custom error handler for a 'Too Many Requests'
+        (HTTP CODE 429) error."""
+        return render_template(
+            "errors/too_many_logins.html", timeout=error.description
+        )
+
+    # Activate rate limiting on the whole blueprint
+    limiter.limit(
+        login_rate_limit, error_message=login_rate_limit_message
+    )(auth)
+
+    register_view(auth, routes=['/logout'], view_func=Logout.as_view('logout'))
+    register_view(auth, routes=['/login'], view_func=Login.as_view('login'))
+    register_view(auth, routes=['/reauth'], view_func=Reauth.as_view('reauth'))
+    register_view(
+        auth, routes=['/register'], view_func=Register.as_view('register')
+    )
+    register_view(
+        auth,
+        routes=['/reset-password'],
+        view_func=ForgotPassword.as_view('forgot_password')
+    )
+    register_view(
+        auth,
+        routes=['/reset-password/<token>'],
+        view_func=ResetPassword.as_view('reset_password')
+    )
+    register_view(
+        auth,
+        routes=['/activate'],
+        view_func=RequestActivationToken.as_view('request_activation_token')
+    )
+    register_view(
+        auth,
+        routes=['/activate/confirm', '/activate/confirm/<token>'],
+        view_func=ActivateAccount.as_view('activate_account')
+    )
+
+    app.register_blueprint(auth, url_prefix=app.config['AUTH_URL_PREFIX'])

+ 331 - 157
flaskbb/forum/views.py

@@ -18,6 +18,7 @@ from flask.views import MethodView
 from flask_allows import And, Permission
 from flask_allows import And, Permission
 from flask_babelplus import gettext as _
 from flask_babelplus import gettext as _
 from flask_login import current_user, login_required
 from flask_login import current_user, login_required
+from pluggy import HookimplMarker
 from sqlalchemy import asc, desc
 from sqlalchemy import asc, desc
 
 
 from flaskbb.extensions import allows, db
 from flaskbb.extensions import allows, db
@@ -36,10 +37,9 @@ from flaskbb.utils.requirements import (CanAccessForum, CanAccessTopic,
                                         IsAtleastModeratorInForum)
                                         IsAtleastModeratorInForum)
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 
 
-logger = logging.getLogger(__name__)
-
+impl = HookimplMarker('flaskbb')
 
 
-forum = Blueprint("forum", __name__)
+logger = logging.getLogger(__name__)
 
 
 
 
 class ForumIndex(MethodView):
 class ForumIndex(MethodView):
@@ -55,7 +55,8 @@ class ForumIndex(MethodView):
 
 
         # Check if we use redis or not
         # Check if we use redis or not
         if not current_app.config['REDIS_ENABLED']:
         if not current_app.config['REDIS_ENABLED']:
-            online_users = User.query.filter(User.lastseen >= time_diff()).count()
+            online_users = User.query.filter(User.lastseen >= time_diff()
+                                             ).count()
 
 
             # Because we do not have server side sessions, we cannot check if there
             # Because we do not have server side sessions, we cannot check if there
             # are online guests
             # are online guests
@@ -79,9 +80,13 @@ class ForumIndex(MethodView):
 class ViewCategory(MethodView):
 class ViewCategory(MethodView):
 
 
     def get(self, category_id, slug=None):
     def get(self, category_id, slug=None):
-        category, forums = Category.get_forums(category_id=category_id, user=real(current_user))
+        category, forums = Category.get_forums(
+            category_id=category_id, user=real(current_user)
+        )
 
 
-        return render_template('forum/category.html', forums=forums, category=category)
+        return render_template(
+            'forum/category.html', forums=forums, category=category
+        )
 
 
 
 
 class ViewForum(MethodView):
 class ViewForum(MethodView):
@@ -90,7 +95,9 @@ class ViewForum(MethodView):
     def get(self, forum_id, slug=None):
     def get(self, forum_id, slug=None):
         page = request.args.get('page', 1, type=int)
         page = request.args.get('page', 1, type=int)
 
 
-        forum_instance, forumsread = Forum.get_forum(forum_id=forum_id, user=real(current_user))
+        forum_instance, forumsread = Forum.get_forum(
+            forum_id=forum_id, user=real(current_user)
+        )
 
 
         if forum_instance.external:
         if forum_instance.external:
             return redirect(forum_instance.external)
             return redirect(forum_instance.external)
@@ -115,9 +122,12 @@ class ViewPost(MethodView):
     def get(self, post_id):
     def get(self, post_id):
         '''Redirects to a post in a topic.'''
         '''Redirects to a post in a topic.'''
         post = Post.query.filter_by(id=post_id).first_or_404()
         post = Post.query.filter_by(id=post_id).first_or_404()
-        post_in_topic = Post.query.filter(Post.topic_id == post.topic_id,
-                                          Post.id <= post_id).order_by(Post.id.asc()).count()
-        page = int(math.ceil(post_in_topic / float(flaskbb_config['POSTS_PER_PAGE'])))
+        post_in_topic = Post.query.filter(
+            Post.topic_id == post.topic_id, Post.id <= post_id
+        ).order_by(Post.id.asc()).count()
+        page = int(
+            math.ceil(post_in_topic / float(flaskbb_config['POSTS_PER_PAGE']))
+        )
 
 
         return redirect(
         return redirect(
             url_for(
             url_for(
@@ -143,7 +153,6 @@ class ViewTopic(MethodView):
         topic.views += 1
         topic.views += 1
         topic.save()
         topic.save()
 
 
-
         # Update the topicsread status if the user hasn't read it
         # Update the topicsread status if the user hasn't read it
         forumsread = None
         forumsread = None
         if current_user.is_authenticated:
         if current_user.is_authenticated:
@@ -166,7 +175,11 @@ class ViewTopic(MethodView):
             abort(404)
             abort(404)
 
 
         return render_template(
         return render_template(
-            'forum/topic.html', topic=topic, posts=posts, last_seen=time_diff(), form=self.form()
+            'forum/topic.html',
+            topic=topic,
+            posts=posts,
+            last_seen=time_diff(),
+            form=self.form()
         )
         )
 
 
     @allows.requires(CanPostReply)
     @allows.requires(CanPostReply)
@@ -185,7 +198,9 @@ class ViewTopic(MethodView):
         else:
         else:
             for e in form.errors.get('content', []):
             for e in form.errors.get('content', []):
                 flash(e, 'danger')
                 flash(e, 'danger')
-            return redirect(url_for('forum.view_topic', topic_id=topic_id, slug=slug))
+            return redirect(
+                url_for('forum.view_topic', topic_id=topic_id, slug=slug)
+            )
 
 
     def form(self):
     def form(self):
         if Permission(CanPostReply):
         if Permission(CanPostReply):
@@ -199,7 +214,9 @@ class NewTopic(MethodView):
 
 
     def get(self, forum_id, slug=None):
     def get(self, forum_id, slug=None):
         forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
         forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
-        return render_template('forum/new_topic.html', forum=forum_instance, form=self.form())
+        return render_template(
+            'forum/new_topic.html', forum=forum_instance, form=self.form()
+        )
 
 
     @allows.requires(CanPostTopic)
     @allows.requires(CanPostTopic)
     def post(self, forum_id, slug=None):
     def post(self, forum_id, slug=None):
@@ -207,14 +224,19 @@ class NewTopic(MethodView):
         form = self.form()
         form = self.form()
         if 'preview' in request.form and form.validate():
         if 'preview' in request.form and form.validate():
             return render_template(
             return render_template(
-                'forum/new_topic.html', forum=forum_instance, form=form, preview=form.content.data
+                'forum/new_topic.html',
+                forum=forum_instance,
+                form=form,
+                preview=form.content.data
             )
             )
         elif 'submit' in request.form and form.validate():
         elif 'submit' in request.form and form.validate():
             topic = form.save(real(current_user), forum_instance)
             topic = form.save(real(current_user), forum_instance)
             # redirect to the new topic
             # redirect to the new topic
             return redirect(url_for('forum.view_topic', topic_id=topic.id))
             return redirect(url_for('forum.view_topic', topic_id=topic.id))
         else:
         else:
-            return render_template('forum/new_topic.html', forum=forum_instance, form=form)
+            return render_template(
+                'forum/new_topic.html', forum=forum_instance, form=form
+            )
 
 
 
 
 class ManageForum(MethodView):
 class ManageForum(MethodView):
@@ -222,7 +244,9 @@ class ManageForum(MethodView):
 
 
     def get(self, forum_id, slug=None):
     def get(self, forum_id, slug=None):
 
 
-        forum_instance, forumsread = Forum.get_forum(forum_id=forum_id, user=real(current_user))
+        forum_instance, forumsread = Forum.get_forum(
+            forum_id=forum_id, user=real(current_user)
+        )
 
 
         if forum_instance.external:
         if forum_instance.external:
             return redirect(forum_instance.external)
             return redirect(forum_instance.external)
@@ -247,9 +271,13 @@ class ManageForum(MethodView):
         )
         )
 
 
     def post(self, forum_id, slug=None):
     def post(self, forum_id, slug=None):
-        forum_instance, __ = Forum.get_forum(forum_id=forum_id, user=real(current_user))
+        forum_instance, __ = Forum.get_forum(
+            forum_id=forum_id, user=real(current_user)
+        )
         mod_forum_url = url_for(
         mod_forum_url = url_for(
-            'forum.manage_forum', forum_id=forum_instance.id, slug=forum_instance.slug
+            'forum.manage_forum',
+            forum_id=forum_instance.id,
+            slug=forum_instance.slug
         )
         )
 
 
         ids = request.form.getlist('rowid')
         ids = request.form.getlist('rowid')
@@ -257,15 +285,20 @@ class ManageForum(MethodView):
 
 
         if not len(tmp_topics) > 0:
         if not len(tmp_topics) > 0:
             flash(
             flash(
-                _('In order to perform this action you have to select at '
-                  'least one topic.'), 'danger'
+                _(
+                    'In order to perform this action you have to select at '
+                    'least one topic.'
+                ), 'danger'
             )
             )
             return redirect(mod_forum_url)
             return redirect(mod_forum_url)
 
 
         # locking/unlocking
         # locking/unlocking
         if 'lock' in request.form:
         if 'lock' in request.form:
             changed = do_topic_action(
             changed = do_topic_action(
-                topics=tmp_topics, user=real(current_user), action='locked', reverse=False
+                topics=tmp_topics,
+                user=real(current_user),
+                action='locked',
+                reverse=False
             )
             )
 
 
             flash(_('%(count)s topics locked.', count=changed), 'success')
             flash(_('%(count)s topics locked.', count=changed), 'success')
@@ -273,7 +306,10 @@ class ManageForum(MethodView):
 
 
         elif 'unlock' in request.form:
         elif 'unlock' in request.form:
             changed = do_topic_action(
             changed = do_topic_action(
-                topics=tmp_topics, user=real(current_user), action='locked', reverse=True
+                topics=tmp_topics,
+                user=real(current_user),
+                action='locked',
+                reverse=True
             )
             )
             flash(_('%(count)s topics unlocked.', count=changed), 'success')
             flash(_('%(count)s topics unlocked.', count=changed), 'success')
             return redirect(mod_forum_url)
             return redirect(mod_forum_url)
@@ -281,14 +317,20 @@ class ManageForum(MethodView):
         # highlighting/trivializing
         # highlighting/trivializing
         elif 'highlight' in request.form:
         elif 'highlight' in request.form:
             changed = do_topic_action(
             changed = do_topic_action(
-                topics=tmp_topics, user=real(current_user), action='important', reverse=False
+                topics=tmp_topics,
+                user=real(current_user),
+                action='important',
+                reverse=False
             )
             )
             flash(_('%(count)s topics highlighted.', count=changed), 'success')
             flash(_('%(count)s topics highlighted.', count=changed), 'success')
             return redirect(mod_forum_url)
             return redirect(mod_forum_url)
 
 
         elif 'trivialize' in request.form:
         elif 'trivialize' in request.form:
             changed = do_topic_action(
             changed = do_topic_action(
-                topics=tmp_topics, user=real(current_user), action='important', reverse=True
+                topics=tmp_topics,
+                user=real(current_user),
+                action='important',
+                reverse=True
             )
             )
             flash(_('%(count)s topics trivialized.', count=changed), 'success')
             flash(_('%(count)s topics trivialized.', count=changed), 'success')
             return redirect(mod_forum_url)
             return redirect(mod_forum_url)
@@ -296,7 +338,10 @@ class ManageForum(MethodView):
         # deleting
         # deleting
         elif 'delete' in request.form:
         elif 'delete' in request.form:
             changed = do_topic_action(
             changed = do_topic_action(
-                topics=tmp_topics, user=real(current_user), action='delete', reverse=False
+                topics=tmp_topics,
+                user=real(current_user),
+                action='delete',
+                reverse=False
             )
             )
             flash(_('%(count)s topics deleted.', count=changed), 'success')
             flash(_('%(count)s topics deleted.', count=changed), 'success')
             return redirect(mod_forum_url)
             return redirect(mod_forum_url)
@@ -312,9 +357,13 @@ class ManageForum(MethodView):
             new_forum = Forum.query.filter_by(id=new_forum_id).first_or_404()
             new_forum = Forum.query.filter_by(id=new_forum_id).first_or_404()
             # check the permission in the current forum and in the new forum
             # check the permission in the current forum and in the new forum
 
 
-            if not Permission(And(IsAtleastModeratorInForum(forum_id=new_forum_id),
-                                  IsAtleastModeratorInForum(forum=forum_instance))):
-                flash(_('You do not have the permissions to move this topic.'), 'danger')
+            if not Permission(
+                    And(IsAtleastModeratorInForum(forum_id=new_forum_id),
+                        IsAtleastModeratorInForum(forum=forum_instance))):
+                flash(
+                    _('You do not have the permissions to move this topic.'),
+                    'danger'
+                )
                 return redirect(mod_forum_url)
                 return redirect(mod_forum_url)
 
 
             if new_forum.move_topics_to(tmp_topics):
             if new_forum.move_topics_to(tmp_topics):
@@ -327,14 +376,20 @@ class ManageForum(MethodView):
         # hiding/unhiding
         # hiding/unhiding
         elif "hide" in request.form:
         elif "hide" in request.form:
             changed = do_topic_action(
             changed = do_topic_action(
-                topics=tmp_topics, user=real(current_user), action="hide", reverse=False
+                topics=tmp_topics,
+                user=real(current_user),
+                action="hide",
+                reverse=False
             )
             )
             flash(_("%(count)s topics hidden.", count=changed), "success")
             flash(_("%(count)s topics hidden.", count=changed), "success")
             return redirect(mod_forum_url)
             return redirect(mod_forum_url)
 
 
         elif "unhide" in request.form:
         elif "unhide" in request.form:
             changed = do_topic_action(
             changed = do_topic_action(
-                topics=tmp_topics, user=real(current_user), action="unhide", reverse=False
+                topics=tmp_topics,
+                user=real(current_user),
+                action="unhide",
+                reverse=False
             )
             )
             flash(_("%(count)s topics unhidden.", count=changed), "success")
             flash(_("%(count)s topics unhidden.", count=changed), "success")
             return redirect(mod_forum_url)
             return redirect(mod_forum_url)
@@ -350,7 +405,9 @@ class NewPost(MethodView):
 
 
     def get(self, topic_id, slug=None):
     def get(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
-        return render_template('forum/new_post.html', topic=topic, form=self.form())
+        return render_template(
+            'forum/new_post.html', topic=topic, form=self.form()
+        )
 
 
     def post(self, topic_id, slug=None):
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -358,7 +415,10 @@ class NewPost(MethodView):
         if form.validate_on_submit():
         if form.validate_on_submit():
             if 'preview' in request.form:
             if 'preview' in request.form:
                 return render_template(
                 return render_template(
-                    'forum/new_post.html', topic=topic, form=form, preview=form.content.data
+                    'forum/new_post.html',
+                    topic=topic,
+                    form=form,
+                    preview=form.content.data
                 )
                 )
             else:
             else:
                 post = form.save(real(current_user), topic)
                 post = form.save(real(current_user), topic)
@@ -373,7 +433,9 @@ class ReplyPost(MethodView):
 
 
     def get(self, topic_id, post_id):
     def get(self, topic_id, post_id):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
-        return render_template('forum/new_post.html', topic=topic, form=self.form())
+        return render_template(
+            'forum/new_post.html', topic=topic, form=self.form()
+        )
 
 
     def post(self, topic_id, post_id):
     def post(self, topic_id, post_id):
         form = self.form()
         form = self.form()
@@ -381,7 +443,10 @@ class ReplyPost(MethodView):
         if form.validate_on_submit():
         if form.validate_on_submit():
             if 'preview' in request.form:
             if 'preview' in request.form:
                 return render_template(
                 return render_template(
-                    'forum/new_post.html', topic=topic, form=form, preview=form.content.data
+                    'forum/new_post.html',
+                    topic=topic,
+                    form=form,
+                    preview=form.content.data
                 )
                 )
             else:
             else:
                 post = form.save(real(current_user), topic)
                 post = form.save(real(current_user), topic)
@@ -389,7 +454,9 @@ class ReplyPost(MethodView):
         else:
         else:
             form.content.data = format_quote(post.username, post.content)
             form.content.data = format_quote(post.username, post.content)
 
 
-        return render_template('forum/new_post.html', topic=post.topic, form=form)
+        return render_template(
+            'forum/new_post.html', topic=post.topic, form=form
+        )
 
 
 
 
 class EditPost(MethodView):
 class EditPost(MethodView):
@@ -399,7 +466,9 @@ class EditPost(MethodView):
     def get(self, post_id):
     def get(self, post_id):
         post = Post.query.filter_by(id=post_id).first_or_404()
         post = Post.query.filter_by(id=post_id).first_or_404()
         form = self.form(obj=post)
         form = self.form(obj=post)
-        return render_template('forum/new_post.html', topic=post.topic, form=form, edit_mode=True)
+        return render_template(
+            'forum/new_post.html', topic=post.topic, form=form, edit_mode=True
+        )
 
 
     def post(self, post_id):
     def post(self, post_id):
         post = Post.query.filter_by(id=post_id).first_or_404()
         post = Post.query.filter_by(id=post_id).first_or_404()
@@ -421,7 +490,9 @@ class EditPost(MethodView):
                 post.save()
                 post.save()
                 return redirect(url_for('forum.view_post', post_id=post.id))
                 return redirect(url_for('forum.view_post', post_id=post.id))
 
 
-        return render_template('forum/new_post.html', topic=post.topic, form=form, edit_mode=True)
+        return render_template(
+            'forum/new_post.html', topic=post.topic, form=form, edit_mode=True
+        )
 
 
 
 
 class ReportView(MethodView):
 class ReportView(MethodView):
@@ -464,7 +535,9 @@ class MemberList(MethodView):
         users = User.query.order_by(order_func(sort_obj)).paginate(
         users = User.query.order_by(order_func(sort_obj)).paginate(
             page, flaskbb_config['USERS_PER_PAGE'], False
             page, flaskbb_config['USERS_PER_PAGE'], False
         )
         )
-        return render_template('forum/memberlist.html', users=users, search_form=self.form())
+        return render_template(
+            'forum/memberlist.html', users=users, search_form=self.form()
+        )
 
 
     def post(self):
     def post(self):
         page = request.args.get('page', 1, type=int)
         page = request.args.get('page', 1, type=int)
@@ -485,13 +558,19 @@ class MemberList(MethodView):
 
 
         form = self.form()
         form = self.form()
         if form.validate():
         if form.validate():
-            users = form.get_results().paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
-            return render_template('forum/memberlist.html', users=users, search_form=form)
+            users = form.get_results().paginate(
+                page, flaskbb_config['USERS_PER_PAGE'], False
+            )
+            return render_template(
+                'forum/memberlist.html', users=users, search_form=form
+            )
 
 
         users = User.query.order_by(order_func(sort_obj)).paginate(
         users = User.query.order_by(order_func(sort_obj)).paginate(
             page, flaskbb_config['USERS_PER_PAGE'], False
             page, flaskbb_config['USERS_PER_PAGE'], False
         )
         )
-        return render_template('forum/memberlist.html', users=users, search_form=form)
+        return render_template(
+            'forum/memberlist.html', users=users, search_form=form
+        )
 
 
 
 
 class TopicTracker(MethodView):
 class TopicTracker(MethodView):
@@ -501,7 +580,10 @@ class TopicTracker(MethodView):
         page = request.args.get('page', 1, type=int)
         page = request.args.get('page', 1, type=int)
         topics = real(current_user).tracked_topics.outerjoin(
         topics = real(current_user).tracked_topics.outerjoin(
             TopicsRead,
             TopicsRead,
-            db.and_(TopicsRead.topic_id == Topic.id, TopicsRead.user_id == real(current_user).id)
+            db.and_(
+                TopicsRead.topic_id == Topic.id,
+                TopicsRead.user_id == real(current_user).id
+            )
         ).add_entity(TopicsRead).order_by(Topic.last_updated.desc()).paginate(
         ).add_entity(TopicsRead).order_by(Topic.last_updated.desc()).paginate(
             page, flaskbb_config['TOPICS_PER_PAGE'], True
             page, flaskbb_config['TOPICS_PER_PAGE'], True
         )
         )
@@ -517,7 +599,10 @@ class TopicTracker(MethodView):
 
 
         real(current_user).save()
         real(current_user).save()
 
 
-        flash(_('%(topic_count)s topics untracked.', topic_count=len(tmp_topics)), 'success')
+        flash(
+            _('%(topic_count)s topics untracked.', topic_count=len(tmp_topics)),
+            'success'
+        )
         return redirect(url_for('forum.topictracker'))
         return redirect(url_for('forum.topictracker'))
 
 
 
 
@@ -531,7 +616,9 @@ class Search(MethodView):
         form = self.form()
         form = self.form()
         if form.validate_on_submit():
         if form.validate_on_submit():
             result = form.get_results()
             result = form.get_results()
-            return render_template('forum/search_result.html', form=form, result=result)
+            return render_template(
+                'forum/search_result.html', form=form, result=result
+            )
 
 
         return render_template('forum/search_form.html', form=form)
         return render_template('forum/search_form.html', form=form)
 
 
@@ -541,8 +628,9 @@ class DeleteTopic(MethodView):
 
 
     def post(self, topic_id, slug=None):
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
-        involved_users = User.query.filter(Post.topic_id == topic.id,
-                                           User.id == Post.user_id).all()
+        involved_users = User.query.filter(
+            Post.topic_id == topic.id, User.id == Post.user_id
+        ).all()
         topic.delete(users=involved_users)
         topic.delete(users=involved_users)
         return redirect(url_for('forum.view_forum', forum_id=topic.forum_id))
         return redirect(url_for('forum.view_forum', forum_id=topic.forum_id))
 
 
@@ -637,7 +725,12 @@ class MarkRead(MethodView):
             db.session.add(forumsread)
             db.session.add(forumsread)
             db.session.commit()
             db.session.commit()
 
 
-            flash(_('Forum %(forum)s marked as read.', forum=forum_instance.title), 'success')
+            flash(
+                _(
+                    'Forum %(forum)s marked as read.',
+                    forum=forum_instance.title
+                ), 'success'
+            )
 
 
             return redirect(forum_instance.url)
             return redirect(forum_instance.url)
 
 
@@ -670,7 +763,9 @@ class WhoIsOnline(MethodView):
             online_users = get_online_users()
             online_users = get_online_users()
         else:
         else:
             online_users = User.query.filter(User.lastseen >= time_diff()).all()
             online_users = User.query.filter(User.lastseen >= time_diff()).all()
-        return render_template('forum/online_users.html', online_users=online_users)
+        return render_template(
+            'forum/online_users.html', online_users=online_users
+        )
 
 
 
 
 class TrackTopic(MethodView):
 class TrackTopic(MethodView):
@@ -698,7 +793,8 @@ class HideTopic(MethodView):
 
 
     def post(self, topic_id, slug=None):
     def post(self, topic_id, slug=None):
         topic = Topic.query.with_hidden().filter_by(id=topic_id).first_or_404()
         topic = Topic.query.with_hidden().filter_by(id=topic_id).first_or_404()
-        if not Permission(Has('makehidden'), IsAtleastModeratorInForum(forum=topic.forum)):
+        if not Permission(Has('makehidden'), IsAtleastModeratorInForum(
+                forum=topic.forum)):
             flash(_("You do not have permission to hide this topic"), "danger")
             flash(_("You do not have permission to hide this topic"), "danger")
             return redirect(topic.url)
             return redirect(topic.url)
         topic.hide(user=current_user)
         topic.hide(user=current_user)
@@ -714,8 +810,11 @@ class UnhideTopic(MethodView):
 
 
     def post(self, topic_id, slug=None):
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
-        if not Permission(Has('makehidden'), IsAtleastModeratorInForum(forum=topic.forum)):
-            flash(_("You do not have permission to unhide this topic"), "danger")
+        if not Permission(Has('makehidden'), IsAtleastModeratorInForum(
+                forum=topic.forum)):
+            flash(
+                _("You do not have permission to unhide this topic"), "danger"
+            )
             return redirect(topic.url)
             return redirect(topic.url)
         topic.unhide()
         topic.unhide()
         topic.save()
         topic.save()
@@ -728,7 +827,8 @@ class HidePost(MethodView):
     def post(self, post_id):
     def post(self, post_id):
         post = Post.query.filter(Post.id == post_id).first_or_404()
         post = Post.query.filter(Post.id == post_id).first_or_404()
 
 
-        if not Permission(Has('makehidden'), IsAtleastModeratorInForum(forum=post.topic.forum)):
+        if not Permission(Has('makehidden'), IsAtleastModeratorInForum(
+                forum=post.topic.forum)):
             flash(_("You do not have permission to hide this post"), "danger")
             flash(_("You do not have permission to hide this post"), "danger")
             return redirect(post.topic.url)
             return redirect(post.topic.url)
 
 
@@ -757,7 +857,8 @@ class UnhidePost(MethodView):
     def post(self, post_id):
     def post(self, post_id):
         post = Post.query.filter(Post.id == post_id).first_or_404()
         post = Post.query.filter(Post.id == post_id).first_or_404()
 
 
-        if not Permission(Has('makehidden'), IsAtleastModeratorInForum(forum=post.topic.forum)):
+        if not Permission(Has('makehidden'), IsAtleastModeratorInForum(
+                forum=post.topic.forum)):
             flash(_("You do not have permission to unhide this post"), "danger")
             flash(_("You do not have permission to unhide this post"), "danger")
             return redirect(post.topic.url)
             return redirect(post.topic.url)
 
 
@@ -771,106 +872,179 @@ class UnhidePost(MethodView):
         return redirect(post.topic.url)
         return redirect(post.topic.url)
 
 
 
 
-register_view(
-    forum,
-    routes=['/category/<int:category_id>', '/category/<int:category_id>-<slug>'],
-    view_func=ViewCategory.as_view('view_category')
-)
-register_view(
-    forum,
-    routes=['/forum/<int:forum_id>/edit', '/forum/<int:forum_id>-<slug>/edit'],
-    view_func=ManageForum.as_view('manage_forum')
-)
-register_view(
-    forum,
-    routes=['/forum/<int:forum_id>', '/forum/<int:forum_id>-<slug>'],
-    view_func=ViewForum.as_view('view_forum')
-)
-register_view(
-    forum,
-    routes=['/<int:forum_id>/markread', '/<int:forum_id>-<slug>/markread'],
-    view_func=MarkRead.as_view('markread')
-)
-register_view(
-    forum,
-    routes=['/<int:forum_id>/topic/new', '/<int:forum_id>-<slug>/topic/new'],
-    view_func=NewTopic.as_view('new_topic')
-)
-register_view(forum, routes=['/memberlist'], view_func=MemberList.as_view('memberlist'))
-register_view(
-    forum, routes=['/post/<int:post_id>/delete'], view_func=DeletePost.as_view('delete_post')
-)
-register_view(forum, routes=['/post/<int:post_id>/edit'], view_func=EditPost.as_view('edit_post'))
-register_view(forum, routes=['/post/<int:post_id>/raw'], view_func=RawPost.as_view('raw_post'))
-register_view(
-    forum, routes=['/post/<int:post_id>/report'], view_func=ReportView.as_view('report_post')
-)
-register_view(forum, routes=['/post/<int:post_id>'], view_func=ViewPost.as_view('view_post'))
-register_view(forum, routes=['/search'], view_func=Search.as_view('search'))
-register_view(
-    forum,
-    routes=['/topic/<int:topic_id>/delete', '/topic/<int:topic_id>-<slug>/delete'],
-    view_func=DeleteTopic.as_view('delete_topic')
-)
-register_view(
-    forum,
-    routes=['/topic/<int:topic_id>/highlight', '/topic/<int:topic_id>-<slug>/highlight'],
-    view_func=HighlightTopic.as_view('highlight_topic')
-)
-register_view(
-    forum,
-    routes=['/topic/<int:topic_id>/lock', '/topic/<int:topic_id>-<slug>/lock'],
-    view_func=LockTopic.as_view('lock_topic')
-)
-register_view(
-    forum,
-    routes=['/topic/<int:topic_id>/post/<int:post_id>/reply'],
-    view_func=ReplyPost.as_view('reply_post')
-)
-register_view(
-    forum,
-    routes=['/topic/<int:topic_id>/post/new', '/topic/<int:topic_id>-<slug>/post/new'],
-    view_func=NewPost.as_view('new_post')
-)
-register_view(
-    forum,
-    routes=['/topic/<int:topic_id>', '/topic/<int:topic_id>-<slug>'],
-    view_func=ViewTopic.as_view('view_topic')
-)
-register_view(
-    forum,
-    routes=['/topic/<int:topic_id>/trivialize', '/topic/<int:topic_id>-<slug>/trivialize'],
-    view_func=TrivializeTopic.as_view('trivialize_topic')
-)
-register_view(
-    forum,
-    routes=['/topic/<int:topic_id>/unlock', '/topic/<int:topic_id>-<slug>/unlock'],
-    view_func=UnlockTopic.as_view('unlock_topic')
-)
-register_view(
-    forum,
-    routes=['/topictracker/<int:topic_id>/add', '/topictracker/<int:topic_id>-<slug>/add'],
-    view_func=TrackTopic.as_view('track_topic')
-)
-register_view(
-    forum,
-    routes=['/topictracker/<int:topic_id>/delete', '/topictracker/<int:topic_id>-<slug>/delete'],
-    view_func=UntrackTopic.as_view('untrack_topic')
-)
-register_view(forum, routes=['/topictracker'], view_func=TopicTracker.as_view('topictracker'))
-register_view(forum, routes=['/'], view_func=ForumIndex.as_view('index'))
-register_view(forum, routes=['/who-is-online'], view_func=WhoIsOnline.as_view('who_is_online'))
-register_view(
-    forum,
-    routes=["/topic/<int:topic_id>/hide", "/topic/<int:topic_id>-<slug>/hide"],
-    view_func=HideTopic.as_view('hide_topic')
-)
-register_view(
-    forum,
-    routes=["/topic/<int:topic_id>/unhide", "/topic/<int:topic_id>-<slug>/unhide"],
-    view_func=UnhideTopic.as_view('unhide_topic')
-)
-register_view(forum, routes=["/post/<int:post_id>/hide"], view_func=HidePost.as_view('hide_post'))
-register_view(
-    forum, routes=["/post/<int:post_id>/unhide"], view_func=UnhidePost.as_view('unhide_post')
-)
+@impl(tryfirst=True)
+def flaskbb_load_blueprints(app):
+    forum = Blueprint("forum", __name__)
+    register_view(
+        forum,
+        routes=[
+            '/category/<int:category_id>', '/category/<int:category_id>-<slug>'
+        ],
+        view_func=ViewCategory.as_view('view_category')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/forum/<int:forum_id>/edit', '/forum/<int:forum_id>-<slug>/edit'
+        ],
+        view_func=ManageForum.as_view('manage_forum')
+    )
+    register_view(
+        forum,
+        routes=['/forum/<int:forum_id>', '/forum/<int:forum_id>-<slug>'],
+        view_func=ViewForum.as_view('view_forum')
+    )
+    register_view(
+        forum,
+        routes=['/<int:forum_id>/markread', '/<int:forum_id>-<slug>/markread'],
+        view_func=MarkRead.as_view('markread')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/<int:forum_id>/topic/new', '/<int:forum_id>-<slug>/topic/new'
+        ],
+        view_func=NewTopic.as_view('new_topic')
+    )
+    register_view(
+        forum,
+        routes=['/memberlist'],
+        view_func=MemberList.as_view('memberlist')
+    )
+    register_view(
+        forum,
+        routes=['/post/<int:post_id>/delete'],
+        view_func=DeletePost.as_view('delete_post')
+    )
+    register_view(
+        forum,
+        routes=['/post/<int:post_id>/edit'],
+        view_func=EditPost.as_view('edit_post')
+    )
+    register_view(
+        forum,
+        routes=['/post/<int:post_id>/raw'],
+        view_func=RawPost.as_view('raw_post')
+    )
+    register_view(
+        forum,
+        routes=['/post/<int:post_id>/report'],
+        view_func=ReportView.as_view('report_post')
+    )
+    register_view(
+        forum,
+        routes=['/post/<int:post_id>'],
+        view_func=ViewPost.as_view('view_post')
+    )
+    register_view(forum, routes=['/search'], view_func=Search.as_view('search'))
+    register_view(
+        forum,
+        routes=[
+            '/topic/<int:topic_id>/delete',
+            '/topic/<int:topic_id>-<slug>/delete'
+        ],
+        view_func=DeleteTopic.as_view('delete_topic')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/topic/<int:topic_id>/highlight',
+            '/topic/<int:topic_id>-<slug>/highlight'
+        ],
+        view_func=HighlightTopic.as_view('highlight_topic')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/topic/<int:topic_id>/lock', '/topic/<int:topic_id>-<slug>/lock'
+        ],
+        view_func=LockTopic.as_view('lock_topic')
+    )
+    register_view(
+        forum,
+        routes=['/topic/<int:topic_id>/post/<int:post_id>/reply'],
+        view_func=ReplyPost.as_view('reply_post')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/topic/<int:topic_id>/post/new',
+            '/topic/<int:topic_id>-<slug>/post/new'
+        ],
+        view_func=NewPost.as_view('new_post')
+    )
+    register_view(
+        forum,
+        routes=['/topic/<int:topic_id>', '/topic/<int:topic_id>-<slug>'],
+        view_func=ViewTopic.as_view('view_topic')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/topic/<int:topic_id>/trivialize',
+            '/topic/<int:topic_id>-<slug>/trivialize'
+        ],
+        view_func=TrivializeTopic.as_view('trivialize_topic')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/topic/<int:topic_id>/unlock',
+            '/topic/<int:topic_id>-<slug>/unlock'
+        ],
+        view_func=UnlockTopic.as_view('unlock_topic')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/topictracker/<int:topic_id>/add',
+            '/topictracker/<int:topic_id>-<slug>/add'
+        ],
+        view_func=TrackTopic.as_view('track_topic')
+    )
+    register_view(
+        forum,
+        routes=[
+            '/topictracker/<int:topic_id>/delete',
+            '/topictracker/<int:topic_id>-<slug>/delete'
+        ],
+        view_func=UntrackTopic.as_view('untrack_topic')
+    )
+    register_view(
+        forum,
+        routes=['/topictracker'],
+        view_func=TopicTracker.as_view('topictracker')
+    )
+    register_view(forum, routes=['/'], view_func=ForumIndex.as_view('index'))
+    register_view(
+        forum,
+        routes=['/who-is-online'],
+        view_func=WhoIsOnline.as_view('who_is_online')
+    )
+    register_view(
+        forum,
+        routes=[
+            "/topic/<int:topic_id>/hide", "/topic/<int:topic_id>-<slug>/hide"
+        ],
+        view_func=HideTopic.as_view('hide_topic')
+    )
+    register_view(
+        forum,
+        routes=[
+            "/topic/<int:topic_id>/unhide",
+            "/topic/<int:topic_id>-<slug>/unhide"
+        ],
+        view_func=UnhideTopic.as_view('unhide_topic')
+    )
+    register_view(
+        forum,
+        routes=["/post/<int:post_id>/hide"],
+        view_func=HidePost.as_view('hide_post')
+    )
+    register_view(
+        forum,
+        routes=["/post/<int:post_id>/unhide"],
+        view_func=UnhidePost.as_view('unhide_post')
+    )
+
+    app.register_blueprint(forum, url_prefix=app.config["FORUM_URL_PREFIX"])

+ 346 - 238
flaskbb/management/views.py

@@ -19,6 +19,8 @@ from flask.views import MethodView
 from flask_allows import Not, Permission
 from flask_allows import Not, Permission
 from flask_babelplus import gettext as _
 from flask_babelplus import gettext as _
 from flask_login import current_user, login_fresh
 from flask_login import current_user, login_fresh
+from pluggy import HookimplMarker
+
 from flaskbb import __version__ as flaskbb_version
 from flaskbb import __version__ as flaskbb_version
 from flaskbb.extensions import allows, celery, db
 from flaskbb.extensions import allows, celery, db
 from flaskbb.forum.forms import UserSearchForm
 from flaskbb.forum.forms import UserSearchForm
@@ -28,28 +30,19 @@ from flaskbb.management.forms import (AddForumForm, AddGroupForm, AddUserForm,
                                       EditGroupForm, EditUserForm)
                                       EditGroupForm, EditUserForm)
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.plugins.models import PluginRegistry, PluginStore
 from flaskbb.plugins.models import PluginRegistry, PluginStore
-from flaskbb.user.models import Group, Guest, User
 from flaskbb.plugins.utils import validate_plugin
 from flaskbb.plugins.utils import validate_plugin
+from flaskbb.user.models import Group, Guest, User
+from flaskbb.utils.forms import populate_settings_dict, populate_settings_form
 from flaskbb.utils.helpers import (get_online_users, register_view,
 from flaskbb.utils.helpers import (get_online_users, register_view,
                                    render_template, time_diff, time_utcnow)
                                    render_template, time_diff, time_utcnow)
 from flaskbb.utils.requirements import (CanBanUser, CanEditUser, IsAdmin,
 from flaskbb.utils.requirements import (CanBanUser, CanEditUser, IsAdmin,
                                         IsAtleastModerator,
                                         IsAtleastModerator,
                                         IsAtleastSuperModerator)
                                         IsAtleastSuperModerator)
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
-from flaskbb.utils.forms import populate_settings_dict, populate_settings_form
-
-logger = logging.getLogger(__name__)
-
 
 
-management = Blueprint("management", __name__)
+impl = HookimplMarker('flaskbb')
 
 
-
-@management.before_request
-def check_fresh_login():
-    """Checks if the login is fresh for the current user, otherwise the user
-    has to reauthenticate."""
-    if not login_fresh():
-        return current_app.login_manager.needs_refresh()
+logger = logging.getLogger(__name__)
 
 
 
 
 class ManagementSettings(MethodView):
 class ManagementSettings(MethodView):
@@ -65,9 +58,14 @@ class ManagementSettings(MethodView):
         active_nav = {}  # used to build the navigation
         active_nav = {}  # used to build the navigation
         plugin_obj = None
         plugin_obj = None
         if plugin is not None:
         if plugin is not None:
-            plugin_obj = PluginRegistry.query.filter_by(name=plugin).first_or_404()
-            active_nav.update({'key': plugin_obj.name,
-                               'title': plugin_obj.name.title()})
+            plugin_obj = PluginRegistry.query.filter_by(name=plugin
+                                                        ).first_or_404()
+            active_nav.update(
+                {
+                    'key': plugin_obj.name,
+                    'title': plugin_obj.name.title()
+                }
+            )
             form = plugin_obj.get_settings_form()
             form = plugin_obj.get_settings_form()
             old_settings = plugin_obj.settings
             old_settings = plugin_obj.settings
 
 
@@ -85,12 +83,18 @@ class ManagementSettings(MethodView):
 
 
         # get all groups and plugins - used to build the navigation
         # get all groups and plugins - used to build the navigation
         all_groups = SettingsGroup.query.all()
         all_groups = SettingsGroup.query.all()
-        all_plugins = PluginRegistry.query.filter(PluginRegistry.values != None).all()
+        all_plugins = PluginRegistry.query.filter(
+            PluginRegistry.values != None
+        ).all()
         form = populate_settings_form(form, old_settings)
         form = populate_settings_form(form, old_settings)
 
 
-        return render_template("management/settings.html", form=form,
-                               all_groups=all_groups, all_plugins=all_plugins,
-                               active_nav=active_nav)
+        return render_template(
+            "management/settings.html",
+            form=form,
+            all_groups=all_groups,
+            all_plugins=all_plugins,
+            active_nav=active_nav
+        )
 
 
     def post(self, slug=None, plugin=None):
     def post(self, slug=None, plugin=None):
         form, old_settings, plugin_obj, active_nav = \
         form, old_settings, plugin_obj, active_nav = \
@@ -108,9 +112,13 @@ class ManagementSettings(MethodView):
 
 
             flash(_("Settings saved."), "success")
             flash(_("Settings saved."), "success")
 
 
-        return render_template("management/settings.html", form=form,
-                               all_groups=all_groups, all_plugins=all_plugins,
-                               active_nav=active_nav)
+        return render_template(
+            "management/settings.html",
+            form=form,
+            all_groups=all_groups,
+            all_plugins=all_plugins,
+            active_nav=active_nav
+        )
 
 
 
 
 class ManageUsers(MethodView):
 class ManageUsers(MethodView):
@@ -125,7 +133,9 @@ class ManageUsers(MethodView):
             page, flaskbb_config['USERS_PER_PAGE'], False
             page, flaskbb_config['USERS_PER_PAGE'], False
         )
         )
 
 
-        return render_template('management/users.html', users=users, search_form=form)
+        return render_template(
+            'management/users.html', users=users, search_form=form
+        )
 
 
     def post(self):
     def post(self):
         page = request.args.get('page', 1, type=int)
         page = request.args.get('page', 1, type=int)
@@ -134,13 +144,17 @@ class ManageUsers(MethodView):
         if form.validate():
         if form.validate():
             users = form.get_results().\
             users = form.get_results().\
                 paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
                 paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
-            return render_template('management/users.html', users=users, search_form=form)
+            return render_template(
+                'management/users.html', users=users, search_form=form
+            )
 
 
         users = User.query.order_by(User.id.asc()).paginate(
         users = User.query.order_by(User.id.asc()).paginate(
             page, flaskbb_config['USERS_PER_PAGE'], False
             page, flaskbb_config['USERS_PER_PAGE'], False
         )
         )
 
 
-        return render_template('management/users.html', users=users, search_form=form)
+        return render_template(
+            'management/users.html', users=users, search_form=form
+        )
 
 
 
 
 class EditUser(MethodView):
 class EditUser(MethodView):
@@ -151,13 +165,15 @@ class EditUser(MethodView):
         user = User.query.filter_by(id=user_id).first_or_404()
         user = User.query.filter_by(id=user_id).first_or_404()
         form = self.form(user)
         form = self.form(user)
         member_group = db.and_(
         member_group = db.and_(
-            *[
+            * [
                 db.not_(getattr(Group, p))
                 db.not_(getattr(Group, p))
                 for p in ['admin', 'mod', 'super_mod', 'banned', 'guest']
                 for p in ['admin', 'mod', 'super_mod', 'banned', 'guest']
             ]
             ]
         )
         )
 
 
-        filt = db.or_(Group.id.in_(g.id for g in current_user.groups), member_group)
+        filt = db.or_(
+            Group.id.in_(g.id for g in current_user.groups), member_group
+        )
 
 
         if Permission(IsAtleastSuperModerator, identity=current_user):
         if Permission(IsAtleastSuperModerator, identity=current_user):
             filt = db.or_(filt, Group.mod)
             filt = db.or_(filt, Group.mod)
@@ -173,19 +189,23 @@ class EditUser(MethodView):
         form.primary_group.query = group_query
         form.primary_group.query = group_query
         form.secondary_groups.query = group_query
         form.secondary_groups.query = group_query
 
 
-        return render_template('management/user_form.html', form=form, title=_('Edit User'))
+        return render_template(
+            'management/user_form.html', form=form, title=_('Edit User')
+        )
 
 
     def post(self, user_id):
     def post(self, user_id):
         user = User.query.filter_by(id=user_id).first_or_404()
         user = User.query.filter_by(id=user_id).first_or_404()
 
 
         member_group = db.and_(
         member_group = db.and_(
-            *[
+            * [
                 db.not_(getattr(Group, p))
                 db.not_(getattr(Group, p))
                 for p in ['admin', 'mod', 'super_mod', 'banned', 'guest']
                 for p in ['admin', 'mod', 'super_mod', 'banned', 'guest']
             ]
             ]
         )
         )
 
 
-        filt = db.or_(Group.id.in_(g.id for g in current_user.groups), member_group)
+        filt = db.or_(
+            Group.id.in_(g.id for g in current_user.groups), member_group
+        )
 
 
         if Permission(IsAtleastSuperModerator, identity=current_user):
         if Permission(IsAtleastSuperModerator, identity=current_user):
             filt = db.or_(filt, Group.mod)
             filt = db.or_(filt, Group.mod)
@@ -214,7 +234,9 @@ class EditUser(MethodView):
             flash(_('User updated.'), 'success')
             flash(_('User updated.'), 'success')
             return redirect(url_for('management.edit_user', user_id=user.id))
             return redirect(url_for('management.edit_user', user_id=user.id))
 
 
-        return render_template('management/user_form.html', form=form, title=_('Edit User'))
+        return render_template(
+            'management/user_form.html', form=form, title=_('Edit User')
+        )
 
 
 
 
 class DeleteUser(MethodView):
 class DeleteUser(MethodView):
@@ -264,7 +286,9 @@ class AddUser(MethodView):
     form = AddUserForm
     form = AddUserForm
 
 
     def get(self):
     def get(self):
-        return render_template('management/user_form.html', form=self.form(), title=_('Add User'))
+        return render_template(
+            'management/user_form.html', form=self.form(), title=_('Add User')
+        )
 
 
     def post(self):
     def post(self):
         form = self.form()
         form = self.form()
@@ -273,7 +297,9 @@ class AddUser(MethodView):
             flash(_('User added.'), 'success')
             flash(_('User added.'), 'success')
             return redirect(url_for('management.users'))
             return redirect(url_for('management.users'))
 
 
-        return render_template('management/user_form.html', form=form, title=_('Add User'))
+        return render_template(
+            'management/user_form.html', form=form, title=_('Add User')
+        )
 
 
 
 
 class BannedUsers(MethodView):
 class BannedUsers(MethodView):
@@ -284,30 +310,38 @@ class BannedUsers(MethodView):
         page = request.args.get('page', 1, type=int)
         page = request.args.get('page', 1, type=int)
         search_form = self.form()
         search_form = self.form()
 
 
-        users = User.query.filter(Group.banned == True, Group.id == User.primary_group_id
-                                  ).paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
+        users = User.query.filter(
+            Group.banned == True, Group.id == User.primary_group_id
+        ).paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
 
         return render_template(
         return render_template(
-            'management/banned_users.html', users=users, search_form=search_form
+            'management/banned_users.html',
+            users=users,
+            search_form=search_form
         )
         )
 
 
     def post(self):
     def post(self):
         page = request.args.get('page', 1, type=int)
         page = request.args.get('page', 1, type=int)
         search_form = self.form()
         search_form = self.form()
 
 
-        users = User.query.filter(Group.banned == True, Group.id == User.primary_group_id
-                                  ).paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
+        users = User.query.filter(
+            Group.banned == True, Group.id == User.primary_group_id
+        ).paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
 
         if search_form.validate():
         if search_form.validate():
             users = search_form.get_results().\
             users = search_form.get_results().\
                 paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
                 paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
 
             return render_template(
             return render_template(
-                'management/banned_users.html', users=users, search_form=search_form
+                'management/banned_users.html',
+                users=users,
+                search_form=search_form
             )
             )
 
 
         return render_template(
         return render_template(
-            'management/banned_users.html', users=users, search_form=search_form
+            'management/banned_users.html',
+            users=users,
+            search_form=search_form
         )
         )
 
 
 
 
@@ -316,7 +350,9 @@ class BanUser(MethodView):
 
 
     def post(self, user_id=None):
     def post(self, user_id=None):
         if not Permission(CanBanUser, identity=current_user):
         if not Permission(CanBanUser, identity=current_user):
-            flash(_("You do not have the permissions to ban this user."), "danger")
+            flash(
+                _("You do not have the permissions to ban this user."), "danger"
+            )
             return redirect(url_for("management.overview"))
             return redirect(url_for("management.overview"))
 
 
         # ajax request
         # ajax request
@@ -328,18 +364,24 @@ class BanUser(MethodView):
             for user in users:
             for user in users:
                 # don't let a user ban himself and do not allow a moderator to ban
                 # don't let a user ban himself and do not allow a moderator to ban
                 # a admin user
                 # a admin user
-                if (current_user.id == user.id or Permission(IsAdmin, identity=user)
+                if (current_user.id == user.id
+                        or Permission(IsAdmin, identity=user)
                         and Permission(Not(IsAdmin), current_user)):
                         and Permission(Not(IsAdmin), current_user)):
                     continue
                     continue
 
 
                 elif user.ban():
                 elif user.ban():
                     data.append(
                     data.append(
                         {
                         {
-                            "id": user.id,
-                            "type": "ban",
-                            "reverse": "unban",
-                            "reverse_name": _("Unban"),
-                            "reverse_url": url_for("management.unban_user", user_id=user.id)
+                            "id":
+                            user.id,
+                            "type":
+                            "ban",
+                            "reverse":
+                            "unban",
+                            "reverse_name":
+                            _("Unban"),
+                            "reverse_url":
+                            url_for("management.unban_user", user_id=user.id)
                         }
                         }
                     )
                     )
 
 
@@ -352,7 +394,8 @@ class BanUser(MethodView):
 
 
         user = User.query.filter_by(id=user_id).first_or_404()
         user = User.query.filter_by(id=user_id).first_or_404()
         # Do not allow moderators to ban admins
         # Do not allow moderators to ban admins
-        if Permission(IsAdmin, identity=user) and Permission(Not(IsAdmin), identity=current_user):
+        if Permission(IsAdmin, identity=user) and Permission(
+                Not(IsAdmin), identity=current_user):
             flash(_("A moderator cannot ban an admin user."), "danger")
             flash(_("A moderator cannot ban an admin user."), "danger")
             return redirect(url_for("management.overview"))
             return redirect(url_for("management.overview"))
 
 
@@ -369,7 +412,10 @@ class UnbanUser(MethodView):
     def post(self, user_id=None):
     def post(self, user_id=None):
 
 
         if not Permission(CanBanUser, identity=current_user):
         if not Permission(CanBanUser, identity=current_user):
-            flash(_("You do not have the permissions to unban this user."), "danger")
+            flash(
+                _("You do not have the permissions to unban this user."),
+                "danger"
+            )
             return redirect(url_for("management.overview"))
             return redirect(url_for("management.overview"))
 
 
         # ajax request
         # ajax request
@@ -426,7 +472,9 @@ class AddGroup(MethodView):
 
 
     def get(self):
     def get(self):
         return render_template(
         return render_template(
-            'management/group_form.html', form=self.form(), title=_('Add Group')
+            'management/group_form.html',
+            form=self.form(),
+            title=_('Add Group')
         )
         )
 
 
     def post(self):
     def post(self):
@@ -436,7 +484,9 @@ class AddGroup(MethodView):
             flash(_('Group added.'), 'success')
             flash(_('Group added.'), 'success')
             return redirect(url_for('management.groups'))
             return redirect(url_for('management.groups'))
 
 
-        return render_template('management/group_form.html', form=form, title=_('Add Group'))
+        return render_template(
+            'management/group_form.html', form=form, title=_('Add Group')
+        )
 
 
 
 
 class EditGroup(MethodView):
 class EditGroup(MethodView):
@@ -446,7 +496,9 @@ class EditGroup(MethodView):
     def get(self, group_id):
     def get(self, group_id):
         group = Group.query.filter_by(id=group_id).first_or_404()
         group = Group.query.filter_by(id=group_id).first_or_404()
         form = self.form(group)
         form = self.form(group)
-        return render_template('management/group_form.html', form=form, title=_('Edit Group'))
+        return render_template(
+            'management/group_form.html', form=form, title=_('Edit Group')
+        )
 
 
     def post(self, group_id):
     def post(self, group_id):
         group = Group.query.filter_by(id=group_id).first_or_404()
         group = Group.query.filter_by(id=group_id).first_or_404()
@@ -462,7 +514,9 @@ class EditGroup(MethodView):
             flash(_('Group updated.'), 'success')
             flash(_('Group updated.'), 'success')
             return redirect(url_for('management.groups', group_id=group.id))
             return redirect(url_for('management.groups', group_id=group.id))
 
 
-        return render_template('management/group_form.html', form=form, title=_('Edit Group'))
+        return render_template(
+            'management/group_form.html', form=form, title=_('Edit Group')
+        )
 
 
 
 
 class DeleteGroup(MethodView):
 class DeleteGroup(MethodView):
@@ -535,11 +589,15 @@ class EditForum(MethodView):
         form = self.form(forum)
         form = self.form(forum)
 
 
         if forum.moderators:
         if forum.moderators:
-            form.moderators.data = ','.join([user.username for user in forum.moderators])
+            form.moderators.data = ','.join(
+                [user.username for user in forum.moderators]
+            )
         else:
         else:
             form.moderators.data = None
             form.moderators.data = None
 
 
-        return render_template('management/forum_form.html', form=form, title=_('Edit Forum'))
+        return render_template(
+            'management/forum_form.html', form=form, title=_('Edit Forum')
+        )
 
 
     def post(self, forum_id):
     def post(self, forum_id):
         forum = Forum.query.filter_by(id=forum_id).first_or_404()
         forum = Forum.query.filter_by(id=forum_id).first_or_404()
@@ -551,11 +609,15 @@ class EditForum(MethodView):
             return redirect(url_for('management.edit_forum', forum_id=forum.id))
             return redirect(url_for('management.edit_forum', forum_id=forum.id))
         else:
         else:
             if forum.moderators:
             if forum.moderators:
-                form.moderators.data = ','.join([user.username for user in forum.moderators])
+                form.moderators.data = ','.join(
+                    [user.username for user in forum.moderators]
+                )
             else:
             else:
                 form.moderators.data = None
                 form.moderators.data = None
 
 
-        return render_template('management/forum_form.html', form=form, title=_('Edit Forum'))
+        return render_template(
+            'management/forum_form.html', form=form, title=_('Edit Forum')
+        )
 
 
 
 
 class AddForum(MethodView):
 class AddForum(MethodView):
@@ -571,7 +633,9 @@ class AddForum(MethodView):
             category = Category.query.filter_by(id=category_id).first()
             category = Category.query.filter_by(id=category_id).first()
             form.category.data = category
             form.category.data = category
 
 
-        return render_template('management/forum_form.html', form=form, title=_('Add Forum'))
+        return render_template(
+            'management/forum_form.html', form=form, title=_('Add Forum')
+        )
 
 
     def post(self, category_id=None):
     def post(self, category_id=None):
         form = self.form()
         form = self.form()
@@ -586,7 +650,9 @@ class AddForum(MethodView):
                 category = Category.query.filter_by(id=category_id).first()
                 category = Category.query.filter_by(id=category_id).first()
                 form.category.data = category
                 form.category.data = category
 
 
-        return render_template('management/forum_form.html', form=form, title=_('Add Forum'))
+        return render_template(
+            'management/forum_form.html', form=form, title=_('Add Forum')
+        )
 
 
 
 
 class DeleteForum(MethodView):
 class DeleteForum(MethodView):
@@ -595,8 +661,9 @@ class DeleteForum(MethodView):
     def post(self, forum_id):
     def post(self, forum_id):
         forum = Forum.query.filter_by(id=forum_id).first_or_404()
         forum = Forum.query.filter_by(id=forum_id).first_or_404()
 
 
-        involved_users = User.query.filter(Topic.forum_id == forum.id,
-                                           Post.user_id == User.id).all()
+        involved_users = User.query.filter(
+            Topic.forum_id == forum.id, Post.user_id == User.id
+        ).all()
 
 
         forum.delete(involved_users)
         forum.delete(involved_users)
 
 
@@ -610,7 +677,9 @@ class AddCategory(MethodView):
 
 
     def get(self):
     def get(self):
         return render_template(
         return render_template(
-            'management/category_form.html', form=self.form(), title=_('Add Category')
+            'management/category_form.html',
+            form=self.form(),
+            title=_('Add Category')
         )
         )
 
 
     def post(self):
     def post(self):
@@ -621,7 +690,9 @@ class AddCategory(MethodView):
             flash(_('Category added.'), 'success')
             flash(_('Category added.'), 'success')
             return redirect(url_for('management.forums'))
             return redirect(url_for('management.forums'))
 
 
-        return render_template('management/category_form.html', form=form, title=_('Add Category'))
+        return render_template(
+            'management/category_form.html', form=form, title=_('Add Category')
+        )
 
 
 
 
 class EditCategory(MethodView):
 class EditCategory(MethodView):
@@ -634,7 +705,9 @@ class EditCategory(MethodView):
         form = self.form(obj=category)
         form = self.form(obj=category)
 
 
         return render_template(
         return render_template(
-            'management/category_form.html', form=form, title=_('Edit Category')
+            'management/category_form.html',
+            form=form,
+            title=_('Edit Category')
         )
         )
 
 
     def post(self, category_id):
     def post(self, category_id):
@@ -648,7 +721,9 @@ class EditCategory(MethodView):
             category.save()
             category.save()
 
 
         return render_template(
         return render_template(
-            'management/category_form.html', form=form, title=_('Edit Category')
+            'management/category_form.html',
+            form=form,
+            title=_('Edit Category')
         )
         )
 
 
 
 
@@ -659,7 +734,8 @@ class DeleteCategory(MethodView):
         category = Category.query.filter_by(id=category_id).first_or_404()
         category = Category.query.filter_by(id=category_id).first_or_404()
 
 
         involved_users = User.query.filter(
         involved_users = User.query.filter(
-            Forum.category_id == category.id, Topic.forum_id == Forum.id, Post.user_id == User.id
+            Forum.category_id == category.id, Topic.forum_id == Forum.id,
+            Post.user_id == User.id
         ).all()
         ).all()
 
 
         category.delete(involved_users)
         category.delete(involved_users)
@@ -727,7 +803,10 @@ class MarkReportRead(MethodView):
         if report_id:
         if report_id:
             report = Report.query.filter_by(id=report_id).first_or_404()
             report = Report.query.filter_by(id=report_id).first_or_404()
             if report.zapped:
             if report.zapped:
-                flash(_("Report %(id)s is already marked as read.", id=report.id), "success")
+                flash(
+                    _("Report %(id)s is already marked as read.", id=report.id),
+                    "success"
+                )
                 return redirect(url_for("management.reports"))
                 return redirect(url_for("management.reports"))
 
 
             report.zapped_by = current_user.id
             report.zapped_by = current_user.id
@@ -790,10 +869,12 @@ class ManagementOverview(MethodView):
 
 
     def get(self):
     def get(self):
         # user and group stats
         # user and group stats
-        banned_users = User.query.filter(Group.banned == True,
-                                         Group.id == User.primary_group_id).count()
+        banned_users = User.query.filter(
+            Group.banned == True, Group.id == User.primary_group_id
+        ).count()
         if not current_app.config["REDIS_ENABLED"]:
         if not current_app.config["REDIS_ENABLED"]:
-            online_users = User.query.filter(User.lastseen >= time_diff()).count()
+            online_users = User.query.filter(User.lastseen >= time_diff()
+                                             ).count()
         else:
         else:
             online_users = len(get_online_users())
             online_users = len(get_online_users())
 
 
@@ -855,14 +936,21 @@ class EnablePlugin(MethodView):
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
 
 
         if plugin.enabled:
         if plugin.enabled:
-            flash(_("Plugin %(plugin)s is already enabled.", plugin=plugin.name), "info")
+            flash(
+                _("Plugin %(plugin)s is already enabled.", plugin=plugin.name),
+                "info"
+            )
             return redirect(url_for("management.plugins"))
             return redirect(url_for("management.plugins"))
 
 
         plugin.enabled = True
         plugin.enabled = True
         plugin.save()
         plugin.save()
 
 
-        flash(_("Plugin %(plugin)s enabled. Please restart FlaskBB now.",
-                plugin=plugin.name), "success")
+        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"))
 
 
 
 
@@ -874,14 +962,20 @@ class DisablePlugin(MethodView):
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
 
 
         if not plugin.enabled:
         if not plugin.enabled:
-            flash(_("Plugin %(plugin)s is already disabled.", plugin=plugin.name),
-                  "info")
+            flash(
+                _("Plugin %(plugin)s is already disabled.", plugin=plugin.name),
+                "info"
+            )
             return redirect(url_for("management.plugins"))
             return redirect(url_for("management.plugins"))
 
 
         plugin.enabled = False
         plugin.enabled = False
         plugin.save()
         plugin.save()
-        flash(_("Plugin %(plugin)s disabled. Please restart FlaskBB now.",
-                plugin=plugin.name), "success")
+        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"))
 
 
 
 
@@ -905,8 +999,12 @@ class InstallPlugin(MethodView):
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
 
 
         if not plugin.enabled:
         if not plugin.enabled:
-            flash(_("Can't install plugin. Enable '%(plugin)s' plugin first.",
-                    plugin=plugin.name), "danger")
+            flash(
+                _(
+                    "Can't install plugin. Enable '%(plugin)s' plugin first.",
+                    plugin=plugin.name
+                ), "danger"
+            )
             return redirect(url_for("management.plugins"))
             return redirect(url_for("management.plugins"))
 
 
         plugin.add_settings(plugin_module.SETTINGS)
         plugin.add_settings(plugin_module.SETTINGS)
@@ -914,161 +1012,171 @@ class InstallPlugin(MethodView):
         return redirect(url_for("management.plugins"))
         return redirect(url_for("management.plugins"))
 
 
 
 
-# Categories
-register_view(
-    management,
-    routes=['/category/add'],
-    view_func=AddCategory.as_view('add_category')
-)
-register_view(
-    management,
-    routes=["/category/<int:category_id>/delete"],
-    view_func=DeleteCategory.as_view('delete_category')
-)
-register_view(
-    management,
-    routes=['/category/<int:category_id>/edit'],
-    view_func=EditCategory.as_view('edit_category')
-)
-
-# Forums
-register_view(
-    management,
-    routes=['/forums/add', '/forums/<int:category_id>/add'],
-    view_func=AddForum.as_view('add_forum')
-)
-register_view(
-    management,
-    routes=['/forums/<int:forum_id>/delete'],
-    view_func=DeleteForum.as_view('delete_forum')
-)
-register_view(
-    management,
-    routes=['/forums/<int:forum_id>/edit'],
-    view_func=EditForum.as_view('edit_forum')
-)
-register_view(
-    management,
-    routes=['/forums'],
-    view_func=Forums.as_view('forums')
-)
-
-# Groups
-register_view(
-    management,
-    routes=['/groups/add'],
-    view_func=AddGroup.as_view('add_group')
-)
-register_view(
-    management,
-    routes=['/groups/<int:group_id>/delete', '/groups/delete'],
-    view_func=DeleteGroup.as_view('delete_group')
-)
-register_view(
-    management,
-    routes=['/groups/<int:group_id>/edit'],
-    view_func=EditGroup.as_view('edit_group')
-)
-register_view(
-    management,
-    routes=['/groups'],
-    view_func=Groups.as_view('groups')
-)
-
-# Plugins
-register_view(
-    management,
-    routes=['/plugins/<path:name>/disable'],
-    view_func=DisablePlugin.as_view('disable_plugin')
-)
-register_view(
-    management,
-    routes=['/plugins/<path:name>/enable'],
-    view_func=EnablePlugin.as_view('enable_plugin')
-)
-register_view(
-    management,
-    routes=['/plugins/<path:name>/install'],
-    view_func=InstallPlugin.as_view('install_plugin')
-)
-register_view(
-    management,
-    routes=['/plugins/<path:name>/uninstall'],
-    view_func=UninstallPlugin.as_view('uninstall_plugin')
-)
-register_view(
-    management,
-    routes=['/plugins'],
-    view_func=PluginsView.as_view('plugins')
-)
-
-# Reports
-register_view(
-    management,
-    routes=['/reports/<int:report_id>/delete', '/reports/delete'],
-    view_func=DeleteReport.as_view('delete_report')
-)
-register_view(
-    management,
-    routes=['/reports/<int:report_id>/markread', '/reports/markread'],
-    view_func=MarkReportRead.as_view('report_markread')
-)
-register_view(
-    management,
-    routes=['/reports/unread'],
-    view_func=UnreadReports.as_view('unread_reports')
-)
-register_view(
-    management,
-    routes=['/reports'],
-    view_func=Reports.as_view('reports')
-)
-
-# Settings
-register_view(
-    management,
-    routes=['/settings', '/settings/<path:slug>', '/settings/plugin/<path:plugin>'],
-    view_func=ManagementSettings.as_view('settings')
-)
-
-# Users
-register_view(
-    management,
-    routes=['/users/add'],
-    view_func=AddUser.as_view('add_user')
-)
-register_view(
-    management,
-    routes=['/users/banned'],
-    view_func=BannedUsers.as_view('banned_users')
-)
-register_view(
-    management,
-    routes=['/users/ban', '/users/<int:user_id>/ban'],
-    view_func=BanUser.as_view('ban_user')
-)
-register_view(
-    management,
-    routes=['/users/delete', '/users/<int:user_id>/delete'],
-    view_func=DeleteUser.as_view('delete_user')
-)
-register_view(
-    management,
-    routes=['/users/<int:user_id>/edit'],
-    view_func=EditUser.as_view('edit_user')
-)
-register_view(
-    management,
-    routes=['/users/unban', '/users/<int:user_id>/unban'],
-    view_func=UnbanUser.as_view('unban_user')
-)
-register_view(
-    management,
-    routes=['/users'],
-    view_func=ManageUsers.as_view('users')
-)
-register_view(
-    management,
-    routes=['/'],
-    view_func=ManagementOverview.as_view('overview')
-)
+@impl(tryfirst=True)
+def flaskbb_load_blueprints(app):
+    management = Blueprint("management", __name__)
+
+    @management.before_request
+    def check_fresh_login():
+        """Checks if the login is fresh for the current user, otherwise the user
+        has to reauthenticate."""
+        if not login_fresh():
+            return current_app.login_manager.needs_refresh()
+
+    # Categories
+    register_view(
+        management,
+        routes=['/category/add'],
+        view_func=AddCategory.as_view('add_category')
+    )
+    register_view(
+        management,
+        routes=["/category/<int:category_id>/delete"],
+        view_func=DeleteCategory.as_view('delete_category')
+    )
+    register_view(
+        management,
+        routes=['/category/<int:category_id>/edit'],
+        view_func=EditCategory.as_view('edit_category')
+    )
+
+    # Forums
+    register_view(
+        management,
+        routes=['/forums/add', '/forums/<int:category_id>/add'],
+        view_func=AddForum.as_view('add_forum')
+    )
+    register_view(
+        management,
+        routes=['/forums/<int:forum_id>/delete'],
+        view_func=DeleteForum.as_view('delete_forum')
+    )
+    register_view(
+        management,
+        routes=['/forums/<int:forum_id>/edit'],
+        view_func=EditForum.as_view('edit_forum')
+    )
+    register_view(
+        management, routes=['/forums'], view_func=Forums.as_view('forums')
+    )
+
+    # Groups
+    register_view(
+        management,
+        routes=['/groups/add'],
+        view_func=AddGroup.as_view('add_group')
+    )
+    register_view(
+        management,
+        routes=['/groups/<int:group_id>/delete', '/groups/delete'],
+        view_func=DeleteGroup.as_view('delete_group')
+    )
+    register_view(
+        management,
+        routes=['/groups/<int:group_id>/edit'],
+        view_func=EditGroup.as_view('edit_group')
+    )
+    register_view(
+        management, routes=['/groups'], view_func=Groups.as_view('groups')
+    )
+
+    # Plugins
+    register_view(
+        management,
+        routes=['/plugins/<path:name>/disable'],
+        view_func=DisablePlugin.as_view('disable_plugin')
+    )
+    register_view(
+        management,
+        routes=['/plugins/<path:name>/enable'],
+        view_func=EnablePlugin.as_view('enable_plugin')
+    )
+    register_view(
+        management,
+        routes=['/plugins/<path:name>/install'],
+        view_func=InstallPlugin.as_view('install_plugin')
+    )
+    register_view(
+        management,
+        routes=['/plugins/<path:name>/uninstall'],
+        view_func=UninstallPlugin.as_view('uninstall_plugin')
+    )
+    register_view(
+        management,
+        routes=['/plugins'],
+        view_func=PluginsView.as_view('plugins')
+    )
+
+    # Reports
+    register_view(
+        management,
+        routes=['/reports/<int:report_id>/delete', '/reports/delete'],
+        view_func=DeleteReport.as_view('delete_report')
+    )
+    register_view(
+        management,
+        routes=['/reports/<int:report_id>/markread', '/reports/markread'],
+        view_func=MarkReportRead.as_view('report_markread')
+    )
+    register_view(
+        management,
+        routes=['/reports/unread'],
+        view_func=UnreadReports.as_view('unread_reports')
+    )
+    register_view(
+        management, routes=['/reports'], view_func=Reports.as_view('reports')
+    )
+
+    # Settings
+    register_view(
+        management,
+        routes=[
+            '/settings', '/settings/<path:slug>',
+            '/settings/plugin/<path:plugin>'
+        ],
+        view_func=ManagementSettings.as_view('settings')
+    )
+
+    # Users
+    register_view(
+        management,
+        routes=['/users/add'],
+        view_func=AddUser.as_view('add_user')
+    )
+    register_view(
+        management,
+        routes=['/users/banned'],
+        view_func=BannedUsers.as_view('banned_users')
+    )
+    register_view(
+        management,
+        routes=['/users/ban', '/users/<int:user_id>/ban'],
+        view_func=BanUser.as_view('ban_user')
+    )
+    register_view(
+        management,
+        routes=['/users/delete', '/users/<int:user_id>/delete'],
+        view_func=DeleteUser.as_view('delete_user')
+    )
+    register_view(
+        management,
+        routes=['/users/<int:user_id>/edit'],
+        view_func=EditUser.as_view('edit_user')
+    )
+    register_view(
+        management,
+        routes=['/users/unban', '/users/<int:user_id>/unban'],
+        view_func=UnbanUser.as_view('unban_user')
+    )
+    register_view(
+        management, routes=['/users'], view_func=ManageUsers.as_view('users')
+    )
+    register_view(
+        management,
+        routes=['/'],
+        view_func=ManagementOverview.as_view('overview')
+    )
+
+    app.register_blueprint(
+        management, url_prefix=app.config["ADMIN_URL_PREFIX"]
+    )

+ 51 - 22
flaskbb/user/views.py

@@ -10,10 +10,12 @@
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
 import logging
 import logging
+
 from flask import Blueprint, flash, request
 from flask import Blueprint, flash, request
 from flask.views import MethodView
 from flask.views import MethodView
 from flask_babelplus import gettext as _
 from flask_babelplus import gettext as _
 from flask_login import current_user, login_required
 from flask_login import current_user, login_required
+from pluggy import HookimplMarker
 
 
 from flaskbb.user.forms import (ChangeEmailForm, ChangePasswordForm,
 from flaskbb.user.forms import (ChangeEmailForm, ChangePasswordForm,
                                 ChangeUserDetailsForm, GeneralSettingsForm)
                                 ChangeUserDetailsForm, GeneralSettingsForm)
@@ -22,10 +24,9 @@ from flaskbb.utils.helpers import (get_available_languages,
                                    get_available_themes, register_view,
                                    get_available_themes, register_view,
                                    render_template)
                                    render_template)
 
 
-logger = logging.getLogger(__name__)
-
+impl = HookimplMarker('flaskbb')
 
 
-user = Blueprint("user", __name__)
+logger = logging.getLogger(__name__)
 
 
 
 
 class UserSettings(MethodView):
 class UserSettings(MethodView):
@@ -36,7 +37,7 @@ class UserSettings(MethodView):
         form = self.form()
         form = self.form()
 
 
         form.theme.choices = get_available_themes()
         form.theme.choices = get_available_themes()
-        form.theme.choices.insert(0,('', 'Default'))
+        form.theme.choices.insert(0, ('', 'Default'))
         form.language.choices = get_available_languages()
         form.language.choices = get_available_languages()
         form.theme.data = current_user.theme
         form.theme.data = current_user.theme
         form.language.data = current_user.language
         form.language.data = current_user.language
@@ -47,7 +48,7 @@ class UserSettings(MethodView):
         form = self.form()
         form = self.form()
 
 
         form.theme.choices = get_available_themes()
         form.theme.choices = get_available_themes()
-        form.theme.choices.insert(0,('', 'Default'))
+        form.theme.choices.insert(0, ('', 'Default'))
         form.language.choices = get_available_languages()
         form.language.choices = get_available_languages()
 
 
         if form.validate_on_submit():
         if form.validate_on_submit():
@@ -85,7 +86,9 @@ class ChangeEmail(MethodView):
     form = ChangeEmailForm
     form = ChangeEmailForm
 
 
     def get(self):
     def get(self):
-        return render_template("user/change_email.html", form=self.form(current_user))
+        return render_template(
+            "user/change_email.html", form=self.form(current_user)
+        )
 
 
     def post(self):
     def post(self):
         form = self.form(current_user)
         form = self.form(current_user)
@@ -102,7 +105,9 @@ class ChangeUserDetails(MethodView):
     form = ChangeUserDetailsForm
     form = ChangeUserDetailsForm
 
 
     def get(self):
     def get(self):
-        return render_template("user/change_user_details.html", form=self.form(obj=current_user))
+        return render_template(
+            "user/change_user_details.html", form=self.form(obj=current_user)
+        )
 
 
     def post(self):
     def post(self):
         form = self.form(obj=current_user)
         form = self.form(obj=current_user)
@@ -141,18 +146,42 @@ class UserProfile(MethodView):
         return render_template("user/profile.html", user=user)
         return render_template("user/profile.html", user=user)
 
 
 
 
-register_view(user, routes=['/settings/email'], view_func=ChangeEmail.as_view('change_email'))
-register_view(user, routes=['/settings/general'], view_func=UserSettings.as_view('settings'))
-register_view(
-    user, routes=['/settings/password'], view_func=ChangePassword.as_view('change_password')
-)
-register_view(
-    user,
-    routes=["/settings/user-details"],
-    view_func=ChangeUserDetails.as_view('change_user_details')
-)
-register_view(user, routes=['/<username>/posts'], view_func=AllUserPosts.as_view('view_all_posts'))
-register_view(
-    user, routes=['/<username>/topics'], view_func=AllUserTopics.as_view('view_all_topics')
-)
-register_view(user, routes=['/<username>'], view_func=UserProfile.as_view('profile'))
+@impl(tryfirst=True)
+def flaskbb_load_blueprints(app):
+    user = Blueprint("user", __name__)
+    register_view(
+        user,
+        routes=['/settings/email'],
+        view_func=ChangeEmail.as_view('change_email')
+    )
+    register_view(
+        user,
+        routes=['/settings/general'],
+        view_func=UserSettings.as_view('settings')
+    )
+    register_view(
+        user,
+        routes=['/settings/password'],
+        view_func=ChangePassword.as_view('change_password')
+    )
+    register_view(
+        user,
+        routes=["/settings/user-details"],
+        view_func=ChangeUserDetails.as_view('change_user_details')
+    )
+    register_view(
+        user,
+        routes=['/<username>/posts'],
+        view_func=AllUserPosts.as_view('view_all_posts')
+    )
+    register_view(
+        user,
+        routes=['/<username>/topics'],
+        view_func=AllUserTopics.as_view('view_all_topics')
+    )
+
+    register_view(
+        user, routes=['/<username>'], view_func=UserProfile.as_view('profile')
+    )
+
+    app.register_blueprint(user, url_prefix=app.config["USER_URL_PREFIX"])