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

Merge pull request #39 from sh4nks/settings

Settings via Admin Panel.
sh4nks 11 лет назад
Родитель
Сommit
4d8f51d3b1

+ 4 - 0
flaskbb/admin/forms.py

@@ -354,3 +354,7 @@ class CategoryForm(Form):
     def save(self):
         category = Category(**self.data)
         return category.save()
+
+
+class SettingsForm(Form):
+    pass

+ 244 - 0
flaskbb/admin/models.py

@@ -0,0 +1,244 @@
+import base64
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+from wtforms import (TextField, IntegerField, BooleanField, SelectField,
+                     FloatField, validators)
+from flask.ext.wtf import Form
+from flaskbb.extensions import db
+
+
+def normalize_to(value, value_type, reverse=False):
+    """Converts a value to a specified value type.
+    Available value types are: string, integer, boolean and array.
+    A boolean type is handled as 0 for false and 1 for true.
+
+    :param value: The value which should be converted.
+    :param value_type: The value_type.
+    :param reverse: If the value should be converted back
+    """
+    if reverse:
+        if value_type == 'array':
+            return value.split(',')
+        if value_type == 'integer':
+            return int(value)
+        if value_type == 'float':
+            return float(value)
+        if value_type == 'boolean':
+            return value == "1"
+
+        # text
+        return value
+    else:
+        if value_type == 'array':
+            return ",".join(value)
+        if value_type == 'integer':
+            return int(value)
+        if value_type == 'float':
+            return float(value)
+        if value_type == 'boolean':
+            return 1 if value else 0
+
+        # text
+        return value
+
+
+class SettingsGroup(db.Model):
+    __tablename__ = "settingsgroup"
+
+    key = db.Column(db.String, primary_key=True)
+    name = db.Column(db.String, nullable=False)
+    description = db.Column(db.String, nullable=False)
+    settings = db.relationship("Setting", lazy="dynamic", backref="group")
+
+    def save(self):
+        db.session.add(self)
+        db.session.commit()
+
+
+class Setting(db.Model):
+    __tablename__ = "settings"
+
+    key = db.Column(db.String, primary_key=True)
+    _value = db.Column("value", db.String, nullable=False)
+    settingsgroup = db.Column(db.String,
+                              db.ForeignKey('settingsgroup.key',
+                                            use_alter=True,
+                                            name="fk_settingsgroup"),
+                              nullable=False)
+
+    # The name (displayed in the form)
+    name = db.Column(db.String, nullable=False)
+
+    # The description (displayed in the form)
+    description = db.Column(db.String, nullable=False)
+
+    # Available types: string, integer, boolean, array, float
+    value_type = db.Column(db.String, nullable=False)
+
+    # Available types: text, number, choice, yesno
+    # They are used in the form creation process
+    input_type = db.Column(db.String, nullable=False)
+
+    # Extra attributes like, validation things (min, max length...)
+    _extra = db.Column("extra", db.String)
+
+    # Properties
+    @property
+    def value(self):
+        return normalize_to(self._value, self.value_type)
+
+    @value.setter
+    def value(self, value):
+        self._value = normalize_to(value, self.value_type, reverse=True)
+
+    @property
+    def extra(self):
+        return pickle.loads(base64.decodestring(self._extra))
+
+    @extra.setter
+    def extra(self, extra):
+        self._extra = base64.encodestring(
+            pickle.dumps((extra), pickle.HIGHEST_PROTOCOL)
+        )
+
+    @classmethod
+    def get_form(cls, group):
+        """Returns a Form for all settings found in :class:`SettingsGroup`.
+
+        :param group: The settingsgroup name. It is used to get the settings
+                      which are in the specified group.
+        """
+
+        class SettingsForm(Form):
+            pass
+
+        # now parse that shit
+        for setting in group.settings:
+            field_validators = []
+
+            # generate the validators
+            # TODO: Do this in another function
+            if "min" in setting.extra:
+                # Min number validator
+                if setting.value_type in ("integer", "float"):
+                    field_validators.append(
+                        validators.NumberRange(min=setting.extra["min"])
+                    )
+
+                # Min text length validator
+                elif setting.value_type in ("string", "array"):
+                    field_validators.append(
+                        validators.Length(min=setting.extra["min"])
+                    )
+
+            if "max" in setting.extra:
+                # Max number validator
+                if setting.value_type in ("integer", "float"):
+                    field_validators.append(
+                        validators.NumberRange(max=setting.extra["max"])
+                    )
+
+                # Max text length validator
+                elif setting.value_type in ("string", "array"):
+                    field_validators.append(
+                        validators.Length(max=setting.extra["max"])
+                    )
+
+            # Generate the fields based on input_type and value_type
+            if setting.input_type == "number":
+                # IntegerField
+                if setting.value_type == "integer":
+                    setattr(
+                        SettingsForm, setting.key,
+                        IntegerField(setting.name, validators=field_validators,
+                                     description=setting.description)
+                    )
+                # FloatField
+                elif setting.value_type == "float":
+                    setattr(
+                        SettingsForm, setting.key,
+                        FloatField(setting.name, validators=field_validators,
+                                   description=setting.description)
+                    )
+
+            # TextField
+            if setting.input_type == "text":
+                setattr(
+                    SettingsForm, setting.key,
+                    TextField(setting.name, validators=field_validators,
+                              description=setting.description)
+                )
+
+            # SelectField
+            if setting.input_type == "choice" and "choices" in setting.extra:
+                setattr(
+                    SettingsForm, setting.key,
+                    SelectField(setting.name, choices=setting.extra['choices'],
+                                description=setting.description)
+                )
+
+            # BooleanField
+            if setting.input_type == "yesno":
+                setattr(
+                    SettingsForm, setting.key,
+                    BooleanField(setting.name, description=setting.description)
+                )
+
+        return SettingsForm
+
+    @classmethod
+    def get_all(cls):
+        return cls.query.all()
+
+    @classmethod
+    def update(cls, settings, app=None):
+        """Updates the current_app's config and stores the changes in the
+        database.
+
+        :param config: A dictionary with configuration items.
+        """
+        updated_settings = {}
+        for key, value in settings.iteritems():
+            setting = cls.query.filter(Setting.key == key.lower()).first()
+
+            setting.value = value
+
+            updated_settings[setting.key.upper()] = setting.value
+
+            db.session.add(setting)
+            db.session.commit()
+
+        if app is not None:
+            app.config.update(updated_settings)
+
+    @classmethod
+    def as_dict(cls, from_group=None, upper=False):
+        """Returns the settings key and value as a dict.
+
+        :param from_group: Returns only the settings from the group as a dict.
+        :param upper: If upper is ``True``, the key will use upper-case
+                      letters. Defaults to ``False``.
+        """
+        settings = {}
+        result = None
+        if from_group is not None:
+            result = SettingsGroup.query.filter_by(key=from_group).\
+                first_or_404()
+            result = result.settings
+        else:
+            result = cls.query.all()
+
+        for setting in result:
+            if upper:
+                settings[setting.key.upper()] = setting.value
+            else:
+                settings[setting.key] = setting.value
+
+        return settings
+
+    def save(self):
+        db.session.add(self)
+        db.session.commit()

