Browse Source

Merge branch 'bulk-actions'

sh4nks 9 years ago
parent
commit
a3cbaf90eb
38 changed files with 1732 additions and 1044 deletions
  1. 70 187
      flaskbb/forum/models.py
  2. 92 58
      flaskbb/forum/views.py
  3. 11 14
      flaskbb/management/forms.py
  4. 3 22
      flaskbb/management/models.py
  5. 158 10
      flaskbb/management/views.py
  6. 3 17
      flaskbb/message/models.py
  7. 33 0
      flaskbb/static/css/flaskbb.css
  8. 0 18
      flaskbb/static/js/conversation.js
  9. 139 0
      flaskbb/static/js/flaskbb.js
  10. 0 19
      flaskbb/static/js/topic.js
  11. 161 0
      flaskbb/templates/forum/edit_forum.html
  12. 6 1
      flaskbb/templates/forum/forum.html
  13. 0 1
      flaskbb/templates/forum/new_post.html
  14. 0 1
      flaskbb/templates/forum/new_topic.html
  15. 0 2
      flaskbb/templates/forum/topic.html
  16. 95 82
      flaskbb/templates/layout.html
  17. 12 7
      flaskbb/templates/macros.html
  18. 103 51
      flaskbb/templates/management/banned_users.html
  19. 19 9
      flaskbb/templates/management/category_form.html
  20. 30 25
      flaskbb/templates/management/forum_form.html
  21. 57 48
      flaskbb/templates/management/forums.html
  22. 36 26
      flaskbb/templates/management/group_form.html
  23. 73 30
      flaskbb/templates/management/groups.html
  24. 34 27
      flaskbb/templates/management/overview.html
  25. 63 56
      flaskbb/templates/management/plugins.html
  26. 46 32
      flaskbb/templates/management/reports.html
  27. 29 22
      flaskbb/templates/management/settings.html
  28. 80 44
      flaskbb/templates/management/unread_reports.html
  29. 27 17
      flaskbb/templates/management/user_form.html
  30. 140 70
      flaskbb/templates/management/users.html
  31. 0 1
      flaskbb/templates/message/conversation.html
  32. 9 1
      flaskbb/themes/bootstrap2/templates/layout.html
  33. 98 81
      flaskbb/themes/bootstrap3/templates/layout.html
  34. 12 14
      flaskbb/user/models.py
  35. 28 0
      flaskbb/utils/database.py
  36. 48 2
      flaskbb/utils/helpers.py
  37. 17 17
      requirements.txt
  38. 0 32
      tests/unit/test_forum_models.py

+ 70 - 187
flaskbb/forum/models.py

@@ -15,7 +15,9 @@ from sqlalchemy.orm import aliased
 
 
 from flaskbb.extensions import db
 from flaskbb.extensions import db
 from flaskbb.utils.decorators import can_access_forum, can_access_topic
 from flaskbb.utils.decorators import can_access_forum, can_access_topic
-from flaskbb.utils.helpers import slugify, get_categories_and_forums, get_forums
+from flaskbb.utils.helpers import slugify, get_categories_and_forums, \
+    get_forums
+from flaskbb.utils.database import CRUDMixin
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 
 
 
 
@@ -37,6 +39,7 @@ topictracker = db.Table(
                             use_alter=True, name="fk_tracker_topic_id"),
                             use_alter=True, name="fk_tracker_topic_id"),
               nullable=False))
               nullable=False))
 
 
+
 # m2m table for group-forum permission mapping
 # m2m table for group-forum permission mapping
 forumgroups = db.Table(
 forumgroups = db.Table(
     'forumgroups',
     'forumgroups',
@@ -54,7 +57,8 @@ forumgroups = db.Table(
     )
     )
 )
 )
 
 
-class TopicsRead(db.Model):
+
+class TopicsRead(db.Model, CRUDMixin):
     __tablename__ = "topicsread"
     __tablename__ = "topicsread"
 
 
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
@@ -69,23 +73,8 @@ class TopicsRead(db.Model):
                          primary_key=True)
                          primary_key=True)
     last_read = db.Column(db.DateTime, default=datetime.utcnow())
     last_read = db.Column(db.DateTime, default=datetime.utcnow())
 
 
-    def __repr__(self):
-        return "<{}>".format(self.__class__.__name__)
-
-    def save(self):
-        """Saves a TopicsRead entry."""
-        db.session.add(self)
-        db.session.commit()
-        return self
 
 
-    def delete(self):
-        """Deletes a TopicsRead entry."""
-        db.session.delete(self)
-        db.session.commit()
-        return self
-
-
-class ForumsRead(db.Model):
+class ForumsRead(db.Model, CRUDMixin):
     __tablename__ = "forumsread"
     __tablename__ = "forumsread"
 
 
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"),
@@ -97,23 +86,8 @@ class ForumsRead(db.Model):
     last_read = db.Column(db.DateTime, default=datetime.utcnow())
     last_read = db.Column(db.DateTime, default=datetime.utcnow())
     cleared = db.Column(db.DateTime)
     cleared = db.Column(db.DateTime)
 
 
-    def __repr__(self):
-        return "<{}>".format(self.__class__.__name__)
 
 
-    def save(self):
-        """Saves a ForumsRead entry."""
-        db.session.add(self)
-        db.session.commit()
-        return self
-
-    def delete(self):
-        """Deletes a ForumsRead entry."""
-        db.session.delete(self)
-        db.session.commit()
-        return self
-
-
-class Report(db.Model):
+class Report(db.Model, CRUDMixin):
     __tablename__ = "reports"
     __tablename__ = "reports"
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
@@ -137,9 +111,7 @@ class Report(db.Model):
         """Saves a report.
         """Saves a report.
 
 
         :param post: The post that should be reported
         :param post: The post that should be reported
-
         :param user: The user who has reported the post
         :param user: The user who has reported the post
-
         :param reason: The reason why the user has reported the post
         :param reason: The reason why the user has reported the post
         """
         """
 
 
@@ -157,14 +129,8 @@ class Report(db.Model):
         db.session.commit()
         db.session.commit()
         return self
         return self
 
 
-    def delete(self):
-        """Deletes a report."""
-        db.session.delete(self)
-        db.session.commit()
-        return self
-
 
 
-class Post(db.Model):
+class Post(db.Model, CRUDMixin):
     __tablename__ = "posts"
     __tablename__ = "posts"
     __searchable__ = ['content', 'username']
     __searchable__ = ['content', 'username']
 
 
@@ -205,7 +171,6 @@ class Post(db.Model):
         operation was successful.
         operation was successful.
 
 
         :param user: The user who has created the post
         :param user: The user who has created the post
-
         :param topic: The topic in which the post was created
         :param topic: The topic in which the post was created
         """
         """
         # update/edit the post
         # update/edit the post
@@ -223,7 +188,7 @@ class Post(db.Model):
 
 
             topic.last_updated = datetime.utcnow()
             topic.last_updated = datetime.utcnow()
 
 
-            # This needs to be done before I update the last_post_id.
+            # This needs to be done before the last_post_id gets updated.
             db.session.add(self)
             db.session.add(self)
             db.session.commit()
             db.session.commit()
 
 
@@ -248,7 +213,7 @@ class Post(db.Model):
             return self
             return self
 
 
     def delete(self):
     def delete(self):
-        """Deletes a post and returns self"""
+        """Deletes a post and returns self."""
         # This will delete the whole topic
         # This will delete the whole topic
         if self.topic.first_post_id == self.id:
         if self.topic.first_post_id == self.id:
             self.topic.delete()
             self.topic.delete()
@@ -292,7 +257,7 @@ class Post(db.Model):
         return self
         return self
 
 
 
 
-class Topic(db.Model):
+class Topic(db.Model, CRUDMixin):
     __tablename__ = "topics"
     __tablename__ = "topics"
     __searchable__ = ['title', 'username']
     __searchable__ = ['title', 'username']
 
 
@@ -368,12 +333,9 @@ class Topic(db.Model):
         Also, if the ``TRACKER_LENGTH`` is configured, it will just recognize
         Also, if the ``TRACKER_LENGTH`` is configured, it will just recognize
         topics that are newer than the ``TRACKER_LENGTH`` (in days) as unread.
         topics that are newer than the ``TRACKER_LENGTH`` (in days) as unread.
 
 
-        TODO: Couldn't think of a better name for this method - ideas?
-
         :param forumsread: The ForumsRead object is needed because we also
         :param forumsread: The ForumsRead object is needed because we also
                            need to check if the forum has been cleared
                            need to check if the forum has been cleared
                            sometime ago.
                            sometime ago.
-
         :param topicsread: The topicsread object is used to check if there is
         :param topicsread: The topicsread object is used to check if there is
                            a new post in the topic.
                            a new post in the topic.
         """
         """
@@ -407,9 +369,7 @@ class Topic(db.Model):
         Returns True if the tracker has been updated.
         Returns True if the tracker has been updated.
 
 
         :param user: The user for whom the readstracker should be updated.
         :param user: The user for whom the readstracker should be updated.
-
         :param forum: The forum in which the topic is.
         :param forum: The forum in which the topic is.
-
         :param forumsread: The forumsread object. It is used to check if there
         :param forumsread: The forumsread object. It is used to check if there
                            is a new post since the forum has been marked as
                            is a new post since the forum has been marked as
                            read.
                            read.
@@ -456,71 +416,40 @@ class Topic(db.Model):
 
 
         return updated
         return updated
 
 
-    def move(self, forum):
+    def move(self, new_forum):
         """Moves a topic to the given forum.
         """Moves a topic to the given forum.
         Returns True if it could successfully move the topic to forum.
         Returns True if it could successfully move the topic to forum.
 
 
-        :param forum: The new forum for the topic
+        :param new_forum: The new forum for the topic
         """
         """
 
 
         # if the target forum is the current forum, abort
         # if the target forum is the current forum, abort
-        if self.forum_id == forum.id:
+        if self.forum_id == new_forum.id:
             return False
             return False
 
 
         old_forum = self.forum
         old_forum = self.forum
         self.forum.post_count -= self.post_count
         self.forum.post_count -= self.post_count
         self.forum.topic_count -= 1
         self.forum.topic_count -= 1
-        self.forum_id = forum.id
+        self.forum_id = new_forum.id
 
 
-        forum.post_count += self.post_count
-        forum.topic_count += 1
+        new_forum.post_count += self.post_count
+        new_forum.topic_count += 1
 
 
         db.session.commit()
         db.session.commit()
 
 
-        forum.update_last_post()
+        new_forum.update_last_post()
         old_forum.update_last_post()
         old_forum.update_last_post()
 
 
         TopicsRead.query.filter_by(topic_id=self.id).delete()
         TopicsRead.query.filter_by(topic_id=self.id).delete()
 
 
         return True
         return True
 
 
-    def merge(self, topic):
-        """Merges a topic with another topic
-
-        :param topic: The new topic for the posts in this topic
-        """
-
-        # You can only merge a topic with a differrent topic in the same forum
-        if self.id == topic.id or not self.forum_id == topic.forum_id:
-            return False
-
-        # Update the topic id
-        Post.query.filter_by(topic_id=self.id).\
-            update({Post.topic_id: topic.id})
-
-        # Update the last post
-        if topic.last_post.date_created < self.last_post.date_created:
-            topic.last_post_id = self.last_post_id
-
-        # Increase the post and views count
-        topic.post_count += self.post_count
-        topic.views += self.views
-
-        topic.save()
-
-        # Finally delete the old topic
-        Topic.query.filter_by(id=self.id).delete()
-
-        return True
-
     def save(self, user=None, forum=None, post=None):
     def save(self, user=None, forum=None, post=None):
         """Saves a topic and returns the topic object. If no parameters are
         """Saves a topic and returns the topic object. If no parameters are
         given, it will only update the topic.
         given, it will only update the topic.
 
 
         :param user: The user who has created the topic
         :param user: The user who has created the topic
-
         :param forum: The forum where the topic is stored
         :param forum: The forum where the topic is stored
-
         :param post: The post object which is connected to the topic
         :param post: The post object which is connected to the topic
         """
         """
 
 
@@ -567,7 +496,7 @@ class Topic(db.Model):
             filter_by(forum_id=self.forum_id).\
             filter_by(forum_id=self.forum_id).\
             order_by(Topic.last_post_id.desc()).limit(2).offset(0).all()
             order_by(Topic.last_post_id.desc()).limit(2).offset(0).all()
 
 
-        # do want to delete the topic with the last post?
+        # do we want to delete the topic with the last post in the forum?
         if topic and topic[0].id == self.id:
         if topic and topic[0].id == self.id:
             try:
             try:
                 # Now the second last post will be the last post
                 # Now the second last post will be the last post
@@ -616,7 +545,7 @@ class Topic(db.Model):
         return self
         return self
 
 
 
 
-class Forum(db.Model):
+class Forum(db.Model, CRUDMixin):
     __tablename__ = "forums"
     __tablename__ = "forums"
     __searchable__ = ['title', 'description']
     __searchable__ = ['title', 'description']
 
 
@@ -777,13 +706,17 @@ class Forum(db.Model):
             forumsread.save()
             forumsread.save()
             return True
             return True
 
 
-        # Nothing updated, because there are still more than 0 unread topicsread
+        # Nothing updated, because there are still more than 0 unread
+        # topicsread
         return False
         return False
 
 
     def save(self, groups=None):
     def save(self, groups=None):
         """Saves a forum
         """Saves a forum
 
 
-        :param groups: A list with group objects."""
+        :param moderators: If given, it will update the moderators in this
+                           forum with the given iterable of user objects.
+        :param groups: A list with group objects.
+        """
         if self.id:
         if self.id:
             db.session.merge(self)
             db.session.merge(self)
         else:
         else:
@@ -822,6 +755,17 @@ class Forum(db.Model):
 
 
         return self
         return self
 
 
+    def move_topics_to(self, topics):
+        """Moves a bunch a topics to the forum. Returns ``True`` if all
+        topics were moved successfully to the forum.
+
+        :param topics: A iterable with topic objects.
+        """
+        status = False
+        for topic in topics:
+            status = topic.move(self)
+        return status
+
     # Classmethods
     # Classmethods
     @classmethod
     @classmethod
     @can_access_forum
     @can_access_forum
@@ -829,7 +773,6 @@ class Forum(db.Model):
         """Returns the forum and forumsread object as a tuple for the user.
         """Returns the forum and forumsread object as a tuple for the user.
 
 
         :param forum_id: The forum id
         :param forum_id: The forum id
-
         :param user: The user object is needed to check if we also need their
         :param user: The user object is needed to check if we also need their
                      forumsread object.
                      forumsread object.
         """
         """
@@ -855,11 +798,8 @@ class Forum(db.Model):
         forumsread relation to check if it is read or unread.
         forumsread relation to check if it is read or unread.
 
 
         :param forum_id: The forum id
         :param forum_id: The forum id
-
         :param user: The user object
         :param user: The user object
-
         :param page: The page whom should be loaded
         :param page: The page whom should be loaded
-
         :param per_page: How many topics per page should be shown
         :param per_page: How many topics per page should be shown
         """
         """
         if user.is_authenticated():
         if user.is_authenticated():
@@ -880,7 +820,7 @@ class Forum(db.Model):
         return topics
         return topics
 
 
 
 
-class Category(db.Model):
+class Category(db.Model, CRUDMixin):
     __tablename__ = "categories"
     __tablename__ = "categories"
     __searchable__ = ['title', 'description']
     __searchable__ = ['title', 'description']
 
 
@@ -914,13 +854,6 @@ class Category(db.Model):
         """
         """
         return "<{} {}>".format(self.__class__.__name__, self.id)
         return "<{} {}>".format(self.__class__.__name__, self.id)
 
 
-    def save(self):
-        """Saves a category"""
-
-        db.session.add(self)
-        db.session.commit()
-        return self
-
     def delete(self, users=None):
     def delete(self, users=None):
         """Deletes a category. If a list with involved user objects is passed,
         """Deletes a category. If a list with involved user objects is passed,
         it will also update their post counts
         it will also update their post counts
@@ -955,48 +888,23 @@ class Category(db.Model):
         :param user: The user object is needed to check if we also need their
         :param user: The user object is needed to check if we also need their
                      forumsread object.
                      forumsread object.
         """
         """
-        # import Group model locally to avoid cicular imports
-        from flaskbb.user.models import Group
         if user.is_authenticated():
         if user.is_authenticated():
-            # get list of user group ids
-            user_groups = [gr.id for gr in user.groups]
-            # filter forums by user groups
-            user_forums = Forum.query.filter(Forum.groups.any(
-                Group.id.in_(user_groups))
-            ).subquery()
-            forum_alias = aliased(Forum, user_forums)
-            # get all
-            forums = cls.query.join(
-                forum_alias,
-                cls.id == forum_alias.category_id
-            ).outerjoin(
-                ForumsRead,
-                db.and_(
-                    ForumsRead.forum_id == forum_alias.id,
-                    ForumsRead.user_id == user.id
-                )
-            ).add_entity(
-                forum_alias
-            ).add_entity(
-                ForumsRead
-            ).order_by(
-                Category.position, Category.id, forum_alias.position
-            ).all()
+            forums = cls.query.\
+                join(Forum, cls.id == Forum.category_id).\
+                outerjoin(ForumsRead,
+                          db.and_(ForumsRead.forum_id == Forum.id,
+                                  ForumsRead.user_id == user.id)).\
+                add_entity(Forum).\
+                add_entity(ForumsRead).\
+                order_by(Category.id, Category.position, Forum.position).\
+                all()
         else:
         else:
-            guest_group = Group.get_guest_group()
-            # filter forums by guest groups
-            guest_forums = Forum.query.filter(
-                Forum.groups.any(Group.id == guest_group.id)
-            ).subquery()
-            forum_alias = aliased(Forum, guest_forums)
-            forums = cls.query.join(
-                forum_alias,
-                cls.id == forum_alias.category_id
-            ).add_entity(
-                forum_alias
-            ).order_by(
-                Category.position, Category.id, forum_alias.position
-            ).all()
+            # Get all the forums
+            forums = cls.query.\
+                join(Forum, cls.id == Forum.category_id).\
+                add_entity(Forum).\
+                order_by(Category.id, Category.position, Forum.position).\
+                all()
 
 
         return get_categories_and_forums(forums, user)
         return get_categories_and_forums(forums, user)
 
 
