Browse Source

Added Moderator "Panel". Fixes #41.
Added two moderator specific permissions.
Bug fixes.

sh4nks 11 years ago
parent
commit
ae0b42909f

+ 9 - 8
flaskbb/app.py

@@ -35,8 +35,8 @@ from flaskbb.utils.helpers import format_date, time_since, crop_title, \
     render_template
 # permission checks (here they are used for the jinja filters)
 from flaskbb.utils.permissions import can_post_reply, can_post_topic, \
-    can_delete_topic, can_delete_post, can_edit_post, can_lock_topic, \
-    can_move_topic
+    can_delete_topic, can_delete_post, can_edit_post, can_edit_user, \
+    can_ban_user, is_admin, is_moderator, is_admin_or_moderator
 # app specific configurations
 from flaskbb.utils.settings import flaskbb_config
 
@@ -149,10 +149,15 @@ def configure_template_filters(app):
     app.jinja_env.filters['edit_post'] = can_edit_post
     app.jinja_env.filters['delete_post'] = can_delete_post
     app.jinja_env.filters['delete_topic'] = can_delete_topic
-    app.jinja_env.filters['move_topic'] = can_move_topic
-    app.jinja_env.filters['lock_topic'] = can_lock_topic
     app.jinja_env.filters['post_reply'] = can_post_reply
     app.jinja_env.filters['post_topic'] = can_post_topic
+    # Moderator permission filters
+    app.jinja_env.filters['is_admin'] = is_admin
+    app.jinja_env.filters['is_moderator'] = is_moderator
+    app.jinja_env.filters['is_admin_or_moderator'] = is_admin_or_moderator
+
+    app.jinja_env.filters['can_edit_user'] = can_edit_user
+    app.jinja_env.filters['can_ban_user'] = can_ban_user
 
 
 def configure_context_processors(app):
@@ -182,10 +187,6 @@ def configure_before_handlers(app):
             db.session.add(current_user)
             db.session.commit()
 
-    @app.before_request
-    def get_user_permissions():
-        current_user.permissions = current_user.get_permissions()
-
     if app.config["REDIS_ENABLED"]:
         @app.before_request
         def mark_current_user_online():

+ 12 - 18
flaskbb/fixtures/groups.py