+ 48 - 0
flaskbb/admin/views.py

@@ -15,6 +15,7 @@ from flask import (Blueprint, current_app, request, redirect, url_for, flash,
                    __version__ as flask_version)
 from flask.ext.login import current_user
 from flask.ext.plugins import get_plugins_list, get_plugin
+from flask.ext.themes2 import get_themes_list
 
 from flaskbb import __version__ as flaskbb_version
 from flaskbb.forum.forms import UserSearchForm
@@ -23,6 +24,7 @@ from flaskbb.utils.decorators import admin_required
 from flaskbb.extensions import db
 from flaskbb.user.models import User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
+from flaskbb.admin.models import Setting, SettingsGroup
 from flaskbb.admin.forms import (AddUserForm, EditUserForm, AddGroupForm,
                                  EditGroupForm, EditForumForm, AddForumForm,
                                  CategoryForm)
@@ -47,6 +49,52 @@ def overview():
                            post_count=post_count)
 
 
+@admin.route("/settings", methods=["GET", "POST"])
+@admin.route("/settings/<path:slug>", methods=["GET", "POST"])
+@admin_required
+def settings(slug=None):
+    slug = slug if slug else "general"
+
+    # get the currently active group
+    active_group = SettingsGroup.query.filter_by(key=slug).first_or_404()
+    # get all groups - used to build the navigation
+    all_groups = SettingsGroup.query.all()
+
+    SettingsForm = Setting.get_form(active_group)
+
+    old_settings = Setting.as_dict(from_group=slug)
+    new_settings = {}
+
+    form = SettingsForm()
+
+    if active_group.key == "themes":
+        # get the list with all available themes
+        form.default_theme.choices = [(theme.identifier, theme.name)
+                                      for theme in get_themes_list()]
+
+    if form.validate_on_submit():
+        for key, value in old_settings.iteritems():
+            try:
+                # check if the value has changed
+                if value == form.__dict__[key].data:
+                    continue
+                else:
+                    new_settings[key] = form.__dict__[key].data
+            except KeyError:
+                pass
+
+        Setting.update(settings=new_settings, app=current_app)
+    else:
+        for key, value in old_settings.iteritems():
+            try:
+                form.__dict__[key].data = value
+            except (KeyError, ValueError):
+                pass
+
+    return render_template("admin/settings.html", form=form,
+                           all_groups=all_groups, active_group=active_group)
+
+
 @admin.route("/users", methods=['GET', 'POST'])
 @admin_required
 def users():

+ 8 - 0
flaskbb/app.py

@@ -22,6 +22,7 @@ from flaskbb.user.models import User, Guest, PrivateMessage
 from flaskbb.auth.views import auth
 # Import the admin blueprint
 from flaskbb.admin.views import admin
+from flaskbb.admin.models import Setting
 # Import the forum blueprint
 from flaskbb.forum.views import forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
@@ -59,6 +60,7 @@ def create_app(config=None):
     configure_before_handlers(app)
     configure_errorhandlers(app)
     configure_logging(app)
+    update_settings_from_db(app)
 
     return app
 
@@ -131,6 +133,12 @@ def configure_extensions(app):
     login_manager.init_app(app)
 
 
