{"action":"created","issue":{"url":"https://api.github.com/repos/learningequality/kolibri/issues/66","repository_url":"https://api.github.com/repos/learningequality/kolibri","labels_url":"https://api.github.com/repos/learningequality/kolibri/issues/66/labels{/name}","comments_url":"https://api.github.com/repos/learningequality/kolibri/issues/66/comments","events_url":"https://api.github.com/repos/learningequality/kolibri/issues/66/events","html_url":"https://github.com/learningequality/kolibri/pull/66","id":153028445,"number":66,"title":"[WIP] doc-driven development of hooks","user":{"login":"benjaoming","id":374612,"avatar_url":"https://avatars.githubusercontent.com/u/374612?v=3","gravatar_id":"","url":"https://api.github.com/users/benjaoming","html_url":"https://github.com/benjaoming","followers_url":"https://api.github.com/users/benjaoming/followers","following_url":"https://api.github.com/users/benjaoming/following{/other_user}","gists_url":"https://api.github.com/users/benjaoming/gists{/gist_id}","starred_url":"https://api.github.com/users/benjaoming/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/benjaoming/subscriptions","organizations_url":"https://api.github.com/users/benjaoming/orgs","repos_url":"https://api.github.com/users/benjaoming/repos","events_url":"https://api.github.com/users/benjaoming/events{/privacy}","received_events_url":"https://api.github.com/users/benjaoming/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":{"login":"rtibbles","id":1680573,"avatar_url":"https://avatars.githubusercontent.com/u/1680573?v=3","gravatar_id":"","url":"https://api.github.com/users/rtibbles","html_url":"https://github.com/rtibbles","followers_url":"https://api.github.com/users/rtibbles/followers","following_url":"https://api.github.com/users/rtibbles/following{/other_user}","gists_url":"https://api.github.com/users/rtibbles/gists{/gist_id}","starred_url":"https://api.github.com/users/rtibbles/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rtibbles/subscriptions","organizations_url":"https://api.github.com/users/rtibbles/orgs","repos_url":"https://api.github.com/users/rtibbles/repos","events_url":"https://api.github.com/users/rtibbles/events{/privacy}","received_events_url":"https://api.github.com/users/rtibbles/received_events","type":"User","site_admin":false},"milestone":null,"comments":21,"created_at":"2016-05-04T14:38:57Z","updated_at":"2016-05-17T19:16:12Z","closed_at":null,"pull_request":{"url":"https://api.github.com/repos/learningequality/kolibri/pulls/66","html_url":"https://github.com/learningequality/kolibri/pull/66","diff_url":"https://github.com/learningequality/kolibri/pull/66.diff","patch_url":"https://github.com/learningequality/kolibri/pull/66.patch"},"body":"## Summary\r\n\r\nThis PR redefines our plugin hooks API.\r\n\r\nThe purpose is to have hooks define exactly what they are and what they do using class types.\r\n\r\n## Features\r\n\r\n- [x] New abstract hooks base and metaclass for class-based hooks definitions.\r\n- [x] `webpack` simplified and moved to `kolibri.core.webpack`\r\n- [x] `KolibriPluginBase` to define standards for including urls, removes uncertainty about how to add url patterns and their views in a plugin!\r\n- [x] A new universal `--debug` flag for Kolibri's CLI.\r\n- [x] Well-known plugins are automatically enabled on first run\r\n\r\n## Example\r\n\r\nThe following hook can be extended for the purpose of registering assets for front-end usage.\r\n\r\nIn this example, the hook inherits from another abstract hook `WebpackBundleHook` that includes a lot of the actual functionality for webpack stuff (not displayed here, look in the code if you want).\r\n\r\n```python\r\nclass FrontEndSyncHook(WebpackBundleHook):\r\n    \"\"\"\r\n    Define something that should be included for sync'ed purposes. Assets will\r\n    always be loaded.\r\n    \"\"\"\r\n\r\n    class Meta:\r\n        abstract = True\r\n```\r\n\r\nTo register a hook, we define a non-abstract inheritor (it doesn't have `abstract=True`). This is **the same concept that you use for django models!**\r\n\r\n```python\r\nclass DefaultFrontEndAsset(FrontEndCoreHook):\r\n    unique_slug = \"default_frontend\"\r\n    src_file = \"kolibri/core/assets/src/kolibri_core_app.js\"\r\n    static_dir = \"kolibri/core/static\"\r\n```\r\n\r\nThe properties that we use for the hook are all defined in `WebpackBundleHook`. As such, we are accessing an explicitly defined class interface and this interface can be traversed by your IDE's code completion mechanisms and easily elaborated in our docs.\r\n\r\nSmall note to @rtibbles: I added a unique slug, that's the explicit way of naming an asset, before it was auto-\"magically\" named by its class and module path, which was then typed into the template code -- one should much rather be explicit about having a unique human name for things, and this way the names of the assets won't depend on their module path. This made a lot of code redundant, also on the JS-side where path.sep was replaced by a '.' to emulate a module path, then passed through command line to a python script that would try an import and give back JSON code.\r\n\r\nHow is it used then?\r\n\r\nWell, if we want to have all the front end assets registered, we just iterate them:\r\n\r\n```python\r\n@register.simple_tag()\r\ndef base_frontend_assets():\r\n    \"\"\"\r\n    This is a script tag for all ``FrontEndAssetHook`` hooks that implement a\r\n    render_to_html() method - this is used in ``/base.html`` template to\r\n    populate any Javascript and CSS that should be loaded at page load.\r\n\r\n    :return: HTML of script tags to insert into base.html\r\n    \"\"\"\r\n    tags = []\r\n    for hook in hooks.FrontEndAssetHook().registered_hooks:\r\n        tags.append(\r\n            hook.render_to_html()\r\n        )\r\n    return mark_safe('\\n'.join(tags))\r\n```\r\n\r\n## Implementation details\r\n\r\nIf you don't know how Python metaclasses work, it's perhaps a bit hard to understand the underlying mechanism.. but I'll try to explain it so its understandable even though you don't know metaclasses!\r\n\r\nEvery time a non-abstract inheritor of the base class `KolibriHook` is defined, its abstract parents have them added to their list of children.\r\n\r\nFor instance, to have a list of ALL registered hooks in Kolibri, we can query `KolibriHook`:\r\n\r\n```\r\n➜  kolibri git:(hooks-as-classes) python -m kolibri shell\r\n>>> from kolibri.plugins import hooks\r\n>>> for hook in hooks.KolibriHook().registered_hooks:\r\n...     print(hook)\r\n... \r\n<kolibri.core.kolibri_plugin.MainNavigationItem object at 0x7fe1b98c2908>\r\n<kolibri.core.kolibri_plugin.DefaultFrontEndAsset object at 0x7fe1b98c2898>\r\n```\r\n\r\nIt's possible to define complicated hook inheritence, for instance we did that for the frontend hook -- it's both a member of full set of Webpack assets while also a member of the frontend subset.\r\n\r\nOne thing I would like to note in case you wondered: Why do we instantiate `KolibriHook()` before accessing `registered_hooks`? Its types, so aren't the registered hooks a static property? Well, I wanted to make sure that we actually always have a real instance of a hook before accessing it. Who knows, perhaps it wants to prepare some resource for the registered hooks before they are accessed? Having a real instance of an abstract hook makes it all the more powerful in comparison to a static implementation.\r\n\r\n## Discussion\r\n\r\nI named them \"abstract hooks\" and \"registered hooks\" to create a distinguishing between the fact that one (the abstract hook) is a definition of how to hook into something, and the other is the actual hook (\"registered hook\").\r\n\r\nI could use a bit of input here if someone can come up with a more intuitive naming?\r\n\r\n## TODO\r\n\r\n- [ ] Have tests been written for the new code?\r\n- [x] Has documentation been written/updated?\r\n- [x] New dependencies (if any) added to requirements file\r\n- [x] Add an entry to CHANGELOG.rst\r\n- [x] Add yourself it AUTHORS.rst if you don't appear there\r\n- [x] Implement @rtibbles' comments, `once` and `events` on all types of bundles\r\n\r\n## Reviewer guidance\r\n\r\nPlease help ensure language and examples in docs are understandable.\r\n\r\n## Screenshots\r\n\r\nA new `--debug` flag went in the mix\r\n\r\n![screenshot from 2016-05-06 13 31 51](https://cloud.githubusercontent.com/assets/374612/15164661/98e7e260-1710-11e6-8976-028671743262.png)\r\n\r\n"},"comment":{"url":"https://api.github.com/repos/learningequality/kolibri/issues/comments/219823801","html_url":"https://github.com/learningequality/kolibri/pull/66#issuecomment-219823801","issue_url":"https://api.github.com/repos/learningequality/kolibri/issues/66","id":219823801,"user":{"login":"benjaoming","id":374612,"avatar_url":"https://avatars.githubusercontent.com/u/374612?v=3","gravatar_id":"","url":"https://api.github.com/users/benjaoming","html_url":"https://github.com/benjaoming","followers_url":"https://api.github.com/users/benjaoming/followers","following_url":"https://api.github.com/users/benjaoming/following{/other_user}","gists_url":"https://api.github.com/users/benjaoming/gists{/gist_id}","starred_url":"https://api.github.com/users/benjaoming/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/benjaoming/subscriptions","organizations_url":"https://api.github.com/users/benjaoming/orgs","repos_url":"https://api.github.com/users/benjaoming/repos","events_url":"https://api.github.com/users/benjaoming/events{/privacy}","received_events_url":"https://api.github.com/users/benjaoming/received_events","type":"User","site_admin":false},"created_at":"2016-05-17T19:16:12Z","updated_at":"2016-05-17T19:16:12Z","body":"> This means that it is impossible to load any frontend assets on another template without also loading them into the base template.\r\n\r\nNot sure that's so -> creating a new asset means creating a new hook that inherits from `FrontEndAssetHook` or `WebpackBundleHook`. Calling `{% webpack_assets 'slug' %}` renders it."}}