Browse Source

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

Load internal blueprints via hooks
Peter Justin 7 years ago
parent
commit
97cf6ad62c
5 changed files with 833 additions and 512 deletions
  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 flaskbb._compat import iteritems, string_types
-from flaskbb.auth.views import auth
 # extensions
 from flaskbb.extensions import (alembic, allows, babel, cache, celery, csrf,
                                 db, debugtoolbar, limiter, login_manager, mail,
                                 redis_store, themes, whooshee)
-from flaskbb.forum.views import forum
-from flaskbb.management.views import management
 from flaskbb.plugins import spec
 from flaskbb.plugins.manager import FlaskBBPluginManager
 from flaskbb.plugins.models import PluginRegistry
 from flaskbb.plugins.utils import remove_zombie_plugins_from_db, template_hook
 # models
 from flaskbb.user.models import Guest, User
-# views
-from flaskbb.user.views import user
 # various helpers
 from flaskbb.utils.helpers import (app_config_from_env, crop_title,
                                    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.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__)
 
@@ -78,9 +77,7 @@ def create_app(config=None, instance_path=None):
     """
 
     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
@@ -149,20 +146,15 @@ def configure_celery_app(app, celery):
     TaskBase = celery.Task
 
     class ContextTask(TaskBase):
+
         def __call__(self, *args, **kwargs):
             with app.app_context():
                 return TaskBase.__call__(self, *args, **kwargs)
+
     celery.Task = ContextTask
 
 
 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)
 
 
@@ -249,9 +241,9 @@ def configure_template_filters(app):
         ('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
     filters['can_moderate'] = TplCanModerate(request)
@@ -297,6 +289,7 @@ def configure_before_handlers(app):
             db.session.commit()
 
     if app.config["REDIS_ENABLED"]:
+
         @app.before_request
         def mark_current_user_online():
             if current_user.is_authenticated:
@@ -423,8 +416,11 @@ def load_plugins(app):
             plugins = PluginRegistry.query.all()
 
     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
         # i.e. when creating the initial database and wanting to install
         # 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()])
     registered_names = set([p.name for p in plugins])
     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
         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.
     :license: BSD, see LICENSE for more details.
 """
-from datetime import datetime
 import logging
+from datetime import datetime
 
 from flask import Blueprint, flash, g, redirect, request, url_for
 from flask.views import MethodView
 from flask_babelplus import gettext as _
 from flask_login import (confirm_login, current_user, login_fresh,
                          login_required, login_user, logout_user)
-
 from flaskbb.auth.forms import (AccountActivationForm, ForgotPasswordForm,
                                 LoginForm, LoginRecaptchaForm, ReauthForm,
                                 RegisterForm, RequestActivationForm,
@@ -33,30 +32,13 @@ from flaskbb.utils.helpers import (anonymous_required, enforce_recaptcha,
                                    requires_unactivated)
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.tokens import get_token_status
+from pluggy import HookimplMarker
 
+impl = HookimplMarker('flaskbb')
 
 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():
     """Dynamically load the rate limiting config from the database."""
     # [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))
 
 
-# Activate rate limiting on the whole blueprint
-limiter.limit(login_rate_limit, error_message=login_rate_limit_message)(auth)
-
-
 class Logout(MethodView):
     decorators = [limiter.exempt, login_required]
 
@@ -107,9 +85,13 @@ class Login(MethodView):
             try:
                 user = User.authenticate(form.login.data, form.password.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 "
-                            "your email address."), "danger")
+                            "your email address."
+                        ), "danger"
+                    )
                 return redirect_or_next(url_for("forum.index"))
             except AuthenticationError:
                 flash(_("Wrong username or password."), "danger")
@@ -168,8 +150,13 @@ class Register(MethodView):
                 send_activation_token.delay(
                     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:
                 login_user(user)
                 flash(_("Thanks for registering."), "success")
@@ -198,8 +185,12 @@ class ForgotPassword(MethodView):
                 flash(_("Email sent! Please check your inbox."), "info")
                 return redirect(url_for("auth.forgot_password"))
             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)
 
 
