| 1 | .. MediaGoblin Documentation |
| 2 | |
| 3 | Written in 2013 by MediaGoblin contributors |
| 4 | |
| 5 | To the extent possible under law, the author(s) have dedicated all |
| 6 | copyright and related and neighboring rights to this software to |
| 7 | the public domain worldwide. This software is distributed without |
| 8 | any warranty. |
| 9 | |
| 10 | You should have received a copy of the CC0 Public Domain |
| 11 | Dedication along with this software. If not, see |
| 12 | <http://creativecommons.org/publicdomain/zero/1.0/>. |
| 13 | |
| 14 | .. _plugin-api-chapter: |
| 15 | |
| 16 | ========== |
| 17 | Plugin API |
| 18 | ========== |
| 19 | |
| 20 | This documents the general plugin API. |
| 21 | |
| 22 | Please note, at this point OUR PLUGIN HOOKS MAY AND WILL CHANGE. |
| 23 | Authors are encouraged to develop plugins and work with the |
| 24 | MediaGoblin community to keep them up to date, but this API will be a |
| 25 | moving target for a few releases. |
| 26 | |
| 27 | Please check the :ref:`release-notes` for updates! |
| 28 | |
| 29 | |
| 30 | How are hooks added? Where do I find them? |
| 31 | ------------------------------------------- |
| 32 | |
| 33 | Much of this document talks about hooks, both as in terms of regular |
| 34 | hooks and template hooks. But where do they come from, and how can |
| 35 | you find a list of them? |
| 36 | |
| 37 | For the moment, the best way to find available hooks is to check the |
| 38 | source code itself. (Yes, we should start a more official hook |
| 39 | listing with descriptions soon.) But many hooks you may need do not |
| 40 | exist yet: what to do then? |
| 41 | |
| 42 | The plan at present is that we are adding hooks as people need them, |
| 43 | with community discussion. If you find that you need a hook and |
| 44 | MediaGoblin at present doesn't provide it at present, please |
| 45 | `http://mediagoblin.org/pages/join.html <talk to us>`_! We'll |
| 46 | evaluate what to do from there. |
| 47 | |
| 48 | |
| 49 | :mod:`pluginapi` Module |
| 50 | ----------------------- |
| 51 | |
| 52 | .. automodule:: mediagoblin.tools.pluginapi |
| 53 | :members: get_config, register_routes, register_template_path, |
| 54 | register_template_hooks, get_hook_templates, |
| 55 | hook_handle, hook_runall, hook_transform |
| 56 | |
| 57 | Configuration |
| 58 | ------------- |
| 59 | |
| 60 | Your plugin may define its own configuration defaults. |
| 61 | |
| 62 | Simply add to the directory of your plugin a config_spec.ini file. An |
| 63 | example might look like:: |
| 64 | |
| 65 | [plugin_spec] |
| 66 | some_string = string(default="blork") |
| 67 | some_int = integer(default=50) |
| 68 | |
| 69 | This means that when people enable your plugin in their config you'll |
| 70 | be able to provide defaults as well as type validation. |
| 71 | |
| 72 | You can access this via the app_config variables in mg_globals, or you |
| 73 | can use a shortcut to get your plugin's config section:: |
| 74 | |
| 75 | >>> from mediagoblin.tools import pluginapi |
| 76 | # Replace with the path to your plugin. |
| 77 | # (If an external package, it won't be part of mediagoblin.plugins) |
| 78 | >>> floobie_config = pluginapi.get_config('mediagoblin.plugins.floobifier') |
| 79 | >>> floobie_dir = floobie_config['floobie_dir'] |
| 80 | # This is the same as the above |
| 81 | >>> from mediagoblin import mg_globals |
| 82 | >>> config = mg_globals.global_config['plugins']['mediagoblin.plugins.floobifier'] |
| 83 | >>> floobie_dir = floobie_config['floobie_dir'] |
| 84 | |
| 85 | A tip: you have access to the `%(here)s` variable in your config, |
| 86 | which is the directory that the user's mediagoblin config is running |
| 87 | out of. So for example, your plugin may need a "floobie" directory to |
| 88 | store floobs in. You could give them a reasonable default that makes |
| 89 | use of the default `user_dev` location, but allow users to override |
| 90 | it, like so:: |
| 91 | |
| 92 | [plugin_spec] |
| 93 | floobie_dir = string(default="%(here)s/user_dev/floobs/") |
| 94 | |
| 95 | Note, this is relative to the user's mediagoblin config directory, |
| 96 | *not* your plugin directory! |
| 97 | |
| 98 | |
| 99 | Context Hooks |
| 100 | ------------- |
| 101 | |
| 102 | View specific hooks |
| 103 | +++++++++++++++++++ |
| 104 | |
| 105 | You can hook up to almost any template called by any specific view |
| 106 | fairly easily. As long as the view directly or indirectly uses the |
| 107 | method ``render_to_response`` you can access the context via a hook |
| 108 | that has a key in the format of the tuple:: |
| 109 | |
| 110 | (view_symbolic_name, view_template_path) |
| 111 | |
| 112 | Where the "view symbolic name" is the same parameter used in |
| 113 | ``request.urlgen()`` to look up the view. So say we're wanting to add |
| 114 | something to the context of the user's homepage. We look in |
| 115 | mediagoblin/user_pages/routing.py and see:: |
| 116 | |
| 117 | add_route('mediagoblin.user_pages.user_home', |
| 118 | '/u/<string:user>/', |
| 119 | 'mediagoblin.user_pages.views:user_home') |
| 120 | |
| 121 | Aha! That means that the name is ``mediagoblin.user_pages.user_home``. |
| 122 | Okay, so then we look at the view at the |
| 123 | ``mediagoblin.user_pages.user_home`` method:: |
| 124 | |
| 125 | @uses_pagination |
| 126 | def user_home(request, page): |
| 127 | # [...] whole bunch of stuff here |
| 128 | return render_to_response( |
| 129 | request, |
| 130 | 'mediagoblin/user_pages/user.html', |
| 131 | {'user': user, |
| 132 | 'user_gallery_url': user_gallery_url, |
| 133 | 'media_entries': media_entries, |
| 134 | 'pagination': pagination}) |
| 135 | |
| 136 | Nice! So the template appears to be |
| 137 | ``mediagoblin/user_pages/user.html``. Cool, that means that the key |
| 138 | is:: |
| 139 | |
| 140 | ("mediagoblin.user_pages.user_home", |
| 141 | "mediagoblin/user_pages/user.html") |
| 142 | |
| 143 | The context hook uses ``hook_transform()`` so that means that if we're |
| 144 | hooking into it, our hook will both accept one argument, ``context``, |
| 145 | and should return that modified object, like so:: |
| 146 | |
| 147 | def add_to_user_home_context(context): |
| 148 | context['foo'] = 'bar' |
| 149 | return context |
| 150 | |
| 151 | hooks = { |
| 152 | ("mediagoblin.user_pages.user_home", |
| 153 | "mediagoblin/user_pages/user.html"): add_to_user_home_context} |
| 154 | |
| 155 | |
| 156 | Global context hooks |
| 157 | ++++++++++++++++++++ |
| 158 | |
| 159 | If you need to add something to the context of *every* view, it is not |
| 160 | hard; there are two hooks hook that also uses hook_transform (like the |
| 161 | above) but make available what you are providing to *every* view. |
| 162 | |
| 163 | Note that there is a slight, but critical, difference between the two. |
| 164 | |
| 165 | The most general one is the ``'template_global_context'`` hook. This |
| 166 | one is run only once, and is read into the global context... all views |
| 167 | will get access to what are in this dict. |
| 168 | |
| 169 | The slightly more expensive but more powerful one is |
| 170 | ``'template_context_prerender'``. This one is not added to the global |
| 171 | context... it is added to the actual context of each individual |
| 172 | template render right before it is run! Because of this you also can |
| 173 | do some powerful and crazy things, such as checking the request object |
| 174 | or other parts of the context before passing them on. |
| 175 | |
| 176 | |
| 177 | Adding static resources |
| 178 | ----------------------- |
| 179 | |
| 180 | It's possible to add static resources for your plugin. Say your |
| 181 | plugin needs some special javascript and images... how to provide |
| 182 | them? Then how to access them? MediaGoblin has a way! |
| 183 | |
| 184 | |
| 185 | Attaching to the hook |
| 186 | +++++++++++++++++++++ |
| 187 | |
| 188 | First, you need to register your plugin's resources with the hook. |
| 189 | This is pretty easy actually: you just need to provide a function that |
| 190 | passes back a PluginStatic object. |
| 191 | |
| 192 | .. autoclass:: mediagoblin.tools.staticdirect.PluginStatic |
| 193 | |
| 194 | |
| 195 | Running plugin assetlink |
| 196 | ++++++++++++++++++++++++ |
| 197 | |
| 198 | In order for your plugin assets to be properly served by MediaGoblin, |
| 199 | your plugin's asset directory needs to be symlinked into the directory |
| 200 | that plugin assets are served from. To set this up, run:: |
| 201 | |
| 202 | ./bin/gmg assetlink |
| 203 | |
| 204 | |
| 205 | Using staticdirect |
| 206 | ++++++++++++++++++ |
| 207 | |
| 208 | Once you have this, you will want to be able to of course link to your |
| 209 | assets! MediaGoblin has a "staticdirect" tool; you want to use this |
| 210 | like so in your templates:: |
| 211 | |
| 212 | staticdirect("css/monkeys.css", "mystaticname") |
| 213 | |
| 214 | Replace "mystaticname" with the name you passed to PluginStatic. The |
| 215 | staticdirect method is, for convenience, attached to the request |
| 216 | object, so you can access this in your templates like: |
| 217 | |
| 218 | .. code-block:: html |
| 219 | |
| 220 | <img alt="A funny bunny" |
| 221 | src="{{ request.staticdirect('images/funnybunny.png', 'mystaticname') }}" /> |
| 222 | |
| 223 | |
| 224 | Additional hook tips |
| 225 | -------------------- |
| 226 | |
| 227 | This section aims to explain some tips in regards to adding hooks to |
| 228 | the MediaGoblin repository. |
| 229 | |
| 230 | WTForms hooks |
| 231 | +++++++++++++ |
| 232 | |
| 233 | We haven't totally settled on a way to tranform wtforms form objects, |
| 234 | but here's one way. In your view:: |
| 235 | |
| 236 | from mediagoblin.foo.forms import SomeForm |
| 237 | |
| 238 | def some_view(request) |
| 239 | form_class = hook_transform('some_form_transform', SomeForm) |
| 240 | form = form_class(request.form) |
| 241 | |
| 242 | Then to hook into this form, do something in your plugin like:: |
| 243 | |
| 244 | import wtforms |
| 245 | |
| 246 | class SomeFormAdditions(wtforms.Form): |
| 247 | new_datefield = wtforms.DateField() |
| 248 | |
| 249 | def transform_some_form(orig_form): |
| 250 | class ModifiedForm(orig_form, SomeFormAdditions) |
| 251 | return ModifiedForm |
| 252 | |
| 253 | hooks = { |
| 254 | 'some_form_transform': transform_some_form} |
| 255 | |
| 256 | |
| 257 | Interfaces |
| 258 | ++++++++++ |
| 259 | |
| 260 | If you want to add a pseudo-interface, it's not difficult to do so. |
| 261 | Just write the interface like so:: |
| 262 | |
| 263 | class FrobInterface(object): |
| 264 | """ |
| 265 | Interface for Frobbing. |
| 266 | |
| 267 | Classes implementing this interface should provide defrob and frob. |
| 268 | They may also implement double_frob, but it is not required; if |
| 269 | not provided, we will use a general technique. |
| 270 | """ |
| 271 | |
| 272 | def defrob(self, frobbed_obj): |
| 273 | """ |
| 274 | Take a frobbed_obj and defrob it. Returns the defrobbed object. |
| 275 | """ |
| 276 | raise NotImplementedError() |
| 277 | |
| 278 | def frob(self, normal_obj): |
| 279 | """ |
| 280 | Take a normal object and frob it. Returns the frobbed object. |
| 281 | """ |
| 282 | raise NotImplementedError() |
| 283 | |
| 284 | def double_frob(self, normal_obj): |
| 285 | """ |
| 286 | Frob this object and return it multiplied by two. |
| 287 | """ |
| 288 | return self.frob(normal_obj) * 2 |
| 289 | |
| 290 | |
| 291 | def some_frob_using_method(): |
| 292 | # something something something |
| 293 | frobber = hook_handle(FrobInterface) |
| 294 | frobber.frob(blah) |
| 295 | |
| 296 | # alternately you could have a default |
| 297 | frobber = hook_handle(FrobInterface) or DefaultFrobber |
| 298 | frobber.defrob(foo) |
| 299 | |
| 300 | |
| 301 | It's fine to use your interface as the key instead of a string if you |
| 302 | like. (Usually this is messy, but since interfaces are public and |
| 303 | since you need to import them into your plugin anyway, interfaces |
| 304 | might as well be keys.) |
| 305 | |
| 306 | Then a plugin providing your interface can be like:: |
| 307 | |
| 308 | from mediagoblin.foo.frobfrogs import FrobInterface |
| 309 | from frogfrobber import utils |
| 310 | |
| 311 | class FrogFrobber(FrobInterface): |
| 312 | """ |
| 313 | Takes a frogputer science approach to frobbing. |
| 314 | """ |
| 315 | def defrob(self, frobbed_obj): |
| 316 | return utils.frog_defrob(frobbed_obj) |
| 317 | |
| 318 | def frob(self, normal_obj): |
| 319 | return utils.frog_frob(normal_obj) |
| 320 | |
| 321 | hooks = { |
| 322 | FrobInterface: lambda: return FrogFrobber} |