Browse Source

Merge pull request #324 from justanr/Transform-To-CBV

Transform to cbv
Peter Justin 7 years ago
parent
commit
189540460a

+ 1 - 0
flaskbb/auth/forms.py

@@ -37,6 +37,7 @@ class LoginForm(FlaskForm):
     remember_me = BooleanField(_("Remember me"), default=False)
 
     submit = SubmitField(_("Login"))
+    recaptcha = HiddenField(_("Captcha"))
 
 
 class LoginRecaptchaForm(LoginForm):

+ 253 - 191
flaskbb/auth/views.py

@@ -11,21 +11,25 @@
 """
 from datetime import datetime
 
-from flask import Blueprint, flash, redirect, url_for, request, g
-from flask_login import (current_user, login_user, login_required,
-                         logout_user, confirm_login, login_fresh)
+from flask import Blueprint, flash, g, redirect, request, url_for
+from flask.views import MethodView
 from flask_babelplus import gettext as _
-
-from flaskbb.extensions import limiter
-from flaskbb.utils.helpers import (render_template, redirect_or_next,
-                                   format_timedelta, get_available_languages)
-from flaskbb.email import send_reset_token, send_activation_token
+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,
+                                ResetPasswordForm)
+from flaskbb.email import send_activation_token, send_reset_token
 from flaskbb.exceptions import AuthenticationError
-from flaskbb.auth.forms import (LoginForm, LoginRecaptchaForm, ReauthForm,
-                                ForgotPasswordForm, ResetPasswordForm,
-                                RegisterForm, AccountActivationForm,
-                                RequestActivationForm)
+from flaskbb.extensions import limiter
 from flaskbb.user.models import User
+from flaskbb.utils.helpers import (anonymous_required, enforce_recaptcha,
+                                   format_timedelta, get_available_languages,
+                                   redirect_or_next, register_view,
+                                   registration_enabled, render_template,
+                                   requires_unactivated)
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.tokens import get_token_status
 
@@ -44,16 +48,14 @@ def check_rate_limiting():
 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)
+    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]
     return "{count}/{timeout}minutes".format(
-        count=flaskbb_config["AUTH_REQUESTS"],
-        timeout=flaskbb_config["AUTH_TIMEOUT"]
+        count=flaskbb_config["AUTH_REQUESTS"], timeout=flaskbb_config["AUTH_TIMEOUT"]
     )
 
 
@@ -72,45 +74,57 @@ def login_rate_limit_message():
 limiter.limit(login_rate_limit, error_message=login_rate_limit_message)(auth)
 
 
-@auth.route("/login", methods=["GET", "POST"])
-def login():
-    """Logs the user in."""
-    if current_user is not None and current_user.is_authenticated:
-        return redirect_or_next(url_for("forum.index"))
+class Logout(MethodView):
+    decorators = [limiter.exempt, login_required]
 
-    current_limit = getattr(g, 'view_rate_limit', None)
-    login_recaptcha = False
-    if current_limit is not None:
-        window_stats = limiter.limiter.get_window_stats(*current_limit)
-        stats_diff = flaskbb_config["AUTH_REQUESTS"] - window_stats[1]
-        login_recaptcha = stats_diff >= flaskbb_config["LOGIN_RECAPTCHA"]
-
-    form = LoginForm()
-    if login_recaptcha and flaskbb_config["RECAPTCHA_ENABLED"]:
-        form = LoginRecaptchaForm()
-
-    if form.validate_on_submit():
-        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 activate it "
-                        "through the link we have sent to your email "
-                        "address."), "danger")
-            return redirect_or_next(url_for("forum.index"))
-        except AuthenticationError:
-            flash(_("Wrong username or password."), "danger")
-
-    return render_template("auth/login.html", form=form,
-                           login_recaptcha=login_recaptcha)
-
-
-@auth.route("/reauth", methods=["GET", "POST"])
-@limiter.exempt
-@login_required
-def reauth():
-    """Reauthenticates a user."""
-    if not login_fresh():
-        form = ReauthForm(request.form)
+    def get(self):
+        logout_user()
+        flash(_("Logged out"), "success")
+        return redirect(url_for("forum.index"))
+
+
+class Login(MethodView):
+    decorators = [anonymous_required]
+
+    def form(self):
+        if enforce_recaptcha(limiter):
+            return LoginRecaptchaForm()
+        return LoginForm()
+
+    def get(self):
+        return render_template("auth/login.html", form=self.form())
+
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            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 activate it "
+                            "through the link we have sent to your email "
+                            "address."
+                        ), "danger"
+                    )
+                return redirect_or_next(url_for("forum.index"))
+            except AuthenticationError:
+                flash(_("Wrong username or password."), "danger")
+
+        return render_template("auth/login.html", form=form)
+
+
+class Reauth(MethodView):
+    decorators = [login_required, limiter.exempt]
+    form = ReauthForm
+
+    def get(self):
+        if not login_fresh():
+            return render_template("auth/reauth.html", form=self.form())
+        return redirect_or_next(current_user.url)
+
+    def post(self):
+        form = self.form()
         if form.validate_on_submit():
             if current_user.check_password(form.password.data):
                 confirm_login()
@@ -119,160 +133,208 @@ def reauth():
 
             flash(_("Wrong password."), "danger")
         return render_template("auth/reauth.html", form=form)
-    return redirect(request.args.get("next") or current_user.url)
-
-
-@auth.route("/logout")
-@limiter.exempt
-@login_required
-def logout():
-    """Logs the user out."""
-    logout_user()
-    flash(_("Logged out"), "success")
-    return redirect(url_for("forum.index"))
-
-
-@auth.route("/register", methods=["GET", "POST"])
-def register():
-    """Register a new user."""
-    if current_user is not None and current_user.is_authenticated:
-        return redirect_or_next(url_for("forum.index"))
-
-    if not flaskbb_config["REGISTRATION_ENABLED"]:
-        flash(_("The registration has been disabled."), "info")
-        return redirect_or_next(url_for("forum.index"))
-
-    form = RegisterForm(request.form)
-
-    form.language.choices = get_available_languages()
-    form.language.default = flaskbb_config['DEFAULT_LANGUAGE']
-    form.process(request.form)  # needed because a default is overriden
-
-    if form.validate_on_submit():
-        user = form.save()
-
-        if flaskbb_config["ACTIVATE_ACCOUNT"]:
-            # Any call to an expired model requires a database hit, so
-            # accessing user.id would cause an DetachedInstanceError.
-            # This happens because the `user`'s session does no longer exist.
-            # So we just fire up another query to make sure that the session
-            # for the newly created user is fresh.
-            # PS: `db.session.merge(user)` did not work for me.
-            user = User.query.filter_by(email=user.email).first()
-            send_activation_token.delay(user)
-            flash(_("An account activation email has been sent to %(email)s",
-                    email=user.email), "success")
-        else:
-            login_user(user)
-            flash(_("Thanks for registering."), "success")
 
-        return redirect_or_next(url_for('forum.index'))
 
-    return render_template("auth/register.html", form=form)
+class Register(MethodView):
+    decorators = [anonymous_required, registration_enabled]
 
+    def form(self):
+        form = RegisterForm()
 
-@auth.route('/reset-password', methods=["GET", "POST"])
-def forgot_password():
-    """Sends a reset password token to the user."""
-    if not current_user.is_anonymous:
-        return redirect(url_for("forum.index"))
+        form.language.choices = get_available_languages()
+        form.language.default = flaskbb_config['DEFAULT_LANGUAGE']
+        form.process(request.form)  # needed because a default is overriden
+        return form
 
-    form = ForgotPasswordForm()
-    if form.validate_on_submit():
-        user = User.query.filter_by(email=form.email.data).first()
+    def get(self):
+        return render_template("auth/register.html", form=self.form())
 
-        if user:
-            send_reset_token.delay(user)
-            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")
-    return render_template("auth/forgot_password.html", form=form)
-
-
-@auth.route("/reset-password/<token>", methods=["GET", "POST"])
-def reset_password(token):
-    """Handles the reset password process."""
-    if not current_user.is_anonymous:
-        return redirect(url_for("forum.index"))
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            user = form.save()
+
+            if flaskbb_config["ACTIVATE_ACCOUNT"]:
+                # Any call to an expired model requires a database hit, so
+                # accessing user.id would cause an DetachedInstanceError.
+                # This happens because the `user`'s session does no longer exist.
+                # So we just fire up another query to make sure that the session
+                # for the newly created user is fresh.
+                # PS: `db.session.merge(user)` did not work for me.
+                user = User.query.filter_by(email=user.email).first()
+                send_activation_token.delay(user)
+                flash(
+                    _("An account activation email has been sent to %(email)s", email=user.email),
+                    "success"
+                )
+            else:
+                login_user(user)
+                flash(_("Thanks for registering."), "success")
+
+            return redirect_or_next(url_for('forum.index'))
+
+        return render_template("auth/register.html", form=form)
+
+
+class ForgotPassword(MethodView):
+    decorators = [anonymous_required]
+    form = ForgotPasswordForm
+
+    def get(self):
+        return render_template("auth/forgot_password.html", form=self.form())
+
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            user = User.query.filter_by(email=form.email.data).first()
+
+            if user:
+                send_reset_token.delay(user)
+                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"
+                )
+        return render_template("auth/forgot_password.html", form=form)
+
+
+class ResetPassword(MethodView):
+    decorators = [anonymous_required]
+    form = ResetPasswordForm
+
+    def get(self, token):
+        form = self.form()
+        form.token.data = token
+        return render_template("auth/reset_password.html", form=form)
+
+    def post(self, token):
+        form = self.form()
+        if form.validate_on_submit():
+            expired, invalid, user = get_token_status(form.token.data, "reset_password")
+
+            if invalid:
+                flash(_("Your password token is invalid."), "danger")
+                return redirect(url_for("auth.forgot_password"))
+
+            if expired:
+                flash(_("Your password token is expired."), "danger")
+                return redirect(url_for("auth.forgot_password"))
 
-    form = ResetPasswordForm()
-    if form.validate_on_submit():
-        expired, invalid, user = get_token_status(form.token.data,
-                                                  "reset_password")
+            if user:
+                user.password = form.password.data
+                user.save()
+                flash(_("Your password has been updated."), "success")
+                return redirect(url_for("auth.login"))
+
+        form.token.data = token
+        return render_template("auth/reset_password.html", form=form)
+
+
+class RequestActivationToken(MethodView):
+    decorators = [requires_unactivated]
+    form = RequestActivationForm
+
+    def get(self):
+        return render_template("auth/request_account_activation.html", form=self.form())
+
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            user = User.query.filter_by(email=form.email.data).first()
+            send_activation_token.delay(user)
+            flash(
+                _("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)
+
+
+class ActivateAccount(MethodView):
+    form = AccountActivationForm
+    decorators = [requires_unactivated]
+
+    def get(self, token=None):
+        expired = invalid = user = None
+        if token is not None:
+            expired, invalid, user = get_token_status(token, "activate_account")
 
         if invalid:
-            flash(_("Your password token is invalid."), "danger")
-            return redirect(url_for("auth.forgot_password"))
+            flash(_("Your account activation token is invalid."), "danger")
+            return redirect(url_for("auth.request_activation_token"))
 
         if expired:
-            flash(_("Your password token is expired."), "danger")
-            return redirect(url_for("auth.forgot_password"))
+            flash(_("Your account activation token is expired."), "danger")
+            return redirect(url_for("auth.request_activation_token"))
 
         if user:
-            user.password = form.password.data
+            user.activated = True
             user.save()
-            flash(_("Your password has been updated."), "success")
-            return redirect(url_for("auth.login"))
-
-    form.token.data = token
-    return render_template("auth/reset_password.html", form=form)
-
-
-@auth.route("/activate", methods=["GET", "POST"])
-def request_activation_token():
-    """Requests a new account activation token."""
-    if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
-        flash(_("This account is already activated."), "info")
-        return redirect(url_for('forum.index'))
-
-    form = RequestActivationForm()
-    if form.validate_on_submit():
-        user = User.query.filter_by(email=form.email.data).first()
-        send_activation_token.delay(user)
-        flash(_("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)
-
-
-@auth.route("/activate/confirm", methods=["GET", "POST"])
-@auth.route("/activate/confirm/<token>", methods=["GET", "POST"])
-def activate_account(token=None):
-    """Handles the account activation process."""
-    if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
-        flash(_("This account is already activated."), "info")
-        return redirect(url_for('forum.index'))
-
-    expired = invalid = user = None
-    form = None
-    if token is not None:
-        expired, invalid, user = get_token_status(token, "activate_account")
-    else:
-        form = AccountActivationForm()
-        if form.validate_on_submit():
-            expired, invalid, user = get_token_status(form.token.data,
-                                                      "activate_account")
 
-    if invalid:
-        flash(_("Your account activation token is invalid."), "danger")
-        return redirect(url_for("auth.request_activation_token"))
+            if current_user != user:
+                logout_user()
+                login_user(user)
 
-    if expired:
-        flash(_("Your account activation token is expired."), "danger")
-        return redirect(url_for("auth.request_activation_token"))
+            flash(_("Your account has been activated."), "success")
+            return redirect(url_for("forum.index"))
 
-    if user:
-        user.activated = True
-        user.save()
+        return render_template("auth/account_activation.html", form=self.form())
 
-        if current_user != user:
-            logout_user()
-            login_user(user)
+    def post(self, token=None):
+        expired = invalid = user = None
+        form = self.form()
 
-        flash(_("Your account has been activated."), "success")
-        return redirect(url_for("forum.index"))
+        if token is not None:
+            expired, invalid, user = get_token_status(token, "activate_account")
+
+        elif form.validate_on_submit():
+            expired, invalid, user = get_token_status(form.token.data, "activate_account")
+
+        if invalid:
+            flash(_("Your account activation token is invalid."), "danger")
+            return redirect(url_for("auth.request_activation_token"))
+
+        if expired:
+            flash(_("Your account activation token is expired."), "danger")
+            return redirect(url_for("auth.request_activation_token"))
+
+        if user:
+            user.activated = True
+            user.save()
 
-    return render_template("auth/account_activation.html", form=form)
+            if current_user != user:
+                logout_user()
+                login_user(user)
+
+            flash(_("Your account has been activated."), "success")
+            return redirect(url_for("forum.index"))
+
+        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')
+)

+ 618 - 527
flaskbb/forum/views.py

@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-"""
+'''
     flaskbb.forum.views
     ~~~~~~~~~~~~~~~~~~~
 
@@ -8,660 +8,751 @@
 
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
-"""
+'''
 import math
-from sqlalchemy import asc, desc
-from flask import (Blueprint, redirect, url_for, current_app, request, flash,
-                   abort)
-from flask_login import login_required, current_user
+
+from flask import (Blueprint, abort, current_app, flash, redirect, request,
+                   url_for)
+from flask.views import MethodView
+from flask_allows import And, Permission
 from flask_babelplus import gettext as _
-from flask_allows import Permission, And
+from flask_login import current_user, login_required
+from sqlalchemy import asc, desc
 
-from flaskbb.extensions import db, allows
-from flaskbb.utils.settings import flaskbb_config
-from flaskbb.utils.helpers import (get_online_users, time_diff, time_utcnow,
-                                   format_quote, render_template,
-                                   do_topic_action, real)
+from flaskbb.extensions import allows, db
+from flaskbb.forum.forms import (NewTopicForm, QuickreplyForm, ReplyForm,
+                                 ReportForm, SearchPageForm, UserSearchForm)
+from flaskbb.forum.models import (Category, Forum, ForumsRead, Post, Topic,
+                                  TopicsRead)
+from flaskbb.user.models import User
+from flaskbb.utils.helpers import (do_topic_action, format_quote,
+                                   get_online_users, real, register_view,
+                                   render_template, time_diff, time_utcnow)
 from flaskbb.utils.requirements import (CanAccessForum, CanAccessTopic,
                                         CanDeletePost, CanDeleteTopic,
                                         CanEditPost, CanPostReply,
                                         CanPostTopic,
                                         IsAtleastModeratorInForum)