@@ -242,8 +233,9 @@ class RequestActivationToken(MethodView):
     form = RequestActivationForm
 
     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):
         form = self.form()
@@ -253,13 +245,16 @@ class RequestActivationToken(MethodView):
                 user_id=user.id, username=user.username, email=user.email
             )
             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 render_template("auth/request_account_activation.html",
-                               form=form)
+        return render_template(
+            "auth/request_account_activation.html", form=form
+        )
 
 
 class ActivateAccount(MethodView):
@@ -269,9 +264,7 @@ class ActivateAccount(MethodView):
     def get(self, token=None):
         expired = invalid = user = 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:
             flash(_("Your account activation token is invalid."), "danger")
@@ -292,18 +285,14 @@ class ActivateAccount(MethodView):
             flash(_("Your account has been activated."), "success")
             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):
         expired = invalid = user = None
         form = self.form()
 
         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():
             expired, invalid, user = get_token_status(
@@ -332,31 +321,55 @@ class ActivateAccount(MethodView):
         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_babelplus import gettext as _
 from flask_login import current_user, login_required
+from pluggy import HookimplMarker
 from sqlalchemy import asc, desc
 
 from flaskbb.extensions import allows, db
@@ -36,10 +37,9 @@ from flaskbb.utils.requirements import (CanAccessForum, CanAccessTopic,
                                         IsAtleastModeratorInForum)
 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):
@@ -55,7 +55,8 @@ class ForumIndex(MethodView):
 
         # Check if we use redis or not
         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
             # are online guests
@@ -79,9 +80,13 @@ class ForumIndex(MethodView):
 class ViewCategory(MethodView):
 
     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):
@@ -90,7 +95,9 @@ class ViewForum(MethodView):
     def get(self, forum_id, slug=None):
         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:
             return redirect(forum_instance.external)
@@ -115,9 +122,12 @@ class ViewPost(MethodView):
     def get(self, post_id):
         '''Redirects to a post in a topic.'''
         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(
             url_for(
@@ -143,7 +153,6 @@ class ViewTopic(MethodView):
         topic.views += 1
         topic.save()
 
-
         # Update the topicsread status if the user hasn't read it
         forumsread = None
         if current_user.is_authenticated:
@@ -166,7 +175,11 @@ class ViewTopic(MethodView):
             abort(404)
 
         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)
@@ -185,7 +198,9 @@ class ViewTopic(MethodView):
         else:
             for e in form.errors.get('content', []):
                 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):
         if Permission(CanPostReply):
@@ -199,7 +214,9 @@ class NewTopic(MethodView):
 
     def get(self, forum_id, slug=None):
         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)
     def post(self, forum_id, slug=None):
@@ -207,14 +224,19 @@ class NewTopic(MethodView):
         form = self.form()
         if 'preview' in request.form and form.validate():
             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():
             topic = form.save(real(current_user), forum_instance)
             # redirect to the new topic
             return redirect(url_for('forum.view_topic', topic_id=topic.id))
         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):
@@ -222,7 +244,9 @@ class ManageForum(MethodView):
 
     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:
             return redirect(forum_instance.external)
@@ -247,9 +271,13 @@ class ManageForum(MethodView):
         )
 
     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(
-            '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')
@@ -257,15 +285,20 @@ class ManageForum(MethodView):
 
         if not len(tmp_topics) > 0:
             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)
 
         # locking/unlocking
         if 'lock' in request.form:
             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')
@@ -273,7 +306,10 @@ class ManageForum(MethodView):
 
         elif 'unlock' in request.form:
             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')
             return redirect(mod_forum_url)
