Custom Plugins
As Tanzawa Plugins are regular Django applications, developing a Tanzawa Plugin has the same ergonomics as Django development.
Requirements
In order for your Plugin to be seen by Tanzawa, it must have:
- A
Plugin
class that inherits fromdata.plugins.plugin.Plugin
. This is the interface between Tanzawa and a plugin. - A
tanzawa_plugin.py
ortanzawa_plugin
python package that instantiates the plugin and registers it with the Tanzawa Plugin Pool.
If your application lives outside of Tanzawa itself, you must install it manually by adding the package name to the PLUGINS
environment variable.
Plugin Interface
Each Plugin is required to provide some base information to Tanzawa so users can understand the plugin's functionality.
Name
The name of the plugin. This is the main identifier for the user, so try to make it as simple and self-descriptive as possible.
Identifier
A reverse-url style unique identifier for the plugin. This is stored in the database to let Tanzawa know which plugins are active and is not shown the user.
e.g. The bundled "Now page" plugin's identifier is blog.tanzawa.plugins.nowpage
.
Description
Provide an easy to understand description of what your plugin provides. This should be a plain text string, but can also be rendered HTML using Django templates if you want to provide links.
from django.template.loader import render_to_string
...
@property
def description(self):
return render_to_string("now/description.html")
Settings URL
Each plugin can provide a single URL that acts as the user's entry point to configure the plugin. This is the "settings" link that is displayed next to each plugin in the admin. Should return None if not configurable.
from django import urls
...
@property
def settings_url(self):
return urls.reverse_lazy("plugin_now_admin:update_now")
Top Navigation
Providing top navigation requires two settings: has_public_top_nav
and render_navigation
.
has_public_top_nav
A boolean value indicating if a plugin is providing top menu items.
render_navigation
A function that renders the actual navigation. It has two inputs: context
and render_location
.
context
is a regular django context containing the entire context for that page.nav
contains the id of the currently selected page. Use this variable to decide if your navigation is selected and should indicate as such e.g. underlined.render_location
is the name of the location that Tanzawa is rendering. This could be used to provide different layouts depending on the location.
from django import template
...
def render_navigation(
self,
*,
context: template.Context,
render_location: str,
) -> str:
"""Render the public facing navigation menu item."""
t = context.template.engine.get_template("now/navigation.html")
return t.render(context=context)
Example top navigation template for desktop display.
<a href="{% url "plugin_now:now" %}" class="ml-2 leading-8 hidden md:inline-block{% if nav == "now" %} border-b-4 border-negroni-900{% endif %}">
<span class="mr-1">👉</span>Now
</a>
Feed Hooks
Allow plugins to add content before or after each entry in a feed.
has_feed_hooks
A boolean value indicating if a plugin is hooking into the feed content generation.
feed_before_content
Called for each post in a feed. Input is a single post
object of type data.posts.TPost
.
feed_after_content
Called for each post in a feed. Input is a single post
object of type data.posts.TPost
.
Example implementation using a template to render content.
from django import template
...
def feed_after_content(self, post: Optional[post_models.TPost] = None) -> str:
template = loader.get_template("comment_by_email/feed.html")
return template.render(context={"post": post})
Registering Your Plugin
from plugins import plugin, pool
class NowPlugin(plugin.Plugin):
...
def get_plugin() -> plugin.Plugin:
return NowPlugin()
pool.plugin_pool.register_plugin(get_plugin())
Admin Views
Views in the admin should be contained in admin_urls.py
. Each view should be protected with the
Django @login_required
decorator to ensure anonymous users cannot change settings or access sensitive data.
from django.urls import path
from . import views
app_name = "plugin_now_admin"
urlpatterns = [
path("edit/", views.UpdateNowAdmin.as_view(), name="update_now"),
]
The admin views will be automatically included at /a/plugins/slugified-plugin-name/
.
Public Views
Views that should be publicly accessible should be contained in urls.py
. Note as plugin urls are included
before stream urls, if your public urls match a user's stream, your plugin will take precedence.
from django.urls import path
from . import views
app_name = "plugin_now"
urlpatterns = [
path("now/", views.PublicViewNow.as_view(), name="now"),
]
Migrations
Tanzawa will automatically run migrations on two occasions:
- When the plugin is activated.
- When the server starts (to allow for automatic upgrades of the DB schema in the future).
To disable automatically migrating when starting the server, set
PLUGINS_RUN_MIGRATIONS_STARTUP
to False
in your .env
file.
Creating Migrations
If your plugin is not enabled, first enable the plugin.
``` $ python3 apps/manage.py enable_plugin blog.tanzawa.plugins.nowpage
Once the plugin is enabled you can make migrations as usual.
$ python3 apps/manage.py makemigrations now
### Running Migrations Manually
After a plugin is enabled you can run migrations as if it were a regular django application.
$ python3 apps/manage.py migrate now ` ```
Templates
Templates are regular Django templates and should be placed in a templates/<plugin-app-name>
directory inside of your plugin.