-from flaskbb.forum.models import (Category, Forum, Topic, Post, ForumsRead,
-                                  TopicsRead)
-from flaskbb.forum.forms import (NewTopicForm, QuickreplyForm, ReplyForm,
-                                 ReportForm, SearchPageForm, UserSearchForm)
-from flaskbb.user.models import User
+from flaskbb.utils.settings import flaskbb_config
 
-forum = Blueprint("forum", __name__)
+forum = Blueprint('forum', __name__)
 
 
-@forum.route("/")
-def index():
-    categories = Category.get_all(user=real(current_user))
-
-    # Fetch a few stats about the forum
-    user_count = User.query.count()
-    topic_count = Topic.query.count()
-    post_count = Post.query.count()
-    newest_user = User.query.order_by(User.id.desc()).first()
-
-    # Check if we use redis or not
-    if not current_app.config["REDIS_ENABLED"]:
-        online_users = User.query.filter(User.lastseen >= time_diff()).count()
-
-        # Because we do not have server side sessions, we cannot check if there
-        # are online guests
-        online_guests = None
-    else:
-        online_users = len(get_online_users())
-        online_guests = len(get_online_users(guest=True))
-
-    return render_template("forum/index.html",
-                           categories=categories,
-                           user_count=user_count,
-                           topic_count=topic_count,
-                           post_count=post_count,
-                           newest_user=newest_user,
-                           online_users=online_users,
-                           online_guests=online_guests)
-
-
-@forum.route("/category/<int:category_id>")
-@forum.route("/category/<int:category_id>-<slug>")
-def view_category(category_id, slug=None):
-    category, forums = Category.\
-        get_forums(category_id=category_id, user=real(current_user))
-
-    return render_template("forum/category.html", forums=forums,
-                           category=category)
-
-
-@forum.route("/forum/<int:forum_id>")
-@forum.route("/forum/<int:forum_id>-<slug>")
-@allows.requires(CanAccessForum())
-def view_forum(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)
-    )
-
-    if forum_instance.external:
-        return redirect(forum_instance.external)
-
-    topics = Forum.get_topics(
-        forum_id=forum_instance.id, user=real(current_user), page=page,
-        per_page=flaskbb_config["TOPICS_PER_PAGE"]
-    )
-
-    return render_template(
-        "forum/forum.html", forum=forum_instance,
-        topics=topics, forumsread=forumsread,
-    )
-
-
-@forum.route("/topic/<int:topic_id>", methods=["POST", "GET"])
-@forum.route("/topic/<int:topic_id>-<slug>", methods=["POST", "GET"])
-@allows.requires(CanAccessTopic())
-def view_topic(topic_id, slug=None):
-    page = request.args.get('page', 1, type=int)
-
-    # Fetch some information about the topic
-    topic = Topic.get_topic(topic_id=topic_id, user=real(current_user))
-
-    # Count the topic views
-    topic.views += 1
-    topic.save()
-
-    # fetch the posts in the topic
-    posts = Post.query.\
-        outerjoin(User, Post.user_id == User.id).\
-        filter(Post.topic_id == topic.id).\
-        add_entity(User).\
-        order_by(Post.id.asc()).\
-        paginate(page, flaskbb_config['POSTS_PER_PAGE'], False)
-
-    # Abort if there are no posts on this page
-    if len(posts.items) == 0:
-        abort(404)
-
-    # Update the topicsread status if the user hasn't read it
-    forumsread = None
-    if current_user.is_authenticated:
-        forumsread = ForumsRead.query.\
-            filter_by(user_id=real(current_user).id,
-                      forum_id=topic.forum.id).first()
-
-    topic.update_read(real(current_user), topic.forum, forumsread)
-
-    form = None
-    if Permission(CanPostReply):
-        form = QuickreplyForm()
-        if form.validate_on_submit():
-            post = form.save(real(current_user), topic)
-            return view_post(post.id)
-
-    return render_template("forum/topic.html", topic=topic, posts=posts,
-                           last_seen=time_diff(), form=form)
+class ForumIndex(MethodView):
 
+    def get(self):
+        categories = Category.get_all(user=real(current_user))
 
-@forum.route("/post/<int:post_id>")
-def view_post(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 = math.ceil(post_in_topic / float(flaskbb_config['POSTS_PER_PAGE']))
+        # Fetch a few stats about the forum
+        user_count = User.query.count()
+        topic_count = Topic.query.count()
+        post_count = Post.query.count()
+        newest_user = User.query.order_by(User.id.desc()).first()
 
-    return redirect(post.topic.url + "?page=%d#pid%s" % (page, post.id))
+        # Check if we use redis or not
+        if not current_app.config['REDIS_ENABLED']:
+            online_users = User.query.filter(User.lastseen >= time_diff()).count()
 
+            # Because we do not have server side sessions, we cannot check if there
+            # are online guests
+            online_guests = None
+        else:
+            online_users = len(get_online_users())
+            online_guests = len(get_online_users(guest=True))
 
-@forum.route("/<int:forum_id>/topic/new", methods=["POST", "GET"])
-@forum.route("/<int:forum_id>-<slug>/topic/new", methods=["POST", "GET"])
-@login_required
-def new_topic(forum_id, slug=None):
-    forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
+        return render_template(
+            'forum/index.html',
+            categories=categories,
+            user_count=user_count,
+            topic_count=topic_count,
+            post_count=post_count,
+            newest_user=newest_user,
+            online_users=online_users,
+            online_guests=online_guests
+        )
 
-    if not Permission(CanPostTopic):
-        flash(_("You do not have the permissions to create a new topic."),
-              "danger")
-        return redirect(forum_instance.url)
 
-    form = NewTopicForm()
-    if request.method == "POST":
-        if "preview" in request.form and form.validate():
-            return render_template(
-                "forum/new_topic.html", forum=forum_instance,
-                form=form, preview=form.content.data
-            )
-        if "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))
+class ViewCategory(MethodView):
 
-    return render_template(
-        "forum/new_topic.html", forum=forum_instance, form=form
-    )
+    def get(self, category_id, slug=None):
+        category, forums = Category.get_forums(category_id=category_id, user=real(current_user))
 
+        return render_template('forum/category.html', forums=forums, category=category)
 
-@forum.route("/topic/<int:topic_id>/delete", methods=["POST"])
-@forum.route("/topic/<int:topic_id>-<slug>/delete", methods=["POST"])
-@login_required
-def delete_topic(topic_id=None, slug=None):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
-    if not Permission(CanDeleteTopic):
-        flash(_("You do not have the permissions to delete this topic."),
-              "danger")
-        return redirect(topic.forum.url)
+class ViewForum(MethodView):
+    decorators = [allows.requires(CanAccessForum())]
 
-    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))
+    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.route("/topic/<int:topic_id>/lock", methods=["POST"])
-@forum.route("/topic/<int:topic_id>-<slug>/lock", methods=["POST"])
-@login_required
-def lock_topic(topic_id=None, slug=None):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        if forum_instance.external:
+            return redirect(forum_instance.external)
 
-    if not Permission(IsAtleastModeratorInForum(forum=topic.forum)):
-        flash(_("You do not have the permissions to lock this topic."),
-              "danger")
-        return redirect(topic.url)
+        topics = Forum.get_topics(
+            forum_id=forum_instance.id,
+            user=real(current_user),
+            page=page,
+            per_page=flaskbb_config['TOPICS_PER_PAGE']
+        )
 
-    topic.locked = True
-    topic.save()
-    return redirect(topic.url)
+        return render_template(
+            'forum/forum.html',
+            forum=forum_instance,
+            topics=topics,
+            forumsread=forumsread,
+        )
 
 
-@forum.route("/topic/<int:topic_id>/unlock", methods=["POST"])
-@forum.route("/topic/<int:topic_id>-<slug>/unlock", methods=["POST"])
-@login_required
-def unlock_topic(topic_id=None, slug=None):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
+class ViewPost(MethodView):
 
-    if not Permission(IsAtleastModeratorInForum(forum=topic.forum)):
-        flash(_("You do not have the permissions to unlock this topic."),
-              "danger")
-        return redirect(topic.url)
+    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 = math.ceil(post_in_topic / float(flaskbb_config['POSTS_PER_PAGE']))
 
-    topic.locked = False
-    topic.save()
-    return redirect(topic.url)
+        return redirect(
+            url_for(
+                'forum.view_topic',
+                topic_id=post.topic.id,
+                slug=post.topic.slug,
+                page=page,
+                _anchor='pid{}'.format(post.id)
+            )
+        )
 
 
-@forum.route("/topic/<int:topic_id>/highlight", methods=["POST"])
-@forum.route("/topic/<int:topic_id>-<slug>/highlight", methods=["POST"])
-@login_required
-def highlight_topic(topic_id=None, slug=None):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
+class ViewTopic(MethodView):
+    decorators = [allows.requires(CanAccessTopic())]
 
-    if not Permission(IsAtleastModeratorInForum(forum=topic.forum)):
-        flash(_("You do not have the permissions to highlight this topic."),
-              "danger")
-        return redirect(topic.url)
+    def get(self, topic_id, slug=None):
+        page = request.args.get('page', 1, type=int)
 
-    topic.important = True
-    topic.save()
-    return redirect(topic.url)
+        # Fetch some information about the topic
+        topic = Topic.get_topic(topic_id=topic_id, user=real(current_user))
 
+        # Count the topic views
+        topic.views += 1
+        topic.save()
 
-@forum.route("/topic/<int:topic_id>/trivialize", methods=["POST"])
-@forum.route("/topic/<int:topic_id>-<slug>/trivialize", methods=["POST"])
-@login_required
-def trivialize_topic(topic_id=None, slug=None):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        # fetch the posts in the topic
+        posts = Post.query.\
+            outerjoin(User, Post.user_id == User.id).\
+            filter(Post.topic_id == topic.id).\
+            add_entity(User).\
+            order_by(Post.id.asc()).\
+            paginate(page, flaskbb_config['POSTS_PER_PAGE'], False)
 
-    # Unlock is basically the same as lock
-    if not Permission(IsAtleastModeratorInForum(forum=topic.forum)):
-        flash(_("You do not have the permissions to trivialize this topic."),
-              "danger")
-        return redirect(topic.url)
+        # Abort if there are no posts on this page
+        if len(posts.items) == 0:
+            abort(404)
+
+        # Update the topicsread status if the user hasn't read it
+        forumsread = None
+        if current_user.is_authenticated:
+            forumsread = ForumsRead.query.\
+                filter_by(user_id=real(current_user).id,
+                          forum_id=topic.forum.id).first()
+
+        topic.update_read(real(current_user), topic.forum, forumsread)
+
+        return render_template(
+            'forum/topic.html', topic=topic, posts=posts, last_seen=time_diff(), form=self.form()
+        )
+
+    @allows.requires(CanPostReply)
+    def post(self, topic_id, slug=None):
+        topic = Topic.get_topic(topic_id=topic_id, user=real(current_user))
+        form = self.form()
 
-    topic.important = False
-    topic.save()
-    return redirect(topic.url)
+        if not form:
+            flash(_('Cannot post reply'), 'warning')
+            return redirect('forum.view_topic', topic_id=topic_id, slug=slug)
 
+        elif form.validate_on_submit():
+            post = form.save(real(current_user), topic)
+            return redirect(url_for('forum.view_post', post_id=post.id))
 
-@forum.route("/forum/<int:forum_id>/edit", methods=["POST", "GET"])
-@forum.route("/forum/<int:forum_id>-<slug>/edit", methods=["POST", "GET"])
-@login_required
-def manage_forum(forum_id, slug=None):
-    page = request.args.get('page', 1, type=int)
+        else:
+            for e in form.errors.get('content', []):
+                flash(e, 'danger')
+            return redirect(url_for('forum.view_topic', topic_id=topic_id, slug=slug))
 
-    forum_instance, forumsread = Forum.get_forum(forum_id=forum_id,
-                                                 user=real(current_user))
+    def form(self):
+        if Permission(CanPostReply):
+            return QuickreplyForm()
+        return None
 
-    # remove the current forum from the select field (move).
-    available_forums = Forum.query.order_by(Forum.position).all()
-    available_forums.remove(forum_instance)
 
-    if not Permission(IsAtleastModeratorInForum(forum=forum_instance)):
-        flash(_("You do not have the permissions to moderate this forum."),
-              "danger")
-        return redirect(forum_instance.url)
+class NewTopic(MethodView):
+    decorators = [login_required]
+    form = NewTopicForm
 
-    if forum_instance.external:
-        return redirect(forum_instance.external)
+    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())
 
-    topics = Forum.get_topics(
-        forum_id=forum_instance.id, user=real(current_user), page=page,
-        per_page=flaskbb_config["TOPICS_PER_PAGE"]
-    )
+    @allows.requires(CanPostTopic)
+    def post(self, forum_id, slug=None):
+        forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
+        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
+            )
+        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)
 
-    mod_forum_url = url_for("forum.manage_forum", forum_id=forum_instance.id,
-                            slug=forum_instance.slug)
 
-    # the code is kind of the same here but it somehow still looks cleaner than
-    # doin some magic
-    if request.method == "POST":
-        ids = request.form.getlist("rowid")
+class ManageForum(MethodView):
+    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+
+    def get(self, forum_id, slug=None):
+
+        forum_instance, forumsread = Forum.get_forum(forum_id=forum_id, user=real(current_user))
+
+        if forum_instance.external:
+            return redirect(forum_instance.external)
+
+        # remove the current forum from the select field (move).
+        available_forums = Forum.query.order_by(Forum.position).all()
+        available_forums.remove(forum_instance)
+        page = request.args.get('page', 1, type=int)
+        topics = Forum.get_topics(
+            forum_id=forum_instance.id,
+            user=real(current_user),
+            page=page,
+            per_page=flaskbb_config['TOPICS_PER_PAGE']
+        )
+
+        return render_template(
+            'forum/edit_forum.html',
+            forum=forum_instance,
+            topics=topics,
+            available_forums=available_forums,
+            forumsread=forumsread,
+        )
+
+    def post(self, forum_id, slug=None):
+        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
+        )
+
+        ids = request.form.getlist('rowid')
         tmp_topics = Topic.query.filter(Topic.id.in_(ids)).all()
 
         if not len(tmp_topics) > 0:
-            flash(_("In order to perform this action you have to select at "
-                    "least one topic."), "danger")
+            flash(
+                _('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)
+        if 'lock' in request.form:
+            changed = do_topic_action(
+                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')
             return redirect(mod_forum_url)
 
-        elif "unlock" in request.form:
-            changed = do_topic_action(topics=tmp_topics, user=real(current_user),
-                                      action="locked", reverse=True)
-            flash(_("%(count)s topics unlocked.", count=changed), "success")
+        elif 'unlock' in request.form:
+            changed = do_topic_action(
+                topics=tmp_topics, user=real(current_user), action='locked', reverse=True
+            )
+            flash(_('%(count)s topics unlocked.', count=changed), 'success')
             return redirect(mod_forum_url)
 
         # highlighting/trivializing
-        elif "highlight" in request.form:
-            changed = do_topic_action(topics=tmp_topics, user=real(current_user),
-                                      action="important", reverse=False)
-            flash(_("%(count)s topics highlighted.", count=changed), "success")
+        elif 'highlight' in request.form:
+            changed = do_topic_action(
+                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)
-            flash(_("%(count)s topics trivialized.", count=changed), "success")
+        elif 'trivialize' in request.form:
+            changed = do_topic_action(
+                topics=tmp_topics, user=real(current_user), action='important', reverse=True
+            )
+            flash(_('%(count)s topics trivialized.', count=changed), 'success')
             return redirect(mod_forum_url)
 
         # deleting
-        elif "delete" in request.form:
-            changed = do_topic_action(topics=tmp_topics, user=real(current_user),
-                                      action="delete", reverse=False)
-            flash(_("%(count)s topics deleted.", count=changed), "success")
+        elif 'delete' in request.form:
+            changed = do_topic_action(
+                topics=tmp_topics, user=real(current_user), action='delete', reverse=False
+            )
+            flash(_('%(count)s topics deleted.', count=changed), 'success')
             return redirect(mod_forum_url)
 
         # moving
-        elif "move" in request.form:
-            new_forum_id = request.form.get("forum")
+        elif 'move' in request.form:
+            new_forum_id = request.form.get('forum')
 
             if not new_forum_id:
-                flash(_("Please choose a new forum for the topics."), "info")
+                flash(_('Please choose a new forum for the topics.'), 'info')
                 return redirect(mod_forum_url)
 
             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)
 
             new_forum.move_topics_to(tmp_topics)
+        else:
+            flash(_('Unknown action requested'), 'danger')
             return redirect(mod_forum_url)
 
-    return render_template(
-        "forum/edit_forum.html", forum=forum_instance, topics=topics,
-        available_forums=available_forums, forumsread=forumsread,
-    )
 
+class NewPost(MethodView):
+    decorators = [allows.requires(CanPostReply), login_required]
+    form = ReplyForm
 
-@forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
-@forum.route("/topic/<int:topic_id>-<slug>/post/new", methods=["POST", "GET"])
-@login_required
-def new_post(topic_id, slug=None):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
+    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())
 
-    if not Permission(CanPostReply):
-        flash(_("You do not have the permissions to post in this topic."),
-              "danger")
-        return redirect(topic.forum.url)
+    def post(self, topic_id, slug=None):
+        topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        form = self.form()
+        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
+                )
+            else:
+                post = form.save(real(current_user), topic)
+                return redirect(url_for('forum.view_post', post_id=post.id))
 
-    form = ReplyForm()
-    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
-            )
-        else:
-            post = form.save(real(current_user), topic)
-            return view_post(post.id)
+        return render_template('forum/new_post.html', topic=topic, form=form)
 
-    return render_template("forum/new_post.html", topic=topic, form=form)
 
+class ReplyPost(MethodView):
+    decorators = [allows.requires(CanPostReply), login_required]
+    form = ReplyForm
 
-@forum.route(
-    "/topic/<int:topic_id>/post/<int:post_id>/reply", methods=["POST", "GET"]
-)
-@login_required
-def reply_post(topic_id, post_id):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
-    post = Post.query.filter_by(id=post_id).first_or_404()
-
-    if not Permission(CanPostReply):
-        flash(_("You do not have the permissions to post in this topic."),
-              "danger")
-        return view_post(post.id)
-
-    form = ReplyForm()
-    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
-            )
+    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())
+
+    def post(self, topic_id, post_id):
+        form = self.form()
+        topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        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
+                )
+            else:
+                post = form.save(real(current_user), topic)
+                return redirect(url_for('forum.view_post', post_id=post.id))
         else:
-            post = form.save(real(current_user), topic)
-            return view_post(post.id)
-    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)
 
 
-@forum.route("/post/<int:post_id>/edit", methods=["POST", "GET"])
-@login_required
-def edit_post(post_id):
-    post = Post.query.filter_by(id=post_id).first_or_404()
+class EditPost(MethodView):
+    decorators = [allows.requires(CanEditPost), login_required]
+    form = ReplyForm
 
-    if not Permission(CanEditPost):
-        flash(_("You do not have the permissions to edit this post."),
-              "danger")
-        return view_post(post.id)
+    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)
 
-    if post.first_post:
-        form = NewTopicForm()
-    else:
-        form = ReplyForm()
+    def post(self, post_id):
+        post = Post.query.filter_by(id=post_id).first_or_404()
+        form = self.form(obj=post)
 
-    if form.validate_on_submit():
-        if "preview" in request.form:
-            return render_template(
-                "forum/new_post.html", topic=post.topic,
-                form=form, preview=form.content.data, edit_mode=True
-            )
-        else:
-            form.populate_obj(post)
-            post.date_modified = time_utcnow()
-            post.modified_by = real(current_user).username
-            post.save()
+        if form.validate_on_submit():
+            if 'preview' in request.form:
+                return render_template(
+                    'forum/new_post.html',
+                    topic=post.topic,
+                    form=form,
+                    preview=form.content.data,
+                    edit_mode=True
+                )
+            else:
+                form.populate_obj(post)
+                post.date_modified = time_utcnow()
+                post.modified_by = real(current_user).username
+                post.save()
+                return redirect(url_for('forum.view_post', post_id=post.id))
 
-            if post.first_post:
-                post.topic.title = form.title.data
-                post.topic.save()
-            return view_post(post.id)
-    else:
-        if post.first_post:
-            form.title.data = post.topic.title
+        return render_template('forum/new_post.html', topic=post.topic, form=form, edit_mode=True)
 
-        form.content.data = post.content
 
-    return render_template("forum/new_post.html", topic=post.topic, form=form,
-                           edit_mode=True)
+class ReportView(MethodView):
+    decorators = [login_required]
+    form = ReportForm
 
+    def get(self, post_id):
+        return render_template('forum/report_post.html', form=self.form())
 
-@forum.route("/post/<int:post_id>/delete", methods=["POST"])
-@login_required
-def delete_post(post_id):
-    post = Post.query.filter_by(id=post_id).first_or_404()
+    def post(self, post_id):
+        form = self.form()
+        if form.validate_on_submit():
+            post = Post.query.filter_by(id=post_id).first_or_404()
+            form.save(real(current_user), post)
+            flash(_('Thanks for reporting.'), 'success')
 
-    # TODO: Bulk delete
+        return render_template('forum/report_post.html', form=form)
 
-    if not Permission(CanDeletePost):
-        flash(_("You do not have the permissions to delete this post."),
-              "danger")
-        return redirect(post.topic.url)
 
-    first_post = post.first_post
-    topic_url = post.topic.url
-    forum_url = post.topic.forum.url
+class MemberList(MethodView):
+    form = UserSearchForm
 
-    post.delete()
+    def get(self):
+        page = request.args.get('page', 1, type=int)
+        sort_by = request.args.get('sort_by', 'reg_date')
+        order_by = request.args.get('order_by', 'asc')
 
-    # If the post was the first post in the topic, redirect to the forums
-    if first_post:
-        return redirect(forum_url)
-    return redirect(topic_url)
+        if order_by == 'asc':
+            order_func = asc
+        else:
+            order_func = desc
 
+        if sort_by == 'reg_date':
+            sort_obj = User.id
+        elif sort_by == 'post_count':
+            sort_obj = User.post_count
+        else:
+            sort_obj = User.username
 
-@forum.route("/post/<int:post_id>/report", methods=["GET", "POST"])
-@login_required
-def report_post(post_id):
-    post = Post.query.filter_by(id=post_id).first_or_404()
+        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())
 
-    form = ReportForm()
-    if form.validate_on_submit():
-        form.save(real(current_user), post)
-        flash(_("Thanks for reporting."), "success")
+    def post(self):
+        page = request.args.get('page', 1, type=int)
+        sort_by = request.args.get('sort_by', 'reg_date')
+        order_by = request.args.get('order_by', 'asc')
 
-    return render_template("forum/report_post.html", form=form)
+        if order_by == 'asc':
+            order_func = asc
+        else:
+            order_func = desc
 
+        if sort_by == 'reg_date':
+            sort_obj = User.id
+        elif sort_by == 'post_count':
+            sort_obj = User.post_count
+        else:
+            sort_obj = User.username
 
-@forum.route("/post/<int:post_id>/raw", methods=["POST", "GET"])
-@login_required
-def raw_post(post_id):
-    post = Post.query.filter_by(id=post_id).first_or_404()
-    return format_quote(username=post.username, content=post.content)
+        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 = 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)
 
-@forum.route("/<int:forum_id>/markread", methods=["POST"])
-@forum.route("/<int:forum_id>-<slug>/markread", methods=["POST"])
-@login_required
-def markread(forum_id=None, slug=None):
-    # Mark a single forum as read
-    if forum_id:
-        forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
-        forumsread = ForumsRead.query.filter_by(
-            user_id=real(current_user).id, forum_id=forum_instance.id
-        ).first()
-        TopicsRead.query.filter_by(user_id=real(current_user).id,
-                                   forum_id=forum_instance.id).delete()
 
-        if not forumsread:
-            forumsread = ForumsRead()
-            forumsread.user = real(current_user)
-            forumsread.forum = forum_instance
+class TopicTracker(MethodView):
+    decorators = [login_required]
 
-        forumsread.last_read = time_utcnow()
-        forumsread.cleared = time_utcnow()
+    def get(self):
+        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)
+        ).add_entity(TopicsRead).order_by(Topic.last_updated.desc()).paginate(
+            page, flaskbb_config['TOPICS_PER_PAGE'], True
+        )
 
-        db.session.add(forumsread)
-        db.session.commit()
+        return render_template('forum/topictracker.html', topics=topics)
 
-        flash(_("Forum %(forum)s marked as read.", forum=forum_instance.title),
-              "success")
-
-        return redirect(forum_instance.url)
-
-    # Mark all forums as read
-    ForumsRead.query.filter_by(user_id=real(current_user).id).delete()
-    TopicsRead.query.filter_by(user_id=real(current_user).id).delete()
-
-    forums = Forum.query.all()
-    forumsread_list = []
-    for forum_instance in forums:
-        forumsread = ForumsRead()
-        forumsread.user = real(current_user)
-        forumsread.forum = forum_instance
-        forumsread.last_read = time_utcnow()
-        forumsread.cleared = time_utcnow()
-        forumsread_list.append(forumsread)
-
-    db.session.add_all(forumsread_list)
-    db.session.commit()
-
-    flash(_("All forums marked as read."), "success")
-
-    return redirect(url_for("forum.index"))
-
-
-@forum.route("/who-is-online")
-def who_is_online():
-    if current_app.config['REDIS_ENABLED']:
-        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)
-
-
-@forum.route("/memberlist", methods=['GET', 'POST'])
-def memberlist():
-    page = request.args.get('page', 1, type=int)
-    sort_by = request.args.get('sort_by', 'reg_date')
-    order_by = request.args.get('order_by', 'asc')
-
-    sort_obj = None
-    order_func = None
-    if order_by == 'asc':
-        order_func = asc
-    else:
-        order_func = desc
-
-    if sort_by == 'reg_date':
-        sort_obj = User.id
-    elif sort_by == 'post_count':
-        sort_obj = User.post_count
-    else:
-        sort_obj = User.username
-
-    search_form = UserSearchForm()
-    if search_form.validate():
-        users = search_form.get_results().\
-            paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
-        return render_template("forum/memberlist.html", users=users,
-                               search_form=search_form)
-    else:
-        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=search_form)
-
-
-@forum.route("/topictracker", methods=["GET", "POST"])
-@login_required
-def topictracker():
-    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)).\
-        add_entity(TopicsRead).\
-        order_by(Topic.last_updated.desc()).\
-        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], True)
-
-    # bulk untracking
-    if request.method == "POST":
-        topic_ids = request.form.getlist("rowid")
+    def post(self):
+        topic_ids = request.form.getlist('rowid')
         tmp_topics = Topic.query.filter(Topic.id.in_(topic_ids)).all()
 
         for topic in tmp_topics:
             real(current_user).untrack_topic(topic)
+
         real(current_user).save()
 
-        flash(_("%(topic_count)s topics untracked.",
-                topic_count=len(tmp_topics)), "success")
-        return redirect(url_for("forum.topictracker"))
+        flash(_('%(topic_count)s topics untracked.', topic_count=len(tmp_topics)), 'success')
+        return redirect(url_for('forum.topictracker'))
+
+
+class Search(MethodView):
+    form = SearchPageForm
+
+    def get(self):
+        return render_template('forum/search_form.html', form=self.form())
+
+    def post(self):
+        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_form.html', form=form)
+
+
+class DeleteTopic(MethodView):
+    decorators = [allows.requires(CanDeleteTopic), login_required]
+
+    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()
+        topic.delete(users=involved_users)
+        return redirect(url_for('forum.view_forum', forum_id=topic.forum_id))
+
+
+class LockTopic(MethodView):
+    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+
+    def post(self, topic_id, slug=None):
+        topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        topic.locked = True
+        topic.save()
+        return redirect(topic.url)
+
+
+class UnlockTopic(MethodView):
+    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+
+    def post(self, topic_id, slug=None):
+        topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        topic.locked = False
+        topic.save()
+        return redirect(topic.url)
+
+
+class HighlightTopic(MethodView):
+    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+
+    def post(self, topic_id, slug=None):
+        topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        topic.important = True
+        topic.save()
+        return redirect(topic.url)
+
+
+class TrivializeTopic(MethodView):
+    decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
+
+    def post(topic_id=None, slug=None):
+        topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        topic.important = False
+        topic.save()
+        return redirect(topic.url)
 
-    return render_template("forum/topictracker.html", topics=topics)
 
+class DeletePost(MethodView):
+    decorators = [allows.requires(CanDeletePost), login_required]
 
-@forum.route("/topictracker/<int:topic_id>/add", methods=["POST"])
-@forum.route("/topictracker/<int:topic_id>-<slug>/add", methods=["POST"])
-@login_required
-def track_topic(topic_id, slug=None):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
-    real(current_user).track_topic(topic)
-    real(current_user).save()
-    return redirect(topic.url)
+    def post(self, post_id):
+        post = Post.query.filter_by(id=post_id).first_or_404()
+        first_post = post.first_post
+        topic_url = post.topic.url
+        forum_url = post.topic.forum.url
 
+        post.delete()
 
-@forum.route("/topictracker/<int:topic_id>/delete", methods=["POST"])
-@forum.route("/topictracker/<int:topic_id>-<slug>/delete", methods=["POST"])
-@login_required
-def untrack_topic(topic_id, slug=None):
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
-    real(current_user).untrack_topic(topic)
-    real(current_user).save()
-    return redirect(topic.url)
+        # If the post was the first post in the topic, redirect to the forums
+        if first_post:
+            return redirect(forum_url)
+        return redirect(topic_url)
 
 
-@forum.route("/search", methods=['GET', 'POST'])
-def search():
-    form = SearchPageForm()
+class RawPost(MethodView):
+    decorators = [login_required]
 
-    if form.validate_on_submit():
-        result = form.get_results()
-        return render_template('forum/search_result.html', form=form,
-                               result=result)
+    def get(self, post_id):
+        post = Post.query.filter_by(id=post_id).first_or_404()
+        return format_quote(username=post.username, content=post.content)
 