@@ -25,9 +25,8 @@ fixture = OrderedDict((
         'deletetopic': True,
         'posttopic': True,
         'postreply': True,
-        'locktopic': True,
-        'movetopic': True,
-        'mergetopic': True
+        'mod_edituser': True,
+        'mod_banuser': True,
     }),
     ('Super Moderator', {
         'description': 'The Super Moderator Group',
@@ -41,9 +40,8 @@ fixture = OrderedDict((
         'deletetopic': True,
         'posttopic': True,
         'postreply': True,
-        'locktopic': True,
-        'movetopic': True,
-        'mergetopic': True
+        'mod_edituser': True,
+        'mod_banuser': True,
     }),
     ('Moderator', {
         'description': 'The Moderator Group',
@@ -57,9 +55,8 @@ fixture = OrderedDict((
         'deletetopic': True,
         'posttopic': True,
         'postreply': True,
-        'locktopic': True,
-        'movetopic': True,
-        'mergetopic': True
+        'mod_edituser': True,
+        'mod_banuser': True,
     }),
     ('Member', {
         'description': 'The Member Group',
@@ -73,9 +70,8 @@ fixture = OrderedDict((
         'deletetopic': False,
         'posttopic': True,
         'postreply': True,
-        'locktopic': False,
-        'movetopic': False,
-        'mergetopic': False
+        'mod_edituser': False,
+        'mod_banuser': False,
     }),
     ('Banned', {
         'description': 'The Banned Group',
@@ -89,9 +85,8 @@ fixture = OrderedDict((
         'deletetopic': False,
         'posttopic': False,
         'postreply': False,
-        'locktopic': False,
-        'movetopic': False,
-        'mergetopic': False
+        'mod_edituser': False,
+        'mod_banuser': False,
     }),
     ('Guest', {
         'description': 'The Guest Group',
@@ -105,8 +100,7 @@ fixture = OrderedDict((
         'deletetopic': False,
         'posttopic': False,
         'postreply': False,
-        'locktopic': False,
-        'movetopic': False,
-        'mergetopic': False
+        'mod_edituser': False,
+        'mod_banuser': False,
     })
 ))

+ 12 - 1
flaskbb/management/forms.py

@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 """
     flaskbb.management.forms
-    ~~~~~~~~~~~~~~~~~~~~
+    ~~~~~~~~~~~~~~~~~~~~~~~~
 
     It provides the forms that are needed for the management views.
 
@@ -180,6 +180,17 @@ class GroupForm(Form):
         description="Check this is the users in this group can post replies"
     )
 
+    mod_edituser = BooleanField(
+        "Moderators can edit user profiles",
+        description=("Allow moderators to edit a another users profile "
+                     "including password and email changes.")
+    )
+
+    mod_banuser = BooleanField(
+        "Moderators can ban users",
+        description="Allow moderators to ban other users"
+    )
+
     def validate_name(self, field):
         if hasattr(self, "group"):
             group = Group.query.filter(

+ 223 - 142
flaskbb/management/views.py

@@ -21,7 +21,8 @@ from flaskbb import __version__ as flaskbb_version
 from flaskbb.forum.forms import UserSearchForm
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.helpers import render_template
-from flaskbb.utils.decorators import admin_required
+from flaskbb.utils.decorators import admin_required, moderator_required
+from flaskbb.utils.permissions import can_ban_user, can_edit_user, is_admin
 from flaskbb.extensions import db
 from flaskbb.user.models import User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
@@ -35,7 +36,7 @@ management = Blueprint("management", __name__)
 
 
 @management.route("/")
-@admin_required
+@moderator_required
 def overview():
     python_version = "%s.%s" % (sys.version_info[0], sys.version_info[1])
     user_count = User.query.count()
@@ -91,8 +92,9 @@ def settings(slug=None):
                            all_groups=all_groups, active_group=active_group)
 
 
+# Users
 @management.route("/users", methods=['GET', 'POST'])
-@admin_required
+@moderator_required
 def users():
     page = request.args.get("page", 1, type=int)
     search_form = UserSearchForm()
@@ -110,122 +112,152 @@ def users():
                            search_form=search_form)
 
 
-@management.route("/groups")
-@admin_required
-def groups():
-    page = request.args.get("page", 1, type=int)
+@management.route("/users/<int:user_id>/edit", methods=["GET", "POST"])
+@moderator_required
+def edit_user(user_id):
+    user = User.query.filter_by(id=user_id).first_or_404()
 
-    groups = Group.query.\
-        paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
+    if not can_edit_user(current_user) or is_admin(user):
+        flash("You are not allowed to edit this user.", "danger")
+        return redirect(url_for("management.users"))
 
-    return render_template("management/groups.html", groups=groups)
+    secondary_group_query = Group.query.filter(
+        db.not_(Group.id == user.primary_group_id),
+        db.not_(Group.banned == True),
+        db.not_(Group.guest == True))
 
+    form = EditUserForm(user)
+    form.secondary_groups.query = secondary_group_query
+    if form.validate_on_submit():
+        form.populate_obj(user)
+        user.primary_group_id = form.primary_group.data.id
 
-@management.route("/forums")
-@admin_required
-def forums():
-    categories = Category.query.order_by(Category.position.asc()).all()
-    return render_template("management/forums.html", categories=categories)
+       # Don't override the password
+        if form.password.data:
+            user.password = form.password.data
 
+        user.save(groups=form.secondary_groups.data)
 
-@management.route("/reports")
-@admin_required
-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)
+        flash("User successfully edited", "success")
+        return redirect(url_for("management.edit_user", user_id=user.id))
+    else:
+        form.username.data = user.username
+        form.email.data = user.email
+        form.birthday.data = user.birthday
+        form.gender.data = user.gender
+        form.website.data = user.website
+        form.location.data = user.location
+        form.signature.data = user.signature
+        form.avatar.data = user.avatar
+        form.notes.data = user.notes
+        form.primary_group.data = user.primary_group
+        form.secondary_groups.data = user.secondary_groups
 
-    return render_template("management/reports.html", reports=reports)
+    return render_template("management/user_form.html", form=form,
+                           title="Edit User")
 
 
-@management.route("/plugins")
+@management.route("/users/<int:user_id>/delete")
 @admin_required
-def plugins():
-    plugins = get_all_plugins()
-    return render_template("management/plugins.html", plugins=plugins)
-
-
-@management.route("/plugins/enable/<plugin>")
-def enable_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
-    if not plugin.enabled:
-        plugin_dir = os.path.join(
-            os.path.abspath(os.path.dirname(os.path.dirname(__file__))),
-            "plugins", plugin.identifier
-        )
+def delete_user(user_id):
+    user = User.query.filter_by(id=user_id).first_or_404()
+    user.delete()
+    flash("User successfully deleted", "success")
+    return redirect(url_for("management.users"))
 
-        disabled_file = os.path.join(plugin_dir, "DISABLED")
 
-        os.remove(disabled_file)
+@management.route("/users/add", methods=["GET", "POST"])
+@admin_required
+def add_user():
+    form = AddUserForm()
+    if form.validate_on_submit():
+        form.save()
+        flash("User successfully added.", "success")
+        return redirect(url_for("management.users"))
 
-        flash("Plugin should be enabled. Please reload your app.", "success")
+    return render_template("management/user_form.html", form=form,
+                           title="Add User")
 
-        flash("If you are using a host which doesn't support writting on the "
-              "disk, this won't work - than you need to delete the "
-              "'DISABLED' file by yourself.", "info")
-    else:
-        flash("Plugin is not enabled", "danger")
 
-    return redirect(url_for("management.plugins"))
+@management.route("/users/banned")
+@moderator_required
+def banned_users():
+    page = request.args.get("page", 1, type=int)
+    search_form = UserSearchForm()
 
+    users = User.query.filter(
+        Group.banned == True,
+        Group.id == User.primary_group_id
+    ).paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
-@management.route("/plugins/disable/<plugin>")
-def disable_plugin(plugin):
-    try:
-        plugin = get_plugin(plugin)
-    except KeyError:
-        flash("Plugin {} not found".format(plugin), "danger")
-        return redirect(url_for("management.plugins"))
 
-    plugin_dir = os.path.join(
-        os.path.abspath(os.path.dirname(os.path.dirname(__file__))),
-        "plugins", plugin.identifier
-    )
+    if search_form.validate():
+        users = search_form.get_results().\
+            paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
-    disabled_file = os.path.join(plugin_dir, "DISABLED")
+        return render_template("management/banned_users.html", users=users,
+                               search_form=search_form)
 
-    open(disabled_file, "a").close()
+    return render_template("management/banned_users.html", users=users,
+                           search_form=search_form)
 
-    flash("Plugin should be disabled. Please reload your app.", "success")
 
-    flash("If you are using a host which doesn't "
-          "support writting on the disk, this won't work - than you need to "
-          "create a 'DISABLED' file by yourself.", "info")
+@management.route("/users/<int:user_id>/ban", methods=["GET", "POST"])
+@moderator_required
+def ban_user(user_id):
+    if not can_ban_user(current_user):
+        flash("You do not have the permissions to ban this user.")
+        return redirect(url_for("management.overview"))
 
-    return redirect(url_for("management.plugins"))
+    user = User.query.filter_by(id=user_id).first_or_404()
 
+    # Do not allow moderators to ban admins
+    if user.get_permissions()['admin'] and \
+            (current_user.permissions['mod'] or
+                current_user.permissions['super_mod']):
 
-@management.route("/plugins/uninstall/<plugin>")
-def uninstall_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
-    if plugin.uninstallable:
-        plugin.uninstall()
-        Setting.invalidate_cache()
+            flash("A moderator cannot ban an admin user.", "danger")
+            return redirect(url_for("management.overview"))
 
-        flash("Plugin {} has been uninstalled.".format(plugin.name), "success")
+    if user.ban():
+        flash("User was banned successfully.", "success")
     else:
-        flash("Cannot uninstall Plugin {}".format(plugin.name), "danger")
+        flash("Could not ban user.", "danger")
 
-    return redirect(url_for("management.plugins"))
+    return redirect(url_for("management.banned_users"))
 
 
-@management.route("/plugins/install/<plugin>")
-def install_plugin(plugin):
-    plugin = get_plugin_from_all(plugin)
-    if plugin.installable and not plugin.uninstallable:
-        plugin.install()
-        Setting.invalidate_cache()
+@management.route("/users/<int:user_id>/unban", methods=["GET", "POST"])
+@moderator_required
+def unban_user(user_id):
+    if not can_ban_user(current_user):
+        flash("You do not have the permissions to unban this user.")
+        return redirect(url_for("management.overview"))
 
-        flash("Plugin {} has been installed.".format(plugin.name), "success")
+    user = User.query.filter_by(id=user_id).first_or_404()
+
+    if user.unban():
+        flash("User is now unbanned.", "success")
     else:
-        flash("Cannot install Plugin {}".format(plugin.name), "danger")
+        flash("Could not unban user.", "danger")
 
-    return redirect(url_for("management.plugins"))
+    return redirect(url_for("management.banned_users"))
+
+
+# Reports
+@management.route("/reports")
+@moderator_required
+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)
 
 
 @management.route("/reports/unread")
-@admin_required
+@moderator_required
 def unread_reports():
     page = request.args.get("page", 1, type=int)
     reports = Report.query.\
@@ -238,7 +270,7 @@ def unread_reports():
 
 @management.route("/reports/<int:report_id>/markread")
 @management.route("/reports/markread")
-@admin_required
+@moderator_required
 def report_markread(report_id=None):
     # mark single report as read
     if report_id:
@@ -269,67 +301,16 @@ def report_markread(report_id=None):
     return redirect(url_for("management.reports"))
 
 
-@management.route("/users/<int:user_id>/edit", methods=["GET", "POST"])
-@admin_required
-def edit_user(user_id):
-    user = User.query.filter_by(id=user_id).first_or_404()
-
-    secondary_group_query = Group.query.filter(
-        db.not_(Group.id == user.primary_group_id),
-        db.not_(Group.banned == True),
-        db.not_(Group.guest == True))
-
-    form = EditUserForm(user)
-    form.secondary_groups.query = secondary_group_query
-    if form.validate_on_submit():
-        form.populate_obj(user)
-        user.primary_group_id = form.primary_group.data.id
-
-       # Don't override the password
-        if form.password.data:
-            user.password = form.password.data
-
-        user.save(groups=form.secondary_groups.data)
-
-        flash("User successfully edited", "success")
-        return redirect(url_for("management.edit_user", user_id=user.id))
-    else:
-        form.username.data = user.username
-        form.email.data = user.email
-        form.birthday.data = user.birthday
-        form.gender.data = user.gender
-        form.website.data = user.website
-        form.location.data = user.location
-        form.signature.data = user.signature
-        form.avatar.data = user.avatar
-        form.notes.data = user.notes
-        form.primary_group.data = user.primary_group
-        form.secondary_groups.data = user.secondary_groups
-
-    return render_template("management/user_form.html", form=form,
-                           title="Edit User")
-
-
-@management.route("/users/<int:user_id>/delete")
+# Groups
+@management.route("/groups")
 @admin_required
-def delete_user(user_id):
-    user = User.query.filter_by(id=user_id).first_or_404()
-    user.delete()
-    flash("User successfully deleted", "success")
-    return redirect(url_for("management.users"))
-
+def groups():
+    page = request.args.get("page", 1, type=int)
 
-@management.route("/users/add", methods=["GET", "POST"])
-@admin_required
-def add_user():
-    form = AddUserForm()
-    if form.validate_on_submit():
-        form.save()
-        flash("User successfully added.", "success")
-        return redirect(url_for("management.users"))
+    groups = Group.query.\
+        paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
-    return render_template("management/user_form.html", form=form,
-                           title="Add User")
+    return render_template("management/groups.html", groups=groups)
 
 
 @management.route("/groups/<int:group_id>/edit", methods=["GET", "POST"])
@@ -357,6 +338,8 @@ def edit_group(group_id):
         form.deletetopic.data = group.deletetopic
         form.posttopic.data = group.posttopic
         form.postreply.data = group.postreply
+        form.mod_edituser.data = group.mod_edituser
+        form.mod_banuser.data = group.mod_banuser
 
     return render_template("management/group_form.html", form=form,
                            title="Edit Group")
@@ -384,6 +367,14 @@ def add_group():
                            title="Add Group")
 
 
+# Forums and Categories
+@management.route("/forums")
+@admin_required
+def forums():
+    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"])
 @admin_required
 def edit_forum(forum_id):
@@ -493,3 +484,93 @@ def delete_category(category_id):
     category.delete(involved_users)
     flash("Category with all associated forums deleted.", "success")
     return redirect(url_for("management.forums"))
+
+
+# Plugins
+@management.route("/plugins")
+@admin_required
+def plugins():
+    plugins = get_all_plugins()
+    return render_template("management/plugins.html", plugins=plugins)
+
+
+@management.route("/plugins/enable/<plugin>")
+@admin_required
+def enable_plugin(plugin):
+    plugin = get_plugin_from_all(plugin)
+    if not plugin.enabled:
+        plugin_dir = os.path.join(
+            os.path.abspath(os.path.dirname(os.path.dirname(__file__))),
+            "plugins", plugin.identifier
+        )
+
+        disabled_file = os.path.join(plugin_dir, "DISABLED")
+
+        os.remove(disabled_file)
+
+        flash("Plugin should be enabled. Please reload your app.", "success")
+
+        flash("If you are using a host which doesn't support writting on the "
+              "disk, this won't work - than you need to delete the "
+              "'DISABLED' file by yourself.", "info")
+    else:
+        flash("Plugin is not enabled", "danger")
+
+    return redirect(url_for("management.plugins"))
+
+
+@management.route("/plugins/disable/<plugin>")
+@admin_required
+def disable_plugin(plugin):
+    try:
+        plugin = get_plugin(plugin)
+    except KeyError:
+        flash("Plugin {} not found".format(plugin), "danger")
+        return redirect(url_for("management.plugins"))
+
+    plugin_dir = os.path.join(
+        os.path.abspath(os.path.dirname(os.path.dirname(__file__))),
+        "plugins", plugin.identifier
+    )
+
+    disabled_file = os.path.join(plugin_dir, "DISABLED")
+
+    open(disabled_file, "a").close()
+
+    flash("Plugin should be disabled. Please reload your app.", "success")
+
+    flash("If you are using a host which doesn't "
+          "support writting on the disk, this won't work - than you need to "
+          "create a 'DISABLED' file by yourself.", "info")
+
+    return redirect(url_for("management.plugins"))
+
+
+@management.route("/plugins/uninstall/<plugin>")
+@admin_required
+def uninstall_plugin(plugin):
+    plugin = get_plugin_from_all(plugin)
+    if plugin.uninstallable:
+        plugin.uninstall()
+        Setting.invalidate_cache()
+
+        flash("Plugin {} has been uninstalled.".format(plugin.name), "success")
+    else:
+        flash("Cannot uninstall Plugin {}".format(plugin.name), "danger")
+
+    return redirect(url_for("management.plugins"))
+
+
+@management.route("/plugins/install/<plugin>")
+@admin_required
+def install_plugin(plugin):
+    plugin = get_plugin_from_all(plugin)
+    if plugin.installable and not plugin.uninstallable:
+        plugin.install()
+        Setting.invalidate_cache()
+
+        flash("Plugin {} has been installed.".format(plugin.name), "success")
+    else:
+        flash("Cannot install Plugin {}".format(plugin.name), "danger")
+
+    return redirect(url_for("management.plugins"))

+ 1 - 1
flaskbb/templates/layout.html

@@ -66,7 +66,7 @@
                                 <li class="divider"></li>
 
                                 <li><a href="{{ url_for('user.settings') }}"><span class="fa fa-cogs"></span> Settings</a></li>
-                                {% if current_user.permissions['admin'] %}
+                                {% if current_user|is_admin_or_moderator %}
                                 <li><a href="{{ url_for('management.overview') }}"><span class="fa fa-cog"></span>Management</a></li>
                                 <li class="divider"></li>
                                 {% endif %}

+ 76 - 0
flaskbb/templates/management/banned_users.html

@@ -0,0 +1,76 @@
+{% set page_title = "Banned Users" %}
+{% set active_management_user_nav=True %}
+
+{% extends theme("management/management_layout.html") %}
+{% block management_content %}
+{% from theme('macros.html') import render_pagination, render_field, group_field, navlink with context %}
+
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('management.users', "Manage Users") }}
+        {{ navlink('management.banned_users', 'Banned Users') }}
+
+        {% if current_user|is_admin %}
+            {{ navlink('management.add_user', "Add User") }}
+        {% endif %}
+    </ul>
+</div><!--/.col-md-3 -->
+
+<div class="col-md-9">
+    <legend>Banned Users</legend>
+
+    <div class="pull-left" style="padding-bottom: 10px">
+        {{ render_pagination(users, url_for('management.users')) }}
+    </div>
+    <div class="pull-right" style="padding-bottom: 10px">
+        <form role="form" method="post">
+            <div class="input-group">
+                {{ search_form.hidden_tag() }}
+                {{ group_field(search_form.search_query) }}
+                <span class="input-group-btn">
+                    <button class="btn btn-primary" type="submit">Search</button>
+                </span>
+            </div>
+        </form>
+    </div>
+
+    <table class="table table-bordered">
+        <thead>
+            <tr>
+                <th>#</th>
+                <th>Username</th>
+                <th>Posts</th>
+                <th>Date registered</th>
+                <th>Group</th>
+                <th>Manage</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for user in users.items %}
+                <tr>
+                    <td>{{ user.id }}</td>
+                    <td><a href="{{ url_for('user.profile', username=user.username) }}">{{ user.username }}</a></td>
+                    <td>{{ user.post_count }}</td>
+                    <td>{{ user.date_joined|format_date('%b %d %Y') }}</td>
+                    <td>{{ user.primary_group.name }}</td>
+                    <td>
+                        {% if current_user|can_ban_user and not user.permissions['banned'] %}
+                            <a href="{{ url_for('management.ban_user', user_id = user.id) }}">Ban</a>
+                        {% endif %}
+
+                        {% if current_user|can_ban_user and user.permissions['banned'] %}
+                            <a href="{{ url_for('management.unban_user', user_id = user.id) }}">Unban</a>
+                        {% endif %}
+                    </td>
+                </tr>
+            {% else %}
+                <tr>
+                    <td colspan="6">
+                        No users found matching your search query
+                    </td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+</div>
+{% endblock %}

+ 4 - 4
flaskbb/templates/management/category_form.html

@@ -1,5 +1,5 @@
 {% set page_title = title %}
-{% set active_admin_forum_nav=True %}
+{% set active_management_forum_nav=True %}
 
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
@@ -7,9 +7,9 @@
 
 <div class="col-md-3">
     <ul class="nav nav-pills nav-stacked">
-        {{ navlink('admin.forums', "Manage Forums") }}
-        {{ navlink('admin.add_forum', "Add Forum") }}
-        {{ navlink('admin.add_category', "Add Category") }}
+        {{ navlink('management.forums', "Manage Forums") }}
+        {{ navlink('management.add_forum', "Add Forum") }}
+        {{ navlink('management.add_category', "Add Category") }}
     </ul>
 </div>
 

+ 1 - 1
flaskbb/templates/management/forum_form.html

@@ -1,5 +1,5 @@
 {% set page_title = title %}
-{% set active_admin_forum_nav=True %}
+{% set active_management_forum_nav=True %}
 
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}

+ 2 - 0
flaskbb/templates/management/forums.html

@@ -1,3 +1,5 @@
+{% set page_title = "Forums" %}
+
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 {% from theme('macros.html') import render_pagination, navlink with context %}

+ 4 - 1
flaskbb/templates/management/group_form.html

@@ -1,5 +1,5 @@
 {% set page_title = title %}
-{% set active_admin_group_nav=True %}
+{% set active_management_group_nav=True %}
 
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
@@ -27,6 +27,9 @@
             {{ render_boolean_field(form.banned) }}
             {{ render_boolean_field(form.guest) }}
 
+            {{ render_boolean_field(form.mod_edituser) }}
+            {{ render_boolean_field(form.mod_banuser) }}
+
             {{ render_boolean_field(form.editpost) }}
             {{ render_boolean_field(form.deletepost) }}
             {{ render_boolean_field(form.deletetopic) }}

+ 2 - 0
flaskbb/templates/management/groups.html

@@ -1,3 +1,5 @@
+{% set page_title = "Groups" %}
+
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 {% from theme('macros.html') import render_pagination, navlink with context %}

+ 8 - 5
flaskbb/templates/management/management_layout.html

@@ -4,13 +4,16 @@
 
 <div class="col-md-12" style="padding-bottom: 10px">
     <ul class="nav nav-tabs nav-justified">
-        {{ navlink('management.overview', 'Overview') }}
+    {{ navlink('management.overview', 'Overview') }}
+    {{ navlink('management.users', 'Users', active=active_management_user_nav) }}
+    {{ navlink('management.unread_reports', 'Reports', active=active_management_report_nav) }}
+
+    {% if current_user|is_admin %}
         {{ navlink('management.settings', 'Settings') }}
-        {{ navlink('management.users', 'Users', active=active_admin_user_nav) }}
-        {{ navlink('management.groups', 'Groups', active=active_admin_group_nav) }}
-        {{ navlink('management.forums', 'Forums', active=active_admin_forum_nav) }}
-        {{ navlink('management.unread_reports', 'Reports', active=active_admin_report_nav) }}
+        {{ navlink('management.groups', 'Groups', active=active_management_group_nav) }}
+        {{ navlink('management.forums', 'Forums', active=active_management_forum_nav) }}
         {{ navlink('management.plugins', 'Plugins') }}
+    {% endif %}
     </ul>
 </div>
 

+ 2 - 0
flaskbb/templates/management/overview.html

@@ -1,3 +1,5 @@
+{% set page_title = "Overview" %}
+
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 

+ 2 - 0
flaskbb/templates/management/plugins.html

@@ -1,3 +1,5 @@
+{% set page_title = "Plugins" %}
+
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 {% from theme('macros.html') import render_pagination %}

+ 2 - 1
flaskbb/templates/management/reports.html

@@ -1,4 +1,5 @@
-{% set active_admin_report_nav=True %}
+{% set page_title = "Reports" %}
+{% set active_management_report_nav=True %}
 
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}

+ 2 - 0
flaskbb/templates/management/settings.html

@@ -1,3 +1,5 @@
+{% set page_title = active_group.name %}
+
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 {% from theme('macros.html') import render_boolean_field, render_select_field, render_field, navlink with context %}

+ 2 - 1
flaskbb/templates/management/unread_reports.html

@@ -1,4 +1,5 @@
-{% set active_admin_report_nav=True %}
+{% set page_title = "Unread Reports" %}
+{% set active_management_report_nav=True %}
 
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}

+ 6 - 2
flaskbb/templates/management/user_form.html

@@ -1,5 +1,5 @@
 {% set page_title = title %}
-{% set active_admin_user_nav=True %}
+{% set active_management_user_nav=True %}
 
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
@@ -8,7 +8,11 @@
 <div class="col-md-3">
     <ul class="nav nav-pills nav-stacked">
         {{ navlink('management.users', "Manage Users") }}
-        {{ navlink('management.add_user', "Add User") }}
+        {{ navlink('management.banned_users', 'Banned Users') }}
+
+        {% if current_user|is_admin %}
+            {{ navlink('management.add_user', "Add User") }}
+        {% endif %}
     </ul>
 </div><!--/.col-md-3 -->
 

+ 19 - 7
flaskbb/templates/management/users.html

@@ -1,3 +1,6 @@
+{% set page_title = "Users" %}
+{% set active_management_user_nav=True %}
+
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 {% from theme('macros.html') import render_pagination, render_field, group_field, navlink with context %}
@@ -5,7 +8,11 @@
 <div class="col-md-3">
     <ul class="nav nav-pills nav-stacked">
         {{ navlink('management.users', "Manage Users") }}
-        {{ navlink('management.add_user', "Add User") }}
+        {{ navlink('management.banned_users', 'Banned Users') }}
+
+        {% if current_user|is_admin %}
+            {{ navlink('management.add_user', "Add User") }}
+        {% endif %}
     </ul>
 </div><!--/.col-md-3 -->
 
@@ -14,15 +21,15 @@
 
     <div class="pull-left" style="padding-bottom: 10px">
         {{ render_pagination(users, url_for('management.users')) }}
-    </div>
+    </div><!-- /.col-pull-left -->
     <div class="pull-right" style="padding-bottom: 10px">
         <form role="form" method="post">
             <div class="input-group">
                 {{ search_form.hidden_tag() }}
                 {{ group_field(search_form.search_query) }}
-          <span class="input-group-btn">
-            <button class="btn btn-primary" type="button">Search</button>
-          </span>
+                <span class="input-group-btn">
+                    <button class="btn btn-primary" type="submit">Search</button>
+                </span>
             </div>
         </form>
     </div>
@@ -47,8 +54,13 @@
                     <td>{{ user.date_joined|format_date('%b %d %Y') }}</td>
                     <td>{{ user.primary_group.name }}</td>
                     <td>
-                        <a href="{{ url_for('management.edit_user', user_id = user.id) }}">Edit</a> |
-                        <a href="{{ url_for('management.delete_user', user_id = user.id) }}">Delete</a>
+                        {% if current_user|can_edit_user and not user|is_admin or current_user|is_admin %}
+                            <a href="{{ url_for('management.edit_user', user_id = user.id) }}">Edit</a>
+                        {% endif %}
+
+                        {% if current_user|is_admin %}
+                            | <a href="{{ url_for('management.delete_user', user_id = user.id) }}">Delete</a>
+                        {% endif %}
                     </td>
                 </tr>
             {% else %}

+ 1 - 1
flaskbb/themes/bootstrap2/templates/layout.html

@@ -67,7 +67,7 @@
                                 <li class="divider"></li>
 
                                 <li><a href="{{ url_for('user.settings') }}"><span class="fa fa-cogs"></span> Settings</a></li>
-                                {% if current_user.permissions['admin'] %}
+                                {% if current_user|is_admin_or_moderator %}
                                 <li><a href="{{ url_for('management.overview') }}"><span class="fa fa-cog"></span> Management</a></li>
                                 <li class="divider"></li>
                                 {% endif %}

+ 1 - 1
flaskbb/themes/bootstrap3/templates/layout.html

@@ -66,7 +66,7 @@
                                 <li class="divider"></li>
 
                                 <li><a href="{{ url_for('user.settings') }}"><span class="fa fa-cogs"></span> Settings</a></li>
-                                {% if current_user.permissions['admin'] %}
+                                {% if current_user|is_admin_or_moderator %}
                                 <li><a href="{{ url_for('management.overview') }}"><span class="fa fa-cog"></span> Management</a></li>
                                 <li class="divider"></li>
                                 {% endif %}

+ 44 - 6
flaskbb/user/models.py

@@ -35,19 +35,21 @@ class Group(db.Model):
     name = db.Column(db.String(63), unique=True, nullable=False)
     description = db.Column(db.String(80))
 
-    # I bet there is a nicer way for this :P
+    # Group types
     admin = db.Column(db.Boolean, default=False, nullable=False)
     super_mod = db.Column(db.Boolean, default=False, nullable=False)
     mod = db.Column(db.Boolean, default=False, nullable=False)
     guest = db.Column(db.Boolean, default=False, nullable=False)
     banned = db.Column(db.Boolean, default=False, nullable=False)
 
+    # Moderator permissions (only available when the user a moderator)
+    mod_edituser = db.Column(db.Boolean, default=False, nullable=False)
+    mod_banuser = db.Column(db.Boolean, default=False, nullable=False)
+
+    # User permissions
     editpost = db.Column(db.Boolean, default=True, nullable=False)
     deletepost = db.Column(db.Boolean, default=False, nullable=False)
     deletetopic = db.Column(db.Boolean, default=False, nullable=False)
-    locktopic = db.Column(db.Boolean, default=False, nullable=False)
-    movetopic = db.Column(db.Boolean, default=False, nullable=False)
-    mergetopic = db.Column(db.Boolean, default=False, nullable=False)
     posttopic = db.Column(db.Boolean, default=True, nullable=False)
     postreply = db.Column(db.Boolean, default=True, nullable=False)
 
@@ -56,7 +58,7 @@ class Group(db.Model):
         """Set to a unique key specific to the object in the database.
         Required for cache.memoize() to work across requests.
         """
-        return "<{} {})>".format(self.__class__.__name__, self.id)
+        return "<{} {}>".format(self.__class__.__name__, self.id)
 
     def save(self):
         """Saves a group"""
@@ -129,12 +131,16 @@ class User(db.Model, UserMixin):
         """Returns the url for the user"""
         return url_for("user.profile", username=self.username)
 
+    @property
+    def permissions(self):
+        return self.get_permissions()
+
     # Methods
     def __repr__(self):
         """Set to a unique key specific to the object in the database.
         Required for cache.memoize() to work across requests.
         """
-        return "Username: %s" % self.username
+        return "<{} {}>".format(self.__class__.__name__, self.username)
 
     def _get_password(self):
         """Returns the hashed password"""
@@ -322,6 +328,38 @@ class User(db.Model, UserMixin):
 
         cache.delete_memoized(self.get_permissions, self)
 
+    def ban(self):
+        """Bans the user. Returns True upon success."""
+
+        if not self.get_permissions()['banned']:
+            banned_group = Group.query.filter(
+                Group.banned == True
+            ).first()
+
+            self.primary_group_id = banned_group.id
+            self.save()
+            self.invalidate_cache()
+            return True
+        return False
+
+    def unban(self):
+        """Unbans the user. Returns True upon success."""
+
+        if self.get_permissions()['banned']:
+            member_group = Group.query.filter(
+                Group.admin == False,
+                Group.super_mod == False,
+                Group.mod == False,
+                Group.guest == False,
+                Group.banned == False
+            ).first()
+
+            self.primary_group_id = member_group.id
+            self.save()
+            self.invalidate_cache()
+            return True
+        return False
+
     def save(self, groups=None):
         """Saves a user. If a list with groups is provided, it will add those
         to the secondary groups from the user.

+ 15 - 0
flaskbb/utils/decorators.py

@@ -23,3 +23,18 @@ def admin_required(f):
             abort(403)
         return f(*args, **kwargs)
     return decorated
+
+
+def moderator_required(f):
+    @wraps(f)
+    def decorated(*args, **kwargs):
+        if current_user.is_anonymous():
+            abort(403)
+
+        if not any([current_user.permissions['admin'],
+                    current_user.permissions['super_mod'],
+                    current_user.permissions['mod']]):
+            abort(403)
+
+        return f(*args, **kwargs)
+    return decorated

+ 52 - 27
flaskbb/utils/permissions.py

@@ -25,21 +25,48 @@ def check_perm(user, perm, forum, post_user_id=None):
     :param post_user_id: If post_user_id is given, it will also perform an
                          check if the user is the owner of this topic or post.
     """
-    if can_moderate(user, forum):
+    if can_moderate(user=user, forum=forum):
         return True
     if post_user_id and user.is_authenticated():
         return user.permissions[perm] and user.id == post_user_id
     return user.permissions[perm]
 
 
-def can_moderate(user, forum, perm=None):
-    """Checks if a user can moderate a forum
+def is_moderator(user):
+    """Returns ``True`` if the user is in a moderator or super moderator group.
+
+    :param user: The user who should be checked.
+    """
+    return user.permissions['mod'] or user.permissions['super_mod']
+
+
+def is_admin(user):
+    """Returns ``True`` if the user is a administrator.
+
+    :param user:  The user who should be checked.
+    """
+    return user.permissions['admin']
+
+
+def is_admin_or_moderator(user):
+    """Returns ``True`` if the user is either a admin or in a moderator group
+
+    :param user: The user who should be checked.
+    """
+    return is_admin(user) or is_moderator(user)
+
+
+def can_moderate(user, forum=None, perm=None):
+    """Checks if a user can moderate a forum or a user.
     He needs to be super moderator or a moderator of the
-    specified forum
+    specified forum.
 
     :param user: The user for whom we should check the permission.
 
-    :param forum: The forum that should be checked.
+    :param forum: The forum that should be checked. If no forum is specified
+                  it will check if the user has at least moderator permissions
+                  and then it will perform another permission check for ``mod``
+                  permissions (they start with ``mod_``).
 
     :param perm: Optional - Check if the user also has the permission to do
                  certain things in the forum. There are a few permissions
@@ -48,11 +75,20 @@ def can_moderate(user, forum, perm=None):
                  it will check if the user has it. Those special permissions
                  are documented here: <INSERT LINK TO DOCS>
     """
-    if user.permissions['mod'] and user in forum.moderators:
-        if perm is not None:
+    # Check if the user has moderator specific permissions (mod_ prefix)
+    if is_admin_or_moderator(user) and forum is None:
+
+        if perm is not None and perm.startswith("mod_"):
             return user.permissions[perm]
+
+        # If no permission is definied, return False
+        return False
+
+    # check if the user is a moderation and is moderating the forum
+    if user.permissions['mod'] and user in forum.moderators:
         return True
 
+    # if the user is a super_mod or admin, he can moderate all forums
     return user.permissions['super_mod'] or user.permissions['admin']
 
 
@@ -89,27 +125,16 @@ def can_post_topic(user, forum):
     return check_perm(user=user, perm='posttopic', forum=forum)
 
 
-def can_lock_topic(user, forum):
-    """Check if the user is allowed to lock a topic in the forum
-    Returns True if the user can moderate the forum and has the permission
-    to do it.
+# Moderator permission checks
+def can_edit_user(user):
+    """Check if the user is allowed to edit another users profile.
+    Requires at least ``mod`` permissions.
     """
+    return can_moderate(user=user, perm="mod_edituser")
 
-    return can_moderate(user, forum, "locktopic")
 
-
-def can_move_topic(user, forum):
-    """Check if the user is allowed to move a topic in the forum
-    Returns True if the user can moderate the forum and has the permission
-    to do it."""
-
-    return can_moderate(user, forum, "movetopic")
-
-
-def can_merge_topic(user, forum):
-    """Check if the user is allowed to merge a topic in the forum.
-    Returns True if the user can moderate the forum and has the permission
-    to do it.
+def can_ban_user(user):
+    """Check if the user is allowed to ban another user.
+    Requires at least ``mod`` permissions.
     """
-
-    return can_moderate(user, forum, "mergetopic")
+    return can_moderate(user=user, perm="mod_banuser")

+ 10 - 27
tests/unit/utils/test_permissions.py

@@ -11,8 +11,6 @@ def test_moderator_permissions_in_forum(
     moderator.
     """
 
-    moderator_user.permissions = moderator_user.get_permissions()
-
     assert moderator_user in forum.moderators
 
     assert can_post_reply(moderator_user, forum)
@@ -23,19 +21,13 @@ def test_moderator_permissions_in_forum(
     assert can_delete_post(moderator_user, topic.user_id, forum)
     assert can_delete_topic(moderator_user, topic.user_id, forum)
 
-    assert can_lock_topic(moderator_user, forum)
-    assert can_merge_topic(moderator_user, forum)
-    assert can_move_topic(moderator_user, forum)
-
 
 def test_moderator_permissions_without_forum(
         forum, moderator_user, topic, topic_moderator):
     """Test the moderator permissions in a forum where the user is not a
     moderator.
     """
-
     forum.moderators.remove(moderator_user)
-    moderator_user.permissions = moderator_user.get_permissions()
 
     assert not moderator_user in forum.moderators
     assert not can_moderate(moderator_user, forum)
@@ -47,20 +39,18 @@ def test_moderator_permissions_without_forum(
     assert not can_delete_post(moderator_user, topic.user_id, forum)
     assert not can_delete_topic(moderator_user, topic.user_id, forum)
 
-    assert not can_lock_topic(moderator_user, forum)
-    assert not can_merge_topic(moderator_user, forum)
-    assert not can_move_topic(moderator_user, forum)
-
     # Test with own topic
     assert can_delete_post(moderator_user, topic_moderator.user_id, forum)
     assert can_delete_topic(moderator_user, topic_moderator.user_id, forum)
     assert can_edit_post(moderator_user, topic_moderator.user_id, forum)
 
+    # Test moderator permissions
+    assert can_edit_user(moderator_user)
+    assert can_ban_user(moderator_user)
+
 
 def test_normal_permissions(forum, user, topic):
     """Test the permissions for a normal user."""
-    user.permissions = user.get_permissions()
-
     assert not can_moderate(user, forum)
 
     assert can_post_reply(user, forum)
@@ -70,15 +60,12 @@ def test_normal_permissions(forum, user, topic):
     assert not can_delete_post(user, topic.user_id, forum)
     assert not can_delete_topic(user, topic.user_id, forum)
 
-    assert not can_lock_topic(user, forum)
-    assert not can_merge_topic(user, forum)
-    assert not can_move_topic(user, forum)
+    assert not can_edit_user(user)
+    assert not can_ban_user(user)
 
 
 def test_admin_permissions(forum, admin_user, topic):
     """Test the permissions for a admin user."""
-    admin_user.permissions = admin_user.get_permissions()
-
     assert can_moderate(admin_user, forum)
 
     assert can_post_reply(admin_user, forum)
@@ -88,15 +75,12 @@ def test_admin_permissions(forum, admin_user, topic):
     assert can_delete_post(admin_user, topic.user_id, forum)
     assert can_delete_topic(admin_user, topic.user_id, forum)
 
-    assert can_lock_topic(admin_user, forum)
-    assert can_merge_topic(admin_user, forum)
-    assert can_move_topic(admin_user, forum)
+    assert can_edit_user(admin_user)
+    assert can_ban_user(admin_user)
 
 
 def test_super_moderator_permissions(forum, super_moderator_user, topic):
     """Test the permissions for a super moderator user."""
-    super_moderator_user.permissions = super_moderator_user.get_permissions()
-
     assert can_moderate(super_moderator_user, forum)
 
     assert can_post_reply(super_moderator_user, forum)
@@ -106,6 +90,5 @@ def test_super_moderator_permissions(forum, super_moderator_user, topic):
     assert can_delete_post(super_moderator_user, topic.user_id, forum)
     assert can_delete_topic(super_moderator_user, topic.user_id, forum)
 
-    assert can_lock_topic(super_moderator_user, forum)
-    assert can_merge_topic(super_moderator_user, forum)
-    assert can_move_topic(super_moderator_user, forum)
+    assert can_edit_user(super_moderator_user)
+    assert can_ban_user(super_moderator_user)