Browse Source

Add proper docs about plugins and hooks

Peter Justin 7 years ago
parent
commit
987bfc40b0
3 changed files with 291 additions and 118 deletions
  1. 1 0
      docs/conf.py
  2. 16 12
      docs/hooks.rst
  3. 274 106
      docs/plugins.rst

+ 1 - 0
docs/conf.py

@@ -373,6 +373,7 @@ intersphinx_mapping = {
     'werkzeug': ('http://werkzeug.pocoo.org/docs/latest/', None),
     'click': ('http://click.pocoo.org/', None),
     'jinja': ('http://jinja.pocoo.org/docs/latest', None),
+    'wtforms': ('https://wtforms.readthedocs.io/en/latest', None),
 }
 
 autodoc_member_order = 'bysource'

+ 16 - 12
docs/hooks.rst

@@ -3,19 +3,23 @@
 Hooks
 =====
 
-In order to extend FlaskBB you will need to connect your callbacks with
-events.
+In FlaskBB we distinguish from `Python Hooks <#python-hooks>`_ and
+`Template Hooks <#template-hooks>`_.
+Python Hooks are prefixed with ``flaskbb_`` and called are called in Python
+files whereas Template Hooks have to be prefixed with ``flaskbb_tpl_`` and are
+executed in the templates.
 
-.. admonition:: Additional hooks
+If you miss a hook, feel free to open a new issue or create a pull
+request. The pull request should always contain a entry in this document
+with a small example.
 
-    If you miss a hook, feel free to open a new issue or create a pull
-    request. The pull request should always contain a entry in this document
-    with a small example.
+A hook needs a hook specification which are defined in
+:mod:`flaskbb.plugins.spec`. All hooks have to be prefixed with
+``flaskbb_`` and template hooks with ``flaskbb_tpl_``.
 
-    A hook needs a hook specification which are defined in
-    :mod:`flaskbb.plugins.spec`. All hooks have to be prefixed with
-    ``flaskbb_`` and template hooks with ``flaskbb_tpl_``.
 
+Python Hooks
+------------
 
 .. currentmodule:: flaskbb.plugins.spec
 
@@ -35,7 +39,7 @@ Template Hooks
 
 .. note::
 
-    Template events, which are used in forms, are usually rendered after the
+    Template hooks, which are used in forms, are usually rendered after the
     hidden CSRF token field and before an submit field.
 
 
@@ -43,5 +47,5 @@ Template Hooks
 .. autofunction:: flaskbb_tpl_after_navigation
 .. autofunction:: flaskbb_tpl_before_registration_form
 .. autofunction:: flaskbb_tpl_after_registration_form
-.. autofunction:: flaskbb_tpl_before_update_user_details
-.. autofunction:: flaskbb_tpl_after_update_user_details
+.. autofunction:: flaskbb_tpl_before_user_details_form
+.. autofunction:: flaskbb_tpl_after_user_details_form

+ 274 - 106
docs/plugins.rst

@@ -5,13 +5,12 @@ Plugins
 
 .. module:: flaskbb.plugins
 
-FlaskBB provides an easy way to extend the functionality of your forum
-via so called `Plugins`. Plugins do not modify the `core` of FlaskBB, so
-you can easily activate and deactivate them anytime. This part of the
-documentation only covers the basic things for creating plugins. If you are
-looking for a tutorial you need to go to this section of the documentation:
-:doc:`plugin_tutorial/index`.
+FlaskBB provides a full featured plugin system. This system allows you to
+easily extend or modify FlaskBB without touching any FlaskBB code. Under the
+hood it uses the `pluggy plugin system`_ which does most of the heavy lifting
+for us.
 
+.. _`pluggy plugin system`: https://pluggy.readthedocs.io/en/latest/
 
 Structure
 ---------
@@ -21,166 +20,335 @@ For example, the structure of a plugin could look like this
 
 .. sourcecode:: text
 
-    my_plugin
-    |-- info.json                Contains the Plugin's metadata
-    |-- license.txt              The full license text of your plugin
-    |-- __init__.py              The plugin's main class is located here
-    |-- views.py
-    |-- models.py
-    |-- forms.py
-    |-- static
-    |   |-- style.css
-    |-- templates
-        |-- myplugin.html
-    |-- migrations
-        |-- 59f7c49b6289_init.py
+    my_plugin_package
+    |-- setup.py
+    |-- my_plugin
+        |-- __init__.py
+        |-- views.py
+        |-- models.py
+        |-- forms.py
+        |-- static
+        |   |-- style.css
+        |-- templates
+            |-- myplugin.html
+        |-- migrations
+            |-- 59f7c49b6289_init.py
 
-Management
-----------
 
-Database
+Metadata
 ~~~~~~~~
 
