Browse Source

Merge branch 'hooks'

* hooks:
  Update docstrings
  Add hook before/after hooks for post content
  Add topic hooks
  Add more template hooks
  Update hooks
  Add hooks for adding new fields to reply form
  Add hook for adding shell context processors
  code style fixes
  Add hook for registering admin panels
Peter Justin 7 years ago
parent
commit
ad6aef8783

+ 41 - 11
docs/hooks.rst

@@ -48,10 +48,30 @@ The hooks below are listed in the order they are called.
 FlaskBB CLI Hooks
 ~~~~~~~~~~~~~~~~~
 
-These are hooks are only invoked when using the ``flaskbb``
+These hooks are only invoked when using the ``flaskbb``
 CLI.
 
 .. autofunction:: flaskbb_cli
+.. autofunction:: flaskbb_shell_context
+
+
+FlaskBB Event Hooks
+~~~~~~~~~~~~~~~~~~~
+
+.. autofunction:: flaskbb_event_post_save_before
+.. autofunction:: flaskbb_event_post_save_after
+.. autofunction:: flaskbb_event_topic_save_before
+.. autofunction:: flaskbb_event_topic_save_after
+
+
+FlaskBB Form Hooks
+~~~~~~~~~~~~~~~~~~
+
+.. autofunction:: flaskbb_form_new_post_save
+.. autofunction:: flaskbb_form_new_post
+
+.. autofunction:: flaskbb_form_new_topic
+.. autofunction:: flaskbb_form_new_topic_save
 
 
 Template Hooks
@@ -63,15 +83,25 @@ Template Hooks
     hidden CSRF token field and before an submit field.
 
 
-.. autofunction:: flaskbb_tpl_before_navigation
-.. autofunction:: flaskbb_tpl_after_navigation
-.. autofunction:: flaskbb_tpl_before_user_nav_loggedin
-.. autofunction:: flaskbb_tpl_after_user_nav_loggedin
-.. autofunction:: flaskbb_tpl_before_registration_form
-.. autofunction:: flaskbb_tpl_after_registration_form
-.. autofunction:: flaskbb_tpl_before_user_details_form
-.. autofunction:: flaskbb_tpl_after_user_details_form
+.. autofunction:: flaskbb_tpl_navigation_before
+.. autofunction:: flaskbb_tpl_navigation_after
+.. autofunction:: flaskbb_tpl_user_nav_loggedin_before
+.. autofunction:: flaskbb_tpl_user_nav_loggedin_after
+.. autofunction:: flaskbb_tpl_form_registration_before
+.. autofunction:: flaskbb_tpl_form_registration_after
+.. autofunction:: flaskbb_tpl_form_user_details_before
+.. autofunction:: flaskbb_tpl_form_user_details_after
+.. autofunction:: flaskbb_tpl_form_new_post_before
+.. autofunction:: flaskbb_tpl_form_new_post_after
+.. autofunction:: flaskbb_tpl_form_new_topic_before
+.. autofunction:: flaskbb_tpl_form_new_topic_after
 .. autofunction:: flaskbb_tpl_profile_settings_menu
 .. autofunction:: flaskbb_tpl_profile_sidebar_stats
-.. autofunction:: flaskbb_tpl_before_post_author_info
-.. autofunction:: flaskbb_tpl_after_post_author_info
+.. autofunction:: flaskbb_tpl_post_author_info_before
+.. autofunction:: flaskbb_tpl_post_author_info_after
+.. autofunction:: flaskbb_tpl_post_content_before
+.. autofunction:: flaskbb_tpl_post_content_after
+.. autofunction:: flaskbb_tpl_post_menu_before
+.. autofunction:: flaskbb_tpl_post_menu_after
+.. autofunction:: flaskbb_tpl_topic_controls
+.. autofunction:: flaskbb_tpl_admin_settings_menu

+ 4 - 0
flaskbb/cli/main.py

@@ -62,6 +62,10 @@ class FlaskBBGroup(FlaskGroup):
                 "Error while loading CLI Plugins",
                 exc_info=traceback.format_exc()
             )
+        else:
+            shell_context_processors = app.pluggy.hook.flaskbb_shell_context()
+            for p in shell_context_processors:
+                app.shell_context_processor(p)
 
     def get_command(self, ctx, name):
         self._load_flaskbb_plugins(ctx)

+ 6 - 0
flaskbb/forum/forms.py

@@ -13,6 +13,8 @@ from flask_wtf import FlaskForm
 from wtforms import (TextAreaField, StringField, SelectMultipleField,
                      BooleanField, SubmitField)
 from wtforms.validators import DataRequired, Optional, Length
+
+from flask import current_app
 from flask_babelplus import lazy_gettext as _
 
 from flaskbb.forum.models import Topic, Post, Report, Forum
@@ -48,6 +50,8 @@ class ReplyForm(FlaskForm):
 
         if self.track_topic.data:
             user.track_topic(topic)