@@ -1011,52 +919,27 @@ class Category(db.Model):
             (<Category 1>, [(<Forum 1>, None), (<Forum 2>, None)])
             (<Category 1>, [(<Forum 1>, None), (<Forum 2>, None)])
 
 
         :param category_id: The category id
         :param category_id: The category id
-
         :param user: The user object is needed to check if we also need their
         :param user: The user object is needed to check if we also need their
                      forumsread object.
                      forumsread object.
         """
         """
-        from flaskbb.user.models import Group
         if user.is_authenticated():
         if user.is_authenticated():
-            # get list of user group ids
-            user_groups = [gr.id for gr in user.groups]
-            # filter forums by user groups
-            user_forums = Forum.query.filter(Forum.groups.any(
-                Group.id.in_(user_groups))
-            ).subquery()
-            forum_alias = aliased(Forum, user_forums)
-            forums = cls.query.filter(
-                cls.id == category_id
-            ).join(
-                forum_alias,
-                cls.id == forum_alias.category_id
-            ).outerjoin(
-                ForumsRead,
-                db.and_(
-                    ForumsRead.forum_id == forum_alias.id,
-                    ForumsRead.user_id == user.id)
-            ).add_entity(
-                forum_alias
-            ).add_entity(
-                ForumsRead
-            ).order_by(
-                forum_alias.position
-            ).all()
+            forums = cls.query.\
+                filter(cls.id == category_id).\
+                join(Forum, cls.id == Forum.category_id).\
+                outerjoin(ForumsRead,
+                          db.and_(ForumsRead.forum_id == Forum.id,
+                                  ForumsRead.user_id == user.id)).\
+                add_entity(Forum).\
+                add_entity(ForumsRead).\
+                order_by(Forum.position).\
+                all()
         else:
         else:
-            guest_group = Group.get_guest_group()
-            # filter forums by guest groups
-            guest_forums = Forum.query.filter(
-                Forum.groups.any(Group.id == guest_group.id)
-            ).subquery()
-            forum_alias = aliased(Forum, guest_forums)
-            forums = cls.query.filter(
-                cls.id == category_id
-            ).join(
-                forum_alias, cls.id == forum_alias.category_id
-            ).add_entity(
-                forum_alias
-            ).order_by(
-                forum_alias.position
-            ).all()
+            forums = cls.query.\
+                filter(cls.id == category_id).\
+                join(Forum, cls.id == Forum.category_id).\
+                add_entity(Forum).\
+                order_by(Forum.position).\
+                all()
 
 
         if not forums:
         if not forums:
             abort(404)
             abort(404)

+ 92 - 58
flaskbb/forum/views.py

@@ -12,14 +12,14 @@
 import datetime
 import datetime
 
 
 from flask import (Blueprint, redirect, url_for, current_app,
 from flask import (Blueprint, redirect, url_for, current_app,
-                   request, flash)
+                   request, flash, jsonify)
 from flask_login import login_required, current_user
 from flask_login import login_required, current_user
 from flask_babelex import gettext as _
 from flask_babelex import gettext as _
 
 
 from flaskbb.extensions import db
 from flaskbb.extensions import db
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.helpers import (get_online_users, time_diff, format_quote,
 from flaskbb.utils.helpers import (get_online_users, time_diff, format_quote,
-                                   render_template)
+                                   render_template, do_topic_action)
 from flaskbb.utils.permissions import (can_post_reply, can_post_topic,
 from flaskbb.utils.permissions import (can_post_reply, can_post_topic,
                                        can_delete_topic, can_delete_post,
                                        can_delete_topic, can_delete_post,
                                        can_edit_post, can_moderate)
                                        can_edit_post, can_moderate)
@@ -125,7 +125,6 @@ def view_topic(topic_id, slug=None):
     topic.update_read(current_user, topic.forum, forumsread)
     topic.update_read(current_user, topic.forum, forumsread)
 
 
     form = None
     form = None
-
     if can_post_reply(user=current_user, topic=topic):
     if can_post_reply(user=current_user, topic=topic):
         form = QuickreplyForm()
         form = QuickreplyForm()
         if form.validate_on_submit():
         if form.validate_on_submit():
@@ -170,9 +169,9 @@ def new_topic(forum_id, slug=None):
             )
             )
         if "submit" in request.form and form.validate():
         if "submit" in request.form and form.validate():
             topic = form.save(current_user, forum_instance)
             topic = form.save(current_user, forum_instance)
-
             # redirect to the new topic
             # redirect to the new topic
             return redirect(url_for('forum.view_topic', topic_id=topic.id))
             return redirect(url_for('forum.view_topic', topic_id=topic.id))
+
     return render_template(
     return render_template(
         "forum/new_topic.html", forum=forum_instance, form=form
         "forum/new_topic.html", forum=forum_instance, form=form
     )
     )
@@ -181,11 +180,10 @@ def new_topic(forum_id, slug=None):
 @forum.route("/topic/<int:topic_id>/delete", methods=["POST"])
 @forum.route("/topic/<int:topic_id>/delete", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/delete", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/delete", methods=["POST"])
 @login_required
 @login_required
-def delete_topic(topic_id, slug=None):
+def delete_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
     if not can_delete_topic(user=current_user, topic=topic):
     if not can_delete_topic(user=current_user, topic=topic):
-
         flash(_("You do not have the permissions to delete this topic."),
         flash(_("You do not have the permissions to delete this topic."),
               "danger")
               "danger")
         return redirect(topic.forum.url)
         return redirect(topic.forum.url)
@@ -199,11 +197,9 @@ def delete_topic(topic_id, slug=None):
 @forum.route("/topic/<int:topic_id>/lock", methods=["POST"])
 @forum.route("/topic/<int:topic_id>/lock", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/lock", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/lock", methods=["POST"])
 @login_required
 @login_required
-def lock_topic(topic_id, slug=None):
+def lock_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
-    # TODO: Bulk lock
-
     if not can_moderate(user=current_user, forum=topic.forum):
     if not can_moderate(user=current_user, forum=topic.forum):
         flash(_("You do not have the permissions to lock this topic."),
         flash(_("You do not have the permissions to lock this topic."),
               "danger")
               "danger")
@@ -217,12 +213,9 @@ def lock_topic(topic_id, slug=None):
 @forum.route("/topic/<int:topic_id>/unlock", methods=["POST"])
 @forum.route("/topic/<int:topic_id>/unlock", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/unlock", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/unlock", methods=["POST"])
 @login_required
 @login_required
-def unlock_topic(topic_id, slug=None):
+def unlock_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
-    # TODO: Bulk unlock
-
-    # Unlock is basically the same as lock
     if not can_moderate(user=current_user, forum=topic.forum):
     if not can_moderate(user=current_user, forum=topic.forum):
         flash(_("You do not have the permissions to unlock this topic."),
         flash(_("You do not have the permissions to unlock this topic."),
               "danger")
               "danger")
@@ -236,7 +229,7 @@ def unlock_topic(topic_id, slug=None):
 @forum.route("/topic/<int:topic_id>/highlight", methods=["POST"])
 @forum.route("/topic/<int:topic_id>/highlight", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/highlight", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/highlight", methods=["POST"])
 @login_required
 @login_required
-def highlight_topic(topic_id, slug=None):
+def highlight_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
     if not can_moderate(user=current_user, forum=topic.forum):
     if not can_moderate(user=current_user, forum=topic.forum):
@@ -252,7 +245,7 @@ def highlight_topic(topic_id, slug=None):
 @forum.route("/topic/<int:topic_id>/trivialize", methods=["POST"])
 @forum.route("/topic/<int:topic_id>/trivialize", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/trivialize", methods=["POST"])
 @forum.route("/topic/<int:topic_id>-<slug>/trivialize", methods=["POST"])
 @login_required
 @login_required
-def trivialize_topic(topic_id, slug=None):
+def trivialize_topic(topic_id=None, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
     # Unlock is basically the same as lock
     # Unlock is basically the same as lock
@@ -266,57 +259,98 @@ def trivialize_topic(topic_id, slug=None):
     return redirect(topic.url)
     return redirect(topic.url)
 
 
 
 
-@forum.route("/topic/<int:topic_id>/move/<int:forum_id>", methods=["POST"])
-@forum.route(
-    "/topic/<int:topic_id>-<topic_slug>/move/<int:forum_id>-<forum_slug>",
-    methods=["POST"]
-)
+@forum.route("/forum/<int:forum_id>/edit", methods=["POST", "GET"])
+@forum.route("/forum/<int:forum_id>-<slug>/edit", methods=["POST", "GET"])
 @login_required
 @login_required
-def move_topic(topic_id, forum_id, topic_slug=None, forum_slug=None):
-    forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
-    topic = Topic.query.filter_by(id=topic_id).first_or_404()
+def manage_forum(forum_id, slug=None):
+    page = request.args.get('page', 1, type=int)
 
 
-    # TODO: Bulk move
+    forum_instance, forumsread = Forum.get_forum(forum_id=forum_id,
+                                                 user=current_user)
 
 
-    if not can_moderate(user=current_user, forum=topic.forum):
-        flash(_("You do not have the permissions to move this topic."),
+    # 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 can_moderate(current_user, forum=forum_instance):
+        flash(_("You do not have the permissions to moderate this forum."),
               "danger")
               "danger")
         return redirect(forum_instance.url)
         return redirect(forum_instance.url)
 
 
-    if not topic.move(forum_instance):
-        flash(_("Could not move the topic to forum %(title)s.",
-                title=forum_instance.title), "danger")
-        return redirect(topic.url)
-
-    flash(_("Topic was moved to forum %(title)s.",
-            title=forum_instance.title), "success")
-    return redirect(topic.url)
-
-
-@forum.route("/topic/<int:old_id>/merge/<int:new_id>", methods=["POST"])
-@forum.route(
-    "/topic/<int:old_id>-<old_slug>/merge/<int:new_id>-<new_slug>",
-    methods=["POST"]
-)
-@login_required
-def merge_topic(old_id, new_id, old_slug=None, new_slug=None):
-    _old_topic = Topic.query.filter_by(id=old_id).first_or_404()
-    _new_topic = Topic.query.filter_by(id=new_id).first_or_404()
+    if forum_instance.external:
+        return redirect(forum_instance.external)
 
 
-    # TODO: Bulk merge
+    topics = Forum.get_topics(
+        forum_id=forum_instance.id, user=current_user, page=page,
+        per_page=flaskbb_config["TOPICS_PER_PAGE"]
+    )
 
 
-    # Looks to me that the user should have permissions on both forums, right?
-    if not can_moderate(user=current_user, forum=_old_topic.forum):
-        flash(_("You do not have the permissions to merge this topic."),
-              "danger")
-        return redirect(_old_topic.url)
+    mod_forum_url = url_for("forum.manage_forum", forum_id=forum_instance.id,
+                            slug=forum_instance.slug)
 
 
-    if not _old_topic.merge(_new_topic):
-        flash(_("Could not merge the topics."), "danger")
-        return redirect(_old_topic.url)
+    # 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")
+        tmp_topics = Topic.query.filter(Topic.id.in_(ids)).all()
+
+        # locking/unlocking
+        if "lock" in request.form:
+            changed = do_topic_action(topics=tmp_topics, user=current_user,
+                                      action="locked", reverse=False)
+
+            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=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=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=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=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")
+
+            if not new_forum_id:
+                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 can_moderate(current_user, forum_instance) or \
+                    not can_moderate(current_user, new_forum):
+                flash(_("You do not have the permissions to move this topic."),
+                      "danger")
+                return redirect(mod_forum_url)
+
+            new_forum.move_topics_to(tmp_topics)
+            return redirect(mod_forum_url)
 
 
-    flash(_("Topics succesfully merged."), "success")
-    return redirect(_new_topic.url)
+    return render_template(
+        "forum/edit_forum.html", forum=forum_instance, topics=topics,
+        available_forums=available_forums, forumsread=forumsread,
+    )
 
 
 
 
 @forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
 @forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
@@ -379,7 +413,8 @@ def edit_post(post_id):
     post = Post.query.filter_by(id=post_id).first_or_404()
     post = Post.query.filter_by(id=post_id).first_or_404()
 
 
     if not can_edit_post(user=current_user, post=post):
     if not can_edit_post(user=current_user, post=post):
-        flash(_("You do not have the permissions to edit this post."), "danger")
+        flash(_("You do not have the permissions to edit this post."),
+              "danger")
         return redirect(post.topic.url)
         return redirect(post.topic.url)
 
 
     form = ReplyForm()
     form = ReplyForm()
@@ -445,7 +480,6 @@ def raw_post(post_id):
     return format_quote(username=post.username, content=post.content)
     return format_quote(username=post.username, content=post.content)
 
 
 
 
-@forum.route("/markread", methods=["POST"])
 @forum.route("/<int:forum_id>/markread", methods=["POST"])
 @forum.route("/<int:forum_id>/markread", methods=["POST"])
 @forum.route("/<int:forum_id>-<slug>/markread", methods=["POST"])
 @forum.route("/<int:forum_id>-<slug>/markread", methods=["POST"])
 @login_required
 @login_required

+ 11 - 14
flaskbb/management/forms.py

@@ -9,16 +9,9 @@
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
 from flask_wtf import Form
 from flask_wtf import Form
-from wtforms import (
-    StringField,
-    TextAreaField,
-    PasswordField,
-    IntegerField,
-    BooleanField,
-    SelectField,
-    SubmitField,
-    HiddenField,
-)
+from wtforms import (StringField, TextAreaField, PasswordField, IntegerField,
+                     BooleanField, SelectField, SubmitField,
+		     HiddenField)
 from wtforms.validators import (DataRequired, Optional, Email, regexp, Length,
 from wtforms.validators import (DataRequired, Optional, Email, regexp, Length,
                                 URL, ValidationError)
                                 URL, ValidationError)
 from wtforms.ext.sqlalchemy.fields import (QuerySelectField,
 from wtforms.ext.sqlalchemy.fields import (QuerySelectField,
@@ -44,9 +37,11 @@ def selectable_forums():
 def selectable_categories():
 def selectable_categories():
     return Category.query.order_by(Category.position)
     return Category.query.order_by(Category.position)
 
 
+
 def selectable_groups():
 def selectable_groups():
     return Group.query.order_by(Group.id.asc()).all()
     return Group.query.order_by(Group.id.asc()).all()
 
 
+
 def select_primary_group():
 def select_primary_group():
     return Group.query.filter(Group.guest != True).order_by(Group.id)
     return Group.query.filter(Group.guest != True).order_by(Group.id)
 
 
@@ -130,7 +125,9 @@ class UserForm(Form):
             raise ValidationError(_("This E-Mail Address is already taken."))
             raise ValidationError(_("This E-Mail Address is already taken."))
 
 
     def save(self):
     def save(self):
-        user = User(**self.data)
+        data = self.data
+        data.pop('submit', None)
+        user = User(**data)
         return user.save()
         return user.save()
 
 
 
 
@@ -254,7 +251,9 @@ class GroupForm(Form):
             raise ValidationError(_("There is already a Guest group."))
             raise ValidationError(_("There is already a Guest group."))
 
 
     def save(self):
     def save(self):
-        group = Group(**self.data)
+        data = self.data
+        data.pop('submit', None)
+        group = Group(**data)
         return group.save()
         return group.save()
 
 
 
 
@@ -385,7 +384,6 @@ class ForumForm(Form):
         # remove the button
         # remove the button
         data.pop('submit', None)
         data.pop('submit', None)
         forum = Forum(**data)
         forum = Forum(**data)
-        # flush SQLA info from created instance so that it can be merged
         return forum.save()
         return forum.save()
 
 
 
 
@@ -435,7 +433,6 @@ class CategoryForm(Form):
 
 
     def save(self):
     def save(self):
         data = self.data
         data = self.data
-        # remove the button
         data.pop('submit', None)
         data.pop('submit', None)
         category = Category(**data)
         category = Category(**data)
         return category.save()
         return category.save()

+ 3 - 22
flaskbb/management/models.py

@@ -14,9 +14,10 @@ from flask_wtf import Form
 
 
 from flaskbb._compat import max_integer, text_type, iteritems
 from flaskbb._compat import max_integer, text_type, iteritems
 from flaskbb.extensions import db, cache
 from flaskbb.extensions import db, cache
+from flaskbb.utils.database import CRUDMixin
 
 
 
 
-class SettingsGroup(db.Model):
+class SettingsGroup(db.Model, CRUDMixin):
     __tablename__ = "settingsgroup"
     __tablename__ = "settingsgroup"
 
 
     key = db.Column(db.String(255), primary_key=True)
     key = db.Column(db.String(255), primary_key=True)
@@ -25,18 +26,8 @@ class SettingsGroup(db.Model):
     settings = db.relationship("Setting", lazy="dynamic", backref="group",
     settings = db.relationship("Setting", lazy="dynamic", backref="group",
                                cascade="all, delete-orphan")
                                cascade="all, delete-orphan")
 
 
-    def save(self):
-        """Saves a settingsgroup."""
-        db.session.add(self)
-        db.session.commit()
 
 
-    def delete(self):
-        """Deletes a settingsgroup."""
-        db.session.delete(self)
-        db.session.commit()
-
-
-class Setting(db.Model):
+class Setting(db.Model, CRUDMixin):
     __tablename__ = "settings"
     __tablename__ = "settings"
 
 
     key = db.Column(db.String(255), primary_key=True)
     key = db.Column(db.String(255), primary_key=True)
@@ -244,13 +235,3 @@ class Setting(db.Model):
     def invalidate_cache(cls):
     def invalidate_cache(cls):
         """Invalidates this objects cached metadata."""
         """Invalidates this objects cached metadata."""
         cache.delete_memoized(cls.as_dict, cls)
         cache.delete_memoized(cls.as_dict, cls)
-
-    def save(self):
-        """Saves a setting"""
-        db.session.add(self)
-        db.session.commit()
-
-    def delete(self):
-        """Deletes a setting"""
-        db.session.delete(self)
-        db.session.commit()

+ 158 - 10
flaskbb/management/views.py

@@ -13,7 +13,7 @@ import os
 from datetime import datetime
 from datetime import datetime
 
 
 from flask import (Blueprint, current_app, request, redirect, url_for, flash,
 from flask import (Blueprint, current_app, request, redirect, url_for, flash,
-                   __version__ as flask_version)
+                   jsonify, __version__ as flask_version)
 from flask_login import current_user
 from flask_login import current_user
 from flask_plugins import get_all_plugins, get_plugin, get_plugin_from_all
 from flask_plugins import get_all_plugins, get_plugin, get_plugin_from_all
 from flask_babelex import gettext as _
 from flask_babelex import gettext as _
@@ -148,10 +148,41 @@ def edit_user(user_id):
                            title=_("Edit User"))
                            title=_("Edit User"))
 
 
 
 
+@management.route("/users/delete", methods=["POST"])
 @management.route("/users/<int:user_id>/delete", methods=["POST"])
 @management.route("/users/<int:user_id>/delete", methods=["POST"])
 @admin_required
 @admin_required
-def delete_user(user_id):
+def delete_user(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
+
+            if user.delete():
+                data.append({
+                    "id": user.id,
+                    "type": "delete",
+                    "reverse": False,
+                    "reverse_name": None,
+                    "reverse_url": None
+                })
+
+        return jsonify(
+            message="{} Users deleted.".format(len(data)),
+            category="success",
+            data=data,
+            status=200
+        )
+
     user = User.query.filter_by(id=user_id).first_or_404()
     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()
     user.delete()
     flash(_("User successfully deleted."), "success")
     flash(_("User successfully deleted."), "success")
     return redirect(url_for("management.users"))
     return redirect(url_for("management.users"))
@@ -192,13 +223,46 @@ def banned_users():
                            search_form=search_form)
                            search_form=search_form)
 
 
 
 
+@management.route("/users/ban", methods=["POST"])
 @management.route("/users/<int:user_id>/ban", methods=["POST"])
 @management.route("/users/<int:user_id>/ban", methods=["POST"])
 @moderator_required
 @moderator_required
-def ban_user(user_id):
+def ban_user(user_id=None):
     if not can_ban_user(current_user):
     if not can_ban_user(current_user):
         flash(_("You do not have the permissions to ban this user."), "danger")
         flash(_("You do not have the permissions to ban this user."), "danger")
         return redirect(url_for("management.overview"))
         return redirect(url_for("management.overview"))
 
 
+    # ajax request
+    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 \
+                    user.get_permissions()['admin'] and \
+                    (current_user.permissions['mod'] or
+                     current_user.permissions['super_mod']):
+                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
+        )
+
     user = User.query.filter_by(id=user_id).first_or_404()
     user = User.query.filter_by(id=user_id).first_or_404()
 
 
     # Do not allow moderators to ban admins
     # Do not allow moderators to ban admins