+A proper plugin should have at least put the following metadata into
+the ``setup.py`` file.
+
+.. sourcecode:: python
+
+    setup(
+        name="myproject",
+        version='1.0',
+        url=<url to your project>,
+        license=<your license>,
+        author=<you>,
+        author_email=<your email>,
+        description=<your short description>,
+        long_description=__doc__,
+        packages=find_packages('.'),
+        include_package_data=True,
+        zip_safe=False,
+        platforms='any',
+
+        entry_points={
+            'flaskbb_plugin': [
+                'unique_name_of_plugin = myproject.pluginmodule',  # most important part
+            ]
+        }
+    )
+
+
+The most important part is the ``entry_point``. Here you tell FlaskBB the
+unique name of your plugin and where your plugin module is located inside
+your project. Entry points are a feature that is provided by setuptools.
+FlaskBB looks up the ``flaskbb_plugin`` entrypoint to discover its plugins.
+
+Have a look at the `setup script`_ documentation and the `sample setup.py`_
+file to get a better idea what the ``setup.py`` file is all about it.
+
+To get a better idea how a plugin looks like, checkout the `Portal Plugin`_.
+
+.. _`setup script`: https://docs.python.org/3.6/distutils/setupscript.html#additional-meta-data
+.. _`sample setup.py`: https://github.com/pypa/sampleproject/blob/master/setup.py
+.. _`Portal Plugin`: https://github.com/sh4nks/flaskbb-plugins/tree/master/portal
+
+
+Settings
+--------
+
+Plugins can create settings which integrate with the 'Settings' tab of
+the Admin Panel.
+
+The settings are stored in a dictionary with a given structure. The name of
+the dictionary must be ``SETTINGS`` and be placed in the plugin module.
+
+The structure of the ``SETTINGS`` dictionary is best explained via an
+example::
+
+    SETTINGS = {
+        # This key has to be unique across FlaskBB.
+        # Using a prefix is recommended.
+        'forum_ids': {
+
+            # Default Value. The type of the default value depends on the
+            # SettingValueType.
+            'value': [1],
+
+            # The Setting Value Type.
+            'value_type': SettingValueType.selectmultiple,
+
+            # The human readable name of your configuration variable
+            'name': "Forum IDs",
+
+            # A short description of what the settings variable does
+            'description': ("The forum ids from which forums the posts "
+                            "should be displayed on the portal."),
+
+            # extra stuff like the 'choices' in a select field or the
+            # validators are defined in here
+            'extra': {"choices": available_forums, "coerce": int}
+        }
+    }
+
+.. currentmodule:: flaskbb.utils.forms
+
+.. table:: Available Setting Value Types
+    :widths: auto
+
+    ======================================== =================
+    Setting Value Type                       Parsed & Saved As
+    ======================================== =================
+    :attr:`SettingValueType.string`          :class:`str`
+    :attr:`SettingValueType.integer`         :class:`int`
+    :attr:`SettingValueType.float`           :class:`float`
+    :attr:`SettingValueType.boolean`         :class:`bool`
+    :attr:`SettingValueType.select`          :class:`list`
+    :attr:`SettingValueType.selectmultiple`  :class:`list`
+    ======================================== =================
+
+.. table:: Available Additional Options via the ``extra`` Keyword
+
+    =========== ====================== ========================================
+    Options     Applicable Types       Description
+    =========== ====================== ========================================
+    ``min``     string, integer, float **Optional.** The minimum required
+                                       length of the setting value. If used on
+                                       a numeric type, it will check the
+                                       minimum value.
+    ``max``     string, integer, float **Optional.** The maximum required
+                                       length of the setting value. If used on
+                                       a numeric type, it will check the
+                                       maximum value.
+    ``choices`` select, selectmultiple **Required.** A callable which returns
+                                       a sequence of (value, label) pairs.
+    ``coerce``  select, selectmultiple **Optional.** Coerces the select values
+                                       to the given type.
+    =========== ====================== ========================================
+
+
+Validating the size of the integer/float and the length of the string fields
+is also possible via the ``min`` and ``max`` keywords::
+
+    'recent_topics': {
+        ...
+        'extra': {"min": 1},
+    },
+
+The ``select`` and ``selectmultiple`` fields have to provide a callback which
+lists all the available choices. This is done via the ``choices`` keyword.
+In addition to that they can also specify the ``coerce`` keyword which will
+coerce the input value into the specified type.::
+
+    'forum_ids': {
+        ...
+        'extra': {"choices": available_forums, "coerce": int}
+    }
+
+For more information see the :doc:`settings` chapter.
+
+
+Database
+--------
+
 Upgrading, downgrading and generating database revisions is all handled