@@ -281,14 +317,20 @@ class ManageForum(MethodView):
         # highlighting/trivializing
         elif 'highlight' in request.form:
             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')
             return redirect(mod_forum_url)
 
         elif 'trivialize' in request.form:
             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')
             return redirect(mod_forum_url)
@@ -296,7 +338,10 @@ class ManageForum(MethodView):
         # deleting
         elif 'delete' in request.form:
             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')
             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()
             # 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)
 
             if new_forum.move_topics_to(tmp_topics):
@@ -327,14 +376,20 @@ class ManageForum(MethodView):
         # hiding/unhiding
         elif "hide" in request.form:
             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")
             return redirect(mod_forum_url)
 
         elif "unhide" in request.form:
             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")
             return redirect(mod_forum_url)
@@ -350,7 +405,9 @@ class NewPost(MethodView):
 
     def get(self, topic_id, slug=None):
         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):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
@@ -358,7 +415,10 @@ class NewPost(MethodView):
         if form.validate_on_submit():
             if 'preview' in request.form:
                 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:
                 post = form.save(real(current_user), topic)
@@ -373,7 +433,9 @@ class ReplyPost(MethodView):
 
     def get(self, topic_id, post_id):
         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):
         form = self.form()
@@ -381,7 +443,10 @@ class ReplyPost(MethodView):
         if form.validate_on_submit():
             if 'preview' in request.form:
                 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:
                 post = form.save(real(current_user), topic)
@@ -389,7 +454,9 @@ class ReplyPost(MethodView):
         else:
             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):
@@ -399,7 +466,9 @@ class EditPost(MethodView):
     def get(self, post_id):
         post = Post.query.filter_by(id=post_id).first_or_404()
         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):
         post = Post.query.filter_by(id=post_id).first_or_404()
@@ -421,7 +490,9 @@ class EditPost(MethodView):
                 post.save()
                 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):
@@ -464,7 +535,9 @@ class MemberList(MethodView):
         users = User.query.order_by(order_func(sort_obj)).paginate(
             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):
         page = request.args.get('page', 1, type=int)
@@ -485,13 +558,19 @@ class MemberList(MethodView):
 
         form = self.form()
         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(
             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):
@@ -501,7 +580,10 @@ class TopicTracker(MethodView):
         page = request.args.get('page', 1, type=int)
         topics = real(current_user).tracked_topics.outerjoin(
             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(
             page, flaskbb_config['TOPICS_PER_PAGE'], True
         )
@@ -517,7 +599,10 @@ class TopicTracker(MethodView):
 
         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'))
 
 
@@ -531,7 +616,9 @@ class Search(MethodView):
         form = self.form()
         if form.validate_on_submit():
             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)
 
@@ -541,8 +628,9 @@ class DeleteTopic(MethodView):
 
     def post(self, topic_id, slug=None):
         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)
         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.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)
 
@@ -670,7 +763,9 @@ class WhoIsOnline(MethodView):
             online_users = get_online_users()
         else:
             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):
@@ -698,7 +793,8 @@ class HideTopic(MethodView):
 
     def post(self, topic_id, slug=None):
         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")
             return redirect(topic.url)
         topic.hide(user=current_user)
@@ -714,8 +810,11 @@ class UnhideTopic(MethodView):
 
     def post(self, topic_id, slug=None):
         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)
         topic.unhide()
         topic.save()
@@ -728,7 +827,8 @@ class HidePost(MethodView):
     def post(self, post_id):
         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")
             return redirect(post.topic.url)
 
@@ -757,7 +857,8 @@ class UnhidePost(MethodView):
     def post(self, post_id):
         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")
             return redirect(post.topic.url)
 
@@ -771,106 +872,179 @@ class UnhidePost(MethodView):
         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_babelplus import gettext as _
 from flask_login import current_user, login_fresh
+from pluggy import HookimplMarker
+
 from flaskbb import __version__ as flaskbb_version
 from flaskbb.extensions import allows, celery, db
 from flaskbb.forum.forms import UserSearchForm
