Commit | Line | Data |
---|---|---|
92c597ca E |
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 | ||
d861ffc9 | 14 | .. _plugin-api-chapter: |
92c597ca E |
15 | |
16 | ========== | |
17 | Plugin API | |
18 | ========== | |
19 | ||
4d0191dc CAW |
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 | ||
b21220e9 | 27 | Please check the :ref:`release-notes` for updates! |
4d0191dc | 28 | |
8ae5d20f CAW |
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 | ||
92c597ca E |
49 | :mod:`pluginapi` Module |
50 | ----------------------- | |
51 | ||
52 | .. automodule:: mediagoblin.tools.pluginapi | |
cf41e7d7 | 53 | :members: get_config, register_routes, register_template_path, |
36748921 | 54 | register_template_hooks, get_hook_templates, |
b835e153 | 55 | hook_handle, hook_runall, hook_transform |
f65bf898 CAW |
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 | ||
7fa4e19f CAW |
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 | ||
b312d2cd CAW |
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 | |
38ebd05d | 113 | ``request.urlgen()`` to look up the view. So say we're wanting to add |
b312d2cd CAW |
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 | |
1344c561 | 123 | ``mediagoblin.user_pages.user_home`` method:: |
b312d2cd CAW |
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 | ||
1344c561 | 140 | ("mediagoblin.user_pages.user_home", |
b312d2cd CAW |
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 = { | |
1344c561 | 152 | ("mediagoblin.user_pages.user_home", |
b312d2cd CAW |
153 | "mediagoblin/user_pages/user.html"): add_to_user_home_context} |
154 | ||
155 | ||
05539761 CAW |
156 | Global context hooks |
157 | ++++++++++++++++++++ | |
b312d2cd | 158 | |
1344c561 CAW |
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. | |
b312d2cd | 162 | |
1344c561 CAW |
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. | |
d6d2c771 CAW |
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 | ||
5de40278 | 192 | .. autoclass:: mediagoblin.tools.staticdirect.PluginStatic |
505b4b39 CAW |
193 | |
194 | ||
195 | Running plugin assetlink | |
196 | ++++++++++++++++++++++++ | |
197 | ||
505b4b39 CAW |
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 | ||
24ede044 | 202 | ./bin/gmg assetlink |
505b4b39 CAW |
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') }}" /> | |
baf2c1c9 CAW |
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 | |
40019095 | 231 | +++++++++++++ |
baf2c1c9 CAW |
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} | |
9d881aee CAW |
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 | |
ea49f378 CAW |
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.) | |
9d881aee CAW |
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} |