+
+        current_app.pluggy.hook.flaskbb_form_new_post_save(form=self)
         return post.save(user=user, topic=topic)
 
 
@@ -70,6 +74,8 @@ class NewTopicForm(ReplyForm):
 
         if self.track_topic.data:
             user.track_topic(topic)
+
+        current_app.pluggy.hook.flaskbb_form_new_topic_save(form=self)
         return topic.save(user=user, forum=forum, post=post)
 
 

+ 21 - 8
flaskbb/forum/models.py

@@ -201,12 +201,14 @@ class Post(HideableCRUDMixin, db.Model):
         :param user: The user who has created the post
         :param topic: The topic in which the post was created
         """
+        current_app.pluggy.hook.flaskbb_event_post_save_before(post=self)
+
         # update/edit the post
         if self.id:
             db.session.add(self)
             db.session.commit()
-            current_app.pluggy.hook.flaskbb_event_after_post(post=self,
-                                                             is_new=False)
+            current_app.pluggy.hook.flaskbb_event_post_save_after(post=self,
+                                                                  is_new=False)
             return self
 
         # Adding a new post
@@ -236,8 +238,8 @@ class Post(HideableCRUDMixin, db.Model):
             # And commit it!
             db.session.add(topic)
             db.session.commit()
-            current_app.pluggy.hook.flaskbb_event_after_post(post=self,
-                                                             is_new=True)
+            current_app.pluggy.hook.flaskbb_event_post_save_after(post=self,
+                                                                  is_new=True)
             return self
 
     def delete(self):
@@ -346,7 +348,9 @@ class Post(HideableCRUDMixin, db.Model):
             topic_post_clauses = clauses + [
                 Post.topic_id == self.topic.id,
             ]
-            self.topic.post_count = Post.query.filter(*topic_post_clauses).count()
+            self.topic.post_count = Post.query.filter(
+                *topic_post_clauses
+            ).count()
 
         forum_post_clauses = clauses + [
             Post.topic_id == Topic.id,
@@ -354,7 +358,9 @@ class Post(HideableCRUDMixin, db.Model):
             Topic.hidden != True,
         ]
 
-        self.topic.forum.post_count = Post.query.filter(*forum_post_clauses).count()
+        self.topic.forum.post_count = Post.query.filter(
+            *forum_post_clauses
+        ).count()
 
     def _restore_post_to_topic(self):
         last_unhidden_post = Post.query.filter(
@@ -368,8 +374,8 @@ class Post(HideableCRUDMixin, db.Model):
             self.topic.last_post = self
             self.second_last_post = last_unhidden_post
 
-            # if we're the newest in the topic again, we might be the newest in the forum again
-            # only set if our parent topic isn't hidden
+            # if we're the newest in the topic again, we might be the newest
+            # in the forum again only set if our parent topic isn't hidden
             if (
                 not self.topic.hidden and
                 (
@@ -627,11 +633,15 @@ class Topic(HideableCRUDMixin, db.Model):
         :param forum: The forum where the topic is stored
         :param post: The post object which is connected to the topic
         """
+        current_app.pluggy.hook.flaskbb_event_topic_save_before(topic=self)
 
         # Updates the topic
         if self.id:
             db.session.add(self)
             db.session.commit()
+            current_app.pluggy.hook.flaskbb_event_topic_save_after(
+                topic=self, is_new=False
+            )
             return self
 
         # Set the forum and user id
@@ -655,6 +665,9 @@ class Topic(HideableCRUDMixin, db.Model):
         # Update the topic count
         forum.topic_count += 1
         db.session.commit()
+
+        current_app.pluggy.hook.flaskbb_event_topic_save_after(topic=self,
+                                                               is_new=True)
         return self
 
     def delete(self, users=None):

+ 29 - 14
flaskbb/forum/views.py

@@ -58,8 +58,8 @@ class ForumIndex(MethodView):
             online_users = User.query.filter(User.lastseen >= time_diff()
                                              ).count()
 
-            # Because we do not have server side sessions, we cannot check if there
-            # are online guests
+            # Because we do not have server side sessions,
+            # we cannot check if there are online guests
             online_guests = None
         else:
             online_users = len(get_online_users())
@@ -156,19 +156,22 @@ class ViewTopic(MethodView):
         # Update the topicsread status if the user hasn't read it
         forumsread = None
         if current_user.is_authenticated:
-            forumsread = ForumsRead.query.\
-                filter_by(user_id=current_user.id,
-                          forum_id=topic.forum_id).first()
+            forumsread = ForumsRead.query.filter_by(
+                user_id=current_user.id,
+                forum_id=topic.forum_id).first()
 
         topic.update_read(real(current_user), topic.forum, forumsread)
 
         # fetch the posts in the topic