@@ -209,7 +273,7 @@ def ban_user(user_id):
         flash(_("A moderator cannot ban an admin user."), "danger")
         flash(_("A moderator cannot ban an admin user."), "danger")
         return redirect(url_for("management.overview"))
         return redirect(url_for("management.overview"))
 
 
-    if user.ban():
+    if not current_user.id == user.id and user.ban():
         flash(_("User is now banned."), "success")
         flash(_("User is now banned."), "success")
     else:
     else:
         flash(_("Could not ban user."), "danger")
         flash(_("Could not ban user."), "danger")
@@ -217,14 +281,38 @@ def ban_user(user_id):
     return redirect(url_for("management.banned_users"))
     return redirect(url_for("management.banned_users"))
 
 
 
 
+@management.route("/users/unban", methods=["POST"])
 @management.route("/users/<int:user_id>/unban", methods=["POST"])
 @management.route("/users/<int:user_id>/unban", methods=["POST"])
 @moderator_required
 @moderator_required
-def unban_user(user_id):
+def unban_user(user_id=None):
     if not can_ban_user(current_user):
     if not can_ban_user(current_user):
         flash(_("You do not have the permissions to unban this user."),
         flash(_("You do not have the permissions to unban this user."),
               "danger")
               "danger")
         return redirect(url_for("management.overview"))
         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
+        )
+
     user = User.query.filter_by(id=user_id).first_or_404()
     user = User.query.filter_by(id=user_id).first_or_404()
 
 
     if user.unban():
     if user.unban():
@@ -263,9 +351,32 @@ def unread_reports():
 @management.route("/reports/markread", methods=["POST"])
 @management.route("/reports/markread", methods=["POST"])
 @moderator_required
 @moderator_required
 def report_markread(report_id=None):
 def report_markread(report_id=None):
+    # AJAX request
+    if request.is_xhr:
+        ids = request.get_json()["ids"]
+        data = []
+
+        for report in Report.query.filter(Report.id.in_(ids)).all():
+            report.zapped_by = current_user.id
+            report.zapped = datetime.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
+        )
+
     # mark single report as read
     # mark single report as read
     if report_id:
     if report_id:
-
         report = Report.query.filter_by(id=report_id).first_or_404()
         report = Report.query.filter_by(id=report_id).first_or_404()
         if report.zapped:
         if report.zapped:
             flash(_("Report %(id)s is already marked as read.", id=report.id),
             flash(_("Report %(id)s is already marked as read.", id=report.id),
@@ -328,11 +439,48 @@ def edit_group(group_id):
 
 
 
 
 @management.route("/groups/<int:group_id>/delete", methods=["POST"])
 @management.route("/groups/<int:group_id>/delete", methods=["POST"])
+@management.route("/groups/delete", methods=["POST"])
 @admin_required
 @admin_required
-def delete_group(group_id):
-    group = Group.query.filter_by(id=group_id).first_or_404()
-    group.delete()
-    flash(_("Group successfully deleted."), "success")
+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
+                })
+
+            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 them instead.", "danger"))
+            return redirect(url_for("management.groups"))
+
+        group = Group.query.filter_by(id=group_id).first_or_404()
+        group.delete()
+        flash(_("Group successfully deleted."), "success")
+        return redirect(url_for("management.groups"))
+
+    flash(_("No group choosen.."), "danger")
     return redirect(url_for("management.groups"))
     return redirect(url_for("management.groups"))
 
 
 
 

+ 3 - 17
flaskbb/message/models.py

@@ -13,9 +13,9 @@ from datetime import datetime
 from sqlalchemy_utils import UUIDType
 from sqlalchemy_utils import UUIDType
 
 
 from flaskbb.extensions import db
 from flaskbb.extensions import db
+from flaskbb.utils.database import CRUDMixin
 
 
-
-class Conversation(db.Model):
+class Conversation(db.Model, CRUDMixin):
     __tablename__ = "conversations"
     __tablename__ = "conversations"
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
@@ -73,15 +73,8 @@ class Conversation(db.Model):
         db.session.commit()
         db.session.commit()
         return self
         return self
 
 
-    def delete(self):
-        """Deletes a private message"""
-
-        db.session.delete(self)
-        db.session.commit()
-        return self
-
 
 
-class Message(db.Model):
+class Message(db.Model, CRUDMixin):
     __tablename__ = "messages"
     __tablename__ = "messages"
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
@@ -108,10 +101,3 @@ class Message(db.Model):
         db.session.add(self)
         db.session.add(self)
         db.session.commit()
         db.session.commit()
         return self
         return self
-
-    def delete(self):
-        """Deletes a private message"""
-
-        db.session.delete(self)
-        db.session.commit()
-        return self

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

@@ -10,6 +10,10 @@ body {
   margin-bottom: 60px;
   margin-bottom: 60px;
 }
 }
 
 
+.forum-moderation, .forum-selectall, .forum-select {
+
+}
+
 .footer {
 .footer {
   position: absolute;
   position: absolute;
   bottom: 0;
   bottom: 0;
@@ -33,6 +37,35 @@ body {
   border-color: #ddd;
   border-color: #ddd;
 }
 }
 
 
+.text-success {
+    color: #5cb85c;
+}
+
+.text-info {
+    color: #5bc0de;
+}
+
+.text-danger {
+    color: #d9534f;
+}
+
+.text-primary {
+    color: #337ab7;
+}
+
+.text-warning {
+    color: #f0ad4e;
+}
+
+.search-form {
+    display: none;
+    padding: 15px;
+}
+
+.management-body {
+    padding: 0px;
+}
+
 /*
 /*
  * Option to remove the borders from the table
  * Option to remove the borders from the table
  */
  */

+ 0 - 18
flaskbb/static/js/conversation.js

@@ -1,18 +0,0 @@
-/**
- * topic.js
- */
-$(document).ready(function () {
-    // Quote
-    $('.reply-btn').click(function (event) {
-        event.preventDefault();
-        var message_id = $(this).attr('data-message-id');
-
-        $.get('/message/message/' + message_id + '/raw', function(text) {
-            var $contents = $('.message-content .md-editor textarea');
-            $contents.val(($contents.val() + '\n' + text).trim() + '\n');
-            $contents.selectionStart = $contents.selectionEnd = $contents.val().length;
-            $contents[0].scrollTop = $contents[0].scrollHeight;
-            window.location.href = '#content';
-        });
-    });
-});

+ 139 - 0
flaskbb/static/js/flaskbb.js

@@ -0,0 +1,139 @@
+/**
+ * flaskbb.js
+ * Copyright: (C) 2015 - FlaskBB Team
+ * License: BSD - See LICENSE for more details.
+ */
+
+
+ // get the csrf token from the header
+var csrftoken = $('meta[name=csrf-token]').attr('content');
+
+var show_management_search = function() {
+    var body = $('.management-body');
+    var form = body.find('.search-form');
+
+    // toggle
+    form.slideToggle(function() {
+        if(form.css('display') != 'none') {
+            //body.css('padding', '15px');
+            form.find('input').focus();
+        }
+    });
+};
+
+var flash_message = function(message) {
+    var container = $('#flashed-messages');
+
+    var flashed_message = '<div class="alert alert-'+ message.category +'">';
+
+    if(message.category == 'success') {
+        flashed_message += '<span class="glyphicon glyphicon-ok-sign"></span>&nbsp;';
+    } else if (message.category == 'error') {
+        flashed_message += '<span class="glyphicon glyphicon-exclamation-sign"></span>&nbsp;';
+    } else {
+        flashed_message += '<span class="glyphicon glyphicon-info-sign"></span>&nbsp;';
+    }
+    flashed_message += '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>' + message.message + '</div>';
+    container.append(flashed_message);
+};
+
+var BulkActions = function() {
+    this.execute = function(url, confirm_message) {
+        var selected = $('input.action-checkbox:checked').size();
+        var data = {"ids": []};
+
+        // don't do anything if nothing is selected
+        if (selected === 0) {
+            return false;
+        }
+
+        $('input.action-checkbox:checked').each(function(k, v) {
+            data.ids.push($(v).val());
+        });
+
+        // http://stackoverflow.com/questions/784929/what-is-the-not-not-operator-in-javascript
+        if(!!confirm_message) {
+            if(!confirm(confirm_message)) {
+                return false;
+            }
+        }
+
+        send_data(url, data)
+
+        return false;
+    };
+};
+
+var send_data = function(endpoint_url, data) {
+    $.ajax({
+        url: endpoint_url,
+        method: "POST",
+        data: JSON.stringify(data),
+        dataType: "json",
+        contentType: "application/json",
+        beforeSend: function(xhr, settings) {
+            if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
+                xhr.setRequestHeader("X-CSRFToken", csrftoken);
+            }
+        }
+    })
+    .done(function(response) {
+        flash_message(response);
+        $.each(response.data, function(k, v) {
+            // get the form
+            var form = $('#' + v.type + '-' + v.id);
+
+            // check if there is something to reverse it, otherwise remove the DOM.
+            if(v.reverse) {
+                form.attr('action', v.reverse_url);
+                if(v.type == 'ban') {
+                    reverse_html = '<span class="fa fa-flag text-success" data-toggle="tooltip" data-placement="top" title="'+ v.reverse_name +'"></span>'
+                } else if (v.type == 'unban') {
+                    reverse_html = '<span class="fa fa-flag text-warning" data-toggle="tooltip" data-placement="top" title="'+ v.reverse_name +'"></span>'
+                }
+                form.find('button').html(reverse_html);
+            } else {
+                form.parents('.action-row').remove();
+            }
+
+        });
+    })
+    .fail(function(error) {
+        flash_message(error);
+    });
+};
+
+$(document).ready(function () {
+    // listen on the action-checkall checkbox to un/check all
+    $('.action-checkall').change(function() {
+        $('input.action-checkbox').prop('checked', this.checked);
+    });
+
+    // Reply conversation
+    $('.reply-btn').click(function (event) {
+        event.preventDefault();
+        var message_id = $(this).attr('data-message-id');
+
+        $.get('/message/message/' + message_id + '/raw', function(text) {
+            var $contents = $('.message-content .md-editor textarea');
+            $contents.val(($contents.val() + '\n' + text).trim() + '\n');
+            $contents.selectionStart = $contents.selectionEnd = $contents.val().length;
+            $contents[0].scrollTop = $contents[0].scrollHeight;
+            window.location.href = '#content';
+        });
+    });
+    // Reply to post
+    $('.quote_btn').click(function (event) {
+        event.preventDefault();
+        var post_id = $(this).attr('data-post-id');
+
+        $.get('/post/' + post_id + '/raw', function(text) {
+            var $contents = $('.reply-content .md-editor textarea');
+            console.log($contents);
+            $contents.val(($contents.val() + '\n' + text).trim() + '\n');
+            $contents.selectionStart = $contents.selectionEnd = $contents.val().length;
+            $contents[0].scrollTop = $contents[0].scrollHeight;
+            window.location.href = '#content';
+        });
+    });
+});

+ 0 - 19
flaskbb/static/js/topic.js

@@ -1,19 +0,0 @@
-/**
- * topic.js
- */
-$(document).ready(function () {
-    // Quote
-    $('.quote_btn').click(function (event) {
-        event.preventDefault();
-        var post_id = $(this).attr('data-post-id');
-
-        $.get('/post/' + post_id + '/raw', function(text) {
-            var $contents = $('.reply-content .md-editor textarea');
-            console.log($contents)
-            $contents.val(($contents.val() + '\n' + text).trim() + '\n');
-            $contents.selectionStart = $contents.selectionEnd = $contents.val().length;
-            $contents[0].scrollTop = $contents[0].scrollHeight;
-            window.location.href = '#content';
-        });
-    });
-});

+ 161 - 0
flaskbb/templates/forum/edit_forum.html

@@ -0,0 +1,161 @@
+    {% set page_title = forum.title %}
+    {% set active_forum_nav=True %}
+
+    {% extends theme("layout.html") %}
+    {% block content %}
+    {% from theme('macros.html') import render_pagination, topic_pages %}
+
+    <ol class="breadcrumb">
+        <li><a href="{{ url_for('forum.index') }}">{% trans %}Forum{% endtrans %}</a></li>
+        <li><a href="{{ forum.category.url }}">{{ forum.category.title }}</a></li>
+        <li class="active">{{ forum.title }}</li>
+    </ol>
+
+    <div class="pull-left">
+        {{ render_pagination(topics, forum.url) }}
+    </div> <!-- end span pagination -->
+
+    <form method="post">
+    <div style="display:none;"><input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token() }}"></div>
+
+    <table class="table table-bordered">
+        <thead>
+            <tr>
+                <th colspan="6">
+                    {{ forum.title }}
+                </th>
+            </tr>
+        </thead>
+
+        <tbody>
+            <tr>
+                <td colspan="2">{% trans %}Topic{% endtrans %}</td>
+
+                <td>{% trans %}Posts{% endtrans %}</td>
+
+                <td>{% trans %}Views{% endtrans %}</td>
+
+                <td>{% trans %}Last Post{% endtrans %}</td>
+
+                <td><input type="checkbox" name="rowtoggle" class="action-checkall" title="Select All"/></td>
+            </tr>
+
+            {% for topic, topicread in topics.items %}
+            <tr>
+                <td class="topic-status" width="4%" style="vertical-align: middle; text-align: center;">
+                {% if topic.locked %}
+                    <span class="fa fa-lock" style="font-size: 2em"></span>
+                {% elif topic.important %}
+                    {% if topic|topic_is_unread(topicread, current_user, forumsread) %}
+                        <span class="fa fa-star" style="font-size: 2em"></span>
+                    {% else %}
+                        <span class="fa fa-star-o" style="font-size: 2em"></span>
+                    {% endif %}
+                {% else %}
+                    {% if topic|topic_is_unread(topicread, current_user, forumsread) %}
+                        <span class="fa fa-comment" style="font-size: 2em"></span>
+                    {% else %}
+                        <span class="fa fa-comment-o" style="font-size: 2em"></span>
+                    {% endif %}
+                {% endif %}
+
+                </td>
+                <td>
+                    <div>
+                        <a href="{{ topic.url }}">{{ topic.title }}</a>
+                        <!-- Topic Pagination -->
+                        {{ topic_pages(topic, flaskbb_config["POSTS_PER_PAGE"]) }}
+                        <br />
+                        <small>
+                            {% trans %}by{% endtrans %}
+                            {% if topic.user_id %}
+                             <a href="{{ topic.user.url }}">{{ topic.user.username }}</a>
+                            {% else %}
+                            {{ topic.username }}
+                            {% endif %}
+                        </small>
+                    </div>
+                </td>
+                <td>
+                    {{ topic.post_count }}
+                </td>
+                <td>
+                    {{ topic.views }}
+                </td>
+                <td>
+                    <a href="{{ topic.last_post.url }}">{{ topic.last_post.date_created|time_since }}</a><br />
+
+                    <small>
+                        {% trans %}by{% endtrans %}
+                        {% if topic.last_post.user_id %}
+                        <a href="{{ topic.last_post.user.url }}">{{ topic.last_post.user.username }}</a>
+                        {% else %}
+                        {{ topic.last_post.username }}
+                        {% endif %}
+                    </small>
+                </td>
+                <td><input type="checkbox" name="rowid" class="action-checkbox" value="{{ topic.id }}" title="Select Topic"/></td>
+            </tr>
+            {% else %}
+            <tr>
+                <td colspan="6">
+                    {% trans %}No Topics.{% endtrans %}
+                </td>
+            </tr>
+            {% endfor %}
+
+        </tbody>
+    </table>
+
+    <div class="row">
+        <div class="col-md-2">
+            <div class="pull-left">
+                <a class="btn btn-default" href="{{ forum.url }}">
+                    <span class="fa fa-arrow-left"></span> {% trans %}Back{% endtrans %}
+                </a>
+            </div>
+        </div>
+
+        <div class="col-md-10">
+            <div class="pull-right">
+                <div class="form-group">
+                <div class="btn-group" role="group">
+                    <button name="lock" class="btn btn-warning">
+                        <span class="fa fa-lock"></span> {% trans %}Lock{% endtrans %}
+                    </button>
+                    <button name="unlock" class="btn btn-warning">
+                        <span class="fa fa-unlock"></span> {% trans %}Unlock{% endtrans %}
+                    </button>
+                </div>
+                <div class="btn-group" role="group">
+                    <button name="highlight" class="btn btn-success">
+                        <span class="fa fa-star"></span> {% trans %}Highlight{% endtrans %}
+                    </button>
+                    <button name="trivialize" class="btn btn-success">
+                        <span class="fa fa-star-o"></span> {% trans %}Trivialize{% endtrans %}
+                    </button>
+                </div>
+
+                <button name="delete" class="btn btn-danger">
+                    <span class="fa fa-trash-o"></span> {% trans %}Delete{% endtrans %}
+                </button>
+                </div>
+
+                <div class="form-group row">
+                    <div class="col-sm-5">
+                        <select class="form-control" id="forum" name="forum">
+                            <option selected value="">{% trans %}Move to...{% endtrans %}</option>
+                            {% for forum in available_forums %}
+                                <option value={{forum.id}}>{{ forum.title }}</option>
+                            {% endfor %}
+                        </select>
+                    </div> <!-- end div_class -->
+                    <button name="move" class="btn btn-info">
+                        <span class="fa fa-plane"></span> {% trans %}Move{% endtrans %}
+                    </button>
+                </div> <!-- end form-group -->
+            </div>
+        </div>
+    </div>
+    </form>
+    {% endblock %}