-    return render_template('forum/search_form.html', form=form)
+
+class MarkRead(MethodView):
+    decorators = [login_required]
+
+    def post(self, forum_id=None, slug=None):
+        # Mark a single forum as read
+        if forum_id is not None:
+            forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
+            forumsread = ForumsRead.query.filter_by(
+                user_id=real(current_user).id, forum_id=forum_instance.id
+            ).first()
+            TopicsRead.query.filter_by(
+                user_id=real(current_user).id, forum_id=forum_instance.id
+            ).delete()
+
+            if not forumsread:
+                forumsread = ForumsRead()
+                forumsread.user = real(current_user)
+                forumsread.forum = forum_instance
+
+            forumsread.last_read = time_utcnow()
+            forumsread.cleared = time_utcnow()
+
+            db.session.add(forumsread)
+            db.session.commit()
+
+            flash(_('Forum %(forum)s marked as read.', forum=forum_instance.title), 'success')
+
+            return redirect(forum_instance.url)
+
+        # Mark all forums as read
+        ForumsRead.query.filter_by(user_id=real(current_user).id).delete()
+        TopicsRead.query.filter_by(user_id=real(current_user).id).delete()
+
+        forums = Forum.query.all()
+        forumsread_list = []
+        for forum_instance in forums:
+            forumsread = ForumsRead()
+            forumsread.user = real(current_user)
+            forumsread.forum = forum_instance
+            forumsread.last_read = time_utcnow()
+            forumsread.cleared = time_utcnow()
+            forumsread_list.append(forumsread)
+
+        db.session.add_all(forumsread_list)
+        db.session.commit()
+
+        flash(_('All forums marked as read.'), 'success')
+
+        return redirect(url_for('forum.index'))
+
+
+class WhoIsOnline(MethodView):
+
+    def get(self):
+        if current_app.config['REDIS_ENABLED']:
+            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)
+
+
+class TrackTopic(MethodView):
+    decorators = [login_required]
+
+    def post(self, topic_id, slug=None):
+        topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        real(current_user).track_topic(topic)
+        real(current_user).save()
+        return redirect(topic.url)
+
+
+class UntrackTopic(MethodView):
+    decorators = [login_required]
+
+    def post(self, topic_id, slug=None):
+        topic = Topic.query.filter_by(id=topic_id).first_or_404()
+        real(current_user).untrack_topic(topic)
+        real(current_user).save()
+        return redirect(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'))

+ 867 - 607
flaskbb/management/views.py

@@ -11,30 +11,31 @@
 import sys
 
 from celery import __version__ as celery_version
-from flask import (Blueprint, current_app, request, redirect, url_for, flash,
-                   jsonify, __version__ as flask_version)
+from flask import __version__ as flask_version
+from flask import (Blueprint, current_app, flash, jsonify, redirect, request,
+                   url_for)
+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 flask_plugins import get_all_plugins, get_plugin, get_plugin_from_all
-from flask_babelplus import gettext as _
-from flask_allows import Permission, Not
 
 from flaskbb import __version__ as flaskbb_version
 from flaskbb._compat import iteritems
+from flaskbb.extensions import allows, celery, db
 from flaskbb.forum.forms import UserSearchForm
-from flaskbb.utils.settings import flaskbb_config
-from flaskbb.utils.requirements import (IsAtleastModerator, IsAdmin,
-                                        CanBanUser, CanEditUser,
-                                        IsAtleastSuperModerator)
-from flaskbb.extensions import db, allows, celery
-from flaskbb.utils.helpers import (render_template, time_diff, time_utcnow,
-                                   get_online_users)
-from flaskbb.user.models import Guest, User, Group
-from flaskbb.forum.models import Post, Topic, Forum, Category, Report
+from flaskbb.forum.models import Category, Forum, Post, Report, Topic
+from flaskbb.management.forms import (AddForumForm, AddGroupForm, AddUserForm,
+                                      CategoryForm, EditForumForm,
+                                      EditGroupForm, EditUserForm)
 from flaskbb.management.models import Setting, SettingsGroup
-from flaskbb.management.forms import (AddUserForm, EditUserForm, AddGroupForm,
-                                      EditGroupForm, EditForumForm,
-                                      AddForumForm, CategoryForm)
-
+from flaskbb.user.models import Group, Guest, User
+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
 
 management = Blueprint("management", __name__)
 
@@ -47,734 +48,993 @@ def check_fresh_login():
         return current_app.login_manager.needs_refresh()
 
 
-@management.route("/")
-@allows.requires(IsAtleastModerator)
-def overview():
-    # user and group stats
-    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()
-    else:
-        online_users = len(get_online_users())
-
-    unread_reports = Report.query.\
-        filter(Report.zapped == None).\
-        order_by(Report.id.desc()).\
-        count()
-
-    celery_inspect = celery.control.inspect()
-    try:
-        celery_running = True if celery_inspect.ping() else False
-    except Exception:
-        # catching Exception is bad, and just catching ConnectionError
-        # from redis is also bad because you can run celery with other
-        # brokers as well.
-        celery_running = False
-
-    python_version = "{}.{}.{}".format(
-        sys.version_info[0], sys.version_info[1], sys.version_info[2]
-    )
-
-    stats = {
-        "current_app": current_app,
-        "unread_reports": unread_reports,
-        # stats stats
-        "all_users": User.query.count(),
-        "banned_users": banned_users,
-        "online_users": online_users,
-        "all_groups": Group.query.count(),
-        "report_count": Report.query.count(),
-        "topic_count": Topic.query.count(),
-        "post_count": Post.query.count(),
-        # components
-        "python_version": python_version,
-        "celery_version": celery_version,
-        "celery_running": celery_running,
-        "flask_version": flask_version,
-        "flaskbb_version": flaskbb_version,
-        # plugins
-        "plugins": get_all_plugins()
-    }
-
-    return render_template("management/overview.html", **stats)
-
-
-@management.route("/settings", methods=["GET", "POST"])
-@management.route("/settings/<path:slug>", methods=["GET", "POST"])
-@allows.requires(IsAdmin)
-def settings(slug=None):
-    slug = slug if slug else "general"
-
-    # get the currently active group
-    active_group = SettingsGroup.query.filter_by(key=slug).first_or_404()
-    # get all groups - used to build the navigation
-    all_groups = SettingsGroup.query.all()
-
-    SettingsForm = Setting.get_form(active_group)
-
-    old_settings = Setting.get_settings(active_group)
-    new_settings = {}
-
-    form = SettingsForm()
-
-    if form.validate_on_submit():
-        for key, values in iteritems(old_settings):
-            try:
-                # check if the value has changed
-                if values['value'] == form[key].data:
-                    continue
-                else:
-                    new_settings[key] = form[key].data
-            except KeyError:
-                pass
-        Setting.update(settings=new_settings, app=current_app)
-        flash(_("Settings saved."), "success")
-    else:
+class ManagementSettings(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    def get(self, slug=None):
+        slug = slug if slug else 'general'
+
+        # get the currently active group
+        active_group = SettingsGroup.query.filter_by(key=slug).first_or_404()
+        # get all groups - used to build the navigation
+        all_groups = SettingsGroup.query.all()
+
+        SettingsForm = Setting.get_form(active_group)
+
+        old_settings = Setting.get_settings(active_group)
+
+        form = SettingsForm()
         for key, values in iteritems(old_settings):
             try:
                 form[key].data = values['value']
             except (KeyError, ValueError):
                 pass
 
-    return render_template("management/settings.html", form=form,
-                           all_groups=all_groups, active_group=active_group)
+        return render_template(
+            'management/settings.html',
+            form=form,
+            all_groups=all_groups,
+            active_group=active_group
+        )
 
+    def post(self, slug=None):
+        slug = slug if slug else 'general'
+
+        # get the currently active group
+        active_group = SettingsGroup.query.filter_by(key=slug).first_or_404()
+        # get all groups - used to build the navigation
+        all_groups = SettingsGroup.query.all()
+
+        SettingsForm = Setting.get_form(active_group)
+
+        old_settings = Setting.get_settings(active_group)
+        new_settings = {}
+
+        form = SettingsForm()
+
+        if form.validate_on_submit():
+            for key, values in iteritems(old_settings):
+                try:
+                    # check if the value has changed
+                    if values['value'] == form[key].data:
+                        continue
+                    else:
+                        new_settings[key] = form[key].data
+                except KeyError:
+                    pass
+            Setting.update(settings=new_settings, app=current_app)
+            flash(_('Settings saved.'), 'success')
+        else:
+            for key, values in iteritems(old_settings):
+                try:
+                    form[key].data = values['value']
+                except (KeyError, ValueError):
+                    pass
+
+        return render_template(
+            'management/settings.html',
+            form=form,
+            all_groups=all_groups,
+            active_group=active_group
+        )
 
-# Users
-@management.route("/users", methods=['GET', 'POST'])
-@allows.requires(IsAtleastModerator)
-def users():
-    page = request.args.get("page", 1, type=int)
-    search_form = UserSearchForm()
 
-    if search_form.validate():
-        users = search_form.get_results().\
-            paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
-        return render_template("management/users.html", users=users,
-                               search_form=search_form)
+class ManageUsers(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
+    form = UserSearchForm
 
-    users = User.query. \
-        order_by(User.id.asc()).\
-        paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
+    def get(self):
+        page = request.args.get('page', 1, type=int)
+        form = self.form()
 
-    return render_template("management/users.html", users=users,
-                           search_form=search_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)
 
-@management.route("/users/<int:user_id>/edit", methods=["GET", "POST"])
-@allows.requires(IsAtleastModerator)
-def edit_user(user_id):
-    user = User.query.filter_by(id=user_id).first_or_404()
+    def post(self):
+        page = request.args.get('page', 1, type=int)
+        form = self.form()
 
-    if not Permission(CanEditUser, identity=current_user):
-        flash(_("You are not allowed to edit this user."), "danger")
-        return redirect(url_for("management.users"))
+        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)
 
-    member_group = db.and_(*[db.not_(getattr(Group, p)) for p in
-                             ['admin', 'mod', 'super_mod', 'banned', 'guest']])
+        users = User.query.order_by(User.id.asc()).paginate(
+            page, flaskbb_config['USERS_PER_PAGE'], False
+        )
 
-    filt = db.or_(
-        Group.id.in_(g.id for g in current_user.groups), member_group
-    )
+        return render_template('management/users.html', users=users, search_form=form)
 
-    if Permission(IsAtleastSuperModerator, identity=current_user):
-        filt = db.or_(filt, Group.mod)
 
-    if Permission(IsAdmin, identity=current_user):
-        filt = db.or_(filt, Group.admin, Group.super_mod)
+class EditUser(MethodView):
+    decorators = [allows.requires(IsAtleastModerator & CanEditUser)]
+    form = EditUserForm
 
-    if Permission(CanBanUser, identity=current_user):
-        filt = db.or_(filt, Group.banned)
+    def get(self, user_id):
+        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']
+            ]
+        )
 
-    group_query = Group.query.filter(filt)
+        filt = db.or_(Group.id.in_(g.id for g in current_user.groups), member_group)
 
-    form = EditUserForm(user)
-    form.primary_group.query = group_query
-    form.secondary_groups.query = group_query
-    if form.validate_on_submit():
-        form.populate_obj(user)
-        user.primary_group_id = form.primary_group.data.id
+        if Permission(IsAtleastSuperModerator, identity=current_user):
+            filt = db.or_(filt, Group.mod)
 
-        # Don't override the password
-        if form.password.data:
-            user.password = form.password.data
+        if Permission(IsAdmin, identity=current_user):
+            filt = db.or_(filt, Group.admin, Group.super_mod)
 
-        user.save(groups=form.secondary_groups.data)
+        if Permission(CanBanUser, identity=current_user):
+            filt = db.or_(filt, Group.banned)
 
-        flash(_("User updated."), "success")
-        return redirect(url_for("management.edit_user", user_id=user.id))
+        group_query = Group.query.filter(filt)
 
-    return render_template("management/user_form.html", form=form,
-                           title=_("Edit User"))
+        form.primary_group.query = group_query
+        form.secondary_groups.query = group_query
 
+        return render_template('management/user_form.html', form=form, title=_('Edit User'))
 
-@management.route("/users/delete", methods=["POST"])
-@management.route("/users/<int:user_id>/delete", methods=["POST"])
-@allows.requires(IsAdmin)
-def delete_user(user_id=None):
-    # ajax request
-    if request.is_xhr:
-        ids = request.get_json()["ids"]
+    def post(self, user_id):
+        user = User.query.filter_by(id=user_id).first_or_404()
 
-        data = []
-        for user in User.query.filter(User.id.in_(ids)).all():
-            # do not delete current user
-            if current_user.id == user.id:
-                continue
+        member_group = db.and_(
+            *[
+                db.not_(getattr(Group, p))
+                for p in ['admin', 'mod', 'super_mod', 'banned', 'guest']
+            ]
+        )
 
-            if user.delete():
-                data.append({
-                    "id": user.id,
-                    "type": "delete",
-                    "reverse": False,
-                    "reverse_name": None,
-                    "reverse_url": None
-                })
+        filt = db.or_(Group.id.in_(g.id for g in current_user.groups), member_group)
 
-        return jsonify(
-            message="{} users deleted.".format(len(data)),
-            category="success",
-            data=data,
-            status=200
-        )
+        if Permission(IsAtleastSuperModerator, identity=current_user):
+            filt = db.or_(filt, Group.mod)
 
-    user = User.query.filter_by(id=user_id).first_or_404()
-    if current_user.id == user.id:
-        flash(_("You cannot delete yourself.", "danger"))
-        return redirect(url_for("management.users"))
+        if Permission(IsAdmin, identity=current_user):
+            filt = db.or_(filt, Group.admin, Group.super_mod)
 
-    user.delete()
-    flash(_("User deleted."), "success")
-    return redirect(url_for("management.users"))
+        if Permission(CanBanUser, identity=current_user):
+            filt = db.or_(filt, Group.banned)
 
+        group_query = Group.query.filter(filt)
 
-@management.route("/users/add", methods=["GET", "POST"])
-@allows.requires(IsAdmin)
-def add_user():
-    form = AddUserForm()
-    if form.validate_on_submit():
-        form.save()
-        flash(_("User added."), "success")
-        return redirect(url_for("management.users"))
+        form = EditUserForm(user)
+        form.primary_group.query = group_query
+        form.secondary_groups.query = group_query
+        if form.validate_on_submit():
+            form.populate_obj(user)
+            user.primary_group_id = form.primary_group.data.id
 
-    return render_template("management/user_form.html", form=form,
-                           title=_("Add User"))
+            # Don't override the password
+            if form.password.data:
+                user.password = form.password.data
 
+            user.save(groups=form.secondary_groups.data)
 
-@management.route("/users/banned", methods=["GET", "POST"])
-@allows.requires(IsAtleastModerator)
-def banned_users():
-    page = request.args.get("page", 1, type=int)
-    search_form = UserSearchForm()
+            flash(_('User updated.'), 'success')
+            return redirect(url_for('management.edit_user', user_id=user.id))
 
-    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/user_form.html', form=form, title=_('Edit User'))
 
-    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)
-
-    return render_template("management/banned_users.html", users=users,
-                           search_form=search_form)
-
-
-@management.route("/users/ban", methods=["POST"])
-@management.route("/users/<int:user_id>/ban", methods=["POST"])
-@allows.requires(IsAtleastModerator)
-def ban_user(user_id=None):
-    if not Permission(CanBanUser, identity=current_user):
-        flash(_("You do not have the permissions to ban this user."), "danger")
-        return redirect(url_for("management.overview"))
-
-    # ajax request
-    if request.is_xhr:
-        ids = request.get_json()["ids"]
-
-        data = []
-        users = User.query.filter(User.id.in_(ids)).all()
-        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) 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)
-                })
-
-        return jsonify(
-            message="{} users banned.".format(len(data)),
-            category="success",
-            data=data,
-            status=200
-        )
+class DeleteUser(MethodView):
+    decorators = [allows.requires(IsAdmin)]
 
