Browse Source

Merge pull request #39 from sh4nks/settings

Settings via Admin Panel.
sh4nks 11 years ago
parent
commit
4d8f51d3b1

+ 4 - 0
flaskbb/admin/forms.py

@@ -354,3 +354,7 @@ class CategoryForm(Form):
     def save(self):
     def save(self):
         category = Category(**self.data)
         category = Category(**self.data)
         return category.save()
         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)
                    __version__ as flask_version)
 from flask.ext.login import current_user
 from flask.ext.login import current_user
 from flask.ext.plugins import get_plugins_list, get_plugin
 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 import __version__ as flaskbb_version
 from flaskbb.forum.forms import UserSearchForm
 from flaskbb.forum.forms import UserSearchForm
@@ -23,6 +24,7 @@ from flaskbb.utils.decorators import admin_required
 from flaskbb.extensions import db
 from flaskbb.extensions import db
 from flaskbb.user.models import User, Group
 from flaskbb.user.models import User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 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,
 from flaskbb.admin.forms import (AddUserForm, EditUserForm, AddGroupForm,
                                  EditGroupForm, EditForumForm, AddForumForm,
                                  EditGroupForm, EditForumForm, AddForumForm,
                                  CategoryForm)
                                  CategoryForm)
@@ -47,6 +49,52 @@ def overview():
                            post_count=post_count)
                            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.route("/users", methods=['GET', 'POST'])
 @admin_required
 @admin_required
 def users():
 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
 from flaskbb.auth.views import auth
 # Import the admin blueprint
 # Import the admin blueprint
 from flaskbb.admin.views import admin
 from flaskbb.admin.views import admin
+from flaskbb.admin.models import Setting
 # Import the forum blueprint
 # Import the forum blueprint
 from flaskbb.forum.views import forum
 from flaskbb.forum.views import forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
@@ -59,6 +60,7 @@ def create_app(config=None):
     configure_before_handlers(app)
     configure_before_handlers(app)
     configure_errorhandlers(app)
     configure_errorhandlers(app)
     configure_logging(app)
     configure_logging(app)
+    update_settings_from_db(app)
 
 
     return app
     return app
 
 
@@ -131,6 +133,12 @@ def configure_extensions(app):
     login_manager.init_app(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):
 def configure_template_filters(app):
     """
     """
     Configures the template filters
     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
     # Where to logger should send the emails to
     ADMINS = ["admin@example.org"]
     ADMINS = ["admin@example.org"]
 
 
+    ## Flask-And-Redis
+    REDIS_ENABLED = False
+    REDIS_HOST = 'localhost'
+    REDIS_PORT = 6379
+    REDIS_DB = 0
+
     ## App specific configs
     ## App specific configs
     # Pagination
     # Pagination
     # How many posts per page are displayed
     # 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
     # The length of the topic title in characters on the index
     TITLE_LENGTH = 15
     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
     # The days for how long the forum should deal with unread topics
     # 0 - Disable it
     # 0 - Disable it
     TRACKER_LENGTH = 7
     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">
           <div class="panel-heading">
             <h3 class="panel-title">News</h3>
             <h3 class="panel-title">News</h3>
           </div>
           </div>
-          <div class="panel-body">
+          <div class="panel-body" style="padding-top: 0px">
+
           {% for topic in news %}
           {% for topic in news %}
             <h1><a href="{{ topic.url }}">{{ topic.title }}</a></h1>
             <h1><a href="{{ topic.url }}">{{ topic.title }}</a></h1>
             <ul class="portal-info">
             <ul class="portal-info">
@@ -96,7 +97,6 @@
             <div class="portal-content">
             <div class="portal-content">
                 {{ topic.first_post.content | markup | safe }}<br />
                 {{ topic.first_post.content | markup | safe }}<br />
             </div>
             </div>
-
             {% if not loop.last %}<hr>{% endif %}
             {% if not loop.last %}<hr>{% endif %}
           {% endfor %}
           {% 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.utils.helpers import render_template
 from flaskbb.forum.models import Topic, Post
 from flaskbb.forum.models import Topic, Post
 from flaskbb.user.models import User
 from flaskbb.user.models import User
 from flaskbb.utils.helpers import time_diff, get_online_users
 from flaskbb.utils.helpers import time_diff, get_online_users
 
 
 
 
-FORUM_IDS = [1, 2]
-
 portal = Blueprint("portal", __name__, template_folder="templates")
 portal = Blueprint("portal", __name__, template_folder="templates")
 
 
 
 
@@ -16,7 +15,14 @@ def inject_portal_link():
 
 
 @portal.route("/")
 @portal.route("/")
 def index():
 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)
     recent_topics = Topic.query.order_by(Topic.date_created).limit(5).offset(0)
 
 
     user_count = User.query.count()
     user_count = User.query.count()

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