-via alembic. We make use of a alembic feature called 'branch_labels'.
-Each plugin's identifier will be used as a branch_label if used with alembic.
-Lets say, that identifier of your plugin is ``portal_plugin``, then you have
-to use the following commands for generaring, upgrading and downgrading
-your plugins database migrations:
+via alembic. We make use of alembic's branching feature to manage seperate
+migrations for the plugins. Each plugin will have it's own branch in alembic
+where migrations can be managed. Following commands are used for generaring,
+upgrading and downgrading your plugins database migrations:
 
 * (Auto-)Generating revisions
-    ``flaskbb db revision --branch portal_plugin "<YOUR_MESSAGE>"``
+    ``flaskbb db revision --branch <plugin_name> "<YOUR_MESSAGE>"``
 
     Replace <YOUR_MESSAGE> with something like "initial migration" if it's
     the first migration or with just a few words that will describe the
     changes of the revision.
 
 * Applying revisions
-    ``flaskbb db upgrade portal_plugin@head``
+    ``flaskbb db upgrade <plugin_name>@head``
 
     If you want to upgrade to specific revision, replace ``head`` with the
     revision id.
 
 * Downgrading revisions
-    ``flaskbb db downgrade portal_plugin@-1``
+    ``flaskbb db downgrade <plugin_name>@-1``
 
     If you just want to revert the latest revision, just use ``-1``.
     To downgrade all database migrations, use ``base``.
 
 
-Deactivating
-~~~~~~~~~~~~
+Management
+----------
 
-You can either deactivate the plugin via the Admin Panel or by running::
+Before plugins can be used in FlaskBB, they have to be downloaded, installed
+and activated.
+Plugins can be very minimalistic with nothing to install at all (just enabling
+and disabling) to be very complex where you have to run migrations and add
+some additional settings.
 
-    flaskbb plugins disable <plugin_name>
+Download
+~~~~~~~~
 
-.. important:: Restart the server.
+Downloading a Plugin is as easy as::
 
-    You must restart the wsgi/in-built server in order to make the changes
-    effect your forum.
+    $ pip install flaskbb-plugin-MYPLUGIN
 
+if the plugin has been uploaded to PyPI. If you haven't uploaded your plugin
+to PyPI or are in the middle of developing one, you can just::
 
-Activating
-~~~~~~~~~~
+    $ pip install -e .
 
-All plugins are activated by default. To activate a deactivated plugin you
-either have to activate it via the Admin Panel again or by running the
-activation command::
+in your plugin's package directory to install it.
 
-    flaskbb plugins enable <plugin_name>
+Remove
+~~~~~~
 
+Removing a plugin is a little bit more tricky. By default, FlaskBB does not
+remove the settings of a plugin by itself because this could lead to some
+unwanted dataloss.
 
+`Disable`_ and `Uninstall`_ the plugin first before continuing.
 
-Example Plugin
---------------
+After taking care of this and you are confident that you won't need the
+plugin anymore you can finally remove it::
 
-A simple Plugin could look like this:
+    $ pip uninstall flaskbb-plugin-MYPLUGIN
 
-.. sourcecode:: python
+There is a setting in FlaskBB which lets you control the deletion of settings
+of a plugin. If ``REMOVE_DEAD_PLUGINS`` is set to ``True``, all not available
+plugins (not available on the filesystem) are constantly removed. Only change
+this if you know what you are doing.
 
-    from flask import flash
-    from flask.ext.plugins import connect_event
+Install
+~~~~~~~
 
-    from flaskbb.plugins import FlaskBBPlugin
+In our context, by installing a plugin, we mean, to install the settings
+and apply the migrations. Personal Note: I can't think of a better name and
+I am open for suggestions.
 
+The migrations have to be applied this way (if any, check the plugins docs)::
 
-    # This is the name of your Plugin class which implements FlaskBBPlugin.
-    # The exact name is needed in order to be recognized as a plugin.
-    __plugin__ "HelloWorldPlugin"
+    flaskbb db upgrade <plugin_name>@head
 
+The plugin can be installed via the Admin Panel (in tab 'Plugins') or by
+running::
 
-    def flash_index():
-        """Flashes a message when visiting the index page."""
+    flaskbb plugins install <plugin_name>
 
-        flash("This is just a demonstration plugin", "success")
+Uninstall
+~~~~~~~~~
 
+Removing a plugin involves two steps. The first one is to check if the plugin
+has applied any migrations on FlaskBB and if so you can
+undo them via::
 
-    class HelloWorldPlugin(FlaskBBPlugin):
-        def setup(self):
-            connect_event(before-forum-index-rendered, flash_index)
+    $ flaskbb db downgrade <plugin_name>@base
 
-        def install(self):
-            # there is nothing to install
-            pass
+The second step is to wipe the settings from FlaskBB which can be done in the
+Admin Panel or by running::
 