-    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):
-
-        flash(_("A moderator cannot ban an admin user."), "danger")
-        return redirect(url_for("management.overview"))
-
-    if not current_user.id == user.id and user.ban():
-        flash(_("User is now banned."), "success")
-    else:
-        flash(_("Could not ban user."), "danger")
-
-    return redirect(url_for("management.banned_users"))
-
-
-@management.route("/users/unban", methods=["POST"])
-@management.route("/users/<int:user_id>/unban", methods=["POST"])
-@allows.requires(IsAtleastModerator)
-def unban_user(user_id=None):
-    if not Permission(CanBanUser, identity=current_user):
-        flash(_("You do not have the permissions to unban this user."),
-              "danger")
-        return redirect(url_for("management.overview"))
-
-    # ajax request
-    if request.is_xhr:
-        ids = request.get_json()["ids"]
-
-        data = []
-        for user in User.query.filter(User.id.in_(ids)).all():
-            if user.unban():
-                data.append({
-                    "id": user.id,
-                    "type": "unban",
-                    "reverse": "ban",
-                    "reverse_name": _("Ban"),
-                    "reverse_url": url_for("management.ban_user",
-                                           user_id=user.id)
-                })
-
-        return jsonify(
-            message="{} users unbanned.".format(len(data)),
-            category="success",
-            data=data,
-            status=200
-        )
+    def post(self, user_id=None):
+        # ajax request
+        if request.is_xhr:
+            ids = request.get_json()["ids"]
+
+            data = []
+            for user in User.query.filter(User.id.in_(ids)).all():
+                # do not delete current user
+                if current_user.id == user.id:
+                    continue
 
-    user = User.query.filter_by(id=user_id).first_or_404()
+                if user.delete():
+                    data.append(
+                        {
+                            "id": user.id,
+                            "type": "delete",
+                            "reverse": False,
+                            "reverse_name": None,
+                            "reverse_url": None
+                        }
+                    )
 
-    if user.unban():
-        flash(_("User is now unbanned."), "success")
-    else:
-        flash(_("Could not unban user."), "danger")
+            return jsonify(
+                message="{} users deleted.".format(len(data)),
+                category="success",
+                data=data,
+                status=200
+            )
 
-    return redirect(url_for("management.banned_users"))
+        user = User.query.filter_by(id=user_id).first_or_404()
+        if current_user.id == user.id:
+            flash(_("You cannot delete yourself.", "danger"))
+            return redirect(url_for("management.users"))
 
+        user.delete()
+        flash(_("User deleted."), "success")
+        return redirect(url_for("management.users"))
 
-# Reports
-@management.route("/reports")
-@allows.requires(IsAtleastModerator)
-def reports():
-    page = request.args.get("page", 1, type=int)
-    reports = Report.query.\
-        order_by(Report.id.asc()).\
-        paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
-    return render_template("management/reports.html", reports=reports)
+class AddUser(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+    form = AddUserForm
 
+    def get(self):
+        return render_template('management/user_form.html', form=self.form(), title=_('Add User'))
 
-@management.route("/reports/unread")
-@allows.requires(IsAtleastModerator)
-def unread_reports():
-    page = request.args.get("page", 1, type=int)
-    reports = Report.query.\
-        filter(Report.zapped == None).\
-        order_by(Report.id.desc()).\
-        paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            form.save()
+            flash(_('User added.'), 'success')
+            return redirect(url_for('management.users'))
 
-    return render_template("management/reports.html", reports=reports)
+        return render_template('management/user_form.html', form=form, title=_('Add User'))
 
 
-@management.route("/reports/<int:report_id>/markread", methods=["POST"])
-@management.route("/reports/markread", methods=["POST"])
-@allows.requires(IsAtleastModerator)
-def report_markread(report_id=None):
-    # AJAX request
-    if request.is_xhr:
-        ids = request.get_json()["ids"]
-        data = []
+class BannedUsers(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
+    form = UserSearchForm
 
-        for report in Report.query.filter(Report.id.in_(ids)).all():
-            report.zapped_by = current_user.id
-            report.zapped = time_utcnow()
-            report.save()
-            data.append({
-                "id": report.id,
-                "type": "read",
-                "reverse": False,
-                "reverse_name": None,
-                "reverse_url": None
-            })
-
-        return jsonify(
-            message="{} reports marked as read.".format(len(data)),
-            category="success",
-            data=data,
-            status=200
+    def get(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)
+
+        return render_template(
+            'management/banned_users.html', users=users, search_form=search_form
         )
 
-    # mark single report as read
-    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")
-            return redirect(url_for("management.reports"))
+    def post(self):
+        page = request.args.get('page', 1, type=int)
+        search_form = self.form()
 
-        report.zapped_by = current_user.id
-        report.zapped = time_utcnow()
-        report.save()
-        flash(_("Report %(id)s marked as read.", id=report.id), "success")
-        return redirect(url_for("management.reports"))
+        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)
 
-    # mark all as read
-    reports = Report.query.filter(Report.zapped == None).all()
-    report_list = []
-    for report in reports:
-        report.zapped_by = current_user.id
-        report.zapped = time_utcnow()
-        report_list.append(report)
-
-    db.session.add_all(report_list)
-    db.session.commit()
-
-    flash(_("All reports were marked as read."), "success")
-    return redirect(url_for("management.reports"))
-
-
-@management.route("/reports/<int:report_id>/delete", methods=["POST"])
-@management.route("/reports/delete", methods=["POST"])
-@allows.requires(IsAtleastModerator)
-def delete_report(report_id=None):
-    if request.is_xhr:
-        ids = request.get_json()["ids"]
-        data = []
-
-        for report in Report.query.filter(Report.id.in_(ids)).all():
-            if report.delete():
-                data.append({
-                    "id": report.id,
-                    "type": "delete",
-                    "reverse": False,
-                    "reverse_name": None,
-                    "reverse_url": None
-                })
-
-        return jsonify(
-            message="{} reports deleted.".format(len(data)),
-            category="success",
-            data=data,
-            status=200
+            return render_template(
+                'management/banned_users.html', users=users, search_form=search_form
+            )
+
+        return render_template(
+            'management/banned_users.html', users=users, search_form=search_form
         )
 
-    report = Report.query.filter_by(id=report_id).first_or_404()
-    report.delete()
-    flash(_("Report deleted."), "success")
-    return redirect(url_for("management.reports"))
 
+class BanUser(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
 
-# Groups
-@management.route("/groups")
-@allows.requires(IsAdmin)
-def groups():
-    page = request.args.get("page", 1, type=int)
+    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")
+            return redirect(url_for("management.overview"))
 
-    groups = Group.query.\
-        order_by(Group.id.asc()).\
-        paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
+        # ajax request
+        if request.is_xhr:
+            ids = request.get_json()["ids"]
 
-    return render_template("management/groups.html", groups=groups)
+            data = []
+            users = User.query.filter(User.id.in_(ids)).all()
+            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)
+                        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)
+                        }
+                    )
+
+            return jsonify(
+                message="{} users banned.".format(len(data)),
+                category="success",
+                data=data,
+                status=200
+            )
 
-@management.route("/groups/<int:group_id>/edit", methods=["GET", "POST"])
-@allows.requires(IsAdmin)
-def edit_group(group_id):
-    group = Group.query.filter_by(id=group_id).first_or_404()
+        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):
+            flash(_("A moderator cannot ban an admin user."), "danger")
+            return redirect(url_for("management.overview"))
 
-    form = EditGroupForm(group)
+        if not current_user.id == user.id and user.ban():
+            flash(_("User is now banned."), "success")
+        else:
+            flash(_("Could not ban user."), "danger")
+        return redirect(url_for("management.banned_users"))
 
-    if form.validate_on_submit():
-        form.populate_obj(group)
-        group.save()
 