+def update_settings_from_db(app):
+    if not app.config["TESTING"]:
+        with app.app_context():
+            app.config.update(Setting.as_dict(upper=True))
+
+
 def configure_template_filters(app):
     """
     Configures the template filters

+ 6 - 6
flaskbb/configs/default.py

@@ -75,6 +75,12 @@ class DefaultConfig(object):
     # Where to logger should send the emails to
     ADMINS = ["admin@example.org"]
 
+    ## Flask-And-Redis
+    REDIS_ENABLED = False
+    REDIS_HOST = 'localhost'
+    REDIS_PORT = 6379
+    REDIS_DB = 0
+
     ## App specific configs
     # Pagination
     # How many posts per page are displayed
@@ -90,12 +96,6 @@ class DefaultConfig(object):
     # The length of the topic title in characters on the index
     TITLE_LENGTH = 15
 
-    # This is really handy if you do not have an redis instance like on windows
-    REDIS_ENABLED = True
-    REDIS_HOST = 'localhost'
-    REDIS_PORT = 6379
-    REDIS_DB = 0
-
     # The days for how long the forum should deal with unread topics
     # 0 - Disable it
     TRACKER_LENGTH = 7

+ 0 - 0
flaskbb/fixtures/__init__.py


+ 112 - 0
flaskbb/fixtures/groups.py

@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.fixtures.groups
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    The fixtures module for our groups.
+
+    :copyright: (c) 2014 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+
+from collections import OrderedDict
+
+
+fixture = OrderedDict((
+    ('Administrator', {
+        'description': 'The Administrator Group',
+        'admin': True,
+        'super_mod': False,
+        'mod': False,
+        'banned': False,
+        'guest': False,
+        'editpost': True,
+        'deletepost': True,
+        'deletetopic': True,
+        'posttopic': True,
+        'postreply': True,
+        'locktopic': True,
+        'movetopic': True,
+        'mergetopic': True
+    }),
+    ('Super Moderator', {
+        'description': 'The Super Moderator Group',
+        'admin': False,
+        'super_mod': True,
+        'mod': False,
+        'banned': False,
+        'guest': False,
+        'editpost': True,
+        'deletepost': True,
+        'deletetopic': True,
+        'posttopic': True,
+        'postreply': True,
+        'locktopic': True,
+        'movetopic': True,
+        'mergetopic': True
+    }),
+    ('Moderator', {
+        'description': 'The Moderator Group',
+        'admin': False,
+        'super_mod': False,
+        'mod': True,
+        'banned': False,
+        'guest': False,
+        'editpost': True,
+        'deletepost': True,
+        'deletetopic': True,
+        'posttopic': True,
+        'postreply': True,
+        'locktopic': True,
+        'movetopic': True,
+        'mergetopic': True
+    }),
+    ('Member', {
+        'description': 'The Member Group',
+        'admin': False,
+        'super_mod': False,
+        'mod': False,
+        'banned': False,
+        'guest': False,
+        'editpost': True,
+        'deletepost': False,
+        'deletetopic': False,
+        'posttopic': True,
+        'postreply': True,
+        'locktopic': False,
+        'movetopic': False,
+        'mergetopic': False
+    }),
+    ('Banned', {
+        'description': 'The Banned Group',
+        'admin': False,
+        'super_mod': False,
+        'mod': False,
+        'banned': True,
+        'guest': False,
+        'editpost': False,
+        'deletepost': False,
+        'deletetopic': False,
+        'posttopic': False,
+        'postreply': False,
+        'locktopic': False,
+        'movetopic': False,
+        'mergetopic': False
+    }),
+    ('Guest', {
+        'description': 'The Guest Group',
+        'admin': False,
+        'super_mod': False,
+        'mod': False,
+        'banned': False,
+        'guest': True,
+        'editpost': False,
+        'deletepost': False,
+        'deletetopic': False,
+        'posttopic': False,
+        'postreply': False,
+        'locktopic': False,
+        'movetopic': False,
+        'mergetopic': False
+    })
+))

+ 102 - 0
flaskbb/fixtures/settings.py

@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.fixtures.settings
+    ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    The fixtures module for our settings.
+
+    :copyright: (c) 2014 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+
+fixture = (
+    # Settings Group
+    ('general', {
+        'name': "General Settings",
+        'description': "How many items per page are displayed.",
+        'settings': (
+            ('project_title', {
+                'value':        "FlaskBB",
+                'value_type':   "string",
+                'input_type':   "text",
+                'name':         "Project title",
+                'description':  "The title of the project.",
+            }),
+            ('project_subtitle', {
+                'value':        "A lightweight forum software in Flask",
+                'value_type':   "string",
+                'input_type':   "text",
+                'name':         "Project subtitle",
+                'description':  "A short description of the project.",
+            }),
+            ('posts_per_page', {
+                'value':        10,
+                'value_type':   "integer",
+                'input_type':   "number",
+                'extra':        {'min': 5},
+                'name':         "Posts per page",
+                'description':  "Number of posts displayed per page.",
+            }),
+            ('topics_per_page', {
+                'value':        10,
+                'value_type':   "integer",
+                'input_type':   "number",
+                'extra':        {'min': 5},
+                'name':         "Topics per page",
+                'description':  "Number of topics displayed per page.",
+            }),
+            ('users_per_page', {
+                'value':        10,
+                'value_type':   "integer",
+                'input_type':   "number",
+                'extra':        {'min': 5},
+                'name':         "Users per page",
+                'description':  "Number of users displayed per page.",
+            }),
+        ),
+    }),
+    ('misc', {
+        'name': "Misc Settings",
+        'description': "Miscellaneous settings.",
+        'settings': (
+            ('online_last_minutes', {
+                'value':        15,
+                'value_type':   "integer",
+                'input_type':   "number",
+                'extra':        {'min': 0},
+                'name':         "Online last minutes",
+                'description':  "How long a user can be inactive before he is marked as offline. 0 to disable it.",
+            }),
+            ('title_length', {
+                'value':        15,
+                'value_type':   "integer",
+                'input_type':   "number",
+                'extra':        {'min': 0},
+                'name':         "Topic title length",
+                'description':  "The length of the topic title shown on the index."
+            }),
+            ('tracker_length', {
+                'value':        7,
+                'value_type':   "integer",
+                'input_type':   "number",
+                'extra':        {'min': 0},
+                'name':         "Tracker length",
+                'description':  "The days for how long the forum should deal with unread topics. 0 to disable it."
+            })
+        ),
+    }),
+    ('themes', {
+        'name': "Theme Settings",
+        "description": "Change the appearance for your forum.",
+        "settings": (
+            ('default_theme', {
+                'value':        "bootstrap3",
+                'value_type':   "text",
+                'input_type':   "choice",
+                'extra':        {'choices': None},
+                'name':         "Default theme",
+                'description':  "Change the default theme for your forum."
+            }),
+        ),
+    }),
+)

+ 2 - 2
flaskbb/plugins/portal/templates/index.html

@@ -85,7 +85,8 @@
           <div class="panel-heading">
             <h3 class="panel-title">News</h3>
           </div>
-          <div class="panel-body">
+          <div class="panel-body" style="padding-top: 0px">
+
           {% for topic in news %}
             <h1><a href="{{ topic.url }}">{{ topic.title }}</a></h1>
             <ul class="portal-info">
@@ -96,7 +97,6 @@
             <div class="portal-content">
                 {{ topic.first_post.content | markup | safe }}<br />
             </div>
-
             {% if not loop.last %}<hr>{% endif %}
           {% endfor %}
 

+ 10 - 4
flaskbb/plugins/portal/views.py

@@ -1,12 +1,11 @@
-from flask import Blueprint, current_app
+# -*- coding: utf-8 -*-
+from flask import Blueprint, current_app, flash
 from flaskbb.utils.helpers import render_template
 from flaskbb.forum.models import Topic, Post
 from flaskbb.user.models import User
 from flaskbb.utils.helpers import time_diff, get_online_users
 
 
-FORUM_IDS = [1, 2]
-
 portal = Blueprint("portal", __name__, template_folder="templates")
 
 
@@ -16,7 +15,14 @@ def inject_portal_link():
 
 @portal.route("/")
 def index():
-    news = Topic.query.filter(Topic.forum_id.in_(FORUM_IDS)).all()
+    try:
+        forum_ids = current_app.config["PLUGIN_PORTAL_FORUM_IDS"]
+    except KeyError:
+        forum_ids = [1]
+        flash("Please install the plugin first to configure the forums "
+              "which should be displayed", "warning")
+
+    news = Topic.query.filter(Topic.forum_id.in_(forum_ids)).all()
     recent_topics = Topic.query.order_by(Topic.date_created).limit(5).offset(0)
 
     user_count = User.query.count()

+ 15 - 19
flaskbb/templates/admin/admin_layout.html

@@ -1,23 +1,19 @@
 {% extends theme("layout.html") %}
 {% block content %}
-{%- from theme('macros.html') import navlink with context-%}
+{%- from theme('macros.html') import navlink with context -%}
+
+<div class="col-md-12" style="padding-bottom: 10px">
+    <ul class="nav nav-tabs nav-justified">
+        {{ navlink('admin.overview', 'Overview') }}
+        {{ navlink('admin.settings', 'Settings') }}
+        {{ navlink('admin.users', 'Users', active=active_admin_user_nav) }}
+        {{ navlink('admin.groups', 'Groups', active=active_admin_group_nav) }}
+        {{ navlink('admin.forums', 'Forums', active=active_admin_forum_nav) }}
+        {{ navlink('admin.unread_reports', 'Reports', active=active_admin_report_nav) }}
+        {{ navlink('admin.plugins', 'Plugins') }}
+    </ul>
+</div>
+
+{% block admin_content %}{% endblock %}
 
-<div class="row">
-    <div class="col-sm-3">
-        <div class="sidebar">
-            <ul class="nav sidenav">
-                <li class="nav-header">Options</li>
-                {{ navlink('admin.overview', 'Overview') }}
-                {{ navlink('admin.users', 'Users') }}
-                {{ navlink('admin.groups', 'Groups') }}
-                {{ navlink('admin.forums', 'Forums') }}
-                {{ navlink('admin.unread_reports', 'Reports') }}
-                {{ navlink('admin.plugins', 'Plugins') }}
-            </ul>
-        </div><!--/.sidebar -->
-    </div><!--/.col-sm-3 -->
-    <div class="col-sm-9">
-        {% block admin_content %}{% endblock %}
-    </div><!--/.col-sm-9 -->
-</div><!--/.row -->
 {% endblock %}

+ 23 - 13
flaskbb/templates/admin/category_form.html

@@ -1,23 +1,33 @@
 {% set page_title = title %}
-{% set active_forum_nav=True %}
+{% set active_admin_forum_nav=True %}
 
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme("macros.html") import horizontal_field, render_boolean_field %}
+{% from theme("macros.html") import horizontal_field, render_boolean_field, navlink with context %}
 
-<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="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.forums', "Manage Forums") }}
+        {{ navlink('admin.add_forum', "Add Forum") }}
+        {{ navlink('admin.add_category', "Add Category") }}
+    </ul>
+</div>
 
-        {{ horizontal_field(form.position) }}
+<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="form-group">
-            <div class="col-lg-offset-0 col-lg-9">
-                <button type="submit" class="btn btn-success">Save</button>
+            {{ horizontal_field(form.position) }}
+
+            <div class="form-group">
+                <div class="col-lg-offset-0 col-lg-9">
+                    <button type="submit" class="btn btn-success">Save</button>
+                </div>
             </div>
-        </div>
-</form>
+    </form>
+</div>
 
 {% endblock %}

+ 28 - 19
flaskbb/templates/admin/forum_form.html

@@ -1,31 +1,40 @@
 {% set page_title = title %}
-{% set active_forum_nav=True %}
+{% set active_admin_forum_nav=True %}
 
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme("macros.html") import horizontal_field, render_boolean_field %}
+{% from theme("macros.html") import horizontal_field, render_boolean_field, navlink with context %}
 
-<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="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.forums', "Manage Forums") }}
+        {{ navlink('admin.add_forum', "Add Forum") }}
+        {{ navlink('admin.add_category', "Add Category") }}
+    </ul>
+</div>
 
-        {{ horizontal_field(form.category) }}
-        {{ horizontal_field(form.position) }}
+<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") }}
 
-        {{ horizontal_field(form.external) }}
+            {{ horizontal_field(form.category) }}
+            {{ horizontal_field(form.position) }}
 
-        {{ horizontal_field(form.moderators) }}
-        {{ render_boolean_field(form.show_moderators) }}
+            {{ horizontal_field(form.external) }}
 
-        {{ render_boolean_field(form.locked) }}
+            {{ horizontal_field(form.moderators) }}
+            {{ render_boolean_field(form.show_moderators) }}
 
-        <div class="form-group">
-            <div class="col-lg-offset-0 col-lg-9">
-                <button type="submit" class="btn btn-success">Save</button>
-            </div>
-        </div>
-</form>
+            {{ render_boolean_field(form.locked) }}
 
+            <div class="form-group">
+                <div class="col-lg-offset-0 col-lg-9">
+                    <button type="submit" class="btn btn-success">Save</button>
+                </div>
+            </div>
+    </form>
+</div>
 {% endblock %}

+ 61 - 50
flaskbb/templates/admin/forums.html

@@ -1,53 +1,64 @@
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme('macros.html') import render_pagination %}
-
-<legend>Manage Forums | <a href="{{ url_for('admin.add_forum') }}">Add Forum</a> | <a href="{{ url_for('admin.add_category') }}">Add Category</a></legend>
-
-{% 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('admin.add_forum', category_id=category.id) }}">Add Forum</a> |
-                <a href="{{ url_for('admin.edit_category', category_id = category.id) }}">Edit</a> |
-                <a href="{{ url_for('admin.delete_category', category_id = category.id) }}">Delete</a>
-            </td>
-        </tr>
-    </thead>
-    <tbody class="forumbody">
-        <tr class="forum_stats">
-            <td colspan="2"><strong>Forum</strong></td>
-            <td width="85" align="center" style="white-space: nowrap"><strong>Management</strong></td>
-        </tr>
-
-        {% for forum in category.forums %}
-        <tr>
-            <td align="center" valign="center" width="4%">
-
-            </td>
-
-            <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>
-
-            <td valign="top" align="center" style="white-space: nowrap">
-                <a href="{{ url_for('admin.edit_forum', forum_id = forum.id) }}">Edit</a> |
-                <a href="{{ url_for('admin.delete_forum', forum_id = forum.id) }}">Delete</a>
-            </td>
-        </tr>
-        {% endfor %}
-
-    </tbody>
-</table>
-{% endfor %}
+{% from theme('macros.html') import render_pagination, navlink with context %}
+
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.forums', "Manage Forums") }}
+        {{ navlink('admin.add_forum', "Add Forum") }}
+        {{ navlink('admin.add_category', "Add Category") }}
+    </ul>
+</div>
+
+
+<div class="col-md-9">
+    <legend>Manage Forums</legend>
+
+    {% 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('admin.add_forum', category_id=category.id) }}">Add Forum</a> |
+                    <a href="{{ url_for('admin.edit_category', category_id = category.id) }}">Edit</a> |
+                    <a href="{{ url_for('admin.delete_category', category_id = category.id) }}">Delete</a>
+                </td>
+            </tr>
+        </thead>
+        <tbody class="forumbody">
+            <tr class="forum_stats">
+                <td colspan="2"><strong>Forum</strong></td>
+                <td width="85" align="center" style="white-space: nowrap"><strong>Management</strong></td>
+            </tr>
+
+            {% for forum in category.forums %}
+            <tr>
+                <td align="center" valign="center" width="4%">
+
+                </td>
+
+                <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>
+
+                <td valign="top" align="center" style="white-space: nowrap">
+                    <a href="{{ url_for('admin.edit_forum', forum_id = forum.id) }}">Edit</a> |
+                    <a href="{{ url_for('admin.delete_forum', forum_id = forum.id) }}">Delete</a>
+                </td>
+            </tr>
+            {% endfor %}
+
+        </tbody>
+    </table>
+    {% endfor %}
+</div>
 {% endblock %}

+ 37 - 29
flaskbb/templates/admin/group_form.html

@@ -1,35 +1,43 @@
 {% set page_title = title %}
-{% set active_forum_nav=True %}
+{% set active_admin_group_nav=True %}
 
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme("macros.html") import horizontal_field, render_boolean_field %}
-
-<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.editpost) }}
-        {{ render_boolean_field(form.deletepost) }}
-        {{ render_boolean_field(form.deletetopic) }}
-        {{ render_boolean_field(form.posttopic) }}
-        {{ render_boolean_field(form.postreply) }}
-
-        <div class="form-group">
-            <div class="col-lg-offset-0 col-lg-9">
-                <button type="submit" class="btn btn-success">Save</button>
+{% from theme("macros.html") import horizontal_field, render_boolean_field, navlink with context %}
+
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.groups', "Manage Groups") }}
+        {{ navlink('admin.add_group', "Add Group") }}
+    </ul>
+</div>
+
+<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.editpost) }}
+            {{ render_boolean_field(form.deletepost) }}
+            {{ render_boolean_field(form.deletetopic) }}
+            {{ render_boolean_field(form.posttopic) }}
+            {{ render_boolean_field(form.postreply) }}
+
+            <div class="form-group">
+                <div class="col-lg-offset-0 col-lg-9">
+                    <button type="submit" class="btn btn-success">Save</button>
+                </div>
             </div>
-        </div>
-</form>
-
+    </form>
+</div>
 {% endblock %}

+ 37 - 28
flaskbb/templates/admin/groups.html

@@ -1,34 +1,43 @@
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme('macros.html') import render_pagination %}
+{% from theme('macros.html') import render_pagination, navlink with context %}
 
-<legend>Manage Groups | <a href="{{ url_for('admin.add_group') }}">Add Group</a></legend>
-
-<div class="pull-left" style="padding-bottom: 10px">
-    {{ render_pagination(groups, url_for('admin.groups')) }}
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.groups', "Manage Groups") }}
+        {{ navlink('admin.add_group', "Add Group") }}
+    </ul>
 </div>
 
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>#</th>
-            <th>Group Name</th>
-            <th>Description</th>
-            <th>Manage</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('admin.edit_group', group_id = group.id) }}">Edit</a> |
-                <a href="{{ url_for('admin.delete_group', group_id = group.id) }}">Delete</a>
-            </td>
-        </tr>
-        {% endfor %}
-    </tbody>
-</table>
+<div class="col-md-9">
+    <legend>Manage Groups</legend>
+
+    <div class="pull-left" style="padding-bottom: 10px">
+        {{ render_pagination(groups, url_for('admin.groups')) }}
+    </div>
+
+    <table class="table table-bordered">
+        <thead>
+            <tr>
+                <th>#</th>
+                <th>Group Name</th>
+                <th>Description</th>
+                <th>Manage</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('admin.edit_group', group_id = group.id) }}">Edit</a> |
+                    <a href="{{ url_for('admin.delete_group', group_id = group.id) }}">Delete</a>
+                </td>
+            </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+</div>
 {% endblock %}

+ 30 - 27
flaskbb/templates/admin/overview.html

@@ -1,30 +1,33 @@
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th colspan="4">Global Statistics</th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <td><b>FlaskBB Version</b></td>
-            <td>{{ flaskbb_version }}</td>
-            <td><b>Posts</b></td>
-            <td>{{ post_count }}</td>
-        </tr>
-        <tr>
-            <td><b>Python Version</b></td>
-            <td>{{ python_version }}</td>
-            <td><b>Topics</b></td>
-            <td>{{ topic_count }}</td>
-        </tr>
-        <tr>
-            <td><b>Flask Version</b></td>
-            <td>{{ flask_version }}</td>
-            <td><b>Users</b></td>
-            <td>{{ user_count }}</td>
-        </tr>
-    </tbody>
-</table>
+
+<div class="col-md-12">
+    <table class="table table-bordered">
+        <thead>
+            <tr>
+                <th colspan="4">Global Statistics</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td><b>FlaskBB Version</b></td>
+                <td>{{ flaskbb_version }}</td>
+                <td><b>Posts</b></td>
+                <td>{{ post_count }}</td>
+            </tr>
+            <tr>
+                <td><b>Python Version</b></td>
+                <td>{{ python_version }}</td>
+                <td><b>Topics</b></td>
+                <td>{{ topic_count }}</td>
+            </tr>
+            <tr>
+                <td><b>Flask Version</b></td>
+                <td>{{ flask_version }}</td>
+                <td><b>Users</b></td>
+                <td>{{ user_count }}</td>
+            </tr>
+        </tbody>
+    </table>
+</div>
 {% endblock %}

+ 44 - 33
flaskbb/templates/admin/reports.html

@@ -1,39 +1,50 @@
+{% set active_admin_report_nav=True %}
+
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme('macros.html') import render_pagination %}
-
-<legend>Manage Reports | <a href="{{ url_for('admin.unread_reports') }}">Unread Reports</a></legend>
+{% from theme('macros.html') import render_pagination, navlink with context %}
 
-<div class="pull-left" style="padding-bottom: 10px">
-    {{ render_pagination(reports, url_for('admin.reports')) }}
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.unread_reports', "Show unread reports") }}
+        {{ navlink('admin.reports', "Show all reports") }}
+    </ul>
 </div>
 
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>#</th>
-            <th>Poster</th>
-            <th>Topic</th>
-            <th>Reporter</th>
-            <th>Reason</th>
-            <th>Reported</th>
-        </tr>
-    </thead>
-    <tbody>
-        {% for report in reports.items %}
-        <tr>
-            <td>{{ report.id }}</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">No reports.</td>
-        </tr>
-        {% endfor %}
-    </tbody>
-</table>
+<div class="col-md-9">
+    <legend>All Reports</legend>
+
+    <div class="pull-left" style="padding-bottom: 10px">
+        {{ render_pagination(reports, url_for('admin.reports')) }}
+    </div>
+
+    <table class="table table-bordered">
+        <thead>
+            <tr>
+                <th>#</th>
+                <th>Poster</th>
+                <th>Topic</th>
+                <th>Reporter</th>
+                <th>Reason</th>
+                <th>Reported</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for report in reports.items %}
+            <tr>
+                <td>{{ report.id }}</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">No reports.</td>
+            </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+</div>
 {% endblock %}

+ 41 - 0
flaskbb/templates/admin/settings.html

@@ -0,0 +1,41 @@
+{% extends theme("admin/admin_layout.html") %}
+{% block admin_content %}
+{% from theme('macros.html') import render_boolean_field, render_select_field, render_field, navlink with context %}
+
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {% for group in all_groups %}
+            {% if group.key == active_group.key %}
+                <li class="active"><a href="{{ url_for('admin.settings', slug=group.key) }}">{{ group.name }}</a></li>
+            {% else %}
+                <li><a href="{{ url_for('admin.settings', slug=group.key) }}">{{ group.name }}</a></li>
+            {% endif %}
+        {% endfor %}
+    </ul>
+</div><!--/.col-md-3 -->
+
+<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">Save</button>
+
+</form>
+</div>
+{% endblock %}

+ 48 - 37
flaskbb/templates/admin/unread_reports.html

@@ -1,43 +1,54 @@
+{% set active_admin_report_nav=True %}
+
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme('macros.html') import render_pagination %}
-
-<legend>Manage Reports | <a href="{{ url_for('admin.reports') }}">All Reports</a></legend>
+{% from theme('macros.html') import render_pagination, navlink with context %}
 
-<div class="pull-left" style="padding-bottom: 10px">
-    {{ render_pagination(reports, url_for('admin.unread_reports')) }}
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.unread_reports', "Show unread reports") }}
+        {{ navlink('admin.reports', "Show all reports") }}
+    </ul>
 </div>
 
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>#</th>
-            <th>Poster</th>
-            <th>Topic</th>
-            <th>Reporter</th>
-            <th>Reason</th>
-            <th>Reported</th>
-            <th><a href="{{ url_for('admin.report_markread') }}">Mark all as Read</a></th>
-        </tr>
-    </thead>
-    <tbody>
-        {% for report in reports.items %}
-        <tr>
-            <td>{{ report.id }}</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>
-                <a href="{{ url_for('admin.report_markread', report_id=report.id) }}">Mark as Read</a>
-            </td>
-        </tr>
-        {% else %}
-        <tr>
-            <td colspan="7">No unread reports.</td>
-        </tr>
-        {% endfor %}
-    </tbody>
-</table>
+<div class="col-md-9">
+    <legend>Unread Reports</legend>
+
+    <div class="pull-left" style="padding-bottom: 10px">
+        {{ render_pagination(reports, url_for('admin.unread_reports')) }}
+    </div>
+
+    <table class="table table-bordered">
+        <thead>
+            <tr>
+                <th>#</th>
+                <th>Poster</th>
+                <th>Topic</th>
+                <th>Reporter</th>
+                <th>Reason</th>
+                <th>Reported</th>
+                <th><a href="{{ url_for('admin.report_markread') }}">Mark all as Read</a></th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for report in reports.items %}
+            <tr>
+                <td>{{ report.id }}</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>
+                    <a href="{{ url_for('admin.report_markread', report_id=report.id) }}">Mark as Read</a>
+                </td>
+            </tr>
+            {% else %}
+            <tr>
+                <td colspan="7">No unread reports.</td>
+            </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+</div>
 {% endblock %}

+ 31 - 23
flaskbb/templates/admin/user_form.html

@@ -1,32 +1,40 @@
 {% set page_title = title %}
-{% set active_forum_nav=True %}
+{% set active_admin_user_nav=True %}
 
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme("macros.html") import horizontal_field %}
+{% from theme("macros.html") import horizontal_field, navlink with context %}
 
-<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_field(form.birthday) }}
-        {{ 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") }}
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.users', "Manage Users") }}
+        {{ navlink('admin.add_user', "Add User") }}
+    </ul>
+</div><!--/.col-md-3 -->
 
+<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_field(form.birthday) }}
+            {{ 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") }}
 
-        <div class="form-group">
-            <div class="col-sm-offset-3 col-sm-9">
-                <button type="submit" class="btn btn-success">Save</button>
-            </div>
-        </div>
-</form>
 
+            <div class="form-group">
+                <div class="col-sm-offset-3 col-sm-9">
+                    <button type="submit" class="btn btn-success">Save</button>
+                </div>
+            </div>
+    </form>
+</div>
 {% endblock %}

+ 56 - 48
flaskbb/templates/admin/users.html

@@ -1,56 +1,64 @@
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
-{% from theme('macros.html') import render_pagination %}
-{% from theme("macros.html") import render_field, group_field %}
+{% from theme('macros.html') import render_pagination, render_field, group_field, navlink with context %}
 
-<legend>Manage Users | <a href="{{ url_for('admin.add_user') }}">Add User</a></legend>
+<div class="col-md-3">
+    <ul class="nav nav-pills nav-stacked">
+        {{ navlink('admin.users', "Manage Users") }}
+        {{ navlink('admin.add_user', "Add User") }}
+    </ul>
+</div><!--/.col-md-3 -->
 
-<div class="pull-left" style="padding-bottom: 10px">
-    {{ render_pagination(users, url_for('admin.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="button">Search</button>
-      </span>
-        </div>
-    </form>
-</div>
+<div class="col-md-9">
+    <legend>Manage Users</legend>
 
-<table class="table table-bordered">
-    <thead>
-        <tr>
-            <th>#</th>
-            <th>Username</th>
-            <th>Posts</th>
-            <th>Date registered</th>
-            <th>Group</th>
-            <th>Manage</th>
-        </tr>
-    </thead>
-    <tbody>
-        {% for user in users.items %}
-            <tr>
-                <td>{{ user.id }}</td>
-                <td><a href="{{ url_for('user.profile', username=user.username) }}">{{ user.username }}</a></td>
-                <td>{{ user.post_count }}</td>
-                <td>{{ user.date_joined|format_date('%b %d %Y') }}</td>
-                <td>{{ user.primary_group.name }}</td>
-                <td>
-                    <a href="{{ url_for('admin.edit_user', user_id = user.id) }}">Edit</a> |
-                    <a href="{{ url_for('admin.delete_user', user_id = user.id) }}">Delete</a>
-                </td>
-            </tr>
-        {% else %}
+    <div class="pull-left" style="padding-bottom: 10px">
+        {{ render_pagination(users, url_for('admin.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="button">Search</button>
+          </span>
+            </div>
+        </form>
+    </div>
+
+    <table class="table table-bordered">
+        <thead>
             <tr>
-                <td colspan="6">
-                    No users found matching your search query
-                </td>
+                <th>#</th>
+                <th>Username</th>
+                <th>Posts</th>
+                <th>Date registered</th>
+                <th>Group</th>
+                <th>Manage</th>
             </tr>
-        {% endfor %}
-    </tbody>
-</table>
+        </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>
+                        <a href="{{ url_for('admin.edit_user', user_id = user.id) }}">Edit</a> |
+                        <a href="{{ url_for('admin.delete_user', user_id = user.id) }}">Delete</a>
+                    </td>
+                </tr>
+            {% else %}
+                <tr>
+                    <td colspan="6">
+                        No users found matching your search query
+                    </td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+</div>
 {% endblock %}

+ 2 - 2
flaskbb/templates/layout.html

@@ -43,14 +43,14 @@
                     </div>
                     <div class="collapse navbar-collapse navbar-ex1-collapse">
                         <ul class="nav navbar-nav">
-                            {{ emit_event("before-first-navigation-element") | safe }}
+                            {{ 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") | safe }}
+                            {{ emit_event("after-last-navigation-element") }}
                         </ul>
 
                     {% if current_user and current_user.is_authenticated() %}

+ 2 - 2
flaskbb/templates/macros.html

@@ -165,8 +165,8 @@
 {% endmacro %}
 
 
-{% macro navlink(endpoint, name) %}
-<li {% if endpoint == request.endpoint %}class="active"{% endif %}>
+{% macro navlink(endpoint, name, active=False) %}
+<li {% if endpoint == request.endpoint or active %}class="active"{% endif %}>
     <a href={{ url_for(endpoint) }}>{{ name }}</a>
 </li>
 {% endmacro %}

+ 30 - 100
flaskbb/utils/populate.py

@@ -9,118 +9,47 @@
     :license: BSD, see LICENSE for more details.
 """
 from datetime import datetime