-        posts = Post.query.\
-            outerjoin(User, Post.user_id == User.id).\
-            filter(Post.topic_id == topic.id).\
-            add_entity(User).\
-            order_by(Post.id.asc()).\
-            paginate(page, flaskbb_config['POSTS_PER_PAGE'], False)
+        posts = Post.query.outerjoin(
+            User, Post.user_id == User.id
+        ).filter(
+            Post.topic_id == topic.id
+        ).add_entity(
+            User
+        ).order_by(
+            Post.id.asc()
+        ).paginate(page, flaskbb_config['POSTS_PER_PAGE'], False)
 
         # Abort if there are no posts on this page
         if len(posts.items) == 0:
@@ -210,7 +213,6 @@ class ViewTopic(MethodView):
 
 class NewTopic(MethodView):
     decorators = [login_required]
-    form = NewTopicForm
 
     def get(self, forum_id, slug=None):
         forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
@@ -238,6 +240,10 @@ class NewTopic(MethodView):
                 'forum/new_topic.html', forum=forum_instance, form=form
             )
 
+    def form(self):
+        current_app.pluggy.hook.flaskbb_form_new_topic(form=NewTopicForm)
+        return NewTopicForm()
+
 
 class ManageForum(MethodView):
     decorators = [allows.requires(IsAtleastModeratorInForum()), login_required]
@@ -401,10 +407,10 @@ class ManageForum(MethodView):
 
 class NewPost(MethodView):
     decorators = [allows.requires(CanPostReply), login_required]