-        def uninstall(self):
-            # and nothing to uninstall
-            pass
+    $ flaskbb plugins uninstall <plugin_name>
 
+Disable
+~~~~~~~
 
-Your plugins also needs a ``info.json`` file, where it stores some meta data
-about the plugin. For more information see the `Metadata <#metadata>`_
-section below.
+Disabling a plugin has the benefit of keeping all the data of the plugin but
+not using the functionality it provides. A plugin can either be deactivated
+via the Admin Panel or by running::
 
+    flaskbb plugins disable <plugin_name>
 
-Metadata
-~~~~~~~~
+.. important:: Restart the server.
 
-A proper plugin should have at least put the following metadata into
-the ``setup.py`` file.
+    You must restart the wsgi/in-built server in order to make the changes
+    effect your forum.
+
+
+Enable
+~~~~~~
+
+All plugins are activated by default. To activate a deactivated plugin you
+either have to activate it via the Admin Panel again or by running the
+activation command::
 
-https://docs.python.org/3.6/distutils/setupscript.html#additional-meta-data
-https://github.com/pypa/sampleproject/blob/master/setup.py
+    flaskbb plugins enable <plugin_name>
 
-``identifier`` : **required**
-    The plugin's identifier. It should be a Python identifier (starts with a
-    letter or underscore, the rest can be letters, underscores, or numbers)
-    and should match the name of the plugin's folder.
 
-``name`` : **required**
-    A human-readable name for the plugin.
+Hooks
+-----
+Hooks are invoked based on an event occurring within FlaskBB. This makes it
+possible to change the behavior of certain actions without modifying the
+actual source code of FlaskBB.
 
-``author`` : **required**
-    The name of the plugin's author, that is, you. It does not have to include
-    an e-mail address, and should be displayed verbatim.
+For your plugin to actually do something useful, you probably want to 'hook'
+your code into FlaskBB. This can be done throughout a lot of places in the
+code. FlaskBB loads and calls the hook calls hook functions from registered
+plugins for any given hook specification.
 
-``description``
-    A description of the plugin in a few sentences. If you can write multiple
-    languages, you can include additional fields in the form
-    ``description_lc``, where ``lc`` is a two-letter language code like ``es``
-    or ``de``. They should contain the description, but in the indicated
-    language.
+Each hook specification has a corresponding hook implementation. By default,
+all hooks that are prefix with ``flaskbb_`` will be marked as a standard
+hook implementation. It is possible to modify the behavior of hooks.
+For example, default hooks are called in LIFO registered order.
+A hookimpl can influence its call-time invocation position using special
+attributes. If marked with a "tryfirst" or "trylast" option it will be executed
+first or last respectively in the hook call loop::
 
-``website``
-    The URL of the plugin's Web site. This can be a Web site specifically for
-    this plugin, Web site for a collection of plugins that includes this plugin,
-    or just the author's Web site.
+    hookimpl = HookimplMarker('flaskbb')
 
-``license``
-    A simple phrase indicating your plugin's license, like ``GPL``,
-    ``MIT/X11``, ``Public Domain``, or ``Creative Commons BY-SA 3.0``. You
-    can put the full license's text in the ``license.txt`` file.
+    @hookimpl(trylast=True)
+    def flaskbb_additional_setup(app):
+        return "save the best for last"
 
-``version``
-    This is simply to make it easier to distinguish between what version
-    of your plugin people are using. It's up to the theme/layout to decide
-    whether or not to show this, though.
 
+In order to extend FlaskBB with your Plugin you will need to connect your
+callbacks to the hooks.
 
-Hooks
------
+Let's look at an actually piece of `used code`_.
+
+.. sourcecode:: python
+
+    def flaskbb_load_blueprints(app):
+        app.register_blueprint(portal, url_prefix="/portal")
+
+By defining a function called ``flaskbb_load_blueprints``, which has a
+corresponding hook specification under the same name. FlaskBB will pass
+in an ``app`` object as specified in the hook spec, which we will use to
+register a new blueprint. It is also possible to completely omit the ``app``
+argument from the function where it is **not possible** to add new arguments to
+the hook implemention.
+
+For a complete list of all available hooks in FlaskBB see the :doc:`hooks`
+section.
+
+pytest and pluggy are good resources to get better understanding on how to
+write `hook functions`_ using `pluggy`_.
 
-FlaskBB uses so called 'Hooks' ...
-Under the hood we use `pluggy`...
-A full list of hooks can be found here :doc:`hooks`.
+.. _`used code`: https://github.com/sh4nks/flaskbb-plugins/blob/master/portal/portal/__init__.py#L31
+.. _`hook functions`: https://docs.pytest.org/en/latest/writing_plugins.html#writing-hook-functions
+.. _`pluggy`: https://pluggy.readthedocs.io/en/latest/#defining-and-collecting-hooks