-from collections import OrderedDict
 
+from flaskbb.admin.models import Setting, SettingsGroup
 from flaskbb.user.models import User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category
 
 
-GROUPS = OrderedDict((
-    ('Administrator', {
-        'description': 'The Administrator Group',
-        'admin': True,
-        'super_mod': False,
-        'mod': False,
-        'banned': False,
-        'guest': False,
-        'editpost': True,
-        'deletepost': True,
-        'deletetopic': True,
-        'posttopic': True,
-        'postreply': True,
-        'locktopic': True,
-        'movetopic': True,
-        'mergetopic': True
-    }),
-    ('Super Moderator', {
-        'description': 'The Super Moderator Group',
-        'admin': False,
-        'super_mod': True,
-        'mod': False,
-        'banned': False,
-        'guest': False,
-        'editpost': True,
-        'deletepost': True,
-        'deletetopic': True,
-        'posttopic': True,
-        'postreply': True,
-        'locktopic': True,
-        'movetopic': True,
-        'mergetopic': True
-    }),
-    ('Moderator', {
-        'description': 'The Moderator Group',
-        'admin': False,
-        'super_mod': False,
-        'mod': True,
-        'banned': False,
-        'guest': False,
-        'editpost': True,
-        'deletepost': True,
-        'deletetopic': True,
-        'posttopic': True,
-        'postreply': True,
-        'locktopic': True,
-        'movetopic': True,
-        'mergetopic': True
-    }),
-    ('Member', {
-        'description': 'The Member Group',
-        'admin': False,
-        'super_mod': False,
-        'mod': False,
-        'banned': False,
-        'guest': False,
-        'editpost': True,
-        'deletepost': False,
-        'deletetopic': False,
-        'posttopic': True,
-        'postreply': True,
-        'locktopic': False,
-        'movetopic': False,
-        'mergetopic': False
-    }),
-    ('Banned', {
-        'description': 'The Banned Group',
-        'admin': False,
-        'super_mod': False,
-        'mod': False,
-        'banned': True,
-        'guest': False,
-        'editpost': False,
-        'deletepost': False,
-        'deletetopic': False,
-        'posttopic': False,
-        'postreply': False,
-        'locktopic': False,
-        'movetopic': False,
-        'mergetopic': False
-    }),
-    ('Guest', {
-        'description': 'The Guest Group',
-        'admin': False,
-        'super_mod': False,
-        'mod': False,
-        'banned': False,
-        'guest': True,
-        'editpost': False,
-        'deletepost': False,
-        'deletetopic': False,
-        'posttopic': False,
-        'postreply': False,
-        'locktopic': False,
-        'movetopic': False,
-        'mergetopic': False
-    })
-))
+def create_default_settings():
+    from flaskbb.fixtures.settings import fixture
+
+    for settingsgroup in fixture:
+
+        group = SettingsGroup(
+            key=settingsgroup[0],
+            name=settingsgroup[1]['name'],
+            description=settingsgroup[1]['description']
+        )
+
+        group.save()
+
+        for settings in settingsgroup[1]['settings']:
+            setting = Setting(
+                key=settings[0],
+                value=settings[1]['value'],
+                value_type=settings[1]['value_type'],
+                input_type=settings[1]['input_type'],
+                name=settings[1]['name'],
+                description=settings[1]['description'],
+                extra=settings[1].get('extra', ""),     # Optional field
+
+                settingsgroup=group.key
+            )
+            setting.save()
 
 
 def create_default_groups():
     """
     This will create the 5 default groups
     """