@@ -1,23 +1,19 @@
 {% extends theme("layout.html") %}
 {% extends theme("layout.html") %}
 {% block content %}
 {% 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 %}
 {% endblock %}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,32 +1,40 @@
 {% set page_title = title %}
 {% set page_title = title %}
-{% set active_forum_nav=True %}
+{% set active_admin_user_nav=True %}
 
 
 {% extends theme("admin/admin_layout.html") %}
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
 {% 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">
+<div class="col-md-3">
-    {{ form.hidden_tag() }}
+    <ul class="nav nav-pills nav-stacked">
-    <legend class="">{{ title }}</legend>
+        {{ navlink('admin.users', "Manage Users") }}
-        {{ horizontal_field(form.username) }}
+        {{ navlink('admin.add_user', "Add User") }}
-        {{ horizontal_field(form.email) }}
+    </ul>
-        {{ horizontal_field(form.password) }}
+</div><!--/.col-md-3 -->
-        {{ 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-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 %}
 {% endblock %}

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

@@ -1,56 +1,64 @@
 {% extends theme("admin/admin_layout.html") %}
 {% extends theme("admin/admin_layout.html") %}
 {% block admin_content %}
 {% block admin_content %}
-{% from theme('macros.html') import render_pagination %}
+{% from theme('macros.html') import render_pagination, render_field, group_field, navlink with context %}
-{% from theme("macros.html") import render_field, group_field %}
 
 
-<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">
+<div class="col-md-9">
-    {{ render_pagination(users, url_for('admin.users')) }}
+    <legend>Manage Users</legend>
-</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">
+    <div class="pull-left" style="padding-bottom: 10px">
-    <thead>
+        {{ render_pagination(users, url_for('admin.users')) }}
-        <tr>
+    </div>
-            <th>#</th>
+    <div class="pull-right" style="padding-bottom: 10px">
-            <th>Username</th>
+        <form role="form" method="post">
-            <th>Posts</th>
+            <div class="input-group">
-            <th>Date registered</th>
+                {{ search_form.hidden_tag() }}
-            <th>Group</th>
+                {{ group_field(search_form.search_query) }}
-            <th>Manage</th>
+          <span class="input-group-btn">
-        </tr>
+            <button class="btn btn-primary" type="button">Search</button>
-    </thead>
+          </span>
-    <tbody>
+            </div>
-        {% for user in users.items %}
+        </form>
-            <tr>
+    </div>
-                <td>{{ user.id }}</td>
+
-                <td><a href="{{ url_for('user.profile', username=user.username) }}">{{ user.username }}</a></td>
+    <table class="table table-bordered">
-                <td>{{ user.post_count }}</td>
+        <thead>
-                <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>
             <tr>
-                <td colspan="6">
+                <th>#</th>
-                    No users found matching your search query
+                <th>Username</th>
-                </td>
+                <th>Posts</th>
+                <th>Date registered</th>
+                <th>Group</th>
+                <th>Manage</th>
             </tr>
             </tr>
-        {% endfor %}
+        </thead>
-    </tbody>
+        <tbody>
-</table>
+            {% 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 %}
 {% endblock %}

+ 2 - 2
flaskbb/templates/layout.html

@@ -43,14 +43,14 @@
                     </div>
                     </div>
                     <div class="collapse navbar-collapse navbar-ex1-collapse">
                     <div class="collapse navbar-collapse navbar-ex1-collapse">
                         <ul class="nav navbar-nav">
                         <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 #}
                             {# 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.index', name='Forum', icon='fa fa-comment', active=active_forum_nav) }}
                             {{ topnav(endpoint='forum.memberlist', name='Memberlist', icon='fa fa-user') }}
                             {{ topnav(endpoint='forum.memberlist', name='Memberlist', icon='fa fa-user') }}
                             {{ topnav(endpoint='forum.search', name='Search', icon='fa fa-search') }}
                             {{ topnav(endpoint='forum.search', name='Search', icon='fa fa-search') }}
 
 
-                            {{ emit_event("after-last-navigation-element") | safe }}
+                            {{ emit_event("after-last-navigation-element") }}
                         </ul>
                         </ul>
 
 
                     {% if current_user and current_user.is_authenticated() %}
                     {% if current_user and current_user.is_authenticated() %}

+ 2 - 2
flaskbb/templates/macros.html

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

+ 30 - 100
flaskbb/utils/populate.py

@@ -9,118 +9,47 @@
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
 from datetime import datetime
 from datetime import datetime
-from collections import OrderedDict
 
 
+from flaskbb.admin.models import Setting, SettingsGroup
 from flaskbb.user.models import User, Group
 from flaskbb.user.models import User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category
 from flaskbb.forum.models import Post, Topic, Forum, Category
 
 
 
 
-GROUPS = OrderedDict((
+def create_default_settings():
-    ('Administrator', {
+    from flaskbb.fixtures.settings import fixture
-        'description': 'The Administrator Group',
+
-        'admin': True,
+    for settingsgroup in fixture:
-        'super_mod': False,
+
-        'mod': False,
+        group = SettingsGroup(
-        'banned': False,
+            key=settingsgroup[0],
-        'guest': False,
+            name=settingsgroup[1]['name'],
-        'editpost': True,
+            description=settingsgroup[1]['description']
-        'deletepost': True,
+        )
-        'deletetopic': True,
+
-        'posttopic': True,
+        group.save()
-        'postreply': True,
+
-        'locktopic': True,
+        for settings in settingsgroup[1]['settings']:
-        'movetopic': True,
+            setting = Setting(
-        'mergetopic': True
+                key=settings[0],
-    }),
+                value=settings[1]['value'],
-    ('Super Moderator', {
+                value_type=settings[1]['value_type'],
-        'description': 'The Super Moderator Group',
+                input_type=settings[1]['input_type'],
-        'admin': False,
+                name=settings[1]['name'],
-        'super_mod': True,
+                description=settings[1]['description'],
-        'mod': False,
+                extra=settings[1].get('extra', ""),     # Optional field
-        'banned': False,
+
-        'guest': False,
+                settingsgroup=group.key
-        'editpost': True,
+            )
-        'deletepost': True,
+            setting.save()
-        '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_groups():
 def create_default_groups():
     """
     """
     This will create the 5 default groups
     This will create the 5 default groups
     """
     """
+    from flaskbb.fixtures.groups import fixture
     result = []
     result = []
-    for key, value in GROUPS.items():
+    for key, value in fixture.items():
         group = Group(name=key)
         group = Group(name=key)
 
 
         for k, v in value.items():
         for k, v in value.items():
@@ -168,6 +97,7 @@ def create_welcome_forum():
 def create_test_data():
 def create_test_data():
 
 
     create_default_groups()
     create_default_groups()
+    create_default_settings()
 
 
     # create 5 users
     # create 5 users
     for u in range(1, 6):
     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 import create_app
 from flaskbb.extensions import db
 from flaskbb.extensions import db
 from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
 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
 # Use the development configuration if available
 try:
 try:
@@ -76,6 +77,7 @@ def createall(dropdb=False, createdb=False):
 
 
     app.logger.info("Creating test data...")
     app.logger.info("Creating test data...")
     create_test_data()
     create_test_data()
+    #create_default_settings()
 
 
 
 
 @manager.option('-u', '--username', dest='username')
 @manager.option('-u', '--username', dest='username')

+ 7 - 6
requirements.txt

@@ -2,10 +2,10 @@ Flask==0.10.1
 Flask-And-Redis==0.5
 Flask-And-Redis==0.5
 Flask-Cache==0.13.1
 Flask-Cache==0.13.1
 Flask-DebugToolbar==0.9.0
 Flask-DebugToolbar==0.9.0
-Flask-Login==0.2.10
+Flask-Login==0.2.11
 Flask-Mail==0.9.0
 Flask-Mail==0.9.0
 Flask-Migrate==1.2.0
 Flask-Migrate==1.2.0
-Flask-Plugins==1.1
+Flask-Plugins==1.3
 Flask-SQLAlchemy==1.0
 Flask-SQLAlchemy==1.0
 Flask-Script==2.0.5
 Flask-Script==2.0.5
 Flask-Themes2==0.1.3
 Flask-Themes2==0.1.3
@@ -15,7 +15,7 @@ Mako==0.9.1
 MarkupSafe==0.23
 MarkupSafe==0.23
 Pygments==1.6
 Pygments==1.6
 SQLAlchemy==0.9.4
 SQLAlchemy==0.9.4
-WTForms==1.0.5
+WTForms==2.0
 Werkzeug==0.9.4
 Werkzeug==0.9.4
 Whoosh==2.6.0
 Whoosh==2.6.0
 alembic==0.6.5
 alembic==0.6.5
@@ -25,8 +25,9 @@ py==1.4.20
 pytest==2.5.2
 pytest==2.5.2
 pytest-random==0.02
 pytest-random==0.02
 redis==2.9.1
 redis==2.9.1
-simplejson==3.4.1
+simplejson==3.5.2
 wsgiref==0.1.2
 wsgiref==0.1.2
 
 
-https://github.com/miguelgrinberg/Flask-WhooshAlchemy/tarball/master#egg=Flask-WhooshAlchemy
+https://github.com/miguelgrinberg/Flask-WhooshAlchemy/tarball/master#egg
-https://github.com/frol/postmarkup/tarball/master#egg=postmarkup
+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
 from flaskbb.user.models import Group
 
 
 
 
@@ -9,9 +10,9 @@ def test_create_default_groups(database):
 
 
     create_default_groups()
     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()
         group = Group.query.filter_by(name=key).first()
 
 
         for attribute, value in attributes.items():
         for attribute, value in attributes.items():