Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
231 lines (180 sloc) 8.81 KB

Django App Plugins

Provides functionality to enable improved plugin support of Django apps.

Once a Django project is enhanced with this functionality, any participating Django app (a.k.a. Plugin App) that is PIP-installed on the system is automatically included in the Django project's INSTALLED_APPS list. In addition, the participating Django app's URLs and Settings are automatically recognized by the Django project. Furthermore, the Plugin Signals feature allows Plugin Apps to shift their dependencies on Django Signal Senders from code-time to runtime.

While Django+Python already support dynamic installation of components/apps, they do not have out-of-the-box support for plugin apps that auto-install into a containing Django project.

This Django App Plugin functionality allows for Django-framework code to be encapsulated within each Django app, rather than having a monolith Project that is aware of the details of its Django apps. It is motivated by the following design principles:

  • Single Responsibility Principle, which says "a class or module should have one, and only one, reason to change." When code related to a single Django app changes, there's no reason for its containing project to also change. The encapsulation and modularity resulting from code being co-located with its owning Django app helps prevent "God objects" that have too much responsibility and knowledge of the details.
  • Open Closed Principle, which says "software entities should be open for extension, but closed for modification." The edx-platform is extensible via installation of Django apps. Having automatic Django App Plugin support allows for this extensibility without modification to the edx-platform. Going forward, we expect this capability to be widely used by external repos that depend on and enhance the edx-platform without the need to modify the core platform.
  • Dependency Inversion Principle, which says "high level modules should not depend upon low level modules." The high-level module here is the Django project, while the participating Django app is the low-level module. For long-term maintenance of a system, dependencies should go from low-level modules/details to higher level ones.

Django Projects

In order to enable this functionality in a Django project, the project needs to update:

1. its settings to extend its INSTALLED_APPS to include the Plugin Apps

INSTALLED_APPS.extend(plugin_apps.get_apps(...))

2. its settings to add all Plugin Settings

plugin_settings.add_plugins(__name__, ...)

3. its urls to add all Plugin URLs

urlpatterns.extend(plugin_urls.get_patterns(...))

4. its setup to register PluginsConfig (for connecting Plugin Signals)

from setuptools import setup
setup(
    ...
    entry_points={
        "lms.djangoapp": [
            "plugins = openedx.core.djangoapps.plugins.apps:PluginsConfig",
        ],
        "cms.djangoapp": [
            "plugins = openedx.core.djangoapps.plugins.apps:PluginsConfig",
        ],
    }
)

Plugin Apps

In order to make use of this functionality, plugin apps need to:

1. create an AppConfig class in their apps module, as described in Django's Application Configuration.

2. add their AppConfig class to the appropriate entry point in their setup.py file:

from setuptools import setup
setup(
    ...
    entry_points={
        "lms.djangoapp": [
            "my_app = full_python_path.my_app.apps:MyAppConfig",
        ],
        "cms.djangoapp": [
        ],
    }
)

3. configure the Plugin App in their AppConfig class:

from django.apps import AppConfig
from openedx.core.djangoapps.plugins.constants import (
    ProjectType, SettingsType, PluginURLs, PluginSettings
)
class MyAppConfig(AppConfig):
    name = u'full_python_path.my_app'

    # Class attribute that configures and enables this app as a Plugin App.
    plugin_app = {

        # Configuration setting for Plugin URLs for this app.
        PluginURLs.CONFIG: {

            # Configure the Plugin URLs for each project type, as needed.
            ProjectType.LMS: {

                # The namespace to provide to django's urls.include.
                PluginURLs.NAMESPACE: u'my_app',

                # The application namespace to provide to django's urls.include.
                # Optional; Defaults to None.
                PluginURLs.APP_NAME: u'my_app',

                # The regex to provide to django's urls.url.
                # Optional; Defaults to r''.
                PluginURLs.REGEX: r'^api/my_app/',

                # The python path (relative to this app) to the URLs module to be plugged into the project.
                # Optional; Defaults to u'urls'.
                PluginURLs.RELATIVE_PATH: u'api.urls',
            }
        },

        # Configuration setting for Plugin Settings for this app.
        PluginSettings.CONFIG: {

            # Configure the Plugin Settings for each Project Type, as needed.
            ProjectType.LMS: {

                # Configure each Settings Type, as needed.
                SettingsType.PRODUCTION: {

                    # The python path (relative to this app) to the settings module for the relevant Project Type and Settings Type.
                    # Optional; Defaults to u'settings'.
                    PluginSettings.RELATIVE_PATH: u'settings.production',
                },
                SettingsType.COMMON: {
                    PluginSettings.RELATIVE_PATH: u'settings.common',
                },
            }
        },

        # Configuration setting for Plugin Signals for this app.
        PluginSignals.CONFIG: {

            # Configure the Plugin Signals for each Project Type, as needed.
            ProjectType.LMS: {

                # The python path (relative to this app) to the Signals module containing this app's Signal receivers.
                # Optional; Defaults to u'signals'.
                PluginSignals.RELATIVE_PATH: u'my_signals',

                # List of all plugin Signal receivers for this app and project type.
                PluginSignals.RECEIVERS: [{

                    # The name of the app's signal receiver function.
                    PluginSignals.RECEIVER_FUNC_NAME: u'on_signal_x',

                    # The full path to the module where the signal is defined.
                    PluginSignals.SIGNAL_PATH: u'full_path_to_signal_x_module.SignalX',

                    # The value for dispatch_uid to pass to Signal.connect to prevent duplicate signals.
                    # Optional; Defaults to full path to the signal's receiver function.
                    PluginSignals.DISPATCH_UID: u'my_app.my_signals.on_signal_x',

                    # The full path to a sender (if connecting to a specific sender) to be passed to Signal.connect.
                    # Optional; Defaults to None.
                    PluginSignals.SENDER_PATH: u'full_path_to_sender_app.ModelZ',
                }],
            }
        }
    }

OR use string constants when they cannot import from djangoapps.plugins:

from django.apps import AppConfig
class MyAppConfig(AppConfig):
    name = u'full_python_path.my_app'

    plugin_app = {
        u'url_config': {
            u'lms.djangoapp': {
                u'namespace': u'my_app',
                u'regex': u'^api/my_app/',
                u'relative_path': u'api.urls',
            }
        },
        u'settings_config': {
            u'lms.djangoapp': {
                u'production': { relative_path: u'settings.production' },
                u'common': { relative_path: u'settings.common'},
            }
        },
        u'signals_config': {
            u'lms.djangoapp': {
                u'relative_path': u'my_signals',
                u'receivers': [{
                    u'receiver_func_name': u'on_signal_x',
                    u'signal_path': u'full_path_to_signal_x_module.SignalX',
                    u'dispatch_uid': u'my_app.my_signals.on_signal_x',
                    u'sender_path': u'full_path_to_sender_app.ModelZ',
                }],
            }
        }
    }

4. For Plugin Settings, insert the following function into each of the Plugin Settings modules:

def plugin_settings(settings):
    # Update the provided settings module with any app-specific settings.
    # For example:
    #     settings.FEATURES['ENABLE_MY_APP'] = True
    #     settings.MY_APP_POLICY = 'foo'
You can’t perform that action at this time.