-        if group.guest:
-            Guest.invalidate_cache()
+class UnbanUser(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
 
-        flash(_("Group updated."), "success")
-        return redirect(url_for("management.groups", group_id=group.id))
+    def post(self, user_id=None):
 
-    return render_template("management/group_form.html", form=form,
-                           title=_("Edit Group"))
+        if not Permission(CanBanUser, identity=current_user):
+            flash(_("You do not have the permissions to unban this user."), "danger")
+            return redirect(url_for("management.overview"))
 
+        # ajax request
+        if request.is_xhr:
+            ids = request.get_json()["ids"]
 
-@management.route("/groups/<int:group_id>/delete", methods=["POST"])
-@management.route("/groups/delete", methods=["POST"])
-@allows.requires(IsAdmin)
-def delete_group(group_id=None):
-    if request.is_xhr:
-        ids = request.get_json()["ids"]
-        if not (set(ids) & set(["1", "2", "3", "4", "5"])):
             data = []
-            for group in Group.query.filter(Group.id.in_(ids)).all():
-                group.delete()
-                data.append({
-                    "id": group.id,
-                    "type": "delete",
-                    "reverse": False,
-                    "reverse_name": None,
-                    "reverse_url": None
-                })
+            for user in User.query.filter(User.id.in_(ids)).all():
+                if user.unban():
+                    data.append(
+                        {
+                            "id": user.id,
+                            "type": "unban",
+                            "reverse": "ban",
+                            "reverse_name": _("Ban"),
+                            "reverse_url": url_for("management.ban_user", user_id=user.id)
+                        }
+                    )
 
             return jsonify(
-                message="{} groups deleted.".format(len(data)),
+                message="{} users unbanned.".format(len(data)),
                 category="success",
                 data=data,
                 status=200
             )
-        return jsonify(
-            message=_("You cannot delete one of the standard groups."),
-            category="danger",
-            data=None,
-            status=404
+
+        user = User.query.filter_by(id=user_id).first_or_404()
+
+        if user.unban():
+            flash(_("User is now unbanned."), "success")
+        else:
+            flash(_("Could not unban user."), "danger")
+
+        return redirect(url_for("management.banned_users"))
+
+
+class Groups(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    def get(self):
+
+        page = request.args.get("page", 1, type=int)
+
+        groups = Group.query.\
+            order_by(Group.id.asc()).\
+            paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
+
+        return render_template("management/groups.html", groups=groups)
+
+
+class AddGroup(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+    form = AddGroupForm
+
+    def get(self):
+        return render_template(
+            'management/group_form.html', form=self.form(), title=_('Add Group')
         )
 
-    if group_id is not None:
-        if group_id <= 5:  # there are 5 standard groups
-            flash(_("You cannot delete the standard groups. "
-                    "Try renaming it instead.", "danger"))
-            return redirect(url_for("management.groups"))
+    def post(self):
+        form = AddGroupForm()
+        if form.validate_on_submit():
+            form.save()
+            flash(_('Group added.'), 'success')
+            return redirect(url_for('management.groups'))
+
+        return render_template('management/group_form.html', form=form, title=_('Add Group'))
+
 
+class EditGroup(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+    form = EditGroupForm
+
+    def get(self, group_id):
         group = Group.query.filter_by(id=group_id).first_or_404()
-        group.delete()
-        flash(_("Group deleted."), "success")
-        return redirect(url_for("management.groups"))
+        form = self.form(group)
+        return render_template('management/group_form.html', form=form, title=_('Edit Group'))
 
-    flash(_("No group chosen."), "danger")
-    return redirect(url_for("management.groups"))
+    def post(self, group_id):
+        group = Group.query.filter_by(id=group_id).first_or_404()
+        form = EditGroupForm(group)
+
+        if form.validate_on_submit():
+            form.populate_obj(group)
+            group.save()
+
+            if group.guest:
+                Guest.invalidate_cache()
+
+            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'))
+
+
+class DeleteGroup(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    def post(self, group_id=None):
+        if request.is_xhr:
+            ids = request.get_json()["ids"]
+            if not (set(ids) & set(["1", "2", "3", "4", "5"])):
+                data = []
+                for group in Group.query.filter(Group.id.in_(ids)).all():
+                    group.delete()
+                    data.append(
+                        {
+                            "id": group.id,
+                            "type": "delete",
+                            "reverse": False,
+                            "reverse_name": None,
+                            "reverse_url": None
+                        }
+                    )
+
+                return jsonify(
+                    message="{} groups deleted.".format(len(data)),
+                    category="success",
+                    data=data,
+                    status=200
+                )
+            return jsonify(
+                message=_("You cannot delete one of the standard groups."),
+                category="danger",
+                data=None,
+                status=404
+            )
 
+        if group_id is not None:
+            if group_id <= 5:  # there are 5 standard groups
+                flash(
+                    _(
+                        "You cannot delete the standard groups. "
+                        "Try renaming it instead.", "danger"
+                    )
+                )
+                return redirect(url_for("management.groups"))
+
+            group = Group.query.filter_by(id=group_id).first_or_404()
+            group.delete()
+            flash(_("Group deleted."), "success")
+            return redirect(url_for("management.groups"))
 
-@management.route("/groups/add", methods=["GET", "POST"])
-@allows.requires(IsAdmin)
-def add_group():
-    form = AddGroupForm()
-    if form.validate_on_submit():
-        form.save()
-        flash(_("Group added."), "success")
+        flash(_("No group chosen."), "danger")
         return redirect(url_for("management.groups"))
 
-    return render_template("management/group_form.html", form=form,
-                           title=_("Add Group"))
 
+class Forums(MethodView):
+    decorators = [allows.requires(IsAdmin)]
 
-# Forums and Categories
-@management.route("/forums")
-@allows.requires(IsAdmin)
-def forums():
-    categories = Category.query.order_by(Category.position.asc()).all()
-    return render_template("management/forums.html", categories=categories)
+    def get(self):
+        categories = Category.query.order_by(Category.position.asc()).all()
+        return render_template("management/forums.html", categories=categories)
 
 
-@management.route("/forums/<int:forum_id>/edit", methods=["GET", "POST"])
-@allows.requires(IsAdmin)
-def edit_forum(forum_id):
-    forum = Forum.query.filter_by(id=forum_id).first_or_404()
+class EditForum(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+    form = EditForumForm
+
+    def get(self, forum_id):
+        forum = Forum.query.filter_by(id=forum_id).first_or_404()
+
+        form = self.form(forum)
 
-    form = EditForumForm(forum)
-    if form.validate_on_submit():
-        form.save()
-        flash(_("Forum updated."), "success")
-        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'))
 
-@management.route("/forums/<int:forum_id>/delete", methods=["POST"])
-@allows.requires(IsAdmin)
-def delete_forum(forum_id):
-    forum = Forum.query.filter_by(id=forum_id).first_or_404()
+    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()
+        form = self.form(forum)
+        if form.validate_on_submit():
+            form.save()
+            flash(_('Forum updated.'), 'success')
+            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])
+            else:
+                form.moderators.data = None
 
-    forum.delete(involved_users)
+        return render_template('management/forum_form.html', form=form, title=_('Edit Forum'))
 
-    flash(_("Forum deleted."), "success")
-    return redirect(url_for("management.forums"))
 
+class AddForum(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+    form = AddForumForm
 
-@management.route("/forums/add", methods=["GET", "POST"])
-@management.route("/forums/<int:category_id>/add", methods=["GET", "POST"])
-@allows.requires(IsAdmin)
-def add_forum(category_id=None):
-    form = AddForumForm()
+    def get(self, category_id=None):
+        form = self.form()
 
-    if form.validate_on_submit():
-        form.save()
-        flash(_("Forum added."), "success")
-        return redirect(url_for("management.forums"))
-    else:
         form.groups.data = Group.query.order_by(Group.id.asc()).all()
+
         if category_id:
             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()
+
+        if form.validate_on_submit():
+            form.save()
+            flash(_('Forum added.'), 'success')
+            return redirect(url_for('management.forums'))
+        else:
+            form.groups.data = Group.query.order_by(Group.id.asc()).all()
+            if category_id:
+                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'))
+
+
+class DeleteForum(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    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()
+
+        forum.delete(involved_users)
+
+        flash(_("Forum deleted."), "success")
+        return redirect(url_for("management.forums"))
+
+
+class AddCategory(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+    form = CategoryForm
+
+    def get(self):
+        return render_template(
+            'management/category_form.html', form=self.form(), title=_('Add Category')
+        )
+
+    def post(self):
+        form = self.form()
+
+        if form.validate_on_submit():
+            form.save()
+            flash(_('Category added.'), 'success')
+            return redirect(url_for('management.forums'))
+
+        return render_template('management/category_form.html', form=form, title=_('Add Category'))
+
+
+class EditCategory(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+    form = CategoryForm
+
+    def get(self, category_id):
+        category = Category.query.filter_by(id=category_id).first_or_404()
 
+        form = self.form(obj=category)
 
-@management.route("/category/add", methods=["GET", "POST"])
-@allows.requires(IsAdmin)
-def add_category():
-    form = CategoryForm()
+        return render_template(
+            'management/category_form.html', form=form, title=_('Edit Category')
+        )
+
+    def post(self, category_id):
+        category = Category.query.filter_by(id=category_id).first_or_404()
+
+        form = self.form(obj=category)
+
+        if form.validate_on_submit():
+            form.populate_obj(category)
+            flash(_('Category updated.'), 'success')
+            category.save()
+
+        return render_template(
+            'management/category_form.html', form=form, title=_('Edit Category')
+        )
 
-    if form.validate_on_submit():
-        form.save()
-        flash(_("Category added."), "success")
+
+class DeleteCategory(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    def post(self, category_id):
+        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
+        ).all()
+
+        category.delete(involved_users)
+        flash(_("Category with all associated forums deleted."), "success")
         return redirect(url_for("management.forums"))
 
-    return render_template("management/category_form.html", form=form,
-                           title=_("Add Category"))
 
+class Reports(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
+
+    def get(self):
+        page = request.args.get("page", 1, type=int)
+        reports = Report.query.\
+            order_by(Report.id.asc()).\
+            paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
-@management.route("/category/<int:category_id>/edit", methods=["GET", "POST"])
-@allows.requires(IsAdmin)
-def edit_category(category_id):
-    category = Category.query.filter_by(id=category_id).first_or_404()
+        return render_template("management/reports.html", reports=reports)
 
-    form = CategoryForm(obj=category)
 
-    if form.validate_on_submit():
-        form.populate_obj(category)
-        flash(_("Category updated."), "success")
-        category.save()
+class UnreadReports(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
 
-    return render_template("management/category_form.html", form=form,
-                           title=_("Edit Category"))
+    def get(self):
+        page = request.args.get("page", 1, type=int)
+        reports = Report.query.\
+            filter(Report.zapped == None).\
+            order_by(Report.id.desc()).\
+            paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
+        return render_template("management/reports.html", reports=reports)
 
-@management.route("/category/<int:category_id>/delete", methods=["POST"])
-@allows.requires(IsAdmin)
-def delete_category(category_id):
-    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).all()
+class MarkReportRead(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
 
-    category.delete(involved_users)
-    flash(_("Category with all associated forums deleted."), "success")
-    return redirect(url_for("management.forums"))
+    def post(self, report_id=None):
 
+        # AJAX request
+        if request.is_xhr:
+            ids = request.get_json()["ids"]
+            data = []
 
-# Plugins
-@management.route("/plugins")
-@allows.requires(IsAdmin)
-def plugins():
-    plugins = get_all_plugins()
-    return render_template("management/plugins.html", plugins=plugins)
+            for report in Report.query.filter(Report.id.in_(ids)).all():
+                report.zapped_by = current_user.id
+                report.zapped = time_utcnow()
+                report.save()
+                data.append(
+                    {
+                        "id": report.id,
+                        "type": "read",
+                        "reverse": False,
+                        "reverse_name": None,
+                        "reverse_url": None
+                    }
+                )
 
+            return jsonify(
+                message="{} reports marked as read.".format(len(data)),
+                category="success",
+                data=data,
+                status=200
+            )
 
-@management.route("/plugins/<path:plugin>/enable", methods=["POST"])
-@allows.requires(IsAdmin)
-def enable_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
+        # mark single report as read
+        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")
+                return redirect(url_for("management.reports"))
+
+            report.zapped_by = current_user.id
+            report.zapped = time_utcnow()
+            report.save()
+            flash(_("Report %(id)s marked as read.", id=report.id), "success")
+            return redirect(url_for("management.reports"))
+
+        # mark all as read
+        reports = Report.query.filter(Report.zapped == None).all()
+        report_list = []
+        for report in reports:
+            report.zapped_by = current_user.id
+            report.zapped = time_utcnow()
+            report_list.append(report)
+
+        db.session.add_all(report_list)
+        db.session.commit()
+
+        flash(_("All reports were marked as read."), "success")
+        return redirect(url_for("management.reports"))
+
+
+class DeleteReport(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
+
+    def post(self, report_id=None):
+
+        if request.is_xhr:
+            ids = request.get_json()["ids"]
+            data = []
+
+            for report in Report.query.filter(Report.id.in_(ids)).all():
+                if report.delete():
+                    data.append(
+                        {
+                            "id": report.id,
+                            "type": "delete",
+                            "reverse": False,
+                            "reverse_name": None,
+                            "reverse_url": None
+                        }
+                    )
+
+            return jsonify(
+                message="{} reports deleted.".format(len(data)),
+                category="success",
+                data=data,
+                status=200
+            )
+
+        report = Report.query.filter_by(id=report_id).first_or_404()
+        report.delete()
+        flash(_("Report deleted."), "success")
+        return redirect(url_for("management.reports"))
+
+
+class ManagementOverview(MethodView):
+    decorators = [allows.requires(IsAtleastModerator)]
+
+    def get(self):
+        # user and group stats
+        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()
+        else:
+            online_users = len(get_online_users())
+
+        unread_reports = Report.query.\
+            filter(Report.zapped == None).\
+            order_by(Report.id.desc()).\
+            count()
+
+        celery_inspect = celery.control.inspect()
+        try:
+            celery_running = True if celery_inspect.ping() else False
+        except Exception:
+            # catching Exception is bad, and just catching ConnectionError
+            # from redis is also bad because you can run celery with other
+            # brokers as well.
+            celery_running = False
+
+        python_version = "{}.{}.{}".format(
+            sys.version_info[0], sys.version_info[1], sys.version_info[2]
+        )
+
+        stats = {
+            "current_app": current_app,
+            "unread_reports": unread_reports,
+            # stats stats
+            "all_users": User.query.count(),
+            "banned_users": banned_users,
+            "online_users": online_users,
+            "all_groups": Group.query.count(),
+            "report_count": Report.query.count(),
+            "topic_count": Topic.query.count(),
+            "post_count": Post.query.count(),
+            # components
+            "python_version": python_version,
+            "celery_version": celery_version,
+            "celery_running": celery_running,
+            "flask_version": flask_version,
+            "flaskbb_version": flaskbb_version,
+            # plugins
+            "plugins": get_all_plugins()
+        }
+
+        return render_template("management/overview.html", **stats)
+
+
+class PluginsView(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    def get(self):
+        plugins = get_all_plugins()
+        return render_template("management/plugins.html", plugins=plugins)
+
+
+class EnablePlugin(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    def post(self, plugin):
+        plugin = get_plugin_from_all(plugin)
+
+        if plugin.enabled:
+            flash(_("Plugin %(plugin)s is already enabled.", plugin=plugin.name), "info")
+            return redirect(url_for("management.plugins"))
+
+        try:
+            plugin.enable()
+            flash(
+                _("Plugin %(plugin)s enabled. Please restart FlaskBB now.", plugin=plugin.name),
+                "success"
+            )
+        except OSError:
+            flash(
+                _(
+                    "It seems that FlaskBB does not have enough filesystem "
+                    "permissions. Try removing the 'DISABLED' file by "
+                    "yourself instead."
+                ), "danger"
+            )
 
-    if plugin.enabled:
-        flash(_("Plugin %(plugin)s is already enabled.", plugin=plugin.name),
-              "info")
         return redirect(url_for("management.plugins"))
 
-    try:
-        plugin.enable()
-        flash(_("Plugin %(plugin)s enabled. Please restart FlaskBB now.",
-                plugin=plugin.name), "success")
-    except OSError:
-        flash(_("It seems that FlaskBB does not have enough filesystem "
-                "permissions. Try removing the 'DISABLED' file by "
-                "yourself instead."), "danger")
-
-    return redirect(url_for("management.plugins"))
-
-
-@management.route("/plugins/<path:plugin>/disable", methods=["POST"])
-@allows.requires(IsAdmin)
-def disable_plugin(plugin):
-    try:
-        plugin = get_plugin(plugin)
-    except KeyError:
-        flash(_("Plugin %(plugin)s not found.", plugin=plugin.name), "danger")
+
+class DisablePlugin(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    def post(self, plugin):
+        try:
+            plugin = get_plugin(plugin)
+        except KeyError:
+            flash(_("Plugin %(plugin)s not found.", plugin=plugin.name), "danger")
+            return redirect(url_for("management.plugins"))
+
+        try:
+            plugin.disable()
+            flash(
+                _("Plugin %(plugin)s disabled. Please restart FlaskBB now.", plugin=plugin.name),
+                "success"
+            )
+        except OSError:
+            flash(
+                _(
+                    "It seems that FlaskBB does not have enough filesystem "
+                    "permissions. Try creating the 'DISABLED' file by "
+                    "yourself instead."
+                ), "danger"
+            )
+
         return redirect(url_for("management.plugins"))
 
-    try:
-        plugin.disable()
-        flash(_("Plugin %(plugin)s disabled. Please restart FlaskBB now.",
-                plugin=plugin.name), "success")
-    except OSError:
-        flash(_("It seems that FlaskBB does not have enough filesystem "
-                "permissions. Try creating the 'DISABLED' file by "
-                "yourself instead."), "danger")
 
-    return redirect(url_for("management.plugins"))
+class UninstallPlugin(MethodView):
+    decorators = [allows.requires(IsAdmin)]
+
+    def post(self, plugin):
+        plugin = get_plugin_from_all(plugin)
+        if plugin.installed:
+            plugin.uninstall()
+            Setting.invalidate_cache()
 
+            flash(_("Plugin has been uninstalled."), "success")
+        else:
+            flash(_("Cannot uninstall plugin."), "danger")
 
-@management.route("/plugins/<path:plugin>/uninstall", methods=["POST"])
-@allows.requires(IsAdmin)
-def uninstall_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
-    if plugin.installed:
-        plugin.uninstall()
-        Setting.invalidate_cache()
+        return redirect(url_for("management.plugins"))
 
-        flash(_("Plugin has been uninstalled."), "success")
-    else:
-        flash(_("Cannot uninstall plugin."), "danger")
 
-    return redirect(url_for("management.plugins"))
+class InstallPlugin(MethodView):
+    decorators = [allows.requires(IsAdmin)]
 
+    def post(self, plugin):
+        plugin = get_plugin_from_all(plugin)
+        if not plugin.installed:
+            plugin.install()
+            Setting.invalidate_cache()
 
-@management.route("/plugins/<path:plugin>/install", methods=["POST"])
-@allows.requires(IsAdmin)
-def install_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
-    if not plugin.installed:
-        plugin.install()
-        Setting.invalidate_cache()
+            flash(_("Plugin has been installed."), "success")
+        else:
+            flash(_("Cannot install plugin."), "danger")
+
+        return redirect(url_for("management.plugins"))
 
-        flash(_("Plugin has been installed."), "success")
-    else:
-        flash(_("Cannot install plugin."), "danger")
 
-    return redirect(url_for("management.plugins"))
+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')
+)
+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'))
+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'))
+register_view(
+    management,
+    routes=['/plugins/<path:plugin>/disable'],
+    view_func=DisablePlugin.as_view('disable_plugin')
+)
+register_view(
+    management,
+    routes=['/plugins/<path:plugin>/enable'],
+    view_func=EnablePlugin.as_view('enable_plugin')
+)
+register_view(
+    management,
+    routes=['/plugins/<path:plugin>/install'],
+    view_func=InstallPlugin.as_view('install_plugin')
+)
+register_view(
+    management,
+    routes=['/plugins/<path:plugin>/uninstall'],
+    view_func=UninstallPlugin.as_view('uninstall_plugin')
+)
+register_view(management, routes=['/plugins'], view_func=PluginsView.as_view('plugins'))
+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'))
+register_view(
+    management,
+    routes=['/settings', '/settings/<path:slug>'],
+    view_func=ManagementSettings.as_view('settings')
+)
+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'))

+ 322 - 247
flaskbb/message/views.py

@@ -9,142 +9,153 @@
     :license: BSD, see LICENSE for more details.
 """
 import uuid
+from functools import wraps
 
-from flask import Blueprint, redirect, request, url_for, flash, abort
-from flask_login import login_required, current_user
+from flask import Blueprint, abort, flash, redirect, request, url_for
+from flask.views import MethodView
 from flask_babelplus import gettext as _
+from flask_login import current_user, login_required
 
 from flaskbb.extensions import db
-from flaskbb.utils.settings import flaskbb_config
-from flaskbb.utils.helpers import render_template, format_quote, time_utcnow
 from flaskbb.message.forms import ConversationForm, MessageForm
 from flaskbb.message.models import Conversation, Message
 from flaskbb.user.models import User
+from flaskbb.utils.helpers import (format_quote, register_view,
+                                   render_template, time_utcnow)
+from flaskbb.utils.settings import flaskbb_config
 
 message = Blueprint("message", __name__)
 
 
-@message.route("/")
-@message.route("/inbox")
-@login_required
-def inbox():
-    page = request.args.get('page', 1, type=int)
+def requires_message_box_space(f):
+
+    @wraps(f)
+    def wrapper(*a, **k):
+        message_count = Conversation.query.filter(Conversation.user_id == current_user.id).count()
+
+        if message_count >= flaskbb_config["MESSAGE_QUOTA"]:
+            flash(
+                _(
+                    "You cannot send any messages anymore because you have "
+                    "reached your message limit."
+                ), "danger"
+            )
+            return redirect(url_for("message.inbox"))
+        return f(*a, **k)
+
+    return wrapper
+
+
+class Inbox(MethodView):
+    decorators = [login_required]
+
+    def get(self):
+        page = request.args.get('page', 1, type=int)
 
-    # the inbox will display both, the recieved and the sent messages
-    conversations = Conversation.query. \
-        filter(
+        # the inbox will display both, the recieved and the sent messages
+        conversations = Conversation.query.filter(
             Conversation.user_id == current_user.id,
             Conversation.draft == False,
             Conversation.trash == False,
-        ).\
-        order_by(Conversation.date_modified.desc()). \
-        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
-
-    # we can't simply do conversations.total because it would ignore
-    # drafted and trashed messages
-    message_count = Conversation.query. \
-        filter(Conversation.user_id == current_user.id).\
-        count()
-
-    return render_template("message/inbox.html", conversations=conversations,
-                           message_count=message_count)
-
-
-@message.route("/<int:conversation_id>/view", methods=["GET", "POST"])
-@login_required
-def view_conversation(conversation_id):
-    # if the user is not linked with the conversation it will abort with 404
-    conversation = Conversation.query.filter_by(
-        id=conversation_id,
-        user_id=current_user.id
-    ).first_or_404()
-
-    if conversation.unread:
-        conversation.unread = False
-        current_user.invalidate_cache(permissions=False)
-        conversation.save()
+        ).order_by(Conversation.date_modified.desc()).paginate(
+            page, flaskbb_config['TOPICS_PER_PAGE'], False
+        )
 
-    form = MessageForm()
-    if form.validate_on_submit():
+        # we can't simply do conversations.total because it would ignore
+        # drafted and trashed messages
+        message_count = Conversation.query.filter(Conversation.user_id == current_user.id).count()
 
-        message_count = Conversation.query. \
-            filter(Conversation.user_id == current_user.id).\
-            count()
+        return render_template(
+            "message/inbox.html", conversations=conversations, message_count=message_count
+        )
 
-        if message_count >= flaskbb_config["MESSAGE_QUOTA"]:
-            flash(_("You cannot send any messages anymore because you have "
-                    "reached your message limit."), "danger")
-            return redirect(url_for("message.view_conversation",
-                                    conversation_id=conversation.id))
-
-        to_user_id = None
-        # If the current_user is the user who recieved the message
-        # then we have to change the id's a bit.
-        if current_user.id == conversation.to_user_id:
-            to_user_id = conversation.from_user_id
-        else:
-            to_user_id = conversation.to_user_id
 
-        form.save(conversation=conversation, user_id=current_user.id)
+class ViewConversation(MethodView):
+    decorators = [login_required]
+    form = MessageForm
 
-        # save the message in the recievers conversation
-        old_conv = conversation
-        conversation = Conversation.query. \
-            filter(
+    def get(self, conversation_id):
+        conversation = Conversation.query.filter_by(
+            id=conversation_id, user_id=current_user.id
+        ).first_or_404()
+
+        if conversation.unread:
+            conversation.unread = False
+            current_user.invalidate_cache(permissions=False)
+            conversation.save()
+
+        form = self.form()
+        return render_template("message/conversation.html", conversation=conversation, form=form)
+
+    @requires_message_box_space
+    def post(self, conversation_id):
+        conversation = Conversation.query.filter_by(
+            id=conversation_id, user_id=current_user.id
+        ).first_or_404()
+
+        form = self.form()
+        if form.validate_on_submit():
+            to_user_id = None
+            # If the current_user is the user who recieved the message
+            # then we have to change the id's a bit.
+            if current_user.id == conversation.to_user_id:
+                to_user_id = conversation.from_user_id
+            else:
+                to_user_id = conversation.to_user_id
+
+            form.save(conversation=conversation, user_id=current_user.id)
+
+            # save the message in the recievers conversation
+            old_conv = conversation
+            conversation = Conversation.query.filter(
                 Conversation.user_id == to_user_id,
                 Conversation.shared_id == conversation.shared_id
             ).first()
 
-        # user deleted the conversation, start a new conversation with just
-        # the recieving message
-        if conversation is None:
-            conversation = Conversation(
-                subject=old_conv.subject,
-                from_user_id=current_user.id,
-                to_user=to_user_id,
-                user_id=to_user_id,
-                shared_id=old_conv.shared_id
-            )
-            conversation.save()
+            # user deleted the conversation, start a new conversation with just
+            # the recieving message
+            if conversation is None:
+                conversation = Conversation(
+                    subject=old_conv.subject,
+                    from_user_id=current_user.id,
+                    to_user=to_user_id,
+                    user_id=to_user_id,
+                    shared_id=old_conv.shared_id
+                )
+                conversation.save()
 
-        form.save(conversation=conversation, user_id=current_user.id,
-                  unread=True)
-        conversation.to_user.invalidate_cache(permissions=False)
+            form.save(conversation=conversation, user_id=current_user.id, unread=True)
+            conversation.to_user.invalidate_cache(permissions=False)
 
-        return redirect(url_for("message.view_conversation",
-                                conversation_id=old_conv.id))
+            return redirect(url_for("message.view_conversation", conversation_id=old_conv.id))
 
-    return render_template("message/conversation.html",
-                           conversation=conversation, form=form)
+        return render_template("message/conversation.html", conversation=conversation, form=form)
 
 
-@message.route("/new", methods=["POST", "GET"])
-@login_required
-def new_conversation():
-    form = ConversationForm()
-    to_user = request.args.get("to_user")
+class NewConversation(MethodView):
+    decorators = [requires_message_box_space, login_required]
+    form = ConversationForm
 
-    message_count = Conversation.query. \
-        filter(Conversation.user_id == current_user.id).\
-        count()
-
-    if message_count >= flaskbb_config["MESSAGE_QUOTA"]:
-        flash(_("You cannot send any messages anymore because you have "
-                "reached your message limit."), "danger")
-        return redirect(url_for("message.inbox"))
+    def get(self):
+        form = self.form()
+        form.to_user.data = request.args.get("to_user")
+        return render_template("message/message_form.html", form=form, title=_("Compose Message"))
 
-    if request.method == "POST":
+    def post(self):
+        form = self.form()
         if "save_message" in request.form and form.validate():
             to_user = User.query.filter_by(username=form.to_user.data).first()
 
             shared_id = uuid.uuid4()
 
-            form.save(from_user=current_user.id,
-                      to_user=to_user.id,
-                      user_id=current_user.id,
-                      unread=False,
-                      as_draft=True,
-                      shared_id=shared_id)
+            form.save(
+                from_user=current_user.id,
+                to_user=to_user.id,
+                user_id=current_user.id,
+                unread=False,
+                as_draft=True,
+                shared_id=shared_id
+            )
 
             flash(_("Message saved."), "success")
             return redirect(url_for("message.drafts"))
@@ -157,196 +168,260 @@ def new_conversation():
             shared_id = uuid.uuid4()
 
             # Save the message in the current users inbox
-            form.save(from_user=current_user.id,
-                      to_user=to_user.id,
-                      user_id=current_user.id,
-                      unread=False,
-                      shared_id=shared_id)
+            form.save(
+                from_user=current_user.id,
+                to_user=to_user.id,
+                user_id=current_user.id,
+                unread=False,
+                shared_id=shared_id
+            )
 
             # Save the message in the recievers inbox
-            form.save(from_user=current_user.id,
-                      to_user=to_user.id,
-                      user_id=to_user.id,
-                      unread=True,
-                      shared_id=shared_id)
+            form.save(
+                from_user=current_user.id,
+                to_user=to_user.id,
+                user_id=to_user.id,
+                unread=True,
+                shared_id=shared_id
+            )
             to_user.invalidate_cache(permissions=False)
 
             flash(_("Message sent."), "success")
             return redirect(url_for("message.sent"))
-    else:
-        form.to_user.data = to_user
 
-    return render_template("message/message_form.html", form=form,
-                           title=_("Compose Message"))
+        return render_template("message/message_form.html", form=form, title=_("Compose Message"))
 
 
-@message.route("/message/<int:message_id>/raw")
-@login_required
-def raw_message(message_id):
-    message = Message.query.filter_by(id=message_id).first_or_404()
+class EditConversation(MethodView):
+    decorators = [login_required]
+    form = ConversationForm
 
-    # abort if the message was not the current_user's one or the one of the
-    # recieved ones
-    if not (message.conversation.from_user_id == current_user.id or
-            message.conversation.to_user_id == current_user.id):
-        abort(404)
+    def get(self, conversation_id):
+        conversation = Conversation.query.filter_by(
+            id=conversation_id, user_id=current_user.id
+        ).first_or_404()
 
-    return format_quote(username=message.user.username,
-                        content=message.message)
+        if not conversation.draft:
+            flash(_("You cannot edit a sent message."), "danger")
+            return redirect(url_for("message.inbox"))
 
+        form = self.form()
+        form.to_user.data = conversation.to_user.username
+        form.subject.data = conversation.subject
+        form.message.data = conversation.first_message.message
 
-@message.route("/<int:conversation_id>/edit", methods=["POST", "GET"])
-@login_required
-def edit_conversation(conversation_id):
-    conversation = Conversation.query.filter_by(
-        id=conversation_id,
-        user_id=current_user.id
-    ).first_or_404()
+        return render_template("message/message_form.html", form=form, title=_("Edit Message"))
+
+    def post(self, conversation_id):
+        conversation = Conversation.query.filter_by(
+            id=conversation_id, user_id=current_user.id
+        ).first_or_404()
+
+        if not conversation.draft:
+            flash(_("You cannot edit a sent message."), "danger")
+            return redirect(url_for("message.inbox"))
+
+        form = self.form()
+
+        if request.method == "POST":
+            if "save_message" in request.form:
+                to_user = User.query.filter_by(username=form.to_user.data).first()
+
+                conversation.draft = True
+                conversation.to_user_id = to_user.id
+                conversation.first_message.message = form.message.data
+                conversation.save()
+
+                flash(_("Message saved."), "success")
+                return redirect(url_for("message.drafts"))
+
+            if "send_message" in request.form and form.validate():
+                to_user = User.query.filter_by(username=form.to_user.data).first()
+                # Save the message in the recievers inbox
+                form.save(
+                    from_user=current_user.id,
+                    to_user=to_user.id,
+                    user_id=to_user.id,
+                    unread=True,
+                    shared_id=conversation.shared_id
+                )
+
+                # Move the message from ``Drafts`` to ``Sent``.
+                conversation.draft = False
+                conversation.to_user = to_user
+                conversation.date_created = time_utcnow()
+                conversation.save()
+
+                flash(_("Message sent."), "success")
+                return redirect(url_for("message.sent"))
+        else:
+            form.to_user.data = conversation.to_user.username
+            form.subject.data = conversation.subject
+            form.message.data = conversation.first_message.message
 
-    if not conversation.draft:
-        flash(_("You cannot edit a sent message."), "danger")
-        return redirect(url_for("message.inbox"))
+        return render_template("message/message_form.html", form=form, title=_("Edit Message"))
 
-    form = ConversationForm()
 
-    if request.method == "POST":
-        if "save_message" in request.form:
-            to_user = User.query.filter_by(username=form.to_user.data).first()
+class RawMessage(MethodView):
+    decorators = [login_required]
 
-            conversation.draft = True
-            conversation.to_user_id = to_user.id
-            conversation.first_message.message = form.message.data
-            conversation.save()
+    def get(self, message_id):
 
-            flash(_("Message saved."), "success")
-            return redirect(url_for("message.drafts"))
+        message = Message.query.filter_by(id=message_id).first_or_404()
 
-        if "send_message" in request.form and form.validate():
-            to_user = User.query.filter_by(username=form.to_user.data).first()
-            # Save the message in the recievers inbox
-            form.save(from_user=current_user.id,
-                      to_user=to_user.id,
-                      user_id=to_user.id,
-                      unread=True,
-                      shared_id=conversation.shared_id)
-
-            # Move the message from ``Drafts`` to ``Sent``.
-            conversation.draft = False
-            conversation.to_user = to_user
-            conversation.date_created = time_utcnow()
-            conversation.save()
+        # abort if the message was not the current_user's one or the one of the
+        # recieved ones
+        if not (message.conversation.from_user_id == current_user.id
+                or message.conversation.to_user_id == current_user.id):
+            abort(404)
 
-            flash(_("Message sent."), "success")
-            return redirect(url_for("message.sent"))
-    else:
-        form.to_user.data = conversation.to_user.username
-        form.subject.data = conversation.subject
-        form.message.data = conversation.first_message.message
+        return format_quote(username=message.user.username, content=message.message)
 
-    return render_template("message/message_form.html", form=form,
-                           title=_("Edit Message"))
 
+class MoveConversation(MethodView):
+    decorators = [login_required]
 
-@message.route("/<int:conversation_id>/move", methods=["POST"])
-@login_required
-def move_conversation(conversation_id):
-    conversation = Conversation.query.filter_by(
-        id=conversation_id,
-        user_id=current_user.id
-    ).first_or_404()
+    def post(self, conversation_id):
+        conversation = Conversation.query.filter_by(
+            id=conversation_id, user_id=current_user.id
+        ).first_or_404()
 
-    conversation.trash = True
-    conversation.save()
+        conversation.trash = True
+        conversation.save()
 
-    return redirect(url_for("message.inbox"))
+        return redirect(url_for("message.inbox"))
 
 
-@message.route("/<int:conversation_id>/restore", methods=["POST"])
-@login_required
-def restore_conversation(conversation_id):
-    conversation = Conversation.query.filter_by(
-        id=conversation_id,
-        user_id=current_user.id
-    ).first_or_404()
+class RestoreConversation(MethodView):
+    decorators = [login_required]
 
-    conversation.trash = False
-    conversation.save()
-    return redirect(url_for("message.inbox"))
+    def post(self, conversation_id):
+        conversation = Conversation.query.filter_by(
+            id=conversation_id, user_id=current_user.id
+        ).first_or_404()
 
+        conversation.trash = False
+        conversation.save()
+        return redirect(url_for("message.inbox"))
 
-@message.route("/<int:conversation_id>/delete", methods=["POST"])
-@login_required
-def delete_conversation(conversation_id):
-    conversation = Conversation.query.filter_by(
-        id=conversation_id,
-        user_id=current_user.id
-    ).first_or_404()
 
-    conversation.delete()
-    return redirect(url_for("message.inbox"))
+class DeleteConversation(MethodView):
+    decorators = [login_required]
 
+    def post(self, conversation_id):
+        conversation = Conversation.query.filter_by(
+            id=conversation_id, user_id=current_user.id
+        ).first_or_404()
 
-@message.route("/sent")
-@login_required
-def sent():
-    page = request.args.get('page', 1, type=int)
+        conversation.delete()
+        return redirect(url_for("message.inbox"))
 
-    conversations = Conversation.query. \
-        filter(
-            Conversation.user_id == current_user.id,
-            Conversation.draft == False,
-            Conversation.trash == False,
-            db.not_(Conversation.to_user_id == current_user.id)
-        ).\
-        order_by(Conversation.date_modified.desc()). \
-        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
 
-    message_count = Conversation.query. \
-        filter(Conversation.user_id == current_user.id).\
-        count()
+class SentMessages(MethodView):
+    decorators = [login_required]
 
-    return render_template("message/sent.html", conversations=conversations,
-                           message_count=message_count)
+    def get(self):
 
+        page = request.args.get('page', 1, type=int)
 
-@message.route("/draft")
-@login_required
-def drafts():
-    page = request.args.get('page', 1, type=int)
+        conversations = Conversation.query. \
+            filter(
+                Conversation.user_id == current_user.id,
+                Conversation.draft == False,
+                Conversation.trash == False,
+                db.not_(Conversation.to_user_id == current_user.id)
+            ).\
+            order_by(Conversation.date_modified.desc()). \
+            paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
 
-    conversations = Conversation.query. \
-        filter(
-            Conversation.user_id == current_user.id,
-            Conversation.draft == True,
-            Conversation.trash == False
-        ).\
-        order_by(Conversation.date_modified.desc()). \
-        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
+        message_count = Conversation.query. \
+            filter(Conversation.user_id == current_user.id).\
+            count()
 
-    message_count = Conversation.query. \
-        filter(Conversation.user_id == current_user.id).\
-        count()
+        return render_template(
+            "message/sent.html", conversations=conversations, message_count=message_count
+        )
 
-    return render_template("message/drafts.html", conversations=conversations,
-                           message_count=message_count)
 
+class DraftMessages(MethodView):
+    decorators = [login_required]
 
-@message.route("/trash")
-@login_required
-def trash():
-    page = request.args.get('page', 1, type=int)
+    def get(self):
 
-    conversations = Conversation.query. \
-        filter(
-            Conversation.user_id == current_user.id,
-            Conversation.trash == True,
-        ).\
-        order_by(Conversation.date_modified.desc()). \
-        paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
+        page = request.args.get('page', 1, type=int)
+
+        conversations = Conversation.query. \
+            filter(
+                Conversation.user_id == current_user.id,
+                Conversation.draft == True,
+                Conversation.trash == False
+            ).\
+            order_by(Conversation.date_modified.desc()). \
+            paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
+
+        message_count = Conversation.query. \
+            filter(Conversation.user_id == current_user.id).\
+            count()
+
+        return render_template(
+            "message/drafts.html", conversations=conversations, message_count=message_count
+        )
 
-    message_count = Conversation.query. \
-        filter(Conversation.user_id == current_user.id).\
-        count()
 
-    return render_template("message/trash.html", conversations=conversations,
-                           message_count=message_count)
+class TrashedMessages(MethodView):
+    decorators = [login_required]
+
+    def get(self):
+
+        page = request.args.get('page', 1, type=int)
+
+        conversations = Conversation.query. \
+            filter(
+                Conversation.user_id == current_user.id,
+                Conversation.trash == True,
+            ).\
+            order_by(Conversation.date_modified.desc()). \
+            paginate(page, flaskbb_config['TOPICS_PER_PAGE'], False)
+
+        message_count = Conversation.query. \
+            filter(Conversation.user_id == current_user.id).\
+            count()
+
+        return render_template(
+            "message/trash.html", conversations=conversations, message_count=message_count
+        )
+
+
+register_view(message, routes=['/drafts'], view_func=DraftMessages.as_view('drafts'))
+register_view(message, routes=['/', '/inbox'], view_func=Inbox.as_view('inbox'))
+register_view(
+    message,
+    routes=['/<int:conversation_id>/delete'],
+    view_func=DeleteConversation.as_view('delete_conversation')
+)
+register_view(
+    message,
+    routes=["/<int:conversation_id>/edit"],
+    view_func=EditConversation.as_view('edit_conversation')
+)
+register_view(
+    message,
+    routes=['/<int:conversation_id>/move'],
+    view_func=MoveConversation.as_view('move_conversation')
+)
+register_view(
+    message,
+    routes=['/<int:conversation_id>/restore'],
+    view_func=RestoreConversation.as_view('restore_conversation')
+)
+register_view(
+    message,
+    routes=["/<int:conversation_id>/view"],
+    view_func=ViewConversation.as_view('view_conversation')
+)
+register_view(
+    message, routes=['/message/<int:message_id>/raw'], view_func=RawMessage.as_view('raw_message')
+)
+register_view(message, routes=["/new"], view_func=NewConversation.as_view('new_conversation'))
+register_view(message, routes=['/sent'], view_func=SentMessages.as_view('sent'))
+register_view(message, routes=['/trash'], view_func=TrashedMessages.as_view('trash'))

+ 1 - 1
flaskbb/templates/auth/login.html

@@ -16,7 +16,7 @@
             {{ horizontal_field(form.login)}}
             {{ horizontal_field(form.password)}}
             {{ horizontal_field(form.remember_me) }}
-            {% if flaskbb_config["RECAPTCHA_ENABLED"] and login_recaptcha %}
+            {% if flaskbb_config["RECAPTCHA_ENABLED"] %}
                 {{ horizontal_field(form.recaptcha) }}
             {% endif %}
 

+ 111 - 64
flaskbb/user/views.py

@@ -10,96 +10,143 @@
     :license: BSD, see LICENSE for more details.
 """
 from flask import Blueprint, flash, request
-from flask_login import login_required, current_user
+from flask.views import MethodView
 from flask_babelplus import gettext as _
+from flask_login import current_user, login_required
 
-from flaskbb.utils.helpers import (render_template, get_available_languages,
-                                   get_available_themes)
-from flaskbb.user.models import User
-from flaskbb.user.forms import (ChangePasswordForm, ChangeEmailForm,
+from flaskbb.user.forms import (ChangeEmailForm, ChangePasswordForm,
                                 ChangeUserDetailsForm, GeneralSettingsForm)
-
+from flaskbb.user.models import User
+from flaskbb.utils.helpers import (get_available_languages,
+                                   get_available_themes, register_view,
+                                   render_template)
 
 user = Blueprint("user", __name__)
 
 
-@user.route("/<username>")
-def profile(username):
-    user = User.query.filter_by(username=username).first_or_404()
+class UserSettings(MethodView):
+    decorators = [login_required]
+    form = GeneralSettingsForm
 
-    return render_template("user/profile.html", user=user)
+    def get(self):
+        form = self.form()
 
+        form.theme.choices = get_available_themes()
+        form.language.choices = get_available_languages()
+        form.theme.data = current_user.theme
+        form.language.data = current_user.language
 
-@user.route("/<username>/topics")
-def view_all_topics(username):
-    page = request.args.get("page", 1, type=int)
-    user = User.query.filter_by(username=username).first_or_404()
-    topics = user.all_topics(page, current_user)
-    return render_template("user/all_topics.html", user=user, topics=topics)
+        return render_template("user/general_settings.html", form=form)
 
+    def post(self):
+        form = self.form()
 
-@user.route("/<username>/posts")
-def view_all_posts(username):
-    page = request.args.get("page", 1, type=int)
-    user = User.query.filter_by(username=username).first_or_404()
-    posts = user.all_posts(page, current_user)
-    return render_template("user/all_posts.html", user=user, posts=posts)
+        form.theme.choices = get_available_themes()
+        form.language.choices = get_available_languages()
 
+        if form.validate_on_submit():
+            current_user.theme = form.theme.data
+            current_user.language = form.language.data
+            current_user.save()
 
-@user.route("/settings/general", methods=["POST", "GET"])
-@login_required
-def settings():
-    form = GeneralSettingsForm()
+            flash(_("Settings updated."), "success")
+        else:
+            form.theme.data = current_user.theme
+            form.language.data = current_user.language
 
-    form.theme.choices = get_available_themes()
-    form.language.choices = get_available_languages()
+        return render_template("user/general_settings.html", form=form)
 
-    if form.validate_on_submit():
-        current_user.theme = form.theme.data
-        current_user.language = form.language.data
-        current_user.save()
 
-        flash(_("Settings updated."), "success")
-    else:
-        form.theme.data = current_user.theme
-        form.language.data = current_user.language
+class ChangePassword(MethodView):
+    decorators = [login_required]
+    form = ChangePasswordForm
+
+    def get(self):
+        return render_template("user/change_password.html", form=self.form())
+
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            current_user.password = form.new_password.data
+            current_user.save()
+
+            flash(_("Password updated."), "success")
+        return render_template("user/change_password.html", form=form)
+
+
+class ChangeEmail(MethodView):
+    decorators = [login_required]
+    form = ChangeEmailForm
+
+    def get(self):
+        return render_template("user/change_email.html", form=self.form(current_user))
+
+    def post(self):
+        form = self.form(current_user)
+        if form.validate_on_submit():
+            current_user.email = form.new_email.data
+            current_user.save()
+
+            flash(_("Email address updated."), "success")
+        return render_template("user/change_email.html", form=form)
+
+
+class ChangeUserDetails(MethodView):
+    decorators = [login_required]
+    form = ChangeUserDetailsForm
+
+    def get(self):
+        return render_template("user/change_user_details.html", form=self.form(obh=current_user))
+
+    def post(self):
+        form = self.form(obj=current_user)
+
+        if form.validate_on_submit():
+            form.populate_obj(current_user)
+            current_user.save()
+
+            flash(_("User details updated."), "success")
 
-    return render_template("user/general_settings.html", form=form)
+        return render_template("user/change_user_details.html", form=form)
 
 
-@user.route("/settings/password", methods=["POST", "GET"])
-@login_required
-def change_password():
-    form = ChangePasswordForm()
-    if form.validate_on_submit():
-        current_user.password = form.new_password.data
-        current_user.save()
+class AllUserTopics(MethodView):
 
-        flash(_("Password updated."), "success")
-    return render_template("user/change_password.html", form=form)
+    def get(self, username):
+        page = request.args.get("page", 1, type=int)
+        user = User.query.filter_by(username=username).first_or_404()
+        topics = user.all_topics(page, current_user)
+        return render_template("user/all_topics.html", user=user, topics=topics)
 
 
-@user.route("/settings/email", methods=["POST", "GET"])
-@login_required
-def change_email():
-    form = ChangeEmailForm(current_user)
-    if form.validate_on_submit():
-        current_user.email = form.new_email.data
-        current_user.save()
+class AllUserPosts(MethodView):
 
-        flash(_("Email address updated."), "success")
-    return render_template("user/change_email.html", form=form)
+    def get(self, username):
+        page = request.args.get("page", 1, type=int)
+        user = User.query.filter_by(username=username).first_or_404()
+        posts = user.all_posts(page, current_user)
+        return render_template("user/all_posts.html", user=user, posts=posts)
 
 
-@user.route("/settings/user-details", methods=["POST", "GET"])
-@login_required
-def change_user_details():
-    form = ChangeUserDetailsForm(obj=current_user)
+class UserProfile(MethodView):
 
-    if form.validate_on_submit():
-        form.populate_obj(current_user)
-        current_user.save()
+    def get(self, username):
+        user = User.query.filter_by(username=username).first_or_404()
+        return render_template("user/profile.html", user=user)
 
-        flash(_("User details updated."), "success")
 
-    return render_template("user/change_user_details.html", form=form)
+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'))

+ 51 - 1
flaskbb/utils/helpers.py

@@ -18,10 +18,11 @@ import glob
 from datetime import datetime, timedelta
 from pytz import UTC
 from PIL import ImageFile
+from functools import wraps
 
 import requests
 import unidecode
-from flask import session, url_for, flash, redirect, request
+from flask import session, url_for, flash, redirect, request, g
 from jinja2 import Markup
 from babel.core import get_locale_identifier
 from babel.dates import format_timedelta as babel_format_timedelta
@@ -629,3 +630,52 @@ def real(obj):
     if isinstance(obj, LocalProxy):
         return obj._get_current_object()
     return obj
+
+
+def anonymous_required(f):
+
+    @wraps(f)
+    def wrapper(*a, **k):
+        if current_user is not None and current_user.is_authenticated:
+            return redirect_or_next(url_for('forum.index'))
+        return f(*a, **k)
+
+    return wrapper
+
+
+def enforce_recaptcha(limiter):
+    current_limit = getattr(g, 'view_rate_limit', None)
+    login_recaptcha = False
+    if current_limit is not None:
+        window_stats = limiter.limiter.get_window_stats(*current_limit)
+        stats_diff = flaskbb_config["AUTH_REQUESTS"] - window_stats[1]
+        login_recaptcha = stats_diff >= flaskbb_config["LOGIN_RECAPTCHA"]
+    return login_recaptcha
+
+
+def registration_enabled(f):
+
+    @wraps(f)
+    def wrapper(*a, **k):
+        if not flaskbb_config["REGISTRATION_ENABLED"]:
+            flash(_("The registration has been disabled."), "info")
+            return redirect_or_next(url_for("forum.index"))
+        return f(*a, **k)
+
+    return wrapper
+
+
+def requires_unactivated(f):
+
+    @wraps(f)
+    def wrapper(*a, **k):
+        if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
+            flash(_("This account is already activated."), "info")
+            return redirect(url_for('forum.index'))
+        return f(*a, **k)
+    return wrapper
+
+
+def register_view(bp_or_app, routes, view_func, *args, **kwargs):
+    for route in routes:
+        bp_or_app.add_url_rule(route, view_func=view_func, *args, **kwargs)

+ 8 - 4
tests/endtoend/test_auth_views.py

@@ -5,28 +5,32 @@ import pytest
 
 
 def test_overview_not_authorized(application, default_settings):
+    view = views.ManagementOverview.as_view('overview')
     with application.test_request_context(), pytest.raises(AuthorizationRequired) as excinfo:  # noqa
-        views.overview()
+        view()
 
     assert "Authorization is required to access this area." == excinfo.value.description  # noqa
 
 
 def test_overview_with_authorized(admin_user, application, default_settings):
+    view = views.ManagementOverview.as_view('overview')
     with application.test_request_context():
         login_user(admin_user)
-        resp = views.overview()
+        resp = view()
         assert 'Overview' in resp
 
 
 def test_overview_with_supermod(super_moderator_user, application, default_settings):  # noqa
+    view = views.ManagementOverview.as_view('overview')
     with application.test_request_context():
         login_user(super_moderator_user)
-        resp = views.overview()
+        resp = view()
         assert 'Overview' in resp
 
 
 def test_overview_with_mod(moderator_user, application, default_settings):
+    view = views.ManagementOverview.as_view('overview')
     with application.test_request_context():
         login_user(moderator_user)
-        resp = views.overview()
+        resp = view()
         assert 'Overview' in resp

+ 23 - 13
tests/endtoend/test_message_views.py

@@ -7,15 +7,17 @@ from flaskbb.message import views
 
 def test_message_not_logged_in(application):
     """ check for redirect if not logged in """
+    view = views.Inbox.as_view('inbox')
     with application.test_request_context():
-        resp = views.inbox()
+        resp = view()
         assert resp.status != 302
 
 
 def test_message_inbox(application, default_settings, conversation_msgs, user):
+    view = views.Inbox()
     with application.test_request_context():
         login_user(user)
-        resp = views.inbox()
+        resp = view.get()
         assert 'From <a href="/user/test_normal">test_normal</a>' in resp
 
 
@@ -24,64 +26,72 @@ def test_message_view_conversation(
         conversation_msgs, user):
     with application.test_request_context():
         login_user(user)
-        resp = views.view_conversation(conversation_msgs.id)
+        view = views.ViewConversation()
+        resp = view.get(conversation_msgs.id)
         assert conversation_msgs.first_message.message in resp
 
 
 def test_message_trash_restore_conversation(
         application, default_settings,
         conversation_msgs, user):
+    move = views.MoveConversation()
+    restore = views.RestoreConversation()
     with application.test_request_context():
         login_user(user)
-        resp = views.move_conversation(conversation_msgs.id)
+        resp = move.post(conversation_msgs.id)
         assert resp.status != 302
         assert conversation_msgs.trash is True
-        resp = views.restore_conversation(conversation_msgs.id)
+        resp = restore.post(conversation_msgs.id)
         assert conversation_msgs.trash is False
 
 
 def test_message_delete_conversation(
         application, default_settings,
         conversation_msgs, user):
+    view = views.DeleteConversation()
     with application.test_request_context():
         login_user(user)
-        resp = views.delete_conversation(conversation_msgs.id)
+        resp = view.post(conversation_msgs.id)
         assert resp.status != 302
 
 
 def test_message_trash(application, default_settings, user):
     # FIXME more sophisticated tests required
+    view = views.TrashedMessages()
     with application.test_request_context():
         login_user(user)
-        resp = views.trash()
+        resp = view.get()
         assert 'No conversations found' in resp
 
 
 def test_message_drafts(application, default_settings, user):
     # FIXME more sophisticated tests required
+    view = views.DraftMessages()
     with application.test_request_context():
         login_user(user)
-        resp = views.drafts()
+        resp = view.get()
         assert 'No conversations found' in resp
 
 
 def test_message_sent(application, default_settings, user):
     # FIXME more sophisticated tests required
+    view = views.SentMessages()
     with application.test_request_context():
         login_user(user)
-        resp = views.sent()
+        resp = view.get()
         assert 'No conversations found' in resp
 
 
 def test_message_view_raw(
-        application, conversation_msgs,
-        default_settings, user, moderator_user):
+    application, conversation_msgs,
+    default_settings, user, moderator_user):
+    view = views.RawMessage()
     with application.test_request_context():
         login_user(user)
-        resp = views.raw_message(conversation_msgs.last_message.id)
+        resp = view.get(conversation_msgs.last_message.id)
         assert conversation_msgs.last_message.message in resp
 
         # same view should raise a 404 for a different user
         login_user(moderator_user)
         with pytest.raises(exceptions.NotFound):
-            resp = views.raw_message(conversation_msgs.last_message.id)
+            resp = view.get(conversation_msgs.last_message.id)