+    from flaskbb.fixtures.groups import fixture
     result = []
-    for key, value in GROUPS.items():
+    for key, value in fixture.items():
         group = Group(name=key)
 
         for k, v in value.items():
@@ -168,6 +97,7 @@ def create_welcome_forum():
 def create_test_data():
 
     create_default_groups()
+    create_default_settings()
 
     # create 5 users
     for u in range(1, 6):

+ 3 - 1
manage.py

@@ -21,7 +21,8 @@ from flask.ext.migrate import MigrateCommand
 from flaskbb import create_app
 from flaskbb.extensions import db
 from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
-                                    create_admin_user, create_default_groups)
+                                    create_admin_user, create_default_groups,
+                                    create_default_settings)
 
 # Use the development configuration if available
 try:
@@ -76,6 +77,7 @@ def createall(dropdb=False, createdb=False):
 
     app.logger.info("Creating test data...")
     create_test_data()
+    #create_default_settings()
 
 
 @manager.option('-u', '--username', dest='username')

+ 7 - 6
requirements.txt

@@ -2,10 +2,10 @@ Flask==0.10.1
 Flask-And-Redis==0.5
 Flask-Cache==0.13.1
 Flask-DebugToolbar==0.9.0
-Flask-Login==0.2.10
+Flask-Login==0.2.11
 Flask-Mail==0.9.0
 Flask-Migrate==1.2.0