+ 6 - 1
flaskbb/templates/forum/forum.html

@@ -58,7 +58,7 @@
 
 
         {% for topic, topicread in topics.items %}
         {% for topic, topicread in topics.items %}
         <tr>
         <tr>
-            <td width="4%" style="vertical-align: middle; text-align: center;">
+            <td class="topic-status" width="4%" style="vertical-align: middle; text-align: center;">
             {% if topic.locked %}
             {% if topic.locked %}
                 <span class="fa fa-lock" style="font-size: 2em"></span>
                 <span class="fa fa-lock" style="font-size: 2em"></span>
             {% elif topic.important %}
             {% elif topic.important %}
@@ -122,4 +122,9 @@
     </tbody>
     </tbody>
 </table>
 </table>
 
 
+<div class="pull-left">
+    <a class="btn btn-default" href="{{ url_for('forum.manage_forum', forum_id=forum.id, slug=forum.slug) }}">
+        <span class="fa fa-tasks"></span> {% trans %}Moderation Mode{% endtrans %}
+    </a>
+</div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
flaskbb/templates/forum/new_post.html

@@ -4,7 +4,6 @@
 {% extends theme("layout.html") %}
 {% extends theme("layout.html") %}
 
 
 {% block css %}
 {% block css %}
-    {{ super() }}
     <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-markdown.min.css') }}">
     <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-markdown.min.css') }}">
 {% endblock %}
 {% endblock %}
 
 

+ 0 - 1
flaskbb/templates/forum/new_topic.html

@@ -4,7 +4,6 @@
 {% extends theme("layout.html") %}
 {% extends theme("layout.html") %}
 
 
 {% block css %}
 {% block css %}
-    {{ super() }}
     <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-markdown.min.css') }}">
     <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-markdown.min.css') }}">
 {% endblock %}
 {% endblock %}
 
 

+ 0 - 2
flaskbb/templates/forum/topic.html

@@ -3,7 +3,6 @@
 {% set active_forum_nav=True %}
 {% set active_forum_nav=True %}
 
 
 {% block css %}
 {% block css %}
-    {{ super() }}
     <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-markdown.min.css') }}">
     <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-markdown.min.css') }}">
 {% endblock %}
 {% endblock %}
 
 
@@ -170,7 +169,6 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block scripts %}
 {% block scripts %}
-    <script type="text/javascript" src="{{ url_for('static', filename='js/topic.js') }}"></script>
     <script type="text/javascript" src="{{ url_for('static', filename='js/marked.js') }}"></script>
     <script type="text/javascript" src="{{ url_for('static', filename='js/marked.js') }}"></script>
     <script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-markdown.js') }}"></script>
     <script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-markdown.js') }}"></script>
 {% endblock %}
 {% endblock %}

+ 95 - 82
flaskbb/templates/layout.html

@@ -11,121 +11,134 @@
             {%- endif -%}
             {%- endif -%}
         {% endblock %}
         {% endblock %}
         </title>
         </title>
+
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <meta name="description" content="FlaskBB is a forum software written in Flask">
         <meta name="description" content="FlaskBB is a forum software written in Flask">
         <meta name="author" content="FlaskBB Team">
         <meta name="author" content="FlaskBB Team">
+        <meta name="csrf-token" content="{{ csrf_token() }}">
 
 
         <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
         <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
 
 
-        {% block css %}
+        {% block stylesheets %}
         <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}" >
         <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}" >
         <link rel="stylesheet" href="{{ url_for('static', filename='css/code.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/code.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/flaskbb.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/flaskbb.css') }}">
         {% endblock %}
         {% endblock %}
+
+        {# for extra stylesheets. e.q. a template has to add something #}
+        {% block css %}
+        {% endblock %}
+
+        {# for various extra things #}
+        {% block head_extra %}
+        {% endblock %}
     </head>
     </head>
 
 
     <body>
     <body>
         {% block navigation %}
         {% block navigation %}
         {%- from theme("macros.html") import topnav with context -%}
         {%- from theme("macros.html") import topnav with context -%}
         <!-- Navigation -->
         <!-- Navigation -->
-            <nav class="navbar navbar-default navbar-static-top">
-                <div class="container">
-                    <div class="navbar-header">
-                        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
-                            <span class="sr-only">Toggle navigation</span>
-                            <span class="icon-bar"></span>
-                            <span class="icon-bar"></span>
-                            <span class="icon-bar"></span>
+        <nav class="navbar navbar-default navbar-static-top">
+            <div class="container">
+                <div class="navbar-header">
+                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
+                        <span class="sr-only">Toggle navigation</span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </button>
+                    <a class="navbar-brand" href="/">FlaskBB</a>
+                </div>
+                <div class="collapse navbar-collapse navbar-ex1-collapse">
+                    <ul class="nav navbar-nav">
+                        {{ emit_event("before-first-navigation-element") }}
+
+                        {# active_forum_nav is set in {forum, category, topic}.html and new_{topic, post}.html #}
+                        {{ topnav(endpoint='forum.index', name=_('Forum'), icon='fa fa-comment', active=active_forum_nav) }}
+                        {{ topnav(endpoint='forum.memberlist', name=_('Memberlist'), icon='fa fa-user') }}
+                        {{ topnav(endpoint='forum.search', name=_('Search'), icon='fa fa-search') }}
+
+                        {{ emit_event("after-last-navigation-element") }}
+                    </ul>
+
+                {% if current_user and current_user.is_authenticated() %}
+                    <div class="btn-group navbar-btn navbar-right" style="padding-left: 15px; margin-right: -10px">
+                        <a class="btn btn-primary" href="{{ url_for('user.profile', username=current_user.username) }}">
+                            <span class="fa fa-user"></span> {{ current_user.username }}
+                        </a>
+                        <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
+                            <span class="caret"></span>
                         </button>
                         </button>
-                        <a class="navbar-brand" href="/">FlaskBB</a>
-                    </div>
-                    <div class="collapse navbar-collapse navbar-ex1-collapse">
-                        <ul class="nav navbar-nav">
-                            {{ emit_event("before-first-navigation-element") }}
+                        <ul class="dropdown-menu" role="menu">
+                            <li><a href="{{ url_for('forum.topictracker') }}"><span class="fa fa-book"></span> {% trans %}Topic Tracker{% endtrans %}</a></li>
+                            <li class="divider"></li>
 
 
-                            {# active_forum_nav is set in {forum, category, topic}.html and new_{topic, post}.html #}
-                            {{ topnav(endpoint='forum.index', name=_('Forum'), icon='fa fa-comment', active=active_forum_nav) }}
-                            {{ topnav(endpoint='forum.memberlist', name=_('Memberlist'), icon='fa fa-user') }}
-                            {{ topnav(endpoint='forum.search', name=_('Search'), icon='fa fa-search') }}
+                            <li><a href="{{ url_for('user.settings') }}"><span class="fa fa-cogs"></span> {% trans %}Settings{% endtrans %}</a></li>
+                            {% if current_user|is_admin_or_moderator %}
+                            <li><a href="{{ url_for('management.overview') }}"><span class="fa fa-cog"></span> {% trans %}Management{% endtrans %}</a></li>
+                            <li class="divider"></li>
+                            {% endif %}
 
 
-                            {{ emit_event("after-last-navigation-element") }}
+                            <li><a href="{{ url_for('auth.logout') }}"><span class="fa fa-power-off"></span> {% trans %}Logout{% endtrans %}</a></li>
                         </ul>
                         </ul>
+                    </div>
 
 
-                    {% if current_user and current_user.is_authenticated() %}
-                        <div class="btn-group navbar-btn navbar-right" style="padding-left: 15px; margin-right: -10px">
-                            <a class="btn btn-primary" href="{{ url_for('user.profile', username=current_user.username) }}">
-                                <span class="fa fa-user"></span> {{ current_user.username }}
-                            </a>
-                            <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
-                                <span class="caret"></span>
-                            </button>
-                            <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('forum.topictracker') }}"><span class="fa fa-book"></span> {% trans %}Topic Tracker{% endtrans %}</a></li>
-                                <li class="divider"></li>
-
-                                <li><a href="{{ url_for('user.settings') }}"><span class="fa fa-cogs"></span> {% trans %}Settings{% endtrans %}</a></li>
-                                {% if current_user|is_admin_or_moderator %}
-                                <li><a href="{{ url_for('management.overview') }}"><span class="fa fa-cog"></span> {% trans %}Management{% endtrans %}</a></li>
-                                <li class="divider"></li>
-                                {% endif %}
-
-                                <li><a href="{{ url_for('auth.logout') }}"><span class="fa fa-power-off"></span> {% trans %}Logout{% endtrans %}</a></li>
-                            </ul>
-                        </div>
-
-                        <div class="btn-group navbar-btn navbar-right">
-                            <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown">
-                                <span class="fa fa-envelope"></span> <span class="badge">{{ current_user.pm_unread }}</span>
-                            </button>
-                            <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Private Messages{% endtrans %}</a></li>
-                                <li><a href="{{ url_for('message.new_conversation') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
-                            </ul>
-                        </div>
-                    {% else %}
-                        <div class="btn-group navbar-btn navbar-right">
-                            <a class="btn btn-primary" href="{{ url_for('auth.login') }}">
-                                <span class="fa fa-user"></span> {% trans %}Login{% endtrans %}
-                            </a>
-                            <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
-                                <span class="caret"></span>
-                            </button>
-                            <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('auth.register') }}">{% trans %}Register{% endtrans %}</a></li>
-                                <li><a href="{{ url_for('auth.forgot_password') }}">{% trans %}Reset Password{% endtrans %}</a></li>
-                            </ul>
-                        </div>
-                    {% endif %}
-                    </div><!-- nav-collapse -->
-                </div><!-- container -->
-            </nav> <!-- navbar navbar-inverse -->
+                    <div class="btn-group navbar-btn navbar-right">
+                        <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown">
+                            <span class="fa fa-envelope"></span> <span class="badge">{{ current_user.pm_unread }}</span>
+                        </button>
+                        <ul class="dropdown-menu" role="menu">
+                            <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Private Messages{% endtrans %}</a></li>
+                            <li><a href="{{ url_for('message.new_conversation') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
+                        </ul>
+                    </div>
+                {% else %}
+                    <div class="btn-group navbar-btn navbar-right">
+                        <a class="btn btn-primary" href="{{ url_for('auth.login') }}">
+                            <span class="fa fa-user"></span> {% trans %}Login{% endtrans %}
+                        </a>
+                        <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
+                            <span class="caret"></span>
+                        </button>
+                        <ul class="dropdown-menu" role="menu">
+                            <li><a href="{{ url_for('auth.register') }}">{% trans %}Register{% endtrans %}</a></li>
+                            <li><a href="{{ url_for('auth.forgot_password') }}">{% trans %}Reset Password{% endtrans %}</a></li>
+                        </ul>
+                    </div>
+                {% endif %}
+                </div><!-- nav-collapse -->
+            </div><!-- container -->
+        </nav> <!-- navbar navbar-inverse -->
+        {% endblock %}
+
+
+        <div class="container">
+            {% block messages %}
+                {% include theme('flashed_messages.html') %}
             {% endblock %}
             {% endblock %}
 
 
+            {% block content %}
+            {% endblock %}
+        </div> <!-- /container -->
 
 
+        {% block footer %}
+        <div id="footer">
             <div class="container">
             <div class="container">
-                {% block messages %}
-                    {% include theme('flashed_messages.html') %}
-                {% endblock %}
-
-                {% block content %}
-                {% endblock %}
-            </div> <!-- /container -->
-
-            {% block footer %}
-            <div id="footer">
-                <div class="container">
-                    <p class="text-muted credit pull-left">powered by <a href="http://flask.pocoo.org">Flask</a></p>
-                    <p class="text-muted credit pull-right">&copy; 2013 - 2015 - <a href="http://flaskbb.org">FlaskBB.org</a></p>
-                </div>
+                <p class="text-muted credit pull-left">powered by <a href="http://flask.pocoo.org">Flask</a></p>
+                <p class="text-muted credit pull-right">&copy; 2013 - 2015 - <a href="http://flaskbb.org">FlaskBB.org</a></p>
             </div>
             </div>