@@ -28,28 +30,19 @@ from flaskbb.management.forms import (AddForumForm, AddGroupForm, AddUserForm,
                                       EditGroupForm, EditUserForm)
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.plugins.models import PluginRegistry, PluginStore
-from flaskbb.user.models import Group, Guest, User
 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,
                                    render_template, time_diff, time_utcnow)
 from flaskbb.utils.requirements import (CanBanUser, CanEditUser, IsAdmin,
                                         IsAtleastModerator,
                                         IsAtleastSuperModerator)
 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):
@@ -65,9 +58,14 @@ class ManagementSettings(MethodView):
         active_nav = {}  # used to build the navigation
         plugin_obj = 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()
             old_settings = plugin_obj.settings
 
@@ -85,12 +83,18 @@ class ManagementSettings(MethodView):
 
         # get all groups and plugins - used to build the navigation
         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)
 
-        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):
         form, old_settings, plugin_obj, active_nav = \
@@ -108,9 +112,13 @@ class ManagementSettings(MethodView):
 
             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):
@@ -125,7 +133,9 @@ class ManageUsers(MethodView):
             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):
         page = request.args.get('page', 1, type=int)
@@ -134,13 +144,17 @@ class ManageUsers(MethodView):
         if form.validate():
             users = form.get_results().\
                 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(
             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):
@@ -151,13 +165,15 @@ class EditUser(MethodView):
         user = User.query.filter_by(id=user_id).first_or_404()
         form = self.form(user)
         member_group = db.and_(
-            *[
+            * [
                 db.not_(getattr(Group, p))
                 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):
             filt = db.or_(filt, Group.mod)
@@ -173,19 +189,23 @@ class EditUser(MethodView):
         form.primary_group.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):
         user = User.query.filter_by(id=user_id).first_or_404()
 
         member_group = db.and_(
-            *[
+            * [
                 db.not_(getattr(Group, p))
                 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):
             filt = db.or_(filt, Group.mod)
@@ -214,7 +234,9 @@ class EditUser(MethodView):
             flash(_('User updated.'), 'success')
             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):
@@ -264,7 +286,9 @@ class AddUser(MethodView):
     form = AddUserForm
 
     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):
         form = self.form()
@@ -273,7 +297,9 @@ class AddUser(MethodView):
             flash(_('User added.'), 'success')
             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):
@@ -284,30 +310,38 @@ class BannedUsers(MethodView):
         page = request.args.get('page', 1, type=int)
         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(
-            'management/banned_users.html', users=users, search_form=search_form
+            'management/banned_users.html',
+            users=users,
+            search_form=search_form
         )
 
     def post(self):
         page = request.args.get('page', 1, type=int)
         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():
             users = search_form.get_results().\
                 paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
             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(
-            '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):
         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"))
 
         # ajax request
@@ -328,18 +364,24 @@ class BanUser(MethodView):
             for user in users:
                 # don't let a user ban himself and do not allow a moderator to ban
                 # 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)):
                     continue
 
                 elif user.ban():
                     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()
         # 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")
             return redirect(url_for("management.overview"))
 
@@ -369,7 +412,10 @@ class UnbanUser(MethodView):
     def post(self, user_id=None):
 
         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"))
 
         # ajax request
@@ -426,7 +472,9 @@ class AddGroup(MethodView):
 
     def get(self):
         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):
@@ -436,7 +484,9 @@ class AddGroup(MethodView):
             flash(_('Group added.'), 'success')
             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):
@@ -446,7 +496,9 @@ class EditGroup(MethodView):
     def get(self, group_id):
         group = Group.query.filter_by(id=group_id).first_or_404()
         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):
         group = Group.query.filter_by(id=group_id).first_or_404()
@@ -462,7 +514,9 @@ class EditGroup(MethodView):
             flash(_('Group updated.'), 'success')
             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):