-Flask-Plugins==1.1
+Flask-Plugins==1.3
 Flask-SQLAlchemy==1.0
 Flask-Script==2.0.5
 Flask-Themes2==0.1.3
@@ -15,7 +15,7 @@ Mako==0.9.1
 MarkupSafe==0.23
 Pygments==1.6
 SQLAlchemy==0.9.4
-WTForms==1.0.5
+WTForms==2.0
 Werkzeug==0.9.4
 Whoosh==2.6.0
 alembic==0.6.5
@@ -25,8 +25,9 @@ py==1.4.20
 pytest==2.5.2
 pytest-random==0.02
 redis==2.9.1
-simplejson==3.4.1
+simplejson==3.5.2
 wsgiref==0.1.2
 
-https://github.com/miguelgrinberg/Flask-WhooshAlchemy/tarball/master#egg=Flask-WhooshAlchemy
-https://github.com/frol/postmarkup/tarball/master#egg=postmarkup
+https://github.com/miguelgrinberg/Flask-WhooshAlchemy/tarball/master#egg
+https://github.com/frol/postmarkup/tarball/master#egg
+

+ 4 - 3
tests/unit/utils/test_populate.py

@@ -1,4 +1,5 @@
-from flaskbb.utils.populate import create_default_groups, GROUPS
+from flaskbb.utils.populate import create_default_groups
+from flaskbb.fixtures.groups import fixture
 from flaskbb.user.models import Group
 
 
@@ -9,9 +10,9 @@ def test_create_default_groups(database):
 
     create_default_groups()
 
-    assert Group.query.count() == len(GROUPS)
+    assert Group.query.count() == len(fixture)
 
-    for key, attributes in GROUPS.items():
+    for key, attributes in fixture.items():
         group = Group.query.filter_by(name=key).first()
 
         for attribute, value in attributes.items():