-            {% endblock %}
+        </div>
+        {% endblock %}
 
 
+        {# standard javascript libs #}
         {% block javascript %}
         {% block javascript %}
         <script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
         <script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
         <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
         <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
+        <script src="{{ url_for('static', filename='js/flaskbb.js') }}"></script>
         {% endblock %}
         {% endblock %}
 
 
+        {# space for extra scripts - to use in other templates #}
         {% block scripts %}
         {% block scripts %}
         {% endblock %}
         {% endblock %}
     </body>
     </body>

+ 12 - 7
flaskbb/templates/macros.html

@@ -67,9 +67,11 @@
 
 
 
 
 {%- macro render_boolean_field(field, inline=False) -%}
 {%- macro render_boolean_field(field, inline=False) -%}
-<div class="checkbox {%- if inline -%}inline{%- endif -%}">
+<div class="checkbox{% if inline %}inline{% endif %}{% if field.errors %}has-error{% endif %}">
+    <label>
     {{ field(**kwargs) }}
     {{ field(**kwargs) }}
-    {{ field_label(field) }}
+    {{ field.label.text }}
+    </label>
     {{ field_description(field) }}
     {{ field_description(field) }}
     {{ field_errors(field) }}
     {{ field_errors(field) }}
 </div>
 </div>
@@ -96,6 +98,7 @@
 </div>
 </div>
 {%- endmacro -%}
 {%- endmacro -%}
 
 
+
 {%- macro render_submit_field(field, div_class='', input_class='') -%}
 {%- macro render_submit_field(field, div_class='', input_class='') -%}
 {% if div_class %}
 {% if div_class %}
 <div class="{{ div_class }}">
 <div class="{{ div_class }}">
@@ -108,6 +111,7 @@
 {% endif %}
 {% endif %}
 {%- endmacro -%}
 {%- endmacro -%}
 
 
+
 {%- macro render_field(field, with_label=True, div_class='', rows='') -%}
 {%- macro render_field(field, with_label=True, div_class='', rows='') -%}
 <div class="form-group{%- if field.errors %} has-error{%- endif %}">
 <div class="form-group{%- if field.errors %} has-error{%- endif %}">
     {% if div_class %}
     {% if div_class %}
@@ -166,21 +170,21 @@
 {%- endmacro -%}
 {%- endmacro -%}
 
 
 
 
-{%- macro group_field(field, label_text='', label_class='') -%}
+{%- macro group_field(field, label_text='', label_class='', css_class='form-control form-grouped') -%}
     <div class="form-group {%- if field.errors %} has-error{%- endif %}" style="margin-bottom: 0px;">
     <div class="form-group {%- if field.errors %} has-error{%- endif %}" style="margin-bottom: 0px;">
         {{field.label(class="sr-only")}}
         {{field.label(class="sr-only")}}
 
 
         {%- if kwargs['required'] or field.flags.required -%}
         {%- if kwargs['required'] or field.flags.required -%}
             {% if label_text %}
             {% if label_text %}
-                {{field(class='form-control form-grouped', placeholder=label_text, required="required", **kwargs)}}
+                {{field(class=css_class, placeholder=label_text, required="required", **kwargs)}}
             {% else %}
             {% else %}
-                {{field(class='form-control form-grouped', placeholder=field.label.text, required="required", **kwargs)}}
+                {{field(class=css_class, placeholder=field.label.text, required="required", **kwargs)}}
             {% endif %}
             {% endif %}
         {%- else -%}
         {%- else -%}
             {% if label_text %}
             {% if label_text %}
-                {{field(class='form-control form-grouped', placeholder=label_text, **kwargs)}}
+                {{field(class=css_class, placeholder=label_text, **kwargs)}}
             {% else %}
             {% else %}
-                {{field(class='form-control form-grouped', placeholder=field.label.text, **kwargs)}}
+                {{field(class=css_class, placeholder=field.label.text, **kwargs)}}
             {% endif %}
             {% endif %}
         {%- endif -%}
         {%- endif -%}
         {{ field_description(field) }}
         {{ field_description(field) }}
@@ -188,6 +192,7 @@
     </div>
     </div>
 {%- endmacro -%}
 {%- endmacro -%}
 
 
+
 {%- macro horizontal_select_field(field, div_class='', label_class='', select_class="form-control", surrounded_div="col-sm-4") -%}
 {%- macro horizontal_select_field(field, div_class='', label_class='', select_class="form-control", surrounded_div="col-sm-4") -%}
 <div class="form-group row {%- if field.errors %} has-error{%- endif %}">
 <div class="form-group row {%- if field.errors %} has-error{%- endif %}">
     {% if label_class %}
     {% if label_class %}

+ 103 - 51
flaskbb/templates/management/banned_users.html

@@ -17,59 +17,111 @@
 </div><!--/.col-md-3 -->
 </div><!--/.col-md-3 -->
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-    <legend>{% trans %}Banned Users{% endtrans %}</legend>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
 
 
-    <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">{% trans %}Search{% endtrans %}</button>
-                </span>
+                    <span class="fa fa-user-times"></span> {% trans %}Banned Users{% endtrans %}
+
+                    <div class="pull-right action-buttons">
+                        <div class="btn-group pull-right">
+                            <button type="button" class="btn btn-default btn-xs" onclick="return show_management_search()">
+                                <span class="fa fa-search" style="margin-right: 0px;"></span> {% trans %}Search{% endtrans %}
+                            </button>
+                        </div>
+                    </div>
+
+                </div>
+                <div class="panel-body management-body">
+
+
+                    <form class="search-form" role="form" method="post">
+                        {{ search_form.hidden_tag() }}
+                        <div class="input-group">
+                            {{ group_field(search_form.search_query, css_class="form-control") }}
+                            <span class="input-group-btn">
+                                <button class="btn btn-default" type="button"><span class="fa fa-search"></span></button>
+                            </span>
+                        </div>
+                    </form>
+                </div>
+
+                    <table class="table table-hover">
+                        <thead>
+                            <tr>
+                                <th><input type="checkbox" name="rowtoggle" class="action-checkall" title="Select All"/></th>
+                                <th>#</th>
+                                <th>{% trans %}Username{% endtrans %}</th>
+                                <th>{% trans %}Posts{% endtrans %}</th>
+                                <th>{% trans %}Date registered{% endtrans %}</th>
+                                <th>{% trans %}Group{% endtrans %}</th>
+                                <th>
+                                    <div class="btn-group">
+                                        <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
+                                            <span class="fa fa-cog" style="margin-right: 0px;"></span> {% trans %}Actions{% endtrans %}
+                                        </button>
+                                        <ul class="dropdown-menu slidedown">
+                                            <li>
+                                                <a href="javascript:void(0)" onclick="return bulk_actions.execute('{{ url_for('management.unban_user') }}', '{% trans %}Are you sure you want to unban these Users?{% endtrans %}')">
+                                                    <span class="fa fa-flag text-success"></span> {% trans %}Unban selected Users{% endtrans %}
+                                                </a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                        {% for user in users.items %}
+                            <tr class="action-row">
+                                <td><input type="checkbox" name="rowid" class="action-checkbox" value="{{ user.id }}" title="Select User"/></td>
+                                <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 user.permissions['banned'] %}
+                                        <form class="inline-form" id="unban-{{user.id}}" method="post" action="{{ url_for('management.unban_user', user_id = user.id) }}">
+                                            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+
+                                            <button class="btn btn-link">
+                                                <span class="fa fa-flag text-success" data-toggle="tooltip" data-placement="top" title="{% trans %}Unban{% endtrans %}"></span>
+                                            </button>
+                                        </form>
+                                    {% endif %}
+                                </td>
+                            </tr>
+                        {% else %}
+                            <tr>
+                                <td colspan="6">
+                                    {% trans %}No users found matching your search criteria.{% endtrans %}
+                                </td>
+                            </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+
+                <div class="panel-footer">
+                    <div class="row">
+                        <div class="col-md-12">
+                            {{ render_pagination(users, url_for('management.users')) }}
+                        </div>
+                    </div>
+                </div>
             </div>
             </div>
-        </form>
+        </div>
     </div>
     </div>
-
-    <table class="table table-bordered">
-        <thead>
-            <tr>
-                <th>#</th>
-                <th>{% trans %}Username{% endtrans %}</th>
-                <th>{% trans %}Posts{% endtrans %}</th>
-                <th>{% trans %}Date registered{% endtrans %}</th>
-                <th>{% trans %}Group{% endtrans %}</th>
-                <th>{% trans %}Manage{% endtrans %}</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 user.permissions['banned'] %}
-                        <form class="inline-form" method="post" action="{{ url_for('management.unban_user', user_id = user.id) }}">
-                            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                            <button class="btn btn-link">{% trans %}Unban{% endtrans %}</button>
-                        </form>
-                        {% endif %}
-                    </td>
-                </tr>
-            {% else %}
-                <tr>
-                    <td colspan="6">
-                        {% trans %}No users found matching your search criteria.{% endtrans %}
-                    </td>
-                </tr>
-            {% endfor %}
-        </tbody>
-    </table>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
+
+{% block scripts %}
+    <script>
+    var bulk_actions = new BulkActions();
+
+    $(function () {
+        $('[data-toggle="tooltip"]').tooltip()
+    })
+    </script>
+{% endblock %}

+ 19 - 9
flaskbb/templates/management/category_form.html

@@ -3,7 +3,7 @@
 
 
 {% extends theme("management/management_layout.html") %}
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 {% block management_content %}
-{% from theme("macros.html") import horizontal_field, render_boolean_field, navlink with context %}
+{% from theme("macros.html") import render_field, render_submit_field, navlink with context %}
 
 
 <div class="col-md-3">
 <div class="col-md-3">
     <ul class="nav nav-pills nav-stacked">
     <ul class="nav nav-pills nav-stacked">
@@ -14,15 +14,25 @@
 </div>
 </div>
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-    <form class="form-horizontal" role="form" method="post">
-        {{ form.hidden_tag() }}
-        <legend class="">{{ title }}</legend>
-            {{ horizontal_field(form.title) }}
-            {{ horizontal_field(form.description, rows=5, div_class="col-lg-9") }}
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-comments-o"></span> {{ title }}
+                </div>
+                <div class="panel-body">
+                    <form class="form-horizontal" role="form" method="post">
+                        {{ form.hidden_tag() }}
+                        {{ render_field(form.title) }}
+                        {{ render_field(form.description, rows=5, div_class="col-md-12") }}
 
 
-            {{ horizontal_field(form.position) }}
-            {{ horizontal_field(form.submit, div_class="col-lg-offset-0 col-lg-9") }}
-    </form>
+                        {{ render_field(form.position) }}
+                        {{ render_submit_field(form.submit) }}
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
 </div>
 </div>
 
 
 {% endblock %}
 {% endblock %}

+ 30 - 25
flaskbb/templates/management/forum_form.html

@@ -3,7 +3,7 @@
 
 
 {% extends theme("management/management_layout.html") %}
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 {% block management_content %}
-{% from theme("macros.html") import horizontal_field, render_boolean_field, navlink  with context %}
+{% from theme("macros.html") import render_field, render_submit_field, render_boolean_field, navlink with context %}
 
 
 <div class="col-md-3">
 <div class="col-md-3">
     <ul class="nav nav-pills nav-stacked">
     <ul class="nav nav-pills nav-stacked">
@@ -13,33 +13,38 @@
     </ul>
     </ul>
 </div>
 </div>
 
 
+<div class="col-md-9">
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-comment-o"></span> {{ title }}
+                </div>
+                <div class="panel-body">
+                    <form class="form-horizontal" method="post">
+                        {{ form.hidden_tag() }}
+                        {{ render_field(form.title) }}
+                        {{ render_field(form.description, rows=5, div_class="col-md-12") }}
+                        {{ render_field(form.category) }}
+                        {{ render_field(form.position) }}
+                        {{ render_field(form.external) }}
+                        {{ render_field(form.moderators) }}
+                        {{ render_boolean_field(form.show_moderators) }}
+                        {{ render_boolean_field(form.locked) }}
 
 
-<div class="col-md-9" role="tabpanel">
-    <form class="form-horizontal" role="form" method="post">
-        {{ form.hidden_tag() }}
-        <legend class="">{{ title }}</legend>
-        {{ form.id }}
-        {{ horizontal_field(form.title) }}
-        {{ horizontal_field(form.description, rows=5, div_class="col-lg-9") }}
+			<div id="perms">
+			    <h4>{{ _("Group Access to the forum") }}</h4>
+			    {{ form.groups }}
+			</div>
 
 
-        {{ horizontal_field(form.category) }}
-        {{ horizontal_field(form.position) }}
-
-        {{ horizontal_field(form.external) }}
-
-        {{ horizontal_field(form.moderators) }}
-        {{ render_boolean_field(form.show_moderators) }}
-
-        {{ render_boolean_field(form.locked) }}
-
-
-        <div id="perms">
-            <h4>{{ _("Group Access to the forum") }}</h4>
-            {{ form.groups }}
+                        <div class="row">
+                            {{ render_submit_field(form.submit, div_class="col-lg-offset-0 col-lg-9") }}
+                        </div>
+                    </form>
+                </div>
+            </div>
         </div>
         </div>
-
-        {{ horizontal_field(form.submit, div_class="col-lg-offset-0 col-lg-9") }}
-    </form>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
 
 

+ 57 - 48
flaskbb/templates/management/forums.html

@@ -14,59 +14,68 @@
 
 
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-    <legend>{% trans %}Manage Forums{% endtrans %}</legend>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-comments"></span> {% trans %}Manage Forums{% endtrans %}
+                </div>
+                <div class="panel-body">
+                    {% for category in categories %}
+                    <table class="table table-bordered">
+                        <thead class="categoryhead">
+                            <tr>
+                                <td colspan="2">
+                                    <div><strong><a href="{{ url_for('forum.view_category', category_id=category.id) }}">{{ category.title }}</a></strong></div>
+                                </td>
+                                <td valign="top" align="center" style="white-space: nowrap">
+                                    <a href="{{ url_for('management.add_forum', category_id=category.id) }}">{% trans %}Add Forum{% endtrans %}</a> |
+                                    <a href="{{ url_for('management.edit_category', category_id = category.id) }}">{% trans %}Edit{% endtrans %}</a> |
+                                    <form class="inline-form" method="post" action="{{ url_for('management.delete_category', category_id=category.id) }}">
+                                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                        <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
+                                    </form>
+                                </td>
+                            </tr>
+                        </thead>
+                        <tbody class="forumbody">
+                            <tr class="forum_stats">
+                                <td colspan="2"><strong>{% trans %}Forum{% endtrans %}</strong></td>
+                                <td width="85" align="center" style="white-space: nowrap"><strong>{% trans %}Management{% endtrans %}</strong></td>
+                            </tr>
 
 
-    {% for category in categories %}
-    <table class="table table-bordered">
-        <thead class="categoryhead">
-            <tr>
-                <td colspan="2">
-                    <div><strong><a href="{{ url_for('forum.view_category', category_id=category.id) }}">{{ category.title }}</a></strong></div>
-                </td>
-                <td valign="top" align="center" style="white-space: nowrap">
-                    <a href="{{ url_for('management.add_forum', category_id=category.id) }}">{% trans %}Add Forum{% endtrans %}</a> |
-                    <a href="{{ url_for('management.edit_category', category_id = category.id) }}">{% trans %}Edit{% endtrans %}</a> |
-                    <form class="inline-form" method="post" action="{{ url_for('management.delete_category', category_id=category.id) }}">
-                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                        <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
-                    </form>
-                </td>
-            </tr>
-        </thead>
-        <tbody class="forumbody">
-            <tr class="forum_stats">
-                <td colspan="2"><strong>{% trans %}Forum{% endtrans %}</strong></td>
-                <td width="85" align="center" style="white-space: nowrap"><strong>{% trans %}Management{% endtrans %}</strong></td>
-            </tr>
+                            {% for forum in category.forums %}
+                            <tr>
+                                <td align="center" valign="center" width="4%">
 
 
-            {% for forum in category.forums %}
-            <tr>
-                <td align="center" valign="center" width="4%">
+                                </td>
 
 
-                </td>
+                                <td valign="top">
+                                    <strong><a href="{{ url_for('forum.view_forum', forum_id=forum.id) }}">{{ forum.title }}</a></strong>
 
 
-                <td valign="top">
-                    <strong><a href="{{ url_for('forum.view_forum', forum_id=forum.id) }}">{{ forum.title }}</a></strong>
+                                    <div class="forum-description">
+                                        {% autoescape false %}
+                                        {{ forum.description|markup }}
+                                        {% endautoescape %}
+                                    </div>
+                                </td>
 
 
-                    <div class="forum-description">
-                        {% autoescape false %}
-                        {{ forum.description|markup }}
-                        {% endautoescape %}
-                    </div>
-                </td>
+                                <td valign="top" align="center" style="white-space: nowrap">
+                                    <a href="{{ url_for('management.edit_forum', forum_id = forum.id) }}">{% trans %}Edit{% endtrans %}</a> |
+                                    <form class="inline-form" method="post" action="{{ url_for('management.delete_forum', forum_id=forum.id) }}">
+                                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                        <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
+                                    </form>
+                                </td>
+                            </tr>
+                            {% endfor %}
 
 
-                <td valign="top" align="center" style="white-space: nowrap">
-                    <a href="{{ url_for('management.edit_forum', forum_id = forum.id) }}">{% trans %}Edit{% endtrans %}</a> |
-                    <form class="inline-form" method="post" action="{{ url_for('management.delete_forum', forum_id=forum.id) }}">
-                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                        <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
-                    </form>
-                </td>
-            </tr>
-            {% endfor %}
-
-        </tbody>
-    </table>
-    {% endfor %}
+                        </tbody>
+                    </table>
+                    {% endfor %}
+                </div>
+            </div>
+        </div>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 36 - 26
flaskbb/templates/management/group_form.html

@@ -3,7 +3,7 @@
 
 
 {% extends theme("management/management_layout.html") %}
 {% extends theme("management/management_layout.html") %}
 {% block management_content %}
 {% block management_content %}
-{% from theme("macros.html") import horizontal_field, render_boolean_field, navlink with context %}
+{% from theme("macros.html") import render_field, render_boolean_field, render_submit_field, navlink with context %}
 
 
 <div class="col-md-3">
 <div class="col-md-3">
     <ul class="nav nav-pills nav-stacked">
     <ul class="nav nav-pills nav-stacked">
@@ -13,30 +13,40 @@
 </div>
 </div>
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-        <form class="form-horizontal" role="form" method="post">
-            {{ form.hidden_tag() }}
-            <legend class="">{{ title }}</legend>
-                {{ horizontal_field(form.name) }}
-                {{ horizontal_field(form.description) }}
-
-                {{ render_boolean_field(form.admin) }}
-                {{ render_boolean_field(form.super_mod) }}
-
-
-                {{ render_boolean_field(form.mod) }}
-                {{ 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) }}
-                {{ render_boolean_field(form.posttopic) }}
-                {{ render_boolean_field(form.postreply) }}
-
-                {{ horizontal_field(form.submit, div_class="col-lg-offset-0 col-lg-9") }}
-        </form>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-user-plus"></span> {{ title }}
+                </div>
+                <div class="panel-body">
+                    <form class="form-horizontal" role="form" method="post">
+                        {{ form.hidden_tag() }}
+                        {{ render_field(form.name) }}
+                        {{ render_field(form.description, rows="4", div_class="col-md-12") }}
+
+                        {{ render_boolean_field(form.admin) }}
+                        {{ render_boolean_field(form.super_mod) }}
+
+                        {{ render_boolean_field(form.mod) }}
+                        {{ 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) }}
+                        {{ render_boolean_field(form.posttopic) }}
+                        {{ render_boolean_field(form.postreply) }}
+                        <div class="row">
+                            {{ render_submit_field(form.submit, div_class="col-lg-offset-0 col-lg-9") }}
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 73 - 30
flaskbb/templates/management/groups.html