-    form = ReplyForm
 
     def get(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
+
         return render_template(
             'forum/new_post.html', topic=topic, form=self.form()
         )
@@ -412,6 +418,7 @@ class NewPost(MethodView):
     def post(self, topic_id, slug=None):
         topic = Topic.query.filter_by(id=topic_id).first_or_404()
         form = self.form()
+
         if form.validate_on_submit():
             if 'preview' in request.form:
                 return render_template(
@@ -426,6 +433,10 @@ class NewPost(MethodView):
 
         return render_template('forum/new_post.html', topic=topic, form=form)
 
+    def form(self):
+        current_app.pluggy.hook.flaskbb_form_new_post(form=ReplyForm)
+        return ReplyForm()
+
 
 class ReplyPost(MethodView):
     decorators = [allows.requires(CanPostReply), login_required]
@@ -461,11 +472,11 @@ class ReplyPost(MethodView):
 
 class EditPost(MethodView):
     decorators = [allows.requires(CanEditPost), login_required]
-    form = ReplyForm
 
     def get(self, post_id):
         post = Post.query.filter_by(id=post_id).first_or_404()
         form = self.form(obj=post)
+
         return render_template(
             'forum/new_post.html', topic=post.topic, form=form, edit_mode=True
         )
@@ -494,6 +505,10 @@ class EditPost(MethodView):
             'forum/new_post.html', topic=post.topic, form=form, edit_mode=True
         )
 
+    def form(self):
+        current_app.pluggy.hook.flaskbb_form_new_post(form=ReplyForm)
+        return ReplyForm()
+
 
 class ReportView(MethodView):
     decorators = [login_required]

+ 14 - 0
flaskbb/management/__init__.py

@@ -1,3 +1,17 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.management
+    ~~~~~~~~~~~~~~~~~~
+
+    This module contains models, forms and views relevant
+    for managing FlaskBB
+
+    :copyright: (c) 2014 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
 import logging
 
+# force plugins to be loaded
+from . import plugins
+
 logger = logging.getLogger(__name__)

+ 32 - 0
flaskbb/management/plugins.py

@@ -0,0 +1,32 @@
+from itertools import chain
+from pluggy import HookimplMarker
+from flask_allows import Permission
+
+
+impl = HookimplMarker('flaskbb')
+
+
+@impl(hookwrapper=True, tryfirst=True)
+def flaskbb_tpl_admin_settings_menu(user):
+    """
+    Flattens the lists that come back from the hook
+    into a single iterable that can be used to populate
+    the menu
+    """
+    from flaskbb.utils.requirements import IsAdmin  # noqa: circular dependency
+    results = [
+        ('management.overview', 'Overview', 'fa fa-tasks'),
+        ('management.unread_reports', 'Reports', 'fa fa-flag'),
+        ('management.users', 'Users', 'fa fa-user')
+    ]
+
+    if Permission(IsAdmin, identity=user):
+        results.extend([
+            ('management.groups', 'Groups', 'fa fa-users'),
+            ('management.forums', 'Forums', 'fa fa-comments'),
+            ('management.settings', 'Settings', 'fa fa-cogs'),
+            ('management.plugins', 'Plugins', 'fa fa-puzzle-piece')
+        ])
+
+    outcome = yield
+    outcome.force_result(chain(results, *outcome.get_result()))

+ 294 - 24
flaskbb/plugins/spec.py

@@ -15,7 +15,6 @@ spec = HookspecMarker('flaskbb')
 
 
 # Setup Hooks
-
 @spec
 def flaskbb_extensions(app):
     """Hook for initializing any plugin loaded extensions."""
@@ -33,44 +32,175 @@ def flaskbb_load_migrations():
 
 @spec
 def flaskbb_load_blueprints(app):
-    """Hook for registering blueprints."""
+    """Hook for registering blueprints.
+
+    :param app: The application object.
+    """
 
 
 @spec
 def flaskbb_request_processors(app):
-    """Hook for registering pre/post request processors."""
+    """Hook for registering pre/post request processors.
+
+    :param app: The application object.
+    """
 
 
 @spec
 def flaskbb_errorhandlers(app):
-    """Hook for registering error handlers."""
+    """Hook for registering error handlers.
+
+    :param app: The application object.
+    """
 
 
 @spec
 def flaskbb_jinja_directives(app):
-    """Hook for registering jinja filters, context processors, etc."""
+    """Hook for registering jinja filters, context processors, etc.
+
+    :param app: The application object.
+    """
 
 
 @spec
 def flaskbb_additional_setup(app, pluggy):
     """Hook for any additional setup a plugin wants to do after all other
     application setup has finished.
+
+    For example, you could apply a WSGI middleware::
+
+        @impl
+        def flaskbb_additional_setup(app):
+            app.wsgi_app = ProxyFix(app.wsgi_app)
+
+    :param app: The application object.
+    :param pluggy: The pluggy object.
+    """
+
+
+@spec
+def flaskbb_cli(cli, app):
+    """Hook for registering CLI commands.
+
+    For example::
+
+        @impl
+        def flaskbb_cli(cli):
+            @cli.command()
+            def testplugin():
+                click.echo("Hello Testplugin")
+
+            return testplugin
+
+    :param app: The application object.
+    :param cli: The FlaskBBGroup CLI object.
+    """
+
+
+@spec
+def flaskbb_shell_context():
+    """Hook for registering shell context handlers
+    Expected to return a single callable function that returns a dictionary or
+    iterable of key value pairs.
+    """
+
+
+# Event hooks
+@spec
+def flaskbb_event_post_save_before(post):
+    """Hook for handling a post before it has been saved.
+
+    :param flaskbb.forum.models.Post post: The post which triggered the event.
+    """
+
+
+@spec
+def flaskbb_event_post_save_after(post, is_new):
+    """Hook for handling a post after it has been saved.
+
+    :param flaskbb.forum.models.Post post: The post which triggered the event.
+    :param bool is_new: True if the post is new, False if it is an edit.
+    """
+
+
+@spec
+def flaskbb_event_topic_save_before(topic):
+    """Hook for handling a topic before it has been saved.
+
+    :param flaskbb.forum.models.Topic topic: The topic which triggered the
+                                             event.
     """
 
 
 @spec
-def flaskbb_cli(cli):
-    """Hook for registering CLI commands."""
+def flaskbb_event_topic_save_after(topic, is_new):
+    """Hook for handling a topic after it has been saved.
+
+    :param flaskbb.forum.models.Topic topic: The topic which triggered the
+                                             event.
+    :param bool is_new: True if the topic is new, False if it is an edit.
+    """
 
 
 @spec
 def flaskbb_user_registered(username):
     """Hook for handling events after a user is registered"""
 
-# Template Hooks
+
+# Form hooks
+@spec
+def flaskbb_form_new_post(form):
+    """Hook for modifying the :class:`~flaskbb.forum.forms.ReplyForm`.
+
+    For example::
+
+        @impl
+        def flaskbb_form_new_post(form):
+            form.example = TextField("Example Field", validators=[
+                DataRequired(message="This field is required"),
+                Length(min=3, max=50)])
+
+    :param form: The :class:`~flaskbb.forum.forms.ReplyForm` class.
+    """
+
+
+@spec
+def flaskbb_form_new_post_save(form):
+    """Hook for modifying the :class:`~flaskbb.forum.forms.ReplyForm`.
+
+    This hook is called while populating the post object with
+    the data from the form. The post object will be saved after the hook
+    call.
+
+    :param form: The form object.
+    :param post: The post object.
+    """
+
+
+@spec
+def flaskbb_form_new_topic(form):
+    """Hook for modifying the :class:`~flaskbb.forum.forms.NewTopicForm`
+
+    :param form: The :class:`~flaskbb.forum.forms.NewTopicForm` class.
+    """
+
 
 @spec
-def flaskbb_tpl_before_navigation():
+def flaskbb_form_new_topic_save(form, topic):
+    """Hook for modifying the :class:`~flaskbb.forum.forms.NewTopicForm`.
+
+    This hook is called while populating the topic object with
+    the data from the form. The topic object will be saved after the hook
+    call.
+
+    :param form: The form object.
+    :param topic: The topic object.
+    """
+
+
+# Template Hooks
+@spec
+def flaskbb_tpl_navigation_before():
     """Hook for registering additional navigation items.
 
     in :file:`templates/layout.html`.
@@ -78,7 +208,7 @@ def flaskbb_tpl_before_navigation():
 
 
 @spec
-def flaskbb_tpl_after_navigation():
+def flaskbb_tpl_navigation_after():
     """Hook for registering additional navigation items.
 
     in :file:`templates/layout.html`.
@@ -86,7 +216,7 @@ def flaskbb_tpl_after_navigation():
 
 
 @spec
-def flaskbb_tpl_before_user_nav_loggedin():
+def flaskbb_tpl_user_nav_loggedin_before():
     """Hook for registering additional user navigational items
     which are only shown when a user is logged in.
 
@@ -95,7 +225,7 @@ def flaskbb_tpl_before_user_nav_loggedin():
 
 
 @spec
-def flaskbb_tpl_after_user_nav_loggedin():
+def flaskbb_tpl_user_nav_loggedin_after():
     """Hook for registering additional user navigational items
     which are only shown when a user is logged in.
 
@@ -104,38 +234,46 @@ def flaskbb_tpl_after_user_nav_loggedin():
 
 
 @spec
-def flaskbb_tpl_before_registration_form():
+def flaskbb_tpl_form_registration_before(form):
     """This hook is emitted in the Registration form **before** the first
     input field but after the hidden CSRF token field.
 
     in :file:`templates/auth/register.html`.
+
+    :param form: The form object.
     """
 
 
 @spec
-def flaskbb_tpl_after_registration_form():
+def flaskbb_tpl_form_registration_after(form):
     """This hook is emitted in the Registration form **after** the last
     input field but before the submit field.
 
     in :file:`templates/auth/register.html`.
+
+    :param form: The form object.
     """
 
 
 @spec
-def flaskbb_tpl_before_user_details_form():
+def flaskbb_tpl_form_user_details_before(form):
     """This hook is emitted in the Change User Details form **before** an
     input field is rendered.
 
     in :file:`templates/user/change_user_details.html`.
+
+    :param form: The form object.
     """
 
 
 @spec
-def flaskbb_tpl_after_user_details_form():
+def flaskbb_tpl_form_user_details_after(form):
     """This hook is emitted in the Change User Details form **after** the last
     input field has been rendered but before the submit field.
 
     in :file:`templates/user/change_user_details.html`.
+
+    :param form: The form object.
     """
 
 
@@ -169,14 +307,32 @@ def flaskbb_tpl_profile_settings_menu():
     """
 
 
-# Event hooks
-
 @spec
-def flaskbb_event_after_post(post, is_new):
-    """Hook for handling a post after it has been saved.
+def flaskbb_tpl_admin_settings_menu(user):
+    """This hook is emitted in the admin panel and used to add additional
+    navigation links to the admin menu.
+
+    Implementations of this hook should return a list of tuples
+    that are view name, display text and optionally an icon.
+    The display text will be provided to the translation service so it
+    is unnecessary to supply translated text.
+
+    For example::
+
+        @impl(trylast=True)
+        def flaskbb_tpl_admin_settings_menu():
+            # only add this item if the user is an admin
+            if Permission(IsAdmin, identity=current_user):
+                return [
+                    ("myplugin.foobar", "Foobar", "fa fa-foobar")
+                ]
 
-    :param flaskbb.forum.models.Post post: The post which triggered the event
-    :param bool is_new: True if the post is new, False if it is an edit
+    Hookwrappers for this spec should not be registered as FlaskBB
+    supplies its own hookwrapper to flatten all the lists into a single list.
+
+    in :file:`templates/management/management_layout.html`
+
+    :param user: The current user object.
     """
 
 
@@ -193,7 +349,7 @@ def flaskbb_tpl_profile_sidebar_stats(user):
 
 
 @spec
-def flaskbb_tpl_before_post_author_info(user, post):
+def flaskbb_tpl_post_author_info_before(user, post):
     """This hook is emitted before the information about the
     author of a post is displayed (but after the username).
 
@@ -205,7 +361,7 @@ def flaskbb_tpl_before_post_author_info(user, post):
 
 
 @spec
-def flaskbb_tpl_after_post_author_info(user, post):
+def flaskbb_tpl_post_author_info_after(user, post):
     """This hook is emitted after the information about the
     author of a post is displayed (but after the username).
 
@@ -214,3 +370,117 @@ def flaskbb_tpl_after_post_author_info(user, post):
     :param user: The user object of the post's author.
     :param post: The post object.
     """
+
+
+@spec
+def flaskbb_tpl_post_content_before(post):
+    """Hook to do some stuff before the post content is rendered.
+
+    in :file:`templates/forum/topic.html`
+
+    :param post: The current post object.
+    """
+
+
+@spec
+def flaskbb_tpl_post_content_after(post):
+    """Hook to do some stuff after the post content is rendered.
+
+    in :file:`templates/forum/topic.html`
+
+    :param post: The current post object.
+    """
+
+
+@spec
+def flaskbb_tpl_post_menu_before(post):
+    """Hook for inserting a new item at the beginning of the post menu.
+
+    in :file:`templates/forum/topic.html`
+
+    :param post: The current post object.
+    """
+
+
+@spec
+def flaskbb_tpl_post_menu_after(post):
+    """Hook for inserting a new item at the end of the post menu.
+
+    in :file:`templates/forum/topic.html`
+
+    :param post: The current post object.
+    """
+
+
+@spec
+def flaskbb_tpl_topic_controls(topic):
+    """Hook for inserting additional topic moderation controls.
+
+    in :file:`templates/forum/topic_controls.html`
+
+    :param topic: The current topic object.
+    """
+
+
+@spec
+def flaskbb_tpl_form_new_post_before(form):
+    """Hook for inserting a new form field before the first field is
+    rendered.
+
+    For example::
+
+        @impl
+        def flaskbb_tpl_form_new_post_after(form):
+            return render_template_string(
+                \"""
+                <div class="form-group">
+                    <div class="col-md-12 col-sm-12 col-xs-12">
+                        <label>{{ form.example.label.text }}</label>
+
+                        {{ form.example(class="form-control",
+                                        placeholder=form.example.label.text) }}
+
+                        {%- for error in form.example.errors -%}
+                        <span class="help-block">{{error}}</span>
+                        {%- endfor -%}
+                    </div>
+                </div>
+                \"""
+
+    in :file:`templates/forum/new_post.html`
+
+    :param form: The form object.
+    """
+
+
+@spec
+def flaskbb_tpl_form_new_post_after(form):
+    """Hook for inserting a new form field after the last field is
+    rendered (but before the submit field).
+
+    in :file:`templates/forum/new_post.html`
+
+    :param form: The form object.
+    """
+
+
+@spec
+def flaskbb_tpl_form_new_topic_before(form):
+    """Hook for inserting a new form field before the first field is
+    rendered (but before the CSRF token).
+
+    in :file:`templates/forum/new_topic.html`
+
+    :param form: The form object.
+    """
+
+
+@spec
+def flaskbb_tpl_form_new_topic_after(form):
+    """Hook for inserting a new form field after the last field is
+    rendered (but before the submit button).
+
+    in :file:`templates/forum/new_topic.html`
+
+    :param form: The form object.
+    """

+ 2 - 2
flaskbb/templates/auth/register.html

@@ -13,7 +13,7 @@
     <div class="panel-body">
         <form class="form-horizontal" role="form" method="POST">
             {{ form.hidden_tag() }}
-            {{ run_hook('flaskbb_tpl_before_registration_form') }}
+            {{ run_hook('flaskbb_tpl_form_registration_before', form=form) }}
             {{ horizontal_field(form.username)}}
             {{ horizontal_field(form.email)}}
             {{ horizontal_field(form.password)}}
@@ -25,8 +25,8 @@
 
             {{ horizontal_field(form.language) }}
             {{ horizontal_field(form.accept_tos)}}
+            {{ run_hook('flaskbb_tpl_form_registration_after', form=form) }}
             {{ horizontal_field(form.submit)}}
-            {{ run_hook('flaskbb_tpl_after_registration_form') }}
         </form>
     </div>
 </div>

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

@@ -40,10 +40,17 @@
 
                     <div class="form-group">
                         <div class="col-md-12 col-sm-12 col-xs-12">
+
                             <div class="editor-box">
+
+                                {{ run_hook("flaskbb_tpl_form_new_post_before", form=form) }}
+
                                 <div class="editor">
                                     {{ render_quickreply(form.content, div_class="new-message", rows=7, cols=75, placeholder="", **{'data-provide': 'markdown', 'data-autofocus': 'false', 'class': 'flaskbb-editor'}) }}
                                 </div>
+
+                                {{ run_hook("flaskbb_tpl_form_new_post_after", form=form) }}
+
                                 <div class="editor-submit">
                                     {{ render_submit_field(form.submit, input_class="btn btn-success pull-right") }}
                                 </div>

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

@@ -30,9 +30,15 @@
                     <div class="form-group">
                         <div class="col-md-12 col-sm-12 col-xs-12">
                             <div class="editor-box">
+
+                                {{ run_hook("flaskbb_tpl_form_new_topic_before", form=form) }}
+
                                 <div class="editor">
                                     {{ render_quickreply(form.content, div_class="new-message", rows=7, cols=75, placeholder="", **{'data-provide': 'markdown', 'data-autofocus': 'false', 'class': 'flaskbb-editor'}) }}
                                 </div>
+
+                                {{ run_hook("flaskbb_tpl_form_new_topic_after", form=form) }}
+
                                 <div class="editor-submit">
                                     {{ render_submit_field(form.submit, input_class="btn btn-success pull-right") }}
                                 </div>

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

@@ -43,7 +43,7 @@
                     {% endif %}
                     <div class="author-title"><h5>{{ user.primary_group.name }}</h5></div>
 
-                    {{ run_hook("flaskbb_tpl_before_post_author_info", user=user, post=post) }}
+                    {{ run_hook("flaskbb_tpl_post_author_info_before", user=user, post=post) }}
 
                     {% if user.avatar %}
                         <div class="author-avatar"><img src="{{ user.avatar }}" alt="avatar"></div>
@@ -52,7 +52,7 @@
                     <div class="author-registered">{% trans %}Joined{% endtrans %}: {{ user.date_joined|format_date('%b %d %Y') }}</div>
                     <div class="author-posts">{% trans %}Posts{% endtrans %}: {{ user.post_count }}</div>
 
-                    {{ run_hook("flaskbb_tpl_after_post_author_info", user=user, post=post) }}
+                    {{ run_hook("flaskbb_tpl_post_author_info_after", user=user, post=post) }}
 
                     {% else %}
                     <!-- user deleted or guest -->
@@ -91,7 +91,13 @@
                                 {{ gettext("This post is hidden (%(when)s  by %(who)s)", who=post.hidden_by.username, when=format_date(post.hidden_at, '%b %d %Y'))}}
                             </div>
                             {% endif %}
+
+                        {{ run_hook("flaskbb_tpl_post_content_before", post=post) }}
+
                         {{ post.content|markup }}
+
+                        {{ run_hook("flaskbb_tpl_post_content_after", post=post) }}
+
                         <!-- Signature Begin -->
                         {% if flaskbb_config["SIGNATURE_ENABLED"] and post.user_id and user.signature %}
                         <div class="post-signature hidden-xs">
@@ -106,6 +112,7 @@
 
                             <!-- Report/Edit/Delete/Quote Post-->
                             <div class="post-menu pull-right">
+                            {{ run_hook("flaskbb_tpl_post_menu_before", post=post) }}
 
                             {% if current_user|post_reply(topic) %}
                             <!-- Quick quote -->
@@ -169,6 +176,7 @@
                                 {% endif %}
                             {% endif %}
 
+                            {{ run_hook("flaskbb_tpl_post_menu_after", post=post) }}
                             </div> <!-- end post-menu -->
                         </div> <!-- end footer -->
 

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

@@ -115,6 +115,8 @@
             {% else %}
             <div class="btn btn-primary"><span class="fa fa-lock fa-fw"></span> {% trans %}Locked{% endtrans %}</div>
             {% endif %}
+
+            {{ run_hook("flaskbb_tpl_topic_controls") }}
         </div>
     </div>
 {% endif %} {# end current_user.is_authenticated #}

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

@@ -101,6 +101,7 @@
 
                             <!-- Report/Edit/Delete/Quote Post-->
                             <div class="post-menu pull-right">
+                            {{ run_hook("flaskbb_tpl_post_menu_before", post=post) }}
 
                             {% if current_user|post_reply(topic) %}
                             <!-- Quick quote -->
@@ -164,6 +165,7 @@
                                 {% endif %}
                             {% endif %}
 
+                            {{ run_hook("flaskbb_tpl_post_menu_after", post=post) }}
                             </div> <!-- end post-menu -->
                         </div> <!-- end footer -->
 

+ 4 - 4
flaskbb/templates/layout.html

@@ -73,18 +73,18 @@
                         <ul class="nav navbar-nav forum-nav">
                             {%- from theme("macros.html") import is_active, topnav with context -%}
 
-                            {{ run_hook("flaskbb_tpl_before_navigation") }}
+                            {{ run_hook("flaskbb_tpl_navigation_before") }}
                             {{ 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') }}
-                            {{ run_hook("flaskbb_tpl_after_navigation") }}
+                            {{ run_hook("flaskbb_tpl_navigation_after") }}
                         </ul>
 
                         <!-- navbar right -->
                         <ul class="nav navbar-nav navbar-right">
                             {% if current_user and current_user.is_authenticated %}
 
-                            {{ run_hook("flaskbb_tpl_before_user_nav_loggedin") }}
+                            {{ run_hook("flaskbb_tpl_user_nav_loggedin_before") }}
 
                             <!-- User Menu -->
                             <li>
@@ -108,7 +108,7 @@
                                 </div>
                             </li>
 
-                            {{ run_hook("flaskbb_tpl_after_user_nav_loggedin") }}
+                            {{ run_hook("flaskbb_tpl_user_nav_loggedin_after") }}
 
                             {% else %}
                             <!-- Not logged in - Login/Register -->

+ 2 - 2
flaskbb/templates/macros.html

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

+ 2 - 2
flaskbb/templates/management/banned_users.html

@@ -1,5 +1,5 @@
 {% set page_title = _("Banned Users") %}
-{% set active_management_user_nav=True %}
+{% set active = "management.users" %}
 
 {% extends theme("management/management_layout.html") %}
 
@@ -12,7 +12,7 @@
 {% endblock %}
 
 {% block management_content %}
-{% from theme('macros.html') import render_pagination, group_field,navlink with context %}
+{% from theme('macros.html') import render_pagination, group_field, navlink with context %}
 
 <div class="col-md-3 settings-col">
     <div class="nav-sidebar">

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

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

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

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

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

@@ -1,5 +1,5 @@
 {% set page_title = title %}
-{% set active_management_group_nav=True %}
+{% set active = "management.groups" %}
 
 {% extends theme("management/management_layout.html") %}
 

+ 3 - 11
flaskbb/templates/management/management_layout.html

@@ -9,17 +9,9 @@
 <div class="panel panel-tabs management-panel">
     <div class="panel-heading management-head">
         <ul class="nav nav-tabs nav-justified">
-        {{ navlink('management.overview', _('Overview'), 'fa fa-tasks') }}
-
-        {% if current_user|is_admin %}
-            {{ navlink('management.settings', _('Settings'), 'fa fa-cogs') }}
-            {{ navlink('management.forums', _('Forums'), 'fa fa-comments',active=active_management_forum_nav) }}
-            {{ navlink('management.plugins', _('Plugins'), 'fa fa-puzzle-piece') }}
-            {{ navlink('management.groups', _('Groups'), 'fa fa-users', active=active_management_group_nav) }}
-        {% endif %}
-
-        {{ navlink('management.users', _('Users'), 'fa fa-user', active=active_management_user_nav) }}
-        {{ navlink('management.unread_reports', _('Reports'), 'fa fa-flag', active=active_management_report_nav) }}
+        {% for view, text, icon in run_hook('flaskbb_tpl_admin_settings_menu', user=current_user, is_markup=False) %}
+            {{ navlink(view, _(text), icon, active) }}
+        {% endfor %}
         </ul>
     </div>
     <div class="panel-body management-body">

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

@@ -76,7 +76,6 @@
                         <a class="btn btn-info" href="{{ url_for('management.settings', plugin=plugin.name) }}">Settings</a>
                         {% endif %}
                     </div>
-                    </div>
                 </div>
                 {% endfor %}
             </div>

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

@@ -1,5 +1,5 @@
 {% set page_title = _("Reports") %}
-{% set active_management_report_nav=True %}
+{% set active = "management.unread_reports" %}
 
 {% extends theme("management/management_layout.html") %}
 

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

@@ -1,5 +1,5 @@
 {% set page_title = title %}
-{% set active_management_user_nav=True %}
+{% set active = "management.users" %}
 
 {% extends theme("management/management_layout.html") %}
 

+ 0 - 1
flaskbb/templates/management/users.html

@@ -1,5 +1,4 @@
 {% set page_title = _("Users") %}
-{% set active_management_user_nav=True %}
 
 {% extends theme("management/management_layout.html") %}
 

+ 2 - 2
flaskbb/templates/user/change_user_details.html

@@ -10,7 +10,7 @@
     <div class="panel-body page-body">
         <form class="form-horizontal" role="form" method="POST">
             {{ form.hidden_tag() }}
-            {{ run_hook('flaskbb_tpl_before_user_details_form') }}
+            {{ run_hook('flaskbb_tpl_form_user_details_before', form=form) }}
             {{ horizontal_select_field(form.birthday, select_class="form-control", surrounded_div="col-sm-4") }}
             {{ horizontal_field(form.gender) }}
             {{ horizontal_field(form.location) }}
@@ -18,7 +18,7 @@
             {{ horizontal_field(form.avatar) }}
             {{ horizontal_field(form.signature, div_class="col-sm-8 editor", rows="5", placeholder="", **{'data-provide': 'markdown', 'class': 'flaskbb-editor'}) }}
             {{ horizontal_field(form.notes, div_class="col-sm-8 editor", rows="12", placeholder="", **{'data-provide': 'markdown', 'class': 'flaskbb-editor'}) }}
-            {{ run_hook('flaskbb_tpl_after_user_details_form') }}
+            {{ run_hook('flaskbb_tpl_form_user_details_after', form=form) }}
             {{ horizontal_field(form.submit) }}
 
             {% include theme('editor_help.html') %}

+ 5 - 5
flaskbb/templates/user/settings_layout.html

@@ -13,11 +13,11 @@
         <div class="sidebar">
             <ul class="nav sidenav">
                 {% for view, text in run_hook('flaskbb_tpl_profile_settings_menu', is_markup=False) %}
-                        {% if view == None %}
-                        <li class="sidenav-header">{{ _(text) }}</li>
-                        {% else %}
-                        {{ navlink(view, _(text)) }}
-                        {% endif %}
+                    {% if view == None %}
+                    <li class="sidenav-header">{{ _(text) }}</li>
+                    {% else %}
+                    {{ navlink(view, _(text)) }}
+                    {% endif %}
                 {% endfor %}
             </ul>
         </div>