Usage

Creating plugins

Create plugins using a Django management command:

This command asks a few questions, creates a basic Django app in the plugin path chosen in PluginManager.find_plugins(). It provides useful defaults as well as a setup.py/setup.cfg file.

If you use git in your project, install the gitpython module (pip/pipenv install gitpython --dev). startplugin will determine your git user/email automatically and use at the right places.

You now have two choices for this plugin:

Static plugins

In most of the cases, you will ship your application with a few “standard” plugins that are statically installed. These plugins must be loaded after the gdaps app.

# ...

INSTALLED_APPS = [
    # ... standard Django apps and GDAPS
    "gdaps",

    # put "static" plugins here too:
    "myproject.plugins.fooplugin.apps.FooConfig",
]

This plugin app is loaded as usual, but your GDAPS enhanced Django application can make use of it’s GDAPS features.

Dynamic plugins

By installing a plugin with pip/pipenv, you can make your application aware of that plugin too:

pipenv install -e myproject/plugins/fooplugin

This installs the plugin as python module into the site-packages and makes it discoverable using setuptools. From this moment on it should be already registered and loaded after a Django server restart. Of course this also works when plugins are installed from PyPi, they don’t have to be in the project’s plugins folder. You can conveniently start developing plugins in there, and later move them to the PyPi repository.

The plugin AppConfig

Django recommends to point ot the app’s AppConfig directly in INSTALLED_APPS. You should do that too with GDAPS plugins. Plugins that are installed via pip(env) are found automatically, as their AppConfig class must be named after the Plugin.

Plugins’ AppConfigs must inherit from gdaps.apps.PluginConfig, and provide an inner class, or a pointer to an external PluginMeta class. For more information see gdaps.apps.PluginConfig.

Interfaces

Plugins can define interfaces, which can then be implemented by other plugins. The startplugin command will create a <app_name>/api/interfaces.py file automatically. It’s not obligatory to put all Interface definitions in that module, but it is a recommended coding style for GDAPS plugins:

from gdaps import Interface

class IFooInterface(Interface):
    """Documentation of the interface"""

    class Meta:
        service = True

    def do_something(self):
        pass

Interfaces can have a default Meta class that defines Interface options. Available options:

service

If service=True (which is the default), then all implementations are instantiated instantly at definition time, having a full class instance availably at any time. Iterations over ExtensionPoints return the instances directly.

If you use service=False, the plugin is not instantiated, and iterations over ExtensionPoints will return classes, not instances. This sometimes may be the desired functionality, e.g. for data classes, or classes that just return staticmethods.

ExtensionPoints

An ExtensionPoint (EP) is a plugin hook that refers to an Interface. An EP can be defined anywhere in code. You can then get all the plugins that implement that interface by just iterating over that ExtensionPoint:

from gdaps import ExtensionPoint from
myproject.plugins.fooplugin.api.interfaces import IFooInterface

class MyPlugin:

    ep = ExtensionPoint(IFooInterface)

    def foo_method(self):
        for plugin in ep:
            print plugin().do_domething()

Depending on the service Meta flag, iterating over an ExtensionPoint returns either a class (service = False) or an already instantiated object (service = True). Depending on your needs, just set service to the correct value. The default is True.

Implementations

You can then easily implement this interface in any other file (in this plugin or in another plugin) using the @implements decorator syntax:

from gdaps import implements
from myproject.plugins.fooplugin.api.interfaces import IFooInterface

@implements(IFooInterface)
class OtherPluginClass:

    def do_something(self):
        print('I did something!')

I didn’t want to force implementations to inherit a Plugin base class, like some other plugin systems do. This would mean that implementations won’t be as flexible as I wanted them. When just using a decorator, you can easily use ANY, even your already existing, class and just ducktype-implement the methods the Interface demands.

Extending Django’s URL patterns

To let your plugin define some URLs that are automatically detected by your Django application, you have to add some code to your global urls.py file:

from gdaps.pluginmanager import PluginManager

urlpatterns =  [
    # add your fixed, non-plugin paths here.
]

# just add this line after the urlpatterns definition:
urlpatterns += PluginManager.urlpatterns()

GDAPS then loads and imports all available plugins’ urls.py files, collects their urlpatterns variables and merges them into the global one.

A typical fooplugin/urls.py would look like this:

from . import views

app_name = fooplugin

urlpatterns =  [
    path("/fooplugin/myurl", views.MyUrlView.as_view()),
]

GDAPS lets your plugin create global, root URLs, they are not namespaced. This is because soms plugins need to create URLS for frameworks like DRF, etc. Plugins are responsible for their URLs, and that they don’t collide with others.

Per-plugin Settings