@@ -12,37 +12,80 @@
 </div>
 </div>
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-    <legend>{% trans %}Manage Groups{% endtrans %}</legend>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-users"></span> {% trans %}Groups{% endtrans %}
+                </div>
+                <div class="panel-body management-body"></div>
 
 
-    <div class="pull-left" style="padding-bottom: 10px">
-        {{ render_pagination(groups, url_for('management.groups')) }}
-    </div>
+                    <table class="table table-hover">
+                        <thead>
+                            <tr>
+                                <th><input type="checkbox" name="rowtoggle" class="action-checkall" title="Select All"/></th>
+                                <th>#</th>
+                                <th>{% trans %}Group Name{% endtrans %}</th>
+                                <th>{% trans %}Description{% endtrans %}</th>
+                                <th>
+                                    <div class="btn-group">
+                                        <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
+                                            <span class="fa fa-cog" style="margin-right: 0px;"></span> {% trans %}Actions{% endtrans %}
+                                        </button>
+                                        <ul class="dropdown-menu slidedown">
+                                            <li>
+                                                <a href="javascript:void(0)" onclick="return bulk_actions.execute('{{ url_for('management.delete_group') }}', '{% trans %}Are you sure you want to delete these Groups?{% endtrans %}')">
+                                                    <span class="fa fa-trash text-danger"></span> {% trans %}Delete selected Groups{% endtrans %}
+                                                </a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                        {% for group in groups.items %}
+                            <tr class="action-row">
+                                <td><input type="checkbox" name="rowid" class="action-checkbox" value="{{ group.id }}" title="Select Group"/></td>
+                                <td>{{ group.id }}</td>
+                                <td><a href="#">{{ group.name }}</a></td>
+                                <td>{{ group.description }}</td>
+                                <td>
+                                    <a href="{{ url_for('management.edit_group', group_id = group.id) }}">
+                                        <span class="fa fa-pencil text-primary" data-toggle="tooltip" data-placement="top" title="{% trans %}Edit{% endtrans %}"></span>
+                                    </a>
+                                    <form class="inline-form" id="delete-{{group.id}}" method="post" action="{{ url_for('management.delete_group', group_id=group.id) }}">
+                                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                        <button class="btn btn-link">
+                                            <span class="fa fa-trash text-danger" data-toggle="tooltip" data-placement="top" title="{% trans %}Delete{% endtrans %}"></span>
+                                        </button>
+                                    </form>
+                                </td>
+                            </tr>
+                        {% endfor %}
+                        </tbody>
+                    </table>
 
 
-    <table class="table table-bordered">
-        <thead>
-            <tr>
-                <th>#</th>
-                <th>{% trans %}Group Name{% endtrans %}</th>
-                <th>{% trans %}Description{% endtrans %}</th>
-                <th>{% trans %}Manage{% endtrans %}</th>
-            </tr>
-        </thead>
-        <tbody>
-            {% for group in groups.items %}
-            <tr>
-                <td>{{ group.id }}</td>
-                <td><a href="#">{{ group.name }}</a></td>
-                <td>{{ group.description }}</td>
-                <td>
-                    <a href="{{ url_for('management.edit_group', group_id = group.id) }}">{% trans %}Edit{% endtrans %}</a> |
-                    <form class="inline-form" method="post" action="{{ url_for('management.delete_group', group_id=group.id) }}">
-                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                        <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
-                    </form>
-                </td>
-            </tr>
-            {% endfor %}
-        </tbody>
-    </table>
+                    <div class="panel-footer">
+                        <div class="row">
+                            <div class="col-md-12">
+                                {{ render_pagination(groups, url_for('management.groups')) }}
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
+
+{% block scripts %}
+    <script>
+    var bulk_actions = new BulkActions();
+
+    $(function () {
+        $('[data-toggle="tooltip"]').tooltip()
+    })
+    </script>
+{% endblock %}

+ 34 - 27
flaskbb/templates/management/overview.html

@@ -4,32 +4,39 @@
 {% block management_content %}
 {% block management_content %}
 
 
 <div class="col-md-12">
 <div class="col-md-12">
-    <table class="table table-bordered">
-        <thead>
-            <tr>
-                <th colspan="4">{% trans %}Global Statistics{% endtrans %}</th>
-            </tr>
-        </thead>
-        <tbody>
-            <tr>
-                <td><b>{% trans %}FlaskBB Version{% endtrans %}</b></td>
-                <td>{{ flaskbb_version }}</td>
-                <td><b>{% trans %}Posts{% endtrans %}</b></td>
-                <td>{{ post_count }}</td>
-            </tr>
-            <tr>
-                <td><b>{% trans %}Python Version{% endtrans %}</b></td>
-                <td>{{ python_version }}</td>
-                <td><b>{% trans %}Topics{% endtrans %}</b></td>
-                <td>{{ topic_count }}</td>
-            </tr>
-            <tr>
-                <td><b>{% trans %}Flask Version{% endtrans %}</b></td>
-                <td>{{ flask_version }}</td>
-                <td><b>{% trans %}Users{% endtrans %}</b></td>
-                <td>{{ user_count }}</td>
-            </tr>
-        </tbody>
-    </table>
+    <div class="panel panel-primary">
+        <div class="panel-heading">
+            <span class="fa fa-tasks"></span> {% trans %}Overview{% endtrans %}
+        </div>
+        <div class="panel-body">
+            <table class="table">
+                <thead>
+                    <tr>
+                        <th colspan="4">{% trans %}Global Statistics{% endtrans %}</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr>
+                        <td><b>{% trans %}FlaskBB Version{% endtrans %}</b></td>
+                        <td>{{ flaskbb_version }}</td>
+                        <td><b>{% trans %}Posts{% endtrans %}</b></td>
+                        <td>{{ post_count }}</td>
+                    </tr>
+                    <tr>
+                        <td><b>{% trans %}Python Version{% endtrans %}</b></td>
+                        <td>{{ python_version }}</td>
+                        <td><b>{% trans %}Topics{% endtrans %}</b></td>
+                        <td>{{ topic_count }}</td>
+                    </tr>
+                    <tr>
+                        <td><b>{% trans %}Flask Version{% endtrans %}</b></td>
+                        <td>{{ flask_version }}</td>
+                        <td><b>{% trans %}Users{% endtrans %}</b></td>
+                        <td>{{ user_count }}</td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 63 - 56
flaskbb/templates/management/plugins.html

@@ -4,61 +4,68 @@
 {% block management_content %}
 {% block management_content %}
 {% from theme('macros.html') import render_pagination %}
 {% from theme('macros.html') import render_pagination %}
 
 
-<legend>{% trans %}Manage Plugins{% endtrans %}</legend>
+<div class="col-md-12">
+    <div class="panel panel-primary">
+        <div class="panel-heading">
+            <span class="fa fa-puzzle-piece"></span> {% trans %}Manage Plugins{% endtrans %}
+        </div>
+        <div class="panel-body">
+            <table class="table">
+                <thead>
+                    <tr>
+                        <th>{% trans %}Plugin{% endtrans %}</th>
+                        <th>{% trans %}Information{% endtrans %}</th>
+                        <th>{% trans %}Manage{% endtrans %}</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for plugin in plugins %}
+                    <tr>
+                      <td>
+                        {% if plugin.website %}
+                          <a href="{{ plugin.website }}">{{ plugin.name }}</a>
+                        {% else %}
+                          {{ plugin.name }}
+                        {% endif %}
+                        </td>
+                        <td>
+                            {% trans %}Version{% endtrans %}: <i>{{ plugin.version }}</i> <br />
+                            {{ plugin.description }} <br />
+                            {% trans %}by{% endtrans %}  <i>{{ plugin.author }}</i>
+                        </td>
+                        <td>
+                            {% if not plugin.enabled %}
+                            <form class="inline-form" method="post" action="{{ url_for('management.enable_plugin', plugin=plugin.identifier) }}">
+                                <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                <button class="btn btn-link">{% trans %}Enable{% endtrans %}</button>
+                            </form>
+                            {% else %}
+                            <form class="inline-form" method="post" action="{{ url_for('management.disable_plugin', plugin=plugin.identifier) }}">
+                                <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                <button class="btn btn-link">{% trans %}Disable{% endtrans %}</button>
+                            </form>
+                            {% endif %}
 
 
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>{% trans %}Plugin{% endtrans %}</th>
-            <th>{% trans %}Information{% endtrans %}</th>
-            <th>{% trans %}Manage{% endtrans %}</th>
-        </tr>
-    </thead>
-    <tbody>
-        {% for plugin in plugins %}
-        <tr>
-          <td>
-            {% if plugin.website %}
-              <a href="{{ plugin.website }}">{{ plugin.name }}</a>
-            {% else %}
-              {{ plugin.name }}
-            {% endif %}
-            </td>
-            <td>
-                {% trans %}Version{% endtrans %}: <i>{{ plugin.version }}</i> <br />
-                {{ plugin.description }} <br />
-                {% trans %}by{% endtrans %}  <i>{{ plugin.author }}</i>
-            </td>
-            <td>
-                {% if not plugin.enabled %}
-                <form class="inline-form" method="post" action="{{ url_for('management.enable_plugin', plugin=plugin.identifier) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Enable{% endtrans %}</button>
-                </form>
-                {% else %}
-                <form class="inline-form" method="post" action="{{ url_for('management.disable_plugin', plugin=plugin.identifier) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Disable{% endtrans %}</button>
-                </form>
-                {% endif %}
-
-                {% set uninstallable = plugin.uninstallable %}
-                {% if plugin.installable and not uninstallable %}
-                <br />
-                <form class="inline-form" method="post" action="{{ url_for('management.install_plugin', plugin=plugin.identifier) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Install{% endtrans %}</button>
-                </form>
-                {% endif %}
-                {% if uninstallable %}
-                <form class="inline-form" method="post" action="{{ url_for('management.uninstall_plugin', plugin=plugin.identifier) }}">
-                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                    <button class="btn btn-link">{% trans %}Uninstall{% endtrans %}</button>
-                </form>
-                {% endif %}
-            </td>
-        </tr>
-        {% endfor %}
-    </tbody>
-</table>
+                            {% set uninstallable = plugin.uninstallable %}
+                            {% if plugin.installable and not uninstallable %}
+                            <br />
+                            <form class="inline-form" method="post" action="{{ url_for('management.install_plugin', plugin=plugin.identifier) }}">
+                                <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                <button class="btn btn-link">{% trans %}Install{% endtrans %}</button>
+                            </form>
+                            {% endif %}
+                            {% if uninstallable %}
+                            <form class="inline-form" method="post" action="{{ url_for('management.uninstall_plugin', plugin=plugin.identifier) }}">
+                                <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                <button class="btn btn-link">{% trans %}Uninstall{% endtrans %}</button>
+                            </form>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
 {% endblock %}
 {% endblock %}

+ 46 - 32
flaskbb/templates/management/reports.html

@@ -13,39 +13,53 @@
 </div>
 </div>
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-    <legend>{% trans %}All Reports{% endtrans %}</legend>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-flag"></span> {% trans %}All Reports{% endtrans %}
+                </div>
+                <div class="panel-body management-body"></div>
 
 
-    <div class="pull-left" style="padding-bottom: 10px">
-        {{ render_pagination(reports, url_for('management.reports')) }}
-    </div>
+                <table class="table table-hover">
+                    <thead>
+                        <tr>
+                            <th>#</th>
+                            <th>{% trans %}Poster{% endtrans %}</th>
+                            <th>{% trans %}Topic{% endtrans %}</th>
+                            <th>{% trans %}Reporter{% endtrans %}</th>
+                            <th>{% trans %}Reason{% endtrans %}</th>
+                            <th>{% trans %}Reported{% endtrans %}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {% for report in reports.items %}
+                        <tr>
+                            <td>{{ report.id }}</td>
+                            <td>{{ report.post.user.username }}</td>
+                            <td><a href="{{ url_for('forum.view_post', post_id=report.post.id) }}" target="_blank">{{ report.post.topic.title }}</a></td>
+                            <td>{{ report.reporter.username }}</td>
+                            <td>{{ report.reason }}</td>
+                            <td>{{ report.reported|time_since }}</td>
+                        </tr>
+                        {% else %}
+                        <tr>
+                            <td colspan="6">{% trans %}No reports.{% endtrans %}</td>
+                        </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+
+                <div class="panel-footer">
+                    <div class="row">
+                        <div class="col-md-12">
+                            {{ render_pagination(reports, url_for('management.reports')) }}
+                        </div>
+                    </div>
+                </div>
 
 
-    <table class="table table-bordered">
-        <thead>
-            <tr>
-                <th>#</th>
-                <th>{% trans %}Poster{% endtrans %}</th>
-                <th>{% trans %}Topic{% endtrans %}</th>
-                <th>{% trans %}Reporter{% endtrans %}</th>
-                <th>{% trans %}Reason{% endtrans %}</th>
-                <th>{% trans %}Reported{% endtrans %}</th>
-            </tr>
-        </thead>
-        <tbody>
-            {% for report in reports.items %}
-            <tr>
-                <td><a href="{{ url_for('forum.view_post', post_id=report.post.id) }}" target="_blank">{{ report.id }}</a></td>
-                <td>{{ report.post.user.username }}</td>
-                <td>{{ report.post.topic.title }}</td>
-                <td>{{ report.reporter.username }}</td>
-                <td>{{ report.reason }}</td>
-                <td>{{ report.reported|time_since }}</td>
-            </tr>
-            {% else %}
-            <tr>
-                <td colspan="6">{% trans %}No reports.{% endtrans %}</td>
-            </tr>
-            {% endfor %}
-        </tbody>
-    </table>
+            </div>
+        </div>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 29 - 22
flaskbb/templates/management/settings.html

@@ -17,27 +17,34 @@
 </div><!--/.col-md-3 -->
 </div><!--/.col-md-3 -->
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-<legend>{{ active_group.name }}</legend>
-
-<form class="form-horizontal" role="form" method="post">
-
-    {{ form.hidden_tag() }}
-    {% for field in form %}
-        {% if field.type not in ["TextField", "IntegerField"] %}
-            {% if field.type == "BooleanField" %}
-                {{ render_boolean_field(field) }}
-            {% endif %}
-
-            {% if field.type in ["SelectField", "SelectMultipleField"] %}
-                {{ render_select_field(field) }}
-            {% endif %}
-        {% else %}
-            {{ render_field(field) }}
-        {% endif %}
-    {%  endfor %}
-
-    <button type="submit" class="btn btn-success">{% trans %}Save{% endtrans %}</button>
-
-</form>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-cogs"></span> {{ active_group.name }}
+                </div>
+                <div class="panel-body">
+                    <form class="form-horizontal" role="form" method="post">
+                        {{ form.hidden_tag() }}
+                        {% for field in form %}
+                            {% if field.type not in ["TextField", "IntegerField"] %}
+                                {% if field.type == "BooleanField" %}
+                                    {{ render_boolean_field(field) }}
+                                {% endif %}
+
+                                {% if field.type in ["SelectField", "SelectMultipleField"] %}
+                                    {{ render_select_field(field) }}
+                                {% endif %}
+                            {% else %}
+                                {{ render_field(field) }}
+                            {% endif %}
+                        {%  endfor %}
+
+                        <button type="submit" class="btn btn-success">{% trans %}Save{% endtrans %}</button>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 80 - 44
flaskbb/templates/management/unread_reports.html

@@ -13,51 +13,87 @@
 </div>
 </div>
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-    <legend>{% trans %}Unread Reports{% endtrans %}</legend>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-flag"></span> {% trans %}Unread Reports{% endtrans %}
+                </div>
+                <div class="panel-body management-body"></div>
 
 
-    <div class="pull-left" style="padding-bottom: 10px">
-        {{ render_pagination(reports, url_for('management.unread_reports')) }}
-    </div>
+                <table class="table table-hover">
+                    <thead>
+                        <tr>
+                            <th><input type="checkbox" name="rowtoggle" class="action-checkall" title="Select All"/></th>
+                            <th>#</th>
+                            <th>{% trans %}Poster{% endtrans %}</th>
+                            <th>{% trans %}Topic{% endtrans %}</th>
+                            <th>{% trans %}Reporter{% endtrans %}</th>
+                            <th>{% trans %}Reason{% endtrans %}</th>
+                            <th>{% trans %}Reported{% endtrans %}</th>
+                            <th>
+                                <div class="btn-group">
+                                    <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
+                                        <span class="fa fa-cog" style="margin-right: 0px;"></span> {% trans %}Actions{% endtrans %}
+                                    </button>
+                                    <ul class="dropdown-menu slidedown">
+                                        <li>
+                                            <a href="javascript:void(0)" onclick="return bulk_actions.execute('{{ url_for('management.report_markread') }}', '{% trans %}Are you sure you want to mark these Reports as read?{% endtrans %}')">
+                                                <span class="fa fa-flag-o text-primary"></span> {% trans %}Mark as Read{% endtrans %}
+                                            </a>
+                                        </li>
+                                    </ul>
+                                </div>
+                            </th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {% for report in reports.items %}
+                        <tr class="action-row">
+                            <td><input type="checkbox" name="rowid" class="action-checkbox" value="{{ report.id }}" title="Select Report"/></td>
+                            <td>{{ report.id }}</td>
+                            <td>{{ report.post.user.username }}</td>
+                            <td><a href="{{ url_for('forum.view_post', post_id=report.post.id) }}" target="_blank">{{ report.post.topic.title }}</a></td>
+                            <td>{{ report.reporter.username }}</td>
+                            <td>{{ report.reason }}</td>
+                            <td>{{ report.reported|time_since }}</td>
+                            <td>
+                                <form class="inline-form" id="read-{{report.id}}" method="post" action="{{ url_for('management.report_markread', report_id=report.id) }}">
+                                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                    <button class="btn btn-link">
+                                        <span class="fa fa-flag-o text-primary" data-toggle="tooltip" data-placement="top" title="{% trans %}Mark as Read{% endtrans %}"></span>
+                                    </button>
+                                </form>
+                            </td>
+                        </tr>
+                        {% else %}
+                        <tr>
+                            <td colspan="7">{% trans %}No unread reports.{% endtrans %}</td>
+                        </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+
+                <div class="panel-footer">
+                    <div class="row">
+                        <div class="col-md-12">
+                            {{ render_pagination(reports, url_for('management.unread_reports')) }}
+                        </div>
+                    </div>
+                </div>
 
 
-    <table class="table table-bordered">
-        <thead>
-            <tr>
-                <th>#</th>
-                <th>{% trans %}Poster{% endtrans %}</th>
-                <th>{% trans %}Topic{% endtrans %}</th>
-                <th>{% trans %}Reporter{% endtrans %}</th>
-                <th>{% trans %}Reason{% endtrans %}</th>
-                <th>{% trans %}Reported{% endtrans %}</th>
-                <th>
-                    <form class="inline-form" method="post" action="{{ url_for('management.report_markread') }}">
-                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                        <button class="btn btn-link">{% trans %}Mark all as Read{% endtrans %}</button>
-                    </form>
-                </th>
-            </tr>
-        </thead>
-        <tbody>
-            {% for report in reports.items %}
-            <tr>
-                <td><a href="{{ url_for('forum.view_post', post_id=report.post.id) }}" target="_blank">{{ report.id }}</a></td>
-                <td>{{ report.post.user.username }}</td>
-                <td>{{ report.post.topic.title }}</td>
-                <td>{{ report.reporter.username }}</td>
-                <td>{{ report.reason }}</td>
-                <td>{{ report.reported|time_since }}</td>
-                <td>
-                    <form class="inline-form" method="post" action="{{ url_for('management.report_markread', report_id=report.id) }}">
-                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                        <button class="btn btn-link">{% trans %}Mark as Read{% endtrans %}</button>
-                    </form>
-                </td>
-            </tr>
-            {% else %}
-            <tr>
-                <td colspan="7">{% trans %}No unread reports.{% endtrans %}</td>
-            </tr>
-            {% endfor %}
-        </tbody>
-    </table>
+            </div>
+        </div>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
+
+{% block scripts %}
+    <script>
+    var bulk_actions = new BulkActions();
+
+    $(function () {
+        $('[data-toggle="tooltip"]').tooltip()
+    })
+    </script>
+{% endblock %}