@@ -535,11 +589,15 @@ class EditForum(MethodView):
         form = self.form(forum)
 
         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:
             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):
         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))
         else:
             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:
                 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):
@@ -571,7 +633,9 @@ class AddForum(MethodView):
             category = Category.query.filter_by(id=category_id).first()
             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):
         form = self.form()
@@ -586,7 +650,9 @@ class AddForum(MethodView):
                 category = Category.query.filter_by(id=category_id).first()
                 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):
@@ -595,8 +661,9 @@ class DeleteForum(MethodView):
     def post(self, forum_id):
         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)
 
@@ -610,7 +677,9 @@ class AddCategory(MethodView):
 
     def get(self):
         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):
@@ -621,7 +690,9 @@ class AddCategory(MethodView):
             flash(_('Category added.'), 'success')
             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):
@@ -634,7 +705,9 @@ class EditCategory(MethodView):
         form = self.form(obj=category)
 
         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):
@@ -648,7 +721,9 @@ class EditCategory(MethodView):
             category.save()
 
         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()
 
         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()
 
         category.delete(involved_users)
@@ -727,7 +803,10 @@ class MarkReportRead(MethodView):
         if report_id:
             report = Report.query.filter_by(id=report_id).first_or_404()
             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"))
 
             report.zapped_by = current_user.id
@@ -790,10 +869,12 @@ class ManagementOverview(MethodView):
 
     def get(self):
         # 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"]:
-            online_users = User.query.filter(User.lastseen >= time_diff()).count()
+            online_users = User.query.filter(User.lastseen >= time_diff()
+                                             ).count()
         else:
             online_users = len(get_online_users())
 
@@ -855,14 +936,21 @@ class EnablePlugin(MethodView):
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
 
         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"))
 
         plugin.enabled = True
         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"))
 
 
@@ -874,14 +962,20 @@ class DisablePlugin(MethodView):
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
 
         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"))
 
         plugin.enabled = False
         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"))
 
 
@@ -905,8 +999,12 @@ class InstallPlugin(MethodView):
         plugin = PluginRegistry.query.filter_by(name=name).first_or_404()
 
         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"))
 
         plugin.add_settings(plugin_module.SETTINGS)
@@ -914,161 +1012,171 @@ class InstallPlugin(MethodView):
         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.
 """
 import logging
+
 from flask import Blueprint, flash, request
 from flask.views import MethodView
 from flask_babelplus import gettext as _
 from flask_login import current_user, login_required
+from pluggy import HookimplMarker
 
 from flaskbb.user.forms import (ChangeEmailForm, ChangePasswordForm,
                                 ChangeUserDetailsForm, GeneralSettingsForm)
@@ -22,10 +24,9 @@ from flaskbb.utils.helpers import (get_available_languages,
                                    get_available_themes, register_view,
                                    render_template)
 
-logger = logging.getLogger(__name__)
-
+impl = HookimplMarker('flaskbb')
 
-user = Blueprint("user", __name__)
+logger = logging.getLogger(__name__)
 
 
 class UserSettings(MethodView):
@@ -36,7 +37,7 @@ class UserSettings(MethodView):
         form = self.form()
 
         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.theme.data = current_user.theme
         form.language.data = current_user.language
@@ -47,7 +48,7 @@ class UserSettings(MethodView):
         form = self.form()
 
         form.theme.choices = get_available_themes()
-        form.theme.choices.insert(0,('', 'Default'))
+        form.theme.choices.insert(0, ('', 'Default'))
         form.language.choices = get_available_languages()
 
         if form.validate_on_submit():
@@ -85,7 +86,9 @@ class ChangeEmail(MethodView):
     form = ChangeEmailForm
 
     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):
         form = self.form(current_user)
@@ -102,7 +105,9 @@ class ChangeUserDetails(MethodView):
     form = ChangeUserDetailsForm
 
     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):
         form = self.form(obj=current_user)
@@ -141,18 +146,42 @@ class UserProfile(MethodView):
         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"])