aboutsummaryrefslogtreecommitdiffstats
path: root/docs/source/pluginwriter
diff options
context:
space:
mode:
authorAditi <aditi.iitr@gmail.com>2013-06-21 23:09:22 +0530
committerAditi <aditi.iitr@gmail.com>2013-06-21 23:09:22 +0530
commit2719d546a57c2332e36cc056ac80ec5d79672c1a (patch)
tree1f62ab8f761026d4faa5442032df133fc90d47f2 /docs/source/pluginwriter
parent1a6f065419290b3f4234ce4a89bb2c46b13e8a12 (diff)
parent92b22e7deac547835f69168f97012b52e87b6de4 (diff)
downloadmediagoblin-2719d546a57c2332e36cc056ac80ec5d79672c1a.tar.lz
mediagoblin-2719d546a57c2332e36cc056ac80ec5d79672c1a.tar.xz
mediagoblin-2719d546a57c2332e36cc056ac80ec5d79672c1a.zip
Merge remote-tracking branch 'cweb/master'
Diffstat (limited to 'docs/source/pluginwriter')
-rw-r--r--docs/source/pluginwriter/api.rst296
-rw-r--r--docs/source/pluginwriter/database.rst114
-rw-r--r--docs/source/pluginwriter/foreward.rst43
-rw-r--r--docs/source/pluginwriter/quickstart.rst188
-rw-r--r--docs/source/pluginwriter/tests.rst64
5 files changed, 705 insertions, 0 deletions
diff --git a/docs/source/pluginwriter/api.rst b/docs/source/pluginwriter/api.rst
new file mode 100644
index 00000000..66def173
--- /dev/null
+++ b/docs/source/pluginwriter/api.rst
@@ -0,0 +1,296 @@
+.. MediaGoblin Documentation
+
+ Written in 2013 by MediaGoblin contributors
+
+ To the extent possible under law, the author(s) have dedicated all
+ copyright and related and neighboring rights to this software to
+ the public domain worldwide. This software is distributed without
+ any warranty.
+
+ You should have received a copy of the CC0 Public Domain
+ Dedication along with this software. If not, see
+ <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+.. _plugin-api-chapter:
+
+==========
+Plugin API
+==========
+
+This documents the general plugin API.
+
+Please note, at this point OUR PLUGIN HOOKS MAY AND WILL CHANGE.
+Authors are encouraged to develop plugins and work with the
+MediaGoblin community to keep them up to date, but this API will be a
+moving target for a few releases.
+
+Please check the :ref:`release-notes` for updates!
+
+
+How are hooks added? Where do I find them?
+-------------------------------------------
+
+Much of this document talks about hooks, both as in terms of regular
+hooks and template hooks. But where do they come from, and how can
+you find a list of them?
+
+For the moment, the best way to find available hooks is to check the
+source code itself. (Yes, we should start a more official hook
+listing with descriptions soon.) But many hooks you may need do not
+exist yet: what to do then?
+
+The plan at present is that we are adding hooks as people need them,
+with community discussion. If you find that you need a hook and
+MediaGoblin at present doesn't provide it at present, please
+`http://mediagoblin.org/pages/join.html <talk to us>`_! We'll
+evaluate what to do from there.
+
+
+:mod:`pluginapi` Module
+-----------------------
+
+.. automodule:: mediagoblin.tools.pluginapi
+ :members: get_config, register_routes, register_template_path,
+ register_template_hooks, get_hook_templates,
+ hook_handle, hook_runall, hook_transform
+
+Configuration
+-------------
+
+Your plugin may define its own configuration defaults.
+
+Simply add to the directory of your plugin a config_spec.ini file. An
+example might look like::
+
+ [plugin_spec]
+ some_string = string(default="blork")
+ some_int = integer(default=50)
+
+This means that when people enable your plugin in their config you'll
+be able to provide defaults as well as type validation.
+
+
+Context Hooks
+-------------
+
+View specific hooks
++++++++++++++++++++
+
+You can hook up to almost any template called by any specific view
+fairly easily. As long as the view directly or indirectly uses the
+method ``render_to_response`` you can access the context via a hook
+that has a key in the format of the tuple::
+
+ (view_symbolic_name, view_template_path)
+
+Where the "view symbolic name" is the same parameter used in
+``request.urlgen()`` to look up the view. So say we're wanting to add
+something to the context of the user's homepage. We look in
+mediagoblin/user_pages/routing.py and see::
+
+ add_route('mediagoblin.user_pages.user_home',
+ '/u/<string:user>/',
+ 'mediagoblin.user_pages.views:user_home')
+
+Aha! That means that the name is ``mediagoblin.user_pages.user_home``.
+Okay, so then we look at the view at the
+``mediagoblin.user_pages.user_home`` method::
+
+ @uses_pagination
+ def user_home(request, page):
+ # [...] whole bunch of stuff here
+ return render_to_response(
+ request,
+ 'mediagoblin/user_pages/user.html',
+ {'user': user,
+ 'user_gallery_url': user_gallery_url,
+ 'media_entries': media_entries,
+ 'pagination': pagination})
+
+Nice! So the template appears to be
+``mediagoblin/user_pages/user.html``. Cool, that means that the key
+is::
+
+ ("mediagoblin.user_pages.user_home",
+ "mediagoblin/user_pages/user.html")
+
+The context hook uses ``hook_transform()`` so that means that if we're
+hooking into it, our hook will both accept one argument, ``context``,
+and should return that modified object, like so::
+
+ def add_to_user_home_context(context):
+ context['foo'] = 'bar'
+ return context
+
+ hooks = {
+ ("mediagoblin.user_pages.user_home",
+ "mediagoblin/user_pages/user.html"): add_to_user_home_context}
+
+
+Global context hooks
+++++++++++++++++++++
+
+If you need to add something to the context of *every* view, it is not
+hard; there are two hooks hook that also uses hook_transform (like the
+above) but make available what you are providing to *every* view.
+
+Note that there is a slight, but critical, difference between the two.
+
+The most general one is the ``'template_global_context'`` hook. This
+one is run only once, and is read into the global context... all views
+will get access to what are in this dict.
+
+The slightly more expensive but more powerful one is
+``'template_context_prerender'``. This one is not added to the global
+context... it is added to the actual context of each individual
+template render right before it is run! Because of this you also can
+do some powerful and crazy things, such as checking the request object
+or other parts of the context before passing them on.
+
+
+Adding static resources
+-----------------------
+
+It's possible to add static resources for your plugin. Say your
+plugin needs some special javascript and images... how to provide
+them? Then how to access them? MediaGoblin has a way!
+
+
+Attaching to the hook
++++++++++++++++++++++
+
+First, you need to register your plugin's resources with the hook.
+This is pretty easy actually: you just need to provide a function that
+passes back a PluginStatic object.
+
+.. autoclass:: mediagoblin.tools.staticdirect.PluginStatic
+
+
+Running plugin assetlink
+++++++++++++++++++++++++
+
+In order for your plugin assets to be properly served by MediaGoblin,
+your plugin's asset directory needs to be symlinked into the directory
+that plugin assets are served from. To set this up, run::
+
+ ./bin/gmg assetlink
+
+
+Using staticdirect
+++++++++++++++++++
+
+Once you have this, you will want to be able to of course link to your
+assets! MediaGoblin has a "staticdirect" tool; you want to use this
+like so in your templates::
+
+ staticdirect("css/monkeys.css", "mystaticname")
+
+Replace "mystaticname" with the name you passed to PluginStatic. The
+staticdirect method is, for convenience, attached to the request
+object, so you can access this in your templates like:
+
+.. code-block:: html
+
+ <img alt="A funny bunny"
+ src="{{ request.staticdirect('images/funnybunny.png', 'mystaticname') }}" />
+
+
+Additional hook tips
+--------------------
+
+This section aims to explain some tips in regards to adding hooks to
+the MediaGoblin repository.
+
+WTForms hooks
++++++++++++++
+
+We haven't totally settled on a way to tranform wtforms form objects,
+but here's one way. In your view::
+
+ from mediagoblin.foo.forms import SomeForm
+
+ def some_view(request)
+ form_class = hook_transform('some_form_transform', SomeForm)
+ form = form_class(request.form)
+
+Then to hook into this form, do something in your plugin like::
+
+ import wtforms
+
+ class SomeFormAdditions(wtforms.Form):
+ new_datefield = wtforms.DateField()
+
+ def transform_some_form(orig_form):
+ class ModifiedForm(orig_form, SomeFormAdditions)
+ return ModifiedForm
+
+ hooks = {
+ 'some_form_transform': transform_some_form}
+
+
+Interfaces
+++++++++++
+
+If you want to add a pseudo-interface, it's not difficult to do so.
+Just write the interface like so::
+
+ class FrobInterface(object):
+ """
+ Interface for Frobbing.
+
+ Classes implementing this interface should provide defrob and frob.
+ They may also implement double_frob, but it is not required; if
+ not provided, we will use a general technique.
+ """
+
+ def defrob(self, frobbed_obj):
+ """
+ Take a frobbed_obj and defrob it. Returns the defrobbed object.
+ """
+ raise NotImplementedError()
+
+ def frob(self, normal_obj):
+ """
+ Take a normal object and frob it. Returns the frobbed object.
+ """
+ raise NotImplementedError()
+
+ def double_frob(self, normal_obj):
+ """
+ Frob this object and return it multiplied by two.
+ """
+ return self.frob(normal_obj) * 2
+
+
+ def some_frob_using_method():
+ # something something something
+ frobber = hook_handle(FrobInterface)
+ frobber.frob(blah)
+
+ # alternately you could have a default
+ frobber = hook_handle(FrobInterface) or DefaultFrobber
+ frobber.defrob(foo)
+
+
+It's fine to use your interface as the key instead of a string if you
+like. (Usually this is messy, but since interfaces are public and
+since you need to import them into your plugin anyway, interfaces
+might as well be keys.)
+
+Then a plugin providing your interface can be like::
+
+ from mediagoblin.foo.frobfrogs import FrobInterface
+ from frogfrobber import utils
+
+ class FrogFrobber(FrobInterface):
+ """
+ Takes a frogputer science approach to frobbing.
+ """
+ def defrob(self, frobbed_obj):
+ return utils.frog_defrob(frobbed_obj)
+
+ def frob(self, normal_obj):
+ return utils.frog_frob(normal_obj)
+
+ hooks = {
+ FrobInterface: lambda: return FrogFrobber}
diff --git a/docs/source/pluginwriter/database.rst b/docs/source/pluginwriter/database.rst
new file mode 100644
index 00000000..603a19eb
--- /dev/null
+++ b/docs/source/pluginwriter/database.rst
@@ -0,0 +1,114 @@
+.. MediaGoblin Documentation
+
+ Written in 2013 by MediaGoblin contributors
+
+ To the extent possible under law, the author(s) have dedicated all
+ copyright and related and neighboring rights to this software to
+ the public domain worldwide. This software is distributed without
+ any warranty.
+
+ You should have received a copy of the CC0 Public Domain
+ Dedication along with this software. If not, see
+ <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+
+.. _plugin-database-chapter:
+
+
+===========================
+Database models for plugins
+===========================
+
+
+Accessing Existing Data
+=======================
+
+If your plugin wants to access existing data, this is quite
+straight forward. Just import the appropiate models and use
+the full power of SQLAlchemy. Take a look at the (upcoming)
+database section in the Developer's Chapter.
+
+
+Creating new Tables
+===================
+
+If your plugin needs some new space to store data, you
+should create a new table. Please do not modify core
+tables. Not doing so might seem inefficient and possibly
+is. It will help keep things sane and easier to upgrade
+versions later.
+
+So if you create a new plugin and need new tables, create a
+file named ``models.py`` in your plugin directory. You
+might take a look at the core's db.models for some ideas.
+Here's a simple one:
+
+.. code-block:: python
+
+ from mediagoblin.db.base import Base
+ from sqlalchemy import Column, Integer, Unicode, ForeignKey
+
+ class MediaSecurity(Base):
+ __tablename__ = "yourplugin__media_security"
+
+ # The primary key *and* reference to the main media_entry
+ media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
+ primary_key=True)
+ get_media_entry = relationship("MediaEntry",
+ backref=backref("security_rating", cascade="all, delete-orphan"))
+
+ rating = Column(Unicode)
+
+ MODELS = [MediaSecurity]
+
+That's it.
+
+Some notes:
+
+* Make sure all your ``__tablename__`` start with your
+ plugin's name so the tables of various plugins can't
+ conflict in the database. (Conflicts in python naming are
+ much easier to fix later).
+* Try to get your database design as good as possible in
+ the first attempt. Changing the database design later,
+ when people already have data using the old design, is
+ possible (see next chapter), but it's not easy.
+
+
+Changing the Database Schema Later
+==================================
+
+If your plugin is in use and instances use it to store some
+data, changing the database design is a tricky thing.
+
+1. Make up your mind how the new schema should look like.
+2. Change ``models.py`` to contain the new schema. Keep a
+ copy of the old version around for your personal
+ reference later.
+3. Now make up your mind (possibly using your old and new
+ ``models.py``) what steps in SQL are needed to convert
+ the old schema to the new one.
+ This is called a "migration".
+4. Create a file ``migrations.py`` that will contain all
+ your migrations and add your new migration.
+
+Take a look at the core's ``db/migrations.py`` for some
+good examples on what you might be able to do. Here's a
+simple one to add one column:
+
+.. code-block:: python
+
+ from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
+ from sqlalchemy import MetaData, Column, Integer
+
+ MIGRATIONS = {}
+
+ @RegisterMigration(1, MIGRATIONS)
+ def add_license_preference(db):
+ metadata = MetaData(bind=db.bind)
+
+ security_table = inspect_table(metadata, 'yourplugin__media_security')
+
+ col = Column('security_level', Integer)
+ col.create(security_table)
+ db.commit()
diff --git a/docs/source/pluginwriter/foreward.rst b/docs/source/pluginwriter/foreward.rst
new file mode 100644
index 00000000..fd3a0c22
--- /dev/null
+++ b/docs/source/pluginwriter/foreward.rst
@@ -0,0 +1,43 @@
+.. MediaGoblin Documentation
+
+ Written in 2011, 2012 by MediaGoblin contributors
+
+ To the extent possible under law, the author(s) have dedicated all
+ copyright and related and neighboring rights to this software to
+ the public domain worldwide. This software is distributed without
+ any warranty.
+
+ You should have received a copy of the CC0 Public Domain
+ Dedication along with this software. If not, see
+ <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+========
+Foreword
+========
+
+About the Plugin Writer's Guide
+===============================
+
+This guide covers writing plugins for GNU MediaGoblin. It's very much
+a work in progress partially because we just started writing it and
+partially because the plugin API is currently in flux.
+
+
+Improving the Plugin Writer's Guide
+===================================
+
+There are a few ways---please pick whichever method is convenient for
+you!
+
+1. Write up a bug report in the bug tracker
+2. Tell someone on IRC ``#mediagoblin`` on Freenode.
+3. Write an email to the devel mailing list.
+
+Information about the bugtracker, IRC and the mailing list is all on
+the `join page`_.
+
+.. _join page: http://mediagoblin.org/join/
+
+Patches are the most helpful, but even feedback on what you think
+could be improved and how to improve it is also helpful.
+
diff --git a/docs/source/pluginwriter/quickstart.rst b/docs/source/pluginwriter/quickstart.rst
new file mode 100644
index 00000000..6d45ea36
--- /dev/null
+++ b/docs/source/pluginwriter/quickstart.rst
@@ -0,0 +1,188 @@
+.. MediaGoblin Documentation
+
+ Written in 2011, 2012 by MediaGoblin contributors
+
+ To the extent possible under law, the author(s) have dedicated all
+ copyright and related and neighboring rights to this software to
+ the public domain worldwide. This software is distributed without
+ any warranty.
+
+ You should have received a copy of the CC0 Public Domain
+ Dedication along with this software. If not, see
+ <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+
+===========
+Quick Start
+===========
+
+This is a quick start. It's not comprehensive, but it walks through
+writing a basic plugin called "sampleplugin" which logs "I've been
+started!" when ``setup_plugin()`` has been called.
+
+.. todo: Rewrite this to be a useful plugin
+
+
+Step 1: Files and directories
+=============================
+
+GNU MediaGoblin plugins are Python projects at heart. As such, you should
+use a standard Python project directory tree::
+
+ sampleplugin/
+ |- README
+ |- LICENSE
+ |- setup.py
+ |- sampleplugin/
+ |- __init__.py
+
+
+The outer ``sampleplugin`` directory holds all the project files.
+
+The ``README`` should cover what your plugin does, how to install it,
+how to configure it, and all the sorts of things a README should
+cover.
+
+The ``LICENSE`` should have the license under which you're
+distributing your plugin.
+
+The inner ``sampleplugin`` directory is the Python package that holds
+your plugin's code.
+
+The ``__init__.py`` denotes that this is a Python package. It also
+holds the plugin code and the ``hooks`` dict that specifies which
+hooks the sampleplugin uses.
+
+
+Step 2: README
+==============
+
+Here's a rough ``README``. Generally, you want more information
+because this is the file that most people open when they want to learn
+more about your project.
+
+::
+
+ README
+ ======
+
+ This is a sample plugin. It logs a line when ``setup__plugin()`` is
+ run.
+
+
+Step 3: LICENSE
+===============
+
+GNU MediaGoblin plugins must be licensed under the AGPLv3 or later. So
+the LICENSE file should be the AGPLv3 text which you can find at
+`<http://www.gnu.org/licenses/agpl-3.0.html>`_
+
+
+Step 4: setup.py
+================
+
+This file is used for packaging and distributing your plugin.
+
+We'll use a basic one::
+
+ from setuptools import setup, find_packages
+
+ setup(
+ name='sampleplugin',
+ version='1.0',
+ packages=find_packages(),
+ include_package_data=True,
+ install_requires=[],
+ license='AGPLv3',
+ )
+
+
+See `<http://docs.python.org/distutils/index.html#distutils-index>`_
+for more details.
+
+
+Step 5: the code
+================
+
+The code for ``__init__.py`` looks like this:
+
+.. code-block:: python
+ :linenos:
+ :emphasize-lines: 12,23
+
+ import logging
+ from mediagoblin.tools.pluginapi import Plugin, get_config
+
+
+ # This creates a logger that you can use to log information to
+ # the console or a log file.
+ _log = logging.getLogger(__name__)
+
+
+ # This is the function that gets called when the setup
+ # hook fires.
+ def setup_plugin():
+ _log.info("I've been started!")
+ config = get_config('sampleplugin')
+ if config:
+ _log.info('%r' % config)
+ else:
+ _log.info('There is no configuration set.')
+
+
+ # This is a dict that specifies which hooks this plugin uses.
+ # This one only uses one hook: setup.
+ hooks = {
+ 'setup': setup_plugin
+ }
+
+
+Line 12 defines the ``setup_plugin`` function.
+
+Line 23 defines ``hooks``. When MediaGoblin loads this file, it sees
+``hooks`` and registers all the callables with their respective hooks.
+
+
+Step 6: Installation and configuration
+======================================
+
+To install the plugin for development, you need to make sure it's
+available to the Python interpreter that's running MediaGoblin.
+
+There are a couple of ways to do this, but we're going to pick the
+easy one.
+
+Use ``python`` from your MediaGoblin virtual environment and do::
+
+ python setup.py develop
+
+Any changes you make to your plugin will be available in your
+MediaGoblin virtual environment.
+
+Then adjust your ``mediagoblin.ini`` file to load the plugin::
+
+ [plugins]
+
+ [[sampleplugin]]
+
+
+Step 7: That's it!
+==================
+
+When you launch MediaGoblin, it'll load the plugin and you'll see
+evidence of that in the log file.
+
+That's it for the quick start!
+
+
+Where to go from here
+=====================
+
+See the documentation on the :ref:`plugin-api-chapter` for code
+samples and other things you can use when building your plugin. If
+your plugin needs its own database models, see
+:ref:`plugin-database-chapter`.
+
+See `Hitchhiker's Guide to Packaging
+<http://guide.python-distribute.org/>`_ for more information on
+packaging your plugin.
diff --git a/docs/source/pluginwriter/tests.rst b/docs/source/pluginwriter/tests.rst
new file mode 100644
index 00000000..fe99688f
--- /dev/null
+++ b/docs/source/pluginwriter/tests.rst
@@ -0,0 +1,64 @@
+.. MediaGoblin Documentation
+
+ Written in 2013 by MediaGoblin contributors
+
+ To the extent possible under law, the author(s) have dedicated all
+ copyright and related and neighboring rights to this software to
+ the public domain worldwide. This software is distributed without
+ any warranty.
+
+ You should have received a copy of the CC0 Public Domain
+ Dedication along with this software. If not, see
+ <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+==============================
+Writing unit tests for plugins
+==============================
+
+Here's a brief guide to writing unit tests for plugins. However, it
+isn't really ideal. It also hasn't been well tested... yes, there's
+some irony there :)
+
+Some notes: we're using py.test and webtest for unit testing stuff.
+Keep that in mind.
+
+My suggestion is to mime the behavior of `mediagoblin/tests/` and put
+that in your own plugin, like `myplugin/tests/`. Copy over
+`conftest.py` and `pytest.ini` to your tests directory, but possibly
+change the `test_app` fixture to match your own tests' config needs.
+For example::
+
+ import pkg_resources
+ # [...]
+
+ @pytest.fixture()
+ def test_app(request):
+ return get_app(
+ request,
+ mgoblin_config=pkg_resources.resource_filename(
+ 'myplugin.tests', 'myplugin_mediagoblin.ini'))
+
+In any test module in your tests directory you can then do::
+
+ def test_somethingorother(test_app):
+ # real code goes here
+ pass
+
+And you'll get a mediagoblin application wrapped in webtest passed in
+to your environment.
+
+If your plugin needs to define multiple configuration setups, you can
+actually set up multiple fixtures very easily for this. You can just
+set up multiple fixtures with different names that point to different
+configs and pass them in as that named argument.
+
+To run the tests, from mediagoblin's directory (make sure that your
+plugin has been added to your mediagoblin checkout's virtualenv!) do::
+
+ ./runtests.sh /path/to/myplugin/tests/
+
+replacing `/path/to/myplugin/` with the actual path to your plugin.
+
+NOTE: again, the above is untested, but it should probably work. If
+you run into trouble, `contact us
+<http://mediagoblin.org/pages/join.html>`_, preferably on IRC!