GDAPS allows your application to have own settings for each plugin easily, which provide defaults, and can be overridden in the global settings.py file. Look at the example conf.py file (created by ./manage.py startplugin fooplugin), and adapt to your needs:

from django.test.signals import setting_changed
from gdaps.conf import PluginSettings

NAMESPACE = "FOOPLUGIN"

# Optional defaults. Leave empty if not needed.
DEFAULTS = {
    "MY_SETTING": "somevalue",
    "FOO_PATH": "django.blah.foo",
    "BAR": [
        "baz",
        "buh",
    ],
}

# Optional list of settings that are allowed to be in "string import" notation. Leave empty if not needed.
IMPORT_STRINGS = (
    "FOO_PATH"
)

# Optional list of settings that have been removed. Leave empty if not needed.
REMOVED_SETTINGS = ( "FOO_SETTING" )


fooplugin_settings = PluginSettings("FOOPLUGIN", None, DEFAULTS, IMPORT_STRINGS)

Detailed explanation:

DEFAULTS

The DEFAULTS are, as the name says, a default array of settings. If fooplugin_setting.BLAH is not set by the user in settings.py, this default value is used.

IMPORT_STRINGS

Settings in a dotted notation are evaluated, they return not the string, but the object they point to. If it does not exist, an ImportError is raised.

REMOVED_SETTINGS

A list of settings that are forbidden to use. If accessed, an RuntimeError is raised.

This allows very flexible settings - as dependant plugins can easily import the fooplugin_settings from your conf.py.

However, the created conf.py file is not needed, so if you don’t use custom settings at all, just delete the file.

Admin site

GDAPS provides support for the Django admin site. The built-in GdapsPlugin model automatically are added to Django’S admin site, and can be administered there.

Note

As GdapsPlugin database entries must not be edited directly, they are shown read-only in the admin. Please use the ‘syncplugins’ management command to update the fields from the file system. However, you can enable/disable or hide/show plugins via the admin interface.

If you want to disable the built-in admin site for GDAPS, or provide a custom GDAPS ModelAdmin, you can do this using:

GDAPS = {
    "ADMIN": False
}

Frontend support

GDAPS supports Javascript frontends for building e.g. SPA applications. ATM only Vue.js ist supported, but PRs are welcome to add more (Angular, React?).

Just add gdaps.frontend to INSTALLED_APPS, before gdaps. Afterwords, there is a new management command available: manage.py initfrontend. It has one mandatory parameter, the frontend engine:

This creates a /frontend/ directory in the project root. Change into that directory and run yarn install once to install all the dependencies of Vue.js needed.

It is recommended to install vue globally, you can do that with yarn global add @vue/cli @vue/cli-service-global.

Now you can start yarn serve in the frontend directory. This starts a development web server that bundles the frontend app using webpack automatically. You then need to start Django using ./manage.py runserver to enable the Django backend. GDAPS manages all the needed background tasks to transparently enable hot-reloading when you change anything in the frontend source code now.

Frontend plugins

Django itself provides a template engine, so you could use templates in your GDAPS apps to build the frontend parts too. But templates are not always the desired way to go. Since a few years, Javascript SPAs (Single Page Applications) have come up and promise fast, responsive software.

But: a SPA mostly is written as monolithic block. All tutorials that describe Django as backend recommend building the Django server modular, but it should serve only as API, namely REST or GraphQL. This API then should be consumed by a monolithic Javascript frontend, built by webpack etc. At least I didn’t find anything else on the internet. So I created my own solution:

GDAPS is a plugin system. It provides backend plugins (Django apps). But using gdaps.frontend, each GDAPS app can use a frontend directory which contains an installable npm module, that is automatically installed when the app is added to the system.

When the gdaps.frontend app is activated in INSTALLED_APPS, the startplugin management command is extended by a frontend part: When a new plugin is created, a frontend directory in that plugin is initialized with a boilerplate javascript file index.js, which is the plugin entry point in the frontend. This is accomplished by webpack and django-webpack-loader.

So all you have to do is:

  1. Add gdaps.frontend to INSTALLED_APPS (before gdaps)

  2. Call ./manage.py initfrontend vue, if you haven’t already

  3. Call ./manage.py startplugin fooplugin and fill out the questions

  4. start yarn serve in the frontend directory

  5. start Django server using ./manage.py runserver

Webpack aggregates all you need into a package, using the frontend/plugins.js file as index where to find plugin entry points. You shouldn’t manually edit that file, but just install GDAPS plugins as usual (pip, pipenv, or by adding them to INSTALLED_APPS) and call manage.py syncplugins then.

This command scans your app for plugins, updates the database with plugin data, and recreates the plugins.js file.