+ 27 - 17
flaskbb/templates/management/user_form.html

@@ -17,22 +17,32 @@
 </div><!--/.col-md-3 -->
 </div><!--/.col-md-3 -->
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-    <form class="form-horizontal" role="form" method="post">
-        {{ form.hidden_tag() }}
-        <legend class="">{{ title }}</legend>
-            {{ horizontal_field(form.username) }}
-            {{ horizontal_field(form.email) }}
-            {{ horizontal_field(form.password) }}
-            {{ horizontal_select_field(form.birthday, surrounded_div="col-sm-4") }}
-            {{ horizontal_field(form.gender) }}
-            {{ horizontal_field(form.location) }}
-            {{ horizontal_field(form.website) }}
-            {{ horizontal_field(form.avatar) }}
-            {{ horizontal_field(form.primary_group) }}
-            {{ horizontal_field(form.secondary_groups) }}
-            {{ horizontal_field(form.signature, rows=5, div_class="col-sm-9") }}
-            {{ horizontal_field(form.notes, rows=12, div_class="col-sm-9") }}
-            {{ horizontal_field(form.submit) }}
-    </form>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+                    <span class="fa fa-user-plus"></span> {{ title }}
+                </div>
+                <div class="panel-body">
+                    <form class="form-horizontal" role="form" method="post">
+                        {{ form.hidden_tag() }}
+                        {{ horizontal_field(form.username) }}
+                        {{ horizontal_field(form.email) }}
+                        {{ horizontal_field(form.password) }}
+                        {{ horizontal_select_field(form.birthday, surrounded_div="col-sm-4") }}
+                        {{ horizontal_field(form.gender) }}
+                        {{ horizontal_field(form.location) }}
+                        {{ horizontal_field(form.website) }}
+                        {{ horizontal_field(form.avatar) }}
+                        {{ horizontal_field(form.primary_group) }}
+                        {{ horizontal_field(form.secondary_groups) }}
+                        {{ horizontal_field(form.signature, rows=5, div_class="col-sm-9") }}
+                        {{ horizontal_field(form.notes, rows=12, div_class="col-sm-9") }}
+                        {{ horizontal_field(form.submit) }}
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 140 - 70
flaskbb/templates/management/users.html

@@ -17,77 +17,147 @@
 </div><!--/.col-md-3 -->
 </div><!--/.col-md-3 -->
 
 
 <div class="col-md-9">
 <div class="col-md-9">
-    <legend>{% trans %}Manage Users{% endtrans %}</legend>
-
-    <div class="pull-left" style="padding-bottom: 10px">
-        {{ render_pagination(users, url_for('management.users')) }}
-    </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="submit">{% trans %}Search{% endtrans %}</button>
-                </span>
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-primary">
+                <div class="panel-heading">
+
+                    <span class="fa fa-user"></span> {% trans %}Users{% endtrans %}
+
+                    <div class="pull-right action-buttons">
+                        <div class="btn-group pull-right">
+                            <button type="button" class="btn btn-default btn-xs" onclick="return show_management_search()">
+                                <span class="fa fa-search" style="margin-right: 0px;"></span> {% trans %}Search{% endtrans %}
+                            </button>
+                        </div>
+                    </div>
+
+                </div>
+                <div class="panel-body management-body">
+                    <form class="search-form" role="form" method="post">
+                        {{ search_form.hidden_tag() }}
+                        <div class="input-group">
+                            {{ group_field(search_form.search_query, css_class="form-control") }}
+                            <span class="input-group-btn">
+                                <button class="btn btn-default" type="button"><span class="fa fa-search"></span></button>
+                            </span>
+                        </div>
+                    </form>
+                </div>
+
+                    <table class="table table-hover">
+                        <thead>
+                            <tr>
+                                <th><input type="checkbox" name="rowtoggle" class="action-checkall" title="Select All"/></th>
+                                <th>#</th>
+                                <th>{% trans %}Username{% endtrans %}</th>
+                                <th>{% trans %}Posts{% endtrans %}</th>
+                                <th>{% trans %}Date registered{% endtrans %}</th>
+                                <th>{% trans %}Group{% endtrans %}</th>
+                                <th>
+                                    <div class="btn-group">
+                                        <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
+                                            <span class="fa fa-cog" style="margin-right: 0px;"></span> {% trans %}Actions{% endtrans %}
+                                        </button>
+                                        <ul class="dropdown-menu slidedown">
+                                            <li>
+                                                <a href="javascript:void(0)" onclick="return bulk_actions.execute('{{ url_for('management.ban_user') }}', '{% trans %}Are you sure you want to ban these Users?{% endtrans %}')">
+                                                    <span class="fa fa-flag text-warning"></span> {% trans %}Ban selected Users{% endtrans %}
+                                                </a>
+                                            </li>
+
+                                            <li>
+                                                <a href="javascript:void(0)" onclick="return bulk_actions.execute('{{ url_for('management.unban_user') }}', '{% trans %}Are you sure you want to unban these Users?{% endtrans %}')">
+                                                    <span class="fa fa-flag text-success"></span> {% trans %}Unban selected Users{% endtrans %}
+                                                </a>
+                                            </li>
+
+                                            <li>
+                                                <a href="javascript:void(0)" onclick="return bulk_actions.execute('{{ url_for('management.delete_user') }}', '{% trans %}Are you sure you want to delete these Users?{% endtrans %}')">
+                                                    <span class="fa fa-trash text-danger"></span> {% trans %}Delete selected Users{% endtrans %}
+                                                </a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                        {% for user in users.items %}
+                            <tr class="action-row">
+                                <td><input type="checkbox" name="rowid" class="action-checkbox" value="{{ user.id }}" title="Select User"/></td>
+                                <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_edit_user and not user|is_admin or current_user|is_admin %}
+                                        <a href="{{ url_for('management.edit_user', user_id = user.id) }}">
+                                            <span class="fa fa-pencil text-primary" data-toggle="tooltip" data-placement="top" title="{% trans %}Edit{% endtrans %}"></span>
+                                        </a>
+                                    {% endif %}
+
+                                    {% if current_user|can_ban_user and not user.permissions['banned'] %}
+                                        <form class="inline-form" id="ban-{{user.id}}" method="post" action="{{ url_for('management.ban_user', user_id = user.id) }}">
+                                            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+
+                                            <button class="btn btn-link">
+                                                <span class="fa fa-flag text-warning" data-toggle="tooltip" data-placement="top" title="{% trans %}Ban{% endtrans %}"></span>
+                                            </button>
+                                        </form>
+                                    {% endif %}
+
+                                    {% if current_user|can_ban_user and user.permissions['banned'] %}
+                                        <form class="inline-form" id="unban-{{user.id}}" method="post" action="{{ url_for('management.unban_user', user_id = user.id) }}">
+                                            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+
+                                            <button class="btn btn-link">
+                                                <span class="fa fa-flag text-success" data-toggle="tooltip" data-placement="top" title="{% trans %}Unban{% endtrans %}"></span>
+                                            </button>
+                                        </form>
+                                    {% endif %}
+
+                                    {% if current_user|is_admin %}
+                                    <form class="inline-form" id="delete-{{user.id}}" method="post" action="{{ url_for('management.delete_user', user_id = user.id) }}">
+                                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+
+                                        <button class="btn btn-link">
+                                            <span class="fa fa-trash text-danger" data-toggle="tooltip" data-placement="top" title="{% trans %}Delete{% endtrans %}"></span>
+                                        </button>
+                                    </form>
+                                    {% endif %}
+                                </td>
+                            </tr>
+                        {% else %}
+                            <tr>
+                                <td colspan="6">
+                                    {% trans %}No users found matching your search criteria.{% endtrans %}
+                                </td>
+                            </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+
+                <div class="panel-footer">
+                    <div class="row">
+                        <div class="col-md-12">
+                            {{ render_pagination(users, url_for('management.users')) }}
+                        </div>
+                    </div>
+                </div>
             </div>
             </div>
-        </form>
+        </div>
     </div>
     </div>
-
-    <table class="table table-bordered">
-        <thead>
-            <tr>
-                <th>#</th>
-                <th>{% trans %}Username{% endtrans %}</th>
-                <th>{% trans %}Posts{% endtrans %}</th>
-                <th>{% trans %}Date registered{% endtrans %}</th>
-                <th>{% trans %}Group{% endtrans %}</th>
-                <th>{% trans %}Manage{% endtrans %}</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_edit_user and not user|is_admin or current_user|is_admin %}
-                            <a href="{{ url_for('management.edit_user', user_id = user.id) }}">{% trans %}Edit{% endtrans %}</a> |
-                        {% endif %}
-
-                        {% if current_user|can_ban_user and not user.permissions['banned'] %}
-                            <form class="inline-form" method="post" action="{{ url_for('management.ban_user', user_id = user.id) }}">
-                                <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                                <button class="btn btn-link">{% trans %}Ban{% endtrans %}</button> |
-                            </form>
-                        {% endif %}
-
-                        {% if current_user|can_ban_user and user.permissions['banned'] %}
-                            <form class="inline-form" method="post" action="{{ url_for('management.unban_user', user_id = user.id) }}">
-                                <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                                <button class="btn btn-link">{% trans %}Unban{% endtrans %}</button> |
-                            </form>
-                        {% endif %}
-
-                        {% if current_user|is_admin %}
-                        <form class="inline-form" method="post" action="{{ url_for('management.delete_user', user_id = user.id) }}">
-                            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                            <button class="btn btn-link">{% trans %}Delete{% endtrans %}</button>
-                        </form>
-                        {% endif %}
-                    </td>
-                </tr>
-            {% else %}
-                <tr>
-                    <td colspan="6">
-                        {% trans %}No users found matching your search criteria.{% endtrans %}
-                    </td>
-                </tr>
-            {% endfor %}
-        </tbody>
-    </table>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
+
+{% block scripts %}
+    <script>
+    var bulk_actions = new BulkActions();
+
+    $(function () {
+        $('[data-toggle="tooltip"]').tooltip()
+    })
+    </script>
+{% endblock %}

+ 0 - 1
flaskbb/templates/message/conversation.html

@@ -81,7 +81,6 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block scripts %}
 {% block scripts %}
-    <script type="text/javascript" src="{{ url_for('static', filename='js/conversation.js') }}"></script>
     <script type="text/javascript" src="{{ url_for('static', filename='js/marked.js') }}"></script>
     <script type="text/javascript" src="{{ url_for('static', filename='js/marked.js') }}"></script>
     <script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-markdown.js') }}"></script>
     <script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-markdown.js') }}"></script>
 {% endblock %}
 {% endblock %}

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

@@ -17,13 +17,21 @@
 
 
         <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
         <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
 
 
-        {% block css %}
+        {% block stylesheets %}
         <link rel="stylesheet" href="{{ url_for('static', filename='css/code.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/code.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
         <link rel="stylesheet" href="{{ theme_static('css/bootstrap-theme.min.css') }}">
         <link rel="stylesheet" href="{{ theme_static('css/bootstrap-theme.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/flaskbb.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/flaskbb.css') }}">
         {% endblock %}
         {% endblock %}
+
+        {# for extra stylesheets. e.q. a template has to add something #}
+        {% block css %}
+        {% endblock %}
+
+        {# for various extra things #}
+        {% block head_extra %}
+        {% endblock %}
     </head>
     </head>
 
 
     <body>
     <body>

+ 98 - 81
flaskbb/themes/bootstrap3/templates/layout.html

@@ -11,121 +11,138 @@
             {%- endif -%}
             {%- endif -%}
         {% endblock %}
         {% endblock %}
         </title>
         </title>
+
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <meta name="description" content="FlaskBB is a forum software written in Flask">
         <meta name="description" content="FlaskBB is a forum software written in Flask">
         <meta name="author" content="FlaskBB Team">
         <meta name="author" content="FlaskBB Team">
+        <meta name="csrf-token" content="{{ csrf_token() }}">
 
 
         <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
         <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
 
 
-        {% block css %}
+        {% block stylesheets %}
+        <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
+        <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}" >
         <link rel="stylesheet" href="{{ url_for('static', filename='css/code.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/code.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/flaskbb.css') }}">
         <link rel="stylesheet" href="{{ url_for('static', filename='css/flaskbb.css') }}">
         {% endblock %}
         {% endblock %}
+
+        {# for extra stylesheets. e.q. a template has to add something #}
+        {% block css %}
+        {% endblock %}
+
+        {# for various extra things #}
+        {% block head_extra %}
+        {% endblock %}
     </head>
     </head>
 
 
     <body>
     <body>
         {% block navigation %}
         {% block navigation %}
         {%- from theme("macros.html") import topnav with context -%}
         {%- from theme("macros.html") import topnav with context -%}
         <!-- Navigation -->
         <!-- Navigation -->
-            <nav class="navbar navbar-default navbar-static-top">
-                <div class="container">
-                    <div class="navbar-header">
-                        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
-                            <span class="sr-only">Toggle navigation</span>
-                            <span class="icon-bar"></span>
-                            <span class="icon-bar"></span>
-                            <span class="icon-bar"></span>
+        <nav class="navbar navbar-default navbar-static-top">
+            <div class="container">
+                <div class="navbar-header">
+                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
+                        <span class="sr-only">Toggle navigation</span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </button>
+                    <a class="navbar-brand" href="/">FlaskBB</a>
+                </div>
+                <div class="collapse navbar-collapse navbar-ex1-collapse">
+                    <ul class="nav navbar-nav">
+                        {{ emit_event("before-first-navigation-element") }}
+
+                        {# active_forum_nav is set in {forum, category, topic}.html and new_{topic, post}.html #}
+                        {{ topnav(endpoint='forum.index', name=_('Forum'), icon='fa fa-comment', active=active_forum_nav) }}
+                        {{ topnav(endpoint='forum.memberlist', name=_('Memberlist'), icon='fa fa-user') }}
+                        {{ topnav(endpoint='forum.search', name=_('Search'), icon='fa fa-search') }}
+
+                        {{ emit_event("after-last-navigation-element") }}
+                    </ul>
+
+                {% if current_user and current_user.is_authenticated() %}
+                    <div class="btn-group navbar-btn navbar-right" style="padding-left: 15px; margin-right: -10px">
+                        <a class="btn btn-primary" href="{{ url_for('user.profile', username=current_user.username) }}">
+                            <span class="fa fa-user"></span> {{ current_user.username }}
+                        </a>
+                        <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
+                            <span class="caret"></span>
                         </button>
                         </button>
-                        <a class="navbar-brand" href="/">FlaskBB</a>
-                    </div>
-                    <div class="collapse navbar-collapse navbar-ex1-collapse">
-                        <ul class="nav navbar-nav">
-                            {{ emit_event("before-first-navigation-element") }}
+                        <ul class="dropdown-menu" role="menu">
+                            <li><a href="{{ url_for('forum.topictracker') }}"><span class="fa fa-book"></span> {% trans %}Topic Tracker{% endtrans %}</a></li>
+                            <li class="divider"></li>
 
 
-                            {# active_forum_nav is set in {forum, category, topic}.html and new_{topic, post}.html #}
-                            {{ topnav(endpoint='forum.index', name=_('Forum'), icon='fa fa-comment', active=active_forum_nav) }}
-                            {{ topnav(endpoint='forum.memberlist', name=_('Memberlist'), icon='fa fa-user') }}
-                            {{ topnav(endpoint='forum.search', name=_('Search'), icon='fa fa-search') }}
+                            <li><a href="{{ url_for('user.settings') }}"><span class="fa fa-cogs"></span> {% trans %}Settings{% endtrans %}</a></li>
+                            {% if current_user|is_admin_or_moderator %}
+                            <li><a href="{{ url_for('management.overview') }}"><span class="fa fa-cog"></span> {% trans %}Management{% endtrans %}</a></li>
+                            <li class="divider"></li>
+                            {% endif %}
 
 
-                            {{ emit_event("after-last-navigation-element") }}
+                            <li><a href="{{ url_for('auth.logout') }}"><span class="fa fa-power-off"></span> {% trans %}Logout{% endtrans %}</a></li>
                         </ul>
                         </ul>
+                    </div>
 
 
-                    {% if current_user and current_user.is_authenticated() %}
-                        <div class="btn-group navbar-btn navbar-right" style="padding-left: 15px; margin-right: -10px">
-                            <a class="btn btn-primary" href="{{ url_for('user.profile', username=current_user.username) }}">
-                                <span class="fa fa-user"></span> {{ current_user.username }}
-                            </a>
-                            <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
-                                <span class="caret"></span>
-                            </button>
-                            <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('forum.topictracker') }}"><span class="fa fa-book"></span> {% trans %}Topic Tracker{% endtrans %}</a></li>
-                                <li class="divider"></li>
-
-                                <li><a href="{{ url_for('user.settings') }}"><span class="fa fa-cogs"></span> {% trans %}Settings{% endtrans %}</a></li>
-                                {% if current_user|is_admin_or_moderator %}
-                                <li><a href="{{ url_for('management.overview') }}"><span class="fa fa-cog"></span> {% trans %}Management{% endtrans %}</a></li>
-                                <li class="divider"></li>
-                                {% endif %}
-
-                                <li><a href="{{ url_for('auth.logout') }}"><span class="fa fa-power-off"></span> {% trans %}Logout{% endtrans %}</a></li>
-                            </ul>
-                        </div>
-
-                        <div class="btn-group navbar-btn navbar-right">
-                            <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown">
-                                <span class="fa fa-envelope"></span> <span class="badge">{{ current_user.pm_unread }}</span>
-                            </button>
-                            <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Inbox{% endtrans %}</a></li>
-                                <li><a href="{{ url_for('message.new_conversation') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
-                            </ul>
-                        </div>
-                    {% else %}
-                        <div class="btn-group navbar-btn navbar-right">
-                            <a class="btn btn-primary" href="{{ url_for('auth.login') }}">
-                                <span class="fa fa-user"></span> {% trans %}Login{% endtrans %}
-                            </a>
-                            <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
-                                <span class="caret"></span>
-                            </button>
-                            <ul class="dropdown-menu" role="menu">
-                                <li><a href="{{ url_for('auth.register') }}">{% trans %}Register{% endtrans %}</a></li>
-                                <li><a href="{{ url_for('auth.forgot_password') }}">{% trans %}Reset Password{% endtrans %}</a></li>
-                            </ul>
-                        </div>
-                    {% endif %}
-                    </div><!-- nav-collapse -->
-                </div><!-- container -->
-            </nav> <!-- navbar navbar-inverse -->
-            {% endblock %}
+                    <div class="btn-group navbar-btn navbar-right">
+                        <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown">
+                            <span class="fa fa-envelope"></span> <span class="badge">{{ current_user.pm_unread }}</span>
+                        </button>
+                        <ul class="dropdown-menu" role="menu">
+                            <li><a href="{{ url_for('message.inbox') }}"><span class="fa fa-envelope"></span> {% trans %}Private Messages{% endtrans %}</a></li>
+                            <li><a href="{{ url_for('message.new_conversation') }}"><span class="fa fa-pencil"></span> {% trans %}New Message{% endtrans %}</a></li>
+                        </ul>
+                    </div>
+                {% else %}
+                    <div class="btn-group navbar-btn navbar-right">
+                        <a class="btn btn-primary" href="{{ url_for('auth.login') }}">
+                            <span class="fa fa-user"></span> {% trans %}Login{% endtrans %}
+                        </a>
+                        <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
+                            <span class="caret"></span>
+                        </button>
+                        <ul class="dropdown-menu" role="menu">
+                            <li><a href="{{ url_for('auth.register') }}">{% trans %}Register{% endtrans %}</a></li>
+                            <li><a href="{{ url_for('auth.forgot_password') }}">{% trans %}Reset Password{% endtrans %}</a></li>
+                        </ul>
+                    </div>
+                {% endif %}
+                </div><!-- nav-collapse -->
+            </div><!-- container -->
+        </nav> <!-- navbar navbar-inverse -->
+        {% endblock %}
 
 
 
 
-            <div class="container">
-                {% block messages %}
+        <div class="container">
+            {% block messages %}
+                <div id="flashed-messages">
                     {% include theme('flashed_messages.html') %}
                     {% include theme('flashed_messages.html') %}
-                {% endblock %}
+                </div>
+            {% endblock %}
 
 
-                {% block content %}
-                {% endblock %}
-            </div> <!-- /container -->
+            {% block content %}
+            {% endblock %}
+        </div> <!-- /container -->
 
 
-            {% block footer %}
-            <div class="footer">
-                <div class="container">
-                    <p class="text-muted pull-left">powered by <a href="http://flask.pocoo.org">Flask</a></p>
-                    <p class="text-muted pull-right">&copy; 2013 - 2015 - <a href="http://flaskbb.org">FlaskBB.org</a></p>
-                </div>
+        {% block footer %}
+        <div id="footer">
+            <div class="container">
+                <p class="text-muted credit pull-left">powered by <a href="http://flask.pocoo.org">Flask</a></p>
+                <p class="text-muted credit pull-right">&copy; 2013 - 2015 - <a href="http://flaskbb.org">FlaskBB.org</a></p>
             </div>
             </div>
-            {% endblock %}
+        </div>
+        {% endblock %}
 
 
+        {# standard javascript libs #}
         {% block javascript %}
         {% block javascript %}
         <script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
         <script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
         <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
         <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
+        <script src="{{ url_for('static', filename='js/flaskbb.js') }}"></script>
         {% endblock %}
         {% endblock %}
 
 
+        {# space for extra scripts - to use in other templates #}
         {% block scripts %}
         {% block scripts %}
         {% endblock %}
         {% endblock %}
     </body>
     </body>

+ 12 - 14
flaskbb/user/models.py

@@ -19,6 +19,7 @@ from flask_login import UserMixin, AnonymousUserMixin
 from flaskbb._compat import max_integer
 from flaskbb._compat import max_integer
 from flaskbb.extensions import db, cache
 from flaskbb.extensions import db, cache
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
+from flaskbb.utils.database import CRUDMixin
 from flaskbb.forum.models import (Post, Topic, topictracker, TopicsRead,
 from flaskbb.forum.models import (Post, Topic, topictracker, TopicsRead,
                                   ForumsRead)
                                   ForumsRead)
 from flaskbb.message.models import Conversation
 from flaskbb.message.models import Conversation
@@ -30,7 +31,7 @@ groups_users = db.Table(
     db.Column('group_id', db.Integer(), db.ForeignKey('groups.id')))
     db.Column('group_id', db.Integer(), db.ForeignKey('groups.id')))
 
 
 
 
-class Group(db.Model):
+class Group(db.Model, CRUDMixin):
     __tablename__ = "groups"
     __tablename__ = "groups"
 
 
     id = db.Column(db.Integer, primary_key=True)
     id = db.Column(db.Integer, primary_key=True)
@@ -62,18 +63,6 @@ class Group(db.Model):
         """
         """
         return "<{} {}>".format(self.__class__.__name__, self.id)
         return "<{} {}>".format(self.__class__.__name__, self.id)
 
 
-    def save(self):
-        """Saves a group"""
-        db.session.add(self)
-        db.session.commit()
-        return self
-
-    def delete(self):
-        """Deletes a group"""
-        db.session.delete(self)
-        db.session.commit()
-        return self
-
     @classmethod
     @classmethod
     def selectable_groups_choices(cls):
     def selectable_groups_choices(cls):
         return Group.query.order_by(Group.name.asc()).with_entities(
         return Group.query.order_by(Group.name.asc()).with_entities(
@@ -85,7 +74,7 @@ class Group(db.Model):
         return Group.query.filter(cls.guest == True).first()
         return Group.query.filter(cls.guest == True).first()
 
 
 
 
-class User(db.Model, UserMixin):
+class User(db.Model, UserMixin, CRUDMixin):
     __tablename__ = "users"
     __tablename__ = "users"
     __searchable__ = ['username', 'email']
     __searchable__ = ['username', 'email']
 
 
@@ -280,6 +269,7 @@ class User(db.Model, UserMixin):
 
 
         :param topic: The topic which should be added to the topic tracker.
         :param topic: The topic which should be added to the topic tracker.
         """
         """
+
         if not self.is_tracking_topic(topic):
         if not self.is_tracking_topic(topic):
             self.tracked_topics.append(topic)
             self.tracked_topics.append(topic)
             return self
             return self
@@ -290,6 +280,7 @@ class User(db.Model, UserMixin):
         :param topic: The topic which should be removed from the
         :param topic: The topic which should be removed from the
                       topic tracker.
                       topic tracker.
         """
         """
+
         if self.is_tracking_topic(topic):
         if self.is_tracking_topic(topic):
             self.tracked_topics.remove(topic)
             self.tracked_topics.remove(topic)
             return self
             return self
@@ -308,6 +299,7 @@ class User(db.Model, UserMixin):
 
 
         :param group: The group which should be added to the user.
         :param group: The group which should be added to the user.
         """
         """
+
         if not self.in_group(group):
         if not self.in_group(group):
             self.secondary_groups.append(group)
             self.secondary_groups.append(group)
             return self
             return self
@@ -342,6 +334,7 @@ class User(db.Model, UserMixin):
 
 
         :param exclude: a list with excluded permissions. default is None.
         :param exclude: a list with excluded permissions. default is None.
         """
         """
+
         exclude = exclude or []
         exclude = exclude or []
         exclude.extend(['id', 'name', 'description'])
         exclude.extend(['id', 'name', 'description'])
 
 
@@ -368,11 +361,13 @@ class User(db.Model, UserMixin):
 
 
     def invalidate_cache(self):
     def invalidate_cache(self):
         """Invalidates this objects cached metadata."""
         """Invalidates this objects cached metadata."""
+
         cache.delete_memoized(self.get_permissions, self)
         cache.delete_memoized(self.get_permissions, self)
         cache.delete_memoized(self.get_groups, self)
         cache.delete_memoized(self.get_groups, self)
 
 
     def ban(self):
     def ban(self):
         """Bans the user. Returns True upon success."""
         """Bans the user. Returns True upon success."""
+
         if not self.get_permissions()['banned']:
         if not self.get_permissions()['banned']:
             banned_group = Group.query.filter(
             banned_group = Group.query.filter(
                 Group.banned == True
                 Group.banned == True
@@ -386,6 +381,7 @@ class User(db.Model, UserMixin):
 
 
     def unban(self):
     def unban(self):
         """Unbans the user. Returns True upon success."""
         """Unbans the user. Returns True upon success."""
+
         if self.get_permissions()['banned']:
         if self.get_permissions()['banned']:
             member_group = Group.query.filter(
             member_group = Group.query.filter(
                 Group.admin == False,
                 Group.admin == False,
@@ -408,6 +404,7 @@ class User(db.Model, UserMixin):
         :param groups: A list with groups that should be added to the
         :param groups: A list with groups that should be added to the
                        secondary groups from user.
                        secondary groups from user.
         """
         """
+
         if groups:
         if groups:
             # TODO: Only remove/add groups that are selected
             # TODO: Only remove/add groups that are selected
             secondary_groups = self.secondary_groups.all()
             secondary_groups = self.secondary_groups.all()
@@ -429,6 +426,7 @@ class User(db.Model, UserMixin):
 
 
     def delete(self):
     def delete(self):
         """Deletes the User."""
         """Deletes the User."""
+
         # This isn't done automatically...
         # This isn't done automatically...
         Conversation.query.filter_by(user_id=self.id).delete()
         Conversation.query.filter_by(user_id=self.id).delete()
         ForumsRead.query.filter_by(user_id=self.id).delete()
         ForumsRead.query.filter_by(user_id=self.id).delete()

+ 28 - 0
flaskbb/utils/database.py

@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.utils.database
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Some database helpers such as a CRUD mixin.
+
+    :copyright: (c) 2015 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+from flaskbb.extensions import db
+
+
+class CRUDMixin(object):
+    def __repr__(self):
+        return "<{}>".format(self.__class__.__name__)
+
+    def save(self):
+        """Saves the object to the database."""
+        db.session.add(self)
+        db.session.commit()
+        return self
+
+    def delete(self):
+        """Delete the object from the database."""
+        db.session.delete(self)
+        db.session.commit()
+        return self

+ 48 - 2
flaskbb/utils/helpers.py

@@ -17,17 +17,19 @@ from io import BytesIO
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 
 
 import requests
 import requests
-from flask import session, url_for
+import unidecode
+from flask import session, url_for, flash
 from babel.dates import format_timedelta
 from babel.dates import format_timedelta
+from flask_babelex import lazy_gettext as _
 from flask_themes2 import render_theme_template
 from flask_themes2 import render_theme_template
 from flask_login import current_user
 from flask_login import current_user
-import unidecode
 
 
 from flaskbb._compat import range_method, text_type
 from flaskbb._compat import range_method, text_type
 from flaskbb.extensions import redis_store
 from flaskbb.extensions import redis_store
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.markup import markdown
 from flaskbb.utils.markup import markdown
 
 
+
 _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
 _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
 
 
 
 
@@ -57,6 +59,50 @@ def render_template(template, **context):  # pragma: no cover
     return render_theme_template(theme, template, **context)
     return render_theme_template(theme, template, **context)
 
 
 
 
+def do_topic_action(topics, user, action, reverse):
+    """Executes a specific action for topics. Returns a list with the modified
+    topic objects.
+
+    :param topics: A iterable with ``Topic`` objects.
+    :param user: The user object which wants to perform the action.
+    :param action: One of the following actions: locked, important and delete.
+    :param reverse: If the action should be done in a reversed way.
+                    For example, to unlock a topic, ``reverse`` should be
+                    set to ``True``.
+    """
+    from flaskbb.utils.permissions import can_moderate, can_delete_topic
+    from flaskbb.user.models import User
+    from flaskbb.forum.models import Post
+
+    modified_topics = 0
+    if action != "delete":
+        for topic in topics:
+            if not can_moderate(user, topic.forum):
+                flash(_("You do not have the permissions to execute this "
+                        "action."), "danger")
+                return False
+
+            if getattr(topic, action) and not reverse:
+                continue
+
+            setattr(topic, action, not reverse)
+            modified_topics += 1
+            topic.save()
+    elif action == "delete":
+        for topic in topics:
+            if not can_delete_topic(user, topic):
+                flash(_("You do not have the permissions to delete this "
+                        "topic."), "danger")
+                return False
+
+            involved_users = User.query.filter(Post.topic_id == topic.id,
+                                               User.id == Post.user_id).all()
+            modified_topics += 1
+            topic.delete(involved_users)
+
+    return modified_topics
+
+
 def get_categories_and_forums(query_result, user):
 def get_categories_and_forums(query_result, user):
     """Returns a list with categories. Every category has a list for all
     """Returns a list with categories. Every category has a list for all
     their associated forums.
     their associated forums.

+ 17 - 17
requirements.txt

@@ -1,4 +1,8 @@
+alembic==0.7.6
 Babel==1.3
 Babel==1.3
+blinker==1.3
+cov-core==1.15.0
+coverage==3.7.1
 Flask==0.10.1
 Flask==0.10.1
 Flask-Cache==0.13.1
 Flask-Cache==0.13.1
 Flask-DebugToolbar==0.10.0
 Flask-DebugToolbar==0.10.0
@@ -7,35 +11,31 @@ Flask-Mail==0.9.1
 Flask-Migrate==1.4.0
 Flask-Migrate==1.4.0
 Flask-Plugins==1.5
 Flask-Plugins==1.5
 Flask-Redis==0.1.0
 Flask-Redis==0.1.0
-Flask-SQLAlchemy==2.0
 Flask-Script==2.0.5
 Flask-Script==2.0.5
+Flask-SQLAlchemy==2.0
 Flask-Themes2==0.1.4
 Flask-Themes2==0.1.4
 Flask-WTF==0.11
 Flask-WTF==0.11
+itsdangerous==0.24
 Jinja2==2.7.3
 Jinja2==2.7.3
 Mako==1.0.1
 Mako==1.0.1
 MarkupSafe==0.23
 MarkupSafe==0.23
+mistune==0.6
+py==1.4.29
 Pygments==2.0.2
 Pygments==2.0.2
-SQLAlchemy==1.0.4
-SQLAlchemy-Utils==0.30.1
-Unidecode==0.04.17
-WTForms==2.0.2
-Werkzeug==0.10.4
-Whoosh==2.7.0
-alembic==0.7.6
-blinker==1.3
-cov-core==1.15.0
-coverage==3.7.1
-itsdangerous==0.24
-mistune==0.5.1
-py==1.4.27
-pytest==2.7.1
+pytest==2.7.2
 pytest-cov==1.8.1
 pytest-cov==1.8.1
-pytest-random==0.02
+pytest-random==0.2
 pytz==2015.4
 pytz==2015.4
 redis==2.10.3
 redis==2.10.3
 requests==2.7.0
 requests==2.7.0
-simplejson==3.7.1
+simplejson==3.7.3
 six==1.9.0
 six==1.9.0
 speaklater==1.3
 speaklater==1.3
+SQLAlchemy==1.0.6
+SQLAlchemy-Utils==0.30.11
+Unidecode==0.4.18
+Werkzeug==0.10.4
+Whoosh==2.7.0
+WTForms==2.0.2
 https://github.com/sh4nks/flask-babelex/tarball/master#egg=Flask-BabelEx
 https://github.com/sh4nks/flask-babelex/tarball/master#egg=Flask-BabelEx
 https://github.com/jshipley/Flask-WhooshAlchemy/archive/master.zip#egg=Flask-Whooshalchemy
 https://github.com/jshipley/Flask-WhooshAlchemy/archive/master.zip#egg=Flask-Whooshalchemy

+ 0 - 32
tests/unit/test_forum_models.py

@@ -336,38 +336,6 @@ def test_topic_delete(topic):
     assert forum.last_post_id is None
     assert forum.last_post_id is None
 
 
 
 
-def test_topic_merge(topic):
-    """Tests the topic merge method."""
-    topic_other = Topic(title="Test Topic Merge")
-    post = Post(content="Test Content Merge")
-    topic_other.save(post=post, user=topic.user, forum=topic.forum)
-
-    # Save the last_post_id in another variable because topic_other will be
-    # overwritten later
-    last_post_other = topic_other.last_post_id
-
-    assert topic_other.merge(topic)
-
-    # I just want to be sure that the topic is deleted
-    topic_other = Topic.query.filter_by(id=topic_other.id).first()
-    assert topic_other is None
-
-    assert topic.post_count == 2
-    assert topic.last_post_id == last_post_other
-
-
-def test_topic_merge_other_forum(topic):
-    """You cannot merge a topic with a topic from another forum."""
-    forum_other = Forum(title="Test Forum 2", category_id=1)
-    forum_other.save()
-
-    topic_other = Topic(title="Test Topic 2")
-    post_other = Post(content="Test Content 2")
-    topic_other.save(user=topic.user, forum=forum_other, post=post_other)
-
-    assert not topic.merge(topic_other)
-
-
 def test_topic_move(topic):
 def test_topic_move(topic):
     """Tests the topic move method."""
     """Tests the topic move method."""
     forum_other = Forum(title="Test Forum 2", category_id=1)
     forum_other = Forum(title="Test Forum 2", category_id=1)