Extracting plugin app from edx-platform (#24678)
* Moving plugins infrastructure to edx-django-utils This PR extracts the code that enables plugins in edx-platform and puts it in edx-django-utils. This is done to allow other IDAS to add plugin functionality.
This commit is contained in:
@@ -2076,9 +2076,11 @@ SYSTEM_WIDE_ROLE_CLASSES = []
|
||||
|
||||
############## Installed Django Apps #########################
|
||||
|
||||
from openedx.core.djangoapps.plugins import plugin_apps, plugin_settings, constants as plugin_constants
|
||||
INSTALLED_APPS.extend(plugin_apps.get_apps(plugin_constants.ProjectType.CMS))
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, plugin_constants.SettingsType.COMMON)
|
||||
from edx_django_utils.plugins import get_plugin_apps, add_plugins
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
INSTALLED_APPS.extend(get_plugin_apps(ProjectType.CMS))
|
||||
add_plugins(__name__, ProjectType.CMS, SettingsType.COMMON)
|
||||
|
||||
# Course exports streamed in blocks of this size. 8192 or 8kb is the default
|
||||
# setting for the FileWrapper class used to iterate over the export file data.
|
||||
|
||||
@@ -200,9 +200,11 @@ BLOCKSTORE_API_URL = "http://edx.devstack.blockstore:18250/api/v1/"
|
||||
#####################################################################
|
||||
|
||||
# pylint: disable=wrong-import-order, wrong-import-position
|
||||
from openedx.core.djangoapps.plugins import constants as plugin_constants, plugin_settings
|
||||
from edx_django_utils.plugins import add_plugins
|
||||
# pylint: disable=wrong-import-order, wrong-import-position
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, plugin_constants.SettingsType.DEVSTACK)
|
||||
add_plugins(__name__, ProjectType.CMS, SettingsType.DEVSTACK)
|
||||
|
||||
|
||||
OPENAPI_CACHE_TIMEOUT = 0
|
||||
|
||||
@@ -15,12 +15,13 @@ import yaml
|
||||
from corsheaders.defaults import default_headers as corsheaders_default_headers
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.urls import reverse_lazy
|
||||
from edx_django_utils.plugins import add_plugins
|
||||
from path import Path as path
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
from .common import *
|
||||
|
||||
from openedx.core.djangoapps.plugins import constants as plugin_constants
|
||||
from openedx.core.djangoapps.plugins import plugin_settings
|
||||
from openedx.core.lib.derived import derive_settings
|
||||
from openedx.core.lib.logsettings import get_logger_config
|
||||
from xmodule.modulestore.modulestore_settings import convert_module_store_setting_if_needed
|
||||
@@ -539,7 +540,7 @@ MAX_BLOCKS_PER_CONTENT_LIBRARY = ENV_TOKENS.get('MAX_BLOCKS_PER_CONTENT_LIBRARY'
|
||||
|
||||
# This is at the bottom because it is going to load more settings after base settings are loaded
|
||||
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, plugin_constants.SettingsType.PRODUCTION)
|
||||
add_plugins(__name__, ProjectType.CMS, SettingsType.PRODUCTION)
|
||||
|
||||
########################## Derive Any Derived Settings #######################
|
||||
|
||||
|
||||
@@ -286,9 +286,11 @@ VIDEO_TRANSCRIPTS_SETTINGS = dict(
|
||||
|
||||
####################### Plugin Settings ##########################
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, plugin_constants.SettingsType.TEST)
|
||||
# pylint: disable=wrong-import-position, wrong-import-order
|
||||
from edx_django_utils.plugins import add_plugins
|
||||
# pylint: disable=wrong-import-position, wrong-import-order
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
add_plugins(__name__, ProjectType.CMS, SettingsType.TEST)
|
||||
|
||||
########################## Derive Any Derived Settings #######################
|
||||
|
||||
|
||||
@@ -303,8 +303,11 @@ if 'openedx.testing.coverage_context_listener' in settings.INSTALLED_APPS:
|
||||
url(r'coverage_context', include('openedx.testing.coverage_context_listener.urls'))
|
||||
]
|
||||
|
||||
from openedx.core.djangoapps.plugins import constants as plugin_constants, plugin_urls
|
||||
urlpatterns.extend(plugin_urls.get_patterns(plugin_constants.ProjectType.CMS))
|
||||
# pylint: disable=wrong-import-position, wrong-import-order
|
||||
from edx_django_utils.plugins import get_plugin_url_patterns
|
||||
# pylint: disable=wrong-import-position
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
urlpatterns.extend(get_plugin_url_patterns(ProjectType.CMS))
|
||||
|
||||
# Contentstore
|
||||
urlpatterns += [
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from edx_django_utils import monitoring as monitoring_utils
|
||||
from edx_django_utils.plugins import get_plugins_view_context
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
from six import iteritems, text_type
|
||||
@@ -35,8 +36,7 @@ from openedx.core.djangoapps.catalog.utils import (
|
||||
get_visible_sessions_for_entitlement
|
||||
)
|
||||
from openedx.core.djangoapps.credit.email_utils import get_credit_provider_attribute_values, make_providers_strings
|
||||
from openedx.core.djangoapps.plugins import constants as plugin_constants
|
||||
from openedx.core.djangoapps.plugins.plugin_contexts import get_plugins_view_context
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs.utils import ProgramDataExtender, ProgramProgressMeter
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
@@ -800,7 +800,7 @@ def student_dashboard(request):
|
||||
}
|
||||
|
||||
context_from_plugins = get_plugins_view_context(
|
||||
plugin_constants.ProjectType.LMS,
|
||||
ProjectType.LMS,
|
||||
COURSE_DASHBOARD_PLUGIN_VIEW_NAME,
|
||||
context
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.core.files.storage import get_storage_class
|
||||
from six import text_type
|
||||
from xblock.fields import List
|
||||
|
||||
from openedx.core.lib.plugins import PluginError
|
||||
from edx_django_utils.plugins import PluginError
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
|
||||
@@ -83,11 +83,11 @@ If you wish to customize aspects of the learner or educator experiences, you'll
|
||||
|
||||
Most python plugins are enabled using one of two methods:
|
||||
|
||||
1. A Python Entry point: the core Open edX platform provides a standard plugin loading mechanism in |openedx.core.lib.plugins|_ which uses `stevedore`_ to find all installed python packages that declare a specific "entry point" in their setup.py file. See the ``entry_points`` defined in edx-platform's own ``setup.py`` for examples.
|
||||
1. A Python Entry point: the core Open edX platform provides a standard plugin loading mechanism in |edx_django_utils.plugins|_ which uses `stevedore`_ to find all installed python packages that declare a specific "entry point" in their setup.py file. See the ``entry_points`` defined in edx-platform's own ``setup.py`` for examples.
|
||||
2. A Django setting: Some plugins require modification of Django settings, which is typically done by editing ``/edx/etc/lms.yml`` (in Production) or ``edx-platform/lms/envs/private.py`` (on Devstack).
|
||||
|
||||
.. |openedx.core.lib.plugins| replace:: ``openedx.core.lib.plugins``
|
||||
.. _openedx.core.lib.plugins: https://github.com/edx/edx-platform/blob/master/openedx/core/lib/plugins.py
|
||||
.. |edx_django_utils.plugins| replace:: ``edx_django_utils.plugins``
|
||||
.. _edx_django_utils.plugins: https://github.com/edx/edx-django-utils/blob/master/edx_django_utils/plugins
|
||||
.. _stevedore: https://pypi.org/project/stevedore/
|
||||
|
||||
Here are the different integration points that python plugins can use:
|
||||
|
||||
@@ -3,7 +3,6 @@ Celery needs to be loaded when the cms modules are so that task
|
||||
registration and discovery can work correctly.
|
||||
"""
|
||||
|
||||
|
||||
# We monkey patch Kombu's entrypoints listing because scanning through this
|
||||
# accounts for the majority of LMS/Studio startup time for tests, and we don't
|
||||
# use custom Kombu serializers (which is what this is for). Still, this is
|
||||
|
||||
@@ -6,9 +6,10 @@ Signal handlers are connected here.
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginSettings, PluginURLs
|
||||
|
||||
from openedx.core.constants import COURSE_ID_PATTERN
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSettings, PluginURLs, ProjectType, SettingsType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
|
||||
class DiscussionConfig(AppConfig):
|
||||
|
||||
@@ -7,9 +7,10 @@ Signal handlers are connected here.
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from edx_django_utils.plugins import PluginSettings, PluginURLs
|
||||
from edx_proctoring.runtime import set_runtime_service
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSettings, PluginURLs, ProjectType, SettingsType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
|
||||
class GradesConfig(AppConfig):
|
||||
|
||||
@@ -5,10 +5,11 @@ Instructor Application Configuration
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from edx_django_utils.plugins import PluginSettings, PluginURLs
|
||||
from edx_proctoring.runtime import set_runtime_service
|
||||
|
||||
from openedx.core.constants import COURSE_ID_PATTERN
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSettings, PluginURLs, ProjectType, SettingsType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
|
||||
class InstructorConfig(AppConfig):
|
||||
|
||||
@@ -5,8 +5,9 @@ ProgramEnrollments Application Configuration
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginURLs
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginURLs, ProjectType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
|
||||
class ProgramEnrollmentsConfig(AppConfig):
|
||||
|
||||
@@ -3880,9 +3880,11 @@ SYSTEM_WIDE_ROLE_CLASSES = []
|
||||
|
||||
############## Plugin Django Apps #########################
|
||||
|
||||
from openedx.core.djangoapps.plugins import plugin_apps, plugin_settings, constants as plugin_constants
|
||||
INSTALLED_APPS.extend(plugin_apps.get_apps(plugin_constants.ProjectType.LMS))
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.LMS, plugin_constants.SettingsType.COMMON)
|
||||
from edx_django_utils.plugins import get_plugin_apps, add_plugins
|
||||
# pylint: disable=wrong-import-position, wrong-import-order
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
INSTALLED_APPS.extend(get_plugin_apps(ProjectType.LMS))
|
||||
add_plugins(__name__, ProjectType.LMS, SettingsType.COMMON)
|
||||
|
||||
DEPRECATED_ADVANCED_COMPONENT_TYPES = []
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ from corsheaders.defaults import default_headers as corsheaders_default_headers
|
||||
|
||||
# pylint: enable=unicode-format-string
|
||||
#####################################################################
|
||||
from openedx.core.djangoapps.plugins import constants as plugin_constants
|
||||
from openedx.core.djangoapps.plugins import plugin_settings
|
||||
from edx_django_utils.plugins import add_plugins
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
from .production import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@@ -309,7 +310,7 @@ JWT_AUTH.update({
|
||||
'y5ZLcTUomo4rZLjghVpq6KZxfS6I1Vz79ZsMVUWEdXOYePCKKsrQG20ogQEkmTf9FT_SouC6jPcHLXw"}]}'
|
||||
),
|
||||
})
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.LMS, plugin_constants.SettingsType.DEVSTACK)
|
||||
add_plugins(__name__, ProjectType.LMS, SettingsType.DEVSTACK)
|
||||
|
||||
|
||||
######################### Django Rest Framework ########################
|
||||
|
||||
@@ -27,9 +27,10 @@ import dateutil
|
||||
import yaml
|
||||
from corsheaders.defaults import default_headers as corsheaders_default_headers
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from edx_django_utils.plugins import add_plugins
|
||||
from path import Path as path
|
||||
|
||||
from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
from openedx.core.lib.derived import derive_settings
|
||||
from openedx.core.lib.logsettings import get_logger_config
|
||||
from xmodule.modulestore.modulestore_settings import convert_module_store_setting_if_needed
|
||||
@@ -949,7 +950,7 @@ MAX_BLOCKS_PER_CONTENT_LIBRARY = ENV_TOKENS.get('MAX_BLOCKS_PER_CONTENT_LIBRARY'
|
||||
# This is at the bottom because it is going to load more settings after base settings are loaded
|
||||
|
||||
# Load production.py in plugins
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.LMS, plugin_constants.SettingsType.PRODUCTION)
|
||||
add_plugins(__name__, ProjectType.LMS, SettingsType.PRODUCTION)
|
||||
|
||||
########################## Derive Any Derived Settings #######################
|
||||
|
||||
|
||||
@@ -23,10 +23,11 @@ from uuid import uuid4
|
||||
|
||||
import openid.oidutil
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from edx_django_utils.plugins import add_plugins
|
||||
from path import Path as path
|
||||
from six.moves import range
|
||||
|
||||
from openedx.core.djangoapps.plugins import plugin_settings, constants as plugin_constants
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
from openedx.core.lib.derived import derive_settings
|
||||
from openedx.core.lib.tempdir import mkdtemp_clean
|
||||
|
||||
@@ -557,7 +558,7 @@ JWT_AUTH.update({
|
||||
# pylint: enable=unicode-format-string
|
||||
####################### Plugin Settings ##########################
|
||||
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.LMS, plugin_constants.SettingsType.TEST)
|
||||
add_plugins(__name__, ProjectType.LMS, SettingsType.TEST)
|
||||
|
||||
########################## Derive Any Derived Settings #######################
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.contrib.admin import autodiscover as django_autodiscover
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic.base import RedirectView
|
||||
from edx_api_doc_tools import make_docs_urls
|
||||
from edx_django_utils.plugins import get_plugin_url_patterns
|
||||
from ratelimitbackend import admin
|
||||
|
||||
from branding import views as branding_views
|
||||
@@ -41,8 +42,7 @@ from openedx.core.djangoapps.django_comment_common.models import ForumsConfig
|
||||
from openedx.core.djangoapps.lang_pref import views as lang_pref_views
|
||||
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
|
||||
from openedx.core.djangoapps.password_policy.forms import PasswordPolicyAwareAdminAuthForm
|
||||
from openedx.core.djangoapps.plugins import constants as plugin_constants
|
||||
from openedx.core.djangoapps.plugins import plugin_urls
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
@@ -961,7 +961,7 @@ urlpatterns.append(
|
||||
),
|
||||
)
|
||||
|
||||
urlpatterns.extend(plugin_urls.get_patterns(plugin_constants.ProjectType.LMS))
|
||||
urlpatterns.extend(get_plugin_url_patterns(ProjectType.LMS))
|
||||
|
||||
# Course Home API urls
|
||||
urlpatterns += [
|
||||
|
||||
@@ -5,8 +5,9 @@ Configuration for the ace_common Django app.
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from edx_django_utils.plugins import PluginSettings
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSettings, ProjectType, SettingsType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
|
||||
class AceCommonConfig(AppConfig):
|
||||
|
||||
@@ -5,8 +5,9 @@ Configuration for bookmarks Django app
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from edx_django_utils.plugins import PluginSettings, PluginURLs
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSettings, PluginURLs, ProjectType, SettingsType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
|
||||
class BookmarksConfig(AppConfig):
|
||||
|
||||
@@ -8,9 +8,9 @@ from base64 import b64encode
|
||||
from hashlib import sha1
|
||||
|
||||
import six
|
||||
from edx_django_utils.plugins import PluginManager
|
||||
|
||||
from openedx.core.lib.cache_utils import process_cached
|
||||
from openedx.core.lib.plugins import PluginManager
|
||||
|
||||
|
||||
class TransformerRegistry(PluginManager):
|
||||
|
||||
@@ -5,8 +5,9 @@ Django AppConfig for Content Libraries Implementation
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginURLs, PluginSettings
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, PluginURLs, PluginSettings
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
|
||||
class ContentLibrariesConfig(AppConfig):
|
||||
|
||||
@@ -6,8 +6,9 @@ Signal handlers are connected here.
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginURLs
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginURLs, ProjectType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
|
||||
class CoursewareAPIConfig(AppConfig):
|
||||
|
||||
@@ -5,8 +5,9 @@ Credentials Configuration
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from edx_django_utils.plugins import PluginSettings, PluginSignals
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSettings, PluginSignals, ProjectType, SettingsType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
|
||||
class CredentialsConfig(AppConfig):
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
olx_rest_api Django application initialization.
|
||||
"""
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginURLs
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginURLs, ProjectType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
|
||||
class OlxRestApiAppConfig(AppConfig):
|
||||
|
||||
@@ -8,8 +8,9 @@ from dateutil.parser import parse as parse_date
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from edx_django_utils.plugins import PluginSettings
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSettings, ProjectType, SettingsType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,248 +1,8 @@
|
||||
Django App Plugins
|
||||
==================
|
||||
Plugins App
|
||||
===========
|
||||
|
||||
Provides functionality to enable improved plugin support of Django apps.
|
||||
This app provides edx-platform specific constants and support for the Django Plugin infrastructure defined in `edx_django_utils/plugins`_.
|
||||
|
||||
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.
|
||||
It enables you to add a plugin to LMS or CMS, including ensuring Django signals work correctly between edx-platform and installed plugins.
|
||||
|
||||
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 <https://docs.djangoproject.com/en/2.0/ref/applications/#django.apps.AppConfig>`_.
|
||||
|
||||
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, PluginContexts
|
||||
)
|
||||
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',
|
||||
}],
|
||||
}
|
||||
},
|
||||
|
||||
# Configuration setting for Plugin Contexts for this app.
|
||||
PluginContexts.CONFIG: {
|
||||
|
||||
# Configure the Plugin Signals for each Project Type, as needed.
|
||||
ProjectType.LMS: {
|
||||
|
||||
# Key is the view that the app wishes to add context to and the value
|
||||
# is the function within the app that will return additional context
|
||||
# when called with the original context
|
||||
u'course_dashboard': u'my_app.context_api.get_dashboard_context'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
}],
|
||||
}
|
||||
},
|
||||
u'view_context_config': {
|
||||
u'lms.djangoapp': {
|
||||
'course_dashboard': u'my_app.context_api.get_dashboard_context'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
.. _edx_django_utils/plugins: https://github.com/edx/edx-django-utils/tree/master/edx_django_utils/plugins
|
||||
@@ -7,14 +7,17 @@ Signal handlers are connected here.
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from . import constants, plugin_signals
|
||||
from edx_django_utils.plugins import connect_plugin_receivers
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
|
||||
class PluginsConfig(AppConfig):
|
||||
"""
|
||||
Application Configuration for Plugins.
|
||||
"""
|
||||
name = u'openedx.core.djangoapps.plugins'
|
||||
|
||||
name = 'openedx.core.djangoapps.plugins'
|
||||
|
||||
plugin_app = {}
|
||||
|
||||
@@ -23,8 +26,8 @@ class PluginsConfig(AppConfig):
|
||||
Connect plugin receivers to their signals.
|
||||
"""
|
||||
if settings.ROOT_URLCONF == 'lms.urls':
|
||||
project_type = constants.ProjectType.LMS
|
||||
project_type = ProjectType.LMS
|
||||
else:
|
||||
project_type = constants.ProjectType.CMS
|
||||
project_type = ProjectType.CMS
|
||||
|
||||
plugin_signals.connect_receivers(project_type)
|
||||
connect_plugin_receivers(project_type)
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
# Name of the class attribute to put in the AppConfig class of the Plugin App.
|
||||
PLUGIN_APP_CLASS_ATTRIBUTE_NAME = u'plugin_app'
|
||||
|
||||
|
||||
# Name of the function that belongs in the plugin Django app's settings file.
|
||||
# The function should be defined as:
|
||||
# def plugin_settings(settings):
|
||||
# # enter code that should be injected into the given settings module.
|
||||
PLUGIN_APP_SETTINGS_FUNC_NAME = u'plugin_settings'
|
||||
|
||||
|
||||
class ProjectType(object):
|
||||
class ProjectType():
|
||||
"""
|
||||
The ProjectType enum defines the possible values for the Django Projects
|
||||
that are available in the edx-platform. Plugin apps use these values to
|
||||
declare explicitly which projects they are extending.
|
||||
"""
|
||||
LMS = u'lms.djangoapp'
|
||||
CMS = u'cms.djangoapp'
|
||||
|
||||
LMS = 'lms.djangoapp'
|
||||
CMS = 'cms.djangoapp'
|
||||
|
||||
|
||||
class SettingsType(object):
|
||||
class SettingsType():
|
||||
"""
|
||||
The SettingsType enum defines the possible values for the settings files
|
||||
that are available for extension in the edx-platform. Plugin apps use these
|
||||
@@ -29,59 +20,8 @@ class SettingsType(object):
|
||||
See https://github.com/edx/edx-platform/master/lms/envs/docs/README.rst for
|
||||
further information on each Settings Type.
|
||||
"""
|
||||
PRODUCTION = u'production'
|
||||
COMMON = u'common'
|
||||
DEVSTACK = u'devstack'
|
||||
TEST = u'test'
|
||||
|
||||
|
||||
class PluginSettings(object):
|
||||
"""
|
||||
The PluginSettings enum defines dictionary field names (and defaults)
|
||||
that can be specified by a Plugin App in order to configure the settings
|
||||
that are injected into the project.
|
||||
"""
|
||||
CONFIG = u'settings_config'
|
||||
RELATIVE_PATH = u'relative_path'
|
||||
DEFAULT_RELATIVE_PATH = u'settings'
|
||||
|
||||
|
||||
class PluginURLs(object):
|
||||
"""
|
||||
The PluginURLs enum defines dictionary field names (and defaults) that can
|
||||
be specified by a Plugin App in order to configure the URLs that are
|
||||
injected into the project.
|
||||
"""
|
||||
CONFIG = u'url_config'
|
||||
APP_NAME = u'app_name'
|
||||
NAMESPACE = u'namespace'
|
||||
REGEX = u'regex'
|
||||
RELATIVE_PATH = u'relative_path'
|
||||
DEFAULT_RELATIVE_PATH = u'urls'
|
||||
|
||||
|
||||
class PluginSignals(object):
|
||||
"""
|
||||
The PluginSignals enum defines dictionary field names (and defaults)
|
||||
that can be specified by a Plugin App in order to configure the signals
|
||||
that it receives.
|
||||
"""
|
||||
CONFIG = u'signals_config'
|
||||
|
||||
RECEIVERS = u'receivers'
|
||||
DISPATCH_UID = u'dispatch_uid'
|
||||
RECEIVER_FUNC_NAME = u'receiver_func_name'
|
||||
SENDER_PATH = u'sender_path'
|
||||
SIGNAL_PATH = u'signal_path'
|
||||
|
||||
RELATIVE_PATH = u'relative_path'
|
||||
DEFAULT_RELATIVE_PATH = u'signals'
|
||||
|
||||
|
||||
class PluginContexts(object):
|
||||
"""
|
||||
The PluginContexts enum defines dictionary field names (and defaults)
|
||||
that can be specified by a Plugin App in order to configure the
|
||||
additional views it would like to add context into.
|
||||
"""
|
||||
CONFIG = u"view_context_config"
|
||||
PRODUCTION = 'production'
|
||||
COMMON = 'common'
|
||||
DEVSTACK = 'devstack'
|
||||
TEST = 'test'
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
Purpose of App: plugins
|
||||
=======================
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
Accepted
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
.. note:: This doc was written long after this app was created, thus explaining the past tense in language below.
|
||||
|
||||
When this was created, the only way to add another app to lms/cms was to create the app in edx-platform repo. This was untenible due to already large size of edx-platform and adding another app increased the chance of further entanglement between the different apps.
|
||||
|
||||
Decision
|
||||
--------
|
||||
|
||||
It was decided the ability to add django apps as plugins was necessary.
|
||||
|
||||
Consequences
|
||||
------------
|
||||
|
||||
This would make it easier to move code for some critical/key apps outside of edx-platform and into their on repository. It would also make it easier to add further functionality into lms/cms without needing to change any code in edx-platform.
|
||||
@@ -0,0 +1,15 @@
|
||||
Extract plugins infrastructure
|
||||
==============================
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
Accepted
|
||||
|
||||
|
||||
Decision
|
||||
---------
|
||||
|
||||
It was decided to extract this plugin infrastructure to make it reusable. More info on this extraction can be found in `extraction decision doc`_ in edx-django-utils repository.
|
||||
|
||||
.. _extraction decision doc: https://github.com/edx/edx-django-utils/blob/master/docs/decisions/0002-extract-plugins-infrastructure-from-edx-platform.rst
|
||||
@@ -1,88 +0,0 @@
|
||||
Plugin Contexts
|
||||
---------------
|
||||
|
||||
Status
|
||||
======
|
||||
Draft
|
||||
|
||||
Context
|
||||
=======
|
||||
edx-platform contains a plugin system (https://github.com/edx/edx-platform/tree/master/openedx/core/djangoapps/plugins) which allows new Django apps to be installed inside the LMS and Studio without requiring the LMS/Studio to know about them. This is what enables us to move to a small and extensible core. While we had the ability to add settings, URLs, and signal handlers in our plugins, there wasn't any way for a plugin to affect the commonly used pages that the core was delivering. Thus a plugin couldn't change any details on the dashboard, courseware, or any other rendered page that the platform delivered.
|
||||
|
||||
Decisions
|
||||
=========
|
||||
We have added the ability to add page context additions to the plugin system. This means that a plugin will be able to add context to any view where it is enabled. To support this we have decided:
|
||||
|
||||
1. Plugins will define a callable function that the LMS and/or studio can import and call, which will return additional context to be added.
|
||||
2. Every page that a plugin wants to add context to, must add a line to add the plugin contexts directly before the render.
|
||||
3. Plugin context will live in a dictionary called "plugins" that will be passed into the context the templates receive. The structure will look like:
|
||||
|
||||
.. code-block::
|
||||
|
||||
{
|
||||
..existing context values..
|
||||
"plugins": {
|
||||
"my_new_plugin": {... my_new_plugins's values ...},
|
||||
"my_other_plugin": {... my_other_plugin's values ...},
|
||||
}
|
||||
}
|
||||
|
||||
4. Each view will have a constant name that will be defined within it's app's API.py which will be used by plugins. These must be globally unique. These will also be recorded in the rendering app's README.rst file.
|
||||
5. Plugin apps have the option to either use the view name strings directly or import the constants from the rendering app's api.py if the plugin is part of the edx-platform repo.
|
||||
6. For now, in order to use these new context data items, we must use theming alongside this to keep the new context out of the core. This may be iterated on in the future.
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
In the plugin app
|
||||
~~~~~~~~~~~~~~~~~
|
||||
Config
|
||||
++++++
|
||||
Inside of the AppConfig of your new plugin app, add a "view_context_config" item like below.
|
||||
|
||||
* The format will be ``{"globally_unique_view_name": "function_inside_plugin_app"}``
|
||||
* The function name & path don't need to be named anything specific, so long as they work
|
||||
* These functions will be called on **every** render of that view, so keep them efficient or memoize them if they aren't user specific.
|
||||
|
||||
.. code-block::
|
||||
|
||||
class MyAppConfig(AppConfig):
|
||||
name = "my_app"
|
||||
|
||||
plugin_app = {
|
||||
"view_context_config": {
|
||||
"lms.djangoapp": {
|
||||
"course_dashboard": "my_app.context_api.get_dashboard_context"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Function
|
||||
++++++++
|
||||
The function that will be called by the plugin system should accept a single parameter which will be the previously existing context. It should then return a dictionary which consists of items which will be added to the context
|
||||
|
||||
Example:
|
||||
.. code-block::
|
||||
|
||||
def my_context_function(existing_context, *args, **kwargs):
|
||||
additional_context = {"some_plugin_value": 10}
|
||||
if existing_context.get("some_core_value"):
|
||||
additional_context.append({"some_other_plugin_value": True})
|
||||
return additional_context
|
||||
|
||||
|
||||
In the core (LMS / Studio)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The view you wish to add context to should have the following pieces enabled:
|
||||
|
||||
* A constant defined inside the app's for the globally unique view name.
|
||||
* The view must call lines similar to the below right before the render so that the plugin has the full context.
|
||||
.. code-block::
|
||||
|
||||
context_from_plugins = get_plugins_view_context(
|
||||
plugin_constants.ProjectType.LMS,
|
||||
current_app.api.THIS_VIEW_NAME,
|
||||
context
|
||||
)
|
||||
context.update(context_from_plugins)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
from logging import getLogger
|
||||
from . import constants, registry
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
def get_apps(project_type):
|
||||
"""
|
||||
Returns a list of all registered Plugin Apps, expected to be added to
|
||||
the INSTALLED_APPS list for the given project_type.
|
||||
"""
|
||||
plugin_apps = [
|
||||
u'{module_name}.{class_name}'.format(
|
||||
module_name=app_config.__module__,
|
||||
class_name=app_config.__name__,
|
||||
)
|
||||
for app_config in registry.get_app_configs(project_type)
|
||||
if getattr(app_config, constants.PLUGIN_APP_CLASS_ATTRIBUTE_NAME, None) is not None
|
||||
]
|
||||
log.debug(u'Plugin Apps: Found %s', plugin_apps)
|
||||
return plugin_apps
|
||||
@@ -1,93 +0,0 @@
|
||||
from importlib import import_module
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from openedx.core.lib.cache_utils import process_cached
|
||||
|
||||
from . import constants, registry
|
||||
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
def get_plugins_view_context(project_type, view_name, existing_context=None):
|
||||
"""
|
||||
Returns a dict of additional view context. Will check if any plugin apps
|
||||
have that view in their view_context_config, and if so will call their
|
||||
selected function to get their context dicts.
|
||||
|
||||
Params:
|
||||
project_type: a string that determines which project (lms or studio) the view is being called in. See the
|
||||
ProjectType enum in plugins/constants.py for valid options
|
||||
view_name: a string that determines which view needs the additional context. These are globally unique and
|
||||
noted in the api.py in the view's app.
|
||||
existing_context: a dictionary which includes all of the data that the page was going to render with prior
|
||||
to the addition of each plugin's context. This is what will be passed to plugins so they may choose
|
||||
what data to add to the view.
|
||||
"""
|
||||
aggregate_context = {"plugins": {}}
|
||||
|
||||
if existing_context is None:
|
||||
existing_context = {}
|
||||
|
||||
context_functions = _get_cached_context_functions_for_view(project_type, view_name)
|
||||
|
||||
for (context_function, plugin_name) in context_functions:
|
||||
try:
|
||||
plugin_context = context_function(existing_context)
|
||||
except Exception as exc:
|
||||
# We're catching this because we don't want the core to blow up when a
|
||||
# plugin is broken. This exception will probably need some sort of
|
||||
# monitoring hooked up to it to make sure that these errors don't go
|
||||
# unseen.
|
||||
log.exception("Failed to call plugin context function. Error: %s", exc)
|
||||
continue
|
||||
|
||||
aggregate_context["plugins"][plugin_name] = plugin_context
|
||||
|
||||
return aggregate_context
|
||||
|
||||
|
||||
@process_cached
|
||||
def _get_cached_context_functions_for_view(project_type, view_name):
|
||||
"""
|
||||
Returns a list of tuples where the first item is the context function
|
||||
and the second item is the name of the plugin it's being called from.
|
||||
|
||||
NOTE: These will be functions will be cached (in RAM not memcache) on this unique
|
||||
combination. If we enable many new views to use this system, we may notice an
|
||||
increase in memory usage as the entirety of these functions will be held in memory.
|
||||
"""
|
||||
context_functions = []
|
||||
for app_config in registry.get_app_configs(project_type):
|
||||
context_function_path = _get_context_function_path(app_config, project_type, view_name)
|
||||
if context_function_path:
|
||||
module_path, _, name = context_function_path.rpartition('.')
|
||||
try:
|
||||
module = import_module(module_path)
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
log.exception(
|
||||
"Failed to import %s plugin when creating %s context",
|
||||
module_path,
|
||||
view_name
|
||||
)
|
||||
continue
|
||||
context_function = getattr(module, name, None)
|
||||
if context_function:
|
||||
plugin_name, _, _ = module_path.partition('.')
|
||||
context_functions.append((context_function, plugin_name))
|
||||
else:
|
||||
log.warning(
|
||||
"Failed to retrieve %s function from %s plugin when creating %s context",
|
||||
name,
|
||||
module_path,
|
||||
view_name
|
||||
)
|
||||
return context_functions
|
||||
|
||||
|
||||
def _get_context_function_path(app_config, project_type, view_name):
|
||||
plugin_config = getattr(app_config, constants.PLUGIN_APP_CLASS_ATTRIBUTE_NAME, {})
|
||||
context_config = plugin_config.get(constants.PluginContexts.CONFIG, {})
|
||||
project_type_settings = context_config.get(project_type, {})
|
||||
return project_type_settings.get(view_name)
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
from logging import getLogger
|
||||
from . import constants, registry, utils
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
def add_plugins(settings_path, project_type, settings_type):
|
||||
"""
|
||||
Updates the module at the given ``settings_path`` with all Plugin
|
||||
Settings appropriate for the given project_type and settings_type.
|
||||
"""
|
||||
settings_module = utils.import_module(settings_path)
|
||||
for plugin_settings in _iter_plugins(project_type, settings_type):
|
||||
settings_func = getattr(plugin_settings, constants.PLUGIN_APP_SETTINGS_FUNC_NAME)
|
||||
settings_func(settings_module)
|
||||
|
||||
|
||||
def _iter_plugins(project_type, settings_type):
|
||||
"""
|
||||
Yields Plugin Settings modules that are registered for the given
|
||||
project_type and settings_type.
|
||||
"""
|
||||
for app_config in registry.get_app_configs(project_type):
|
||||
settings_config = _get_config(app_config, project_type, settings_type)
|
||||
if settings_config is None:
|
||||
log.debug(
|
||||
u'Plugin Apps [Settings]: Did NOT find %s for %s and %s',
|
||||
app_config.name,
|
||||
project_type,
|
||||
settings_type,
|
||||
)
|
||||
continue
|
||||
|
||||
plugin_settings_path = utils.get_module_path(app_config, settings_config, constants.PluginSettings)
|
||||
|
||||
log.debug(u'Plugin Apps [Settings]: Found %s for %s and %s', app_config.name, project_type, settings_type)
|
||||
yield utils.import_module(plugin_settings_path)
|
||||
|
||||
|
||||
def _get_config(app_config, project_type, settings_type):
|
||||
plugin_config = getattr(app_config, constants.PLUGIN_APP_CLASS_ATTRIBUTE_NAME, {})
|
||||
settings_config = plugin_config.get(constants.PluginSettings.CONFIG, {})
|
||||
project_type_settings = settings_config.get(project_type, {})
|
||||
return project_type_settings.get(settings_type)
|
||||
@@ -1,65 +0,0 @@
|
||||
|
||||
from logging import getLogger
|
||||
from . import constants, registry, utils
|
||||
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
def connect_receivers(project_type):
|
||||
for signals_module, signals_config in _iter_plugins(project_type):
|
||||
for signal, receiver_func, receiver_config in _iter_receivers(signals_module, signals_config):
|
||||
signal.connect(
|
||||
receiver_func,
|
||||
sender=_get_sender(receiver_config),
|
||||
dispatch_uid=_get_dispatch_uuid(receiver_config, receiver_func),
|
||||
)
|
||||
|
||||
|
||||
def _iter_receivers(signals_module, signals_config):
|
||||
for receiver_config in signals_config.get(constants.PluginSignals.RECEIVERS, []):
|
||||
receiver_func = utils.import_attr_in_module(
|
||||
signals_module,
|
||||
receiver_config[constants.PluginSignals.RECEIVER_FUNC_NAME],
|
||||
)
|
||||
signal = utils.import_attr(receiver_config[constants.PluginSignals.SIGNAL_PATH])
|
||||
yield signal, receiver_func, receiver_config
|
||||
|
||||
|
||||
def _iter_plugins(project_type):
|
||||
for app_config in registry.get_app_configs(project_type):
|
||||
signals_config = _get_config(app_config, project_type)
|
||||
if signals_config is None:
|
||||
log.debug(u'Plugin Apps [Signals]: Did NOT find %s for %s', app_config.name, project_type)
|
||||
continue
|
||||
|
||||
signals_module_path = utils.get_module_path(app_config, signals_config, constants.PluginSignals)
|
||||
signals_module = utils.import_module(signals_module_path)
|
||||
|
||||
log.debug(
|
||||
u'Plugin Apps [Signals]: Found %s with %d receiver(s) for %s',
|
||||
app_config.name,
|
||||
len(signals_config.get(constants.PluginSignals.RECEIVERS, [])),
|
||||
project_type,
|
||||
)
|
||||
yield signals_module, signals_config
|
||||
|
||||
|
||||
def _get_config(app_config, project_type):
|
||||
plugin_config = getattr(app_config, constants.PLUGIN_APP_CLASS_ATTRIBUTE_NAME, {})
|
||||
signals_config = plugin_config.get(constants.PluginSignals.CONFIG, {})
|
||||
return signals_config.get(project_type)
|
||||
|
||||
|
||||
def _get_sender(receiver_config):
|
||||
sender_path = receiver_config.get(constants.PluginSignals.SENDER_PATH)
|
||||
if sender_path:
|
||||
sender = utils.import_attr(sender_path)
|
||||
return sender
|
||||
|
||||
|
||||
def _get_dispatch_uuid(receiver_config, receiver_func):
|
||||
dispatch_uid = receiver_config.get(constants.PluginSignals.DISPATCH_UID)
|
||||
if dispatch_uid is None:
|
||||
dispatch_uid = u'{}.{}'.format(receiver_func.__module__, receiver_func.__name__)
|
||||
return dispatch_uid
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
from logging import getLogger
|
||||
from django.conf.urls import include, url
|
||||
from . import constants, registry, utils
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
def get_url(url_module_path, url_config):
|
||||
"""
|
||||
function constructs the appropriate URL
|
||||
"""
|
||||
namespace = url_config[constants.PluginURLs.NAMESPACE]
|
||||
app_name = url_config.get(constants.PluginURLs.APP_NAME)
|
||||
regex = url_config.get(constants.PluginURLs.REGEX, r'')
|
||||
|
||||
if namespace:
|
||||
return url(regex, include((url_module_path, app_name), namespace=namespace))
|
||||
else:
|
||||
return url(regex, include(url_module_path))
|
||||
|
||||
|
||||
def get_patterns(project_type):
|
||||
"""
|
||||
Returns a list of all registered Plugin URLs, expected to be added to
|
||||
the URL patterns for the given project_type.
|
||||
"""
|
||||
return [get_url(url_module_path, url_config) for url_module_path, url_config in _iter_plugins(project_type)]
|
||||
|
||||
|
||||
def _iter_plugins(project_type):
|
||||
"""
|
||||
Yields the module path and configuration for Plugin URLs registered for
|
||||
the given project_type.
|
||||
"""
|
||||
for app_config in registry.get_app_configs(project_type):
|
||||
url_config = _get_config(app_config, project_type)
|
||||
if url_config is None:
|
||||
log.debug(u'Plugin Apps [URLs]: Did NOT find %s for %s', app_config.name, project_type)
|
||||
continue
|
||||
|
||||
urls_module_path = utils.get_module_path(app_config, url_config, constants.PluginURLs)
|
||||
url_config[constants.PluginURLs.NAMESPACE] = url_config.get(constants.PluginURLs.NAMESPACE, app_config.name)
|
||||
url_config[constants.PluginURLs.APP_NAME] = app_config.name
|
||||
log.debug(
|
||||
u'Plugin Apps [URLs]: Found %s with namespace %s for %s',
|
||||
app_config.name,
|
||||
url_config[constants.PluginURLs.NAMESPACE],
|
||||
project_type,
|
||||
)
|
||||
yield urls_module_path, url_config
|
||||
|
||||
|
||||
def _get_config(app_config, project_type):
|
||||
plugin_config = getattr(app_config, constants.PLUGIN_APP_CLASS_ATTRIBUTE_NAME, {})
|
||||
url_config = plugin_config.get(constants.PluginURLs.CONFIG, {})
|
||||
return url_config.get(project_type)
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
from openedx.core.lib.plugins import PluginManager
|
||||
import six
|
||||
|
||||
|
||||
class DjangoAppRegistry(PluginManager):
|
||||
"""
|
||||
DjangoAppRegistry is a registry of django app plugins.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def get_app_configs(project_type):
|
||||
return six.itervalues(DjangoAppRegistry.get_available_plugins(project_type))
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
from importlib import import_module as system_import_module
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
|
||||
def import_module(module_path):
|
||||
"""
|
||||
Import and returns the module at the specific path.
|
||||
|
||||
Args:
|
||||
module_path is the full path to the module, including the package name.
|
||||
"""
|
||||
return system_import_module(module_path)
|
||||
|
||||
|
||||
def get_module_path(app_config, plugin_config, plugin_cls):
|
||||
return u'{package_path}.{module_path}'.format(
|
||||
package_path=app_config.name,
|
||||
module_path=plugin_config.get(plugin_cls.RELATIVE_PATH, plugin_cls.DEFAULT_RELATIVE_PATH),
|
||||
)
|
||||
|
||||
|
||||
def import_attr(attr_path):
|
||||
"""
|
||||
Import and returns a module's attribute at the specific path.
|
||||
|
||||
Args:
|
||||
attr_path should be of the form:
|
||||
{full_module_path}.attr_name
|
||||
"""
|
||||
return import_string(attr_path)
|
||||
|
||||
|
||||
def import_attr_in_module(imported_module, attr_name):
|
||||
"""
|
||||
Import and returns the attribute with name attr_name
|
||||
in the given module.
|
||||
"""
|
||||
return getattr(imported_module, attr_name)
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from edx_django_utils.plugins import PluginSignals
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSignals, ProjectType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
|
||||
class SchedulesConfig(AppConfig):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginURLs
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginURLs, ProjectType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
plugin_urls_config = {PluginURLs.NAMESPACE: u'theming', PluginURLs.REGEX: r'^theming/'}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
User Authentication Configuration
|
||||
"""
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginURLs
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginURLs, ProjectType
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
|
||||
class UserAuthnConfig(AppConfig):
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""
|
||||
Helper methods for working with learning contexts
|
||||
"""
|
||||
from edx_django_utils.plugins import PluginManager
|
||||
from opaque_keys import OpaqueKey
|
||||
from opaque_keys.edx.keys import LearningContextKey, UsageKeyV2
|
||||
|
||||
from openedx.core.djangoapps.xblock.apps import get_xblock_app_config
|
||||
from openedx.core.lib.plugins import PluginManager
|
||||
|
||||
|
||||
class LearningContextPluginManager(PluginManager):
|
||||
|
||||
@@ -5,8 +5,9 @@ Zendesk Proxy Configuration
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginURLs, PluginSettings
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType, PluginURLs, PluginSettings
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
|
||||
class ZendeskProxyConfig(AppConfig):
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
Tabs for courseware.
|
||||
"""
|
||||
|
||||
from edx_django_utils.plugins import PluginManager
|
||||
from functools import cmp_to_key
|
||||
|
||||
from openedx.core.lib.plugins import PluginManager
|
||||
|
||||
|
||||
# Stevedore extension point namespaces
|
||||
COURSE_TAB_NAMESPACE = 'openedx.course_tab'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
A plugin manager to retrieve the dynamic course partitions generators.
|
||||
"""
|
||||
|
||||
from openedx.core.lib.plugins import PluginManager
|
||||
from edx_django_utils.plugins import PluginManager
|
||||
|
||||
|
||||
class DynamicPartitionGeneratorsPluginManager(PluginManager):
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
"""
|
||||
Adds support for first class plugins that can be added to the edX platform.
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from stevedore.extension import ExtensionManager
|
||||
from openedx.core.lib.cache_utils import process_cached
|
||||
|
||||
|
||||
class PluginError(Exception):
|
||||
"""
|
||||
Base Exception for when an error was found regarding plugins.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
"""
|
||||
Base class that manages plugins for the edX platform.
|
||||
"""
|
||||
@classmethod
|
||||
@process_cached
|
||||
def get_available_plugins(cls, namespace=None):
|
||||
"""
|
||||
Returns a dict of all the plugins that have been made available through the platform.
|
||||
"""
|
||||
# Note: we're creating the extension manager lazily to ensure that the Python path
|
||||
# has been correctly set up. Trying to create this statically will fail, unfortunately.
|
||||
plugins = OrderedDict()
|
||||
extension_manager = ExtensionManager(namespace=namespace or cls.NAMESPACE) # pylint: disable=no-member
|
||||
for plugin_name in extension_manager.names():
|
||||
plugins[plugin_name] = extension_manager[plugin_name].plugin
|
||||
return plugins
|
||||
|
||||
@classmethod
|
||||
def get_plugin(cls, name, namespace=None):
|
||||
"""
|
||||
Returns the plugin with the given name.
|
||||
"""
|
||||
plugins = cls.get_available_plugins(namespace)
|
||||
if name not in plugins:
|
||||
raise PluginError(u"No such plugin {name} for entry point {namespace}".format(
|
||||
name=name,
|
||||
namespace=namespace or cls.NAMESPACE, # pylint: disable=no-member
|
||||
))
|
||||
return plugins[name]
|
||||
@@ -4,8 +4,8 @@ Tests for the plugin API
|
||||
|
||||
|
||||
from django.test import TestCase
|
||||
from edx_django_utils.plugins import PluginError
|
||||
|
||||
from openedx.core.lib.plugins import PluginError
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ A module containing the CallToActionService which can be used for an xblock Runt
|
||||
specific CTAs for specific XBlock contexts.
|
||||
"""
|
||||
|
||||
from openedx.core.lib.plugins import PluginManager
|
||||
from edx_django_utils.plugins import PluginManager
|
||||
|
||||
|
||||
class CallToActionService(PluginManager):
|
||||
|
||||
@@ -4,7 +4,9 @@ Announcements Application Configuration
|
||||
|
||||
|
||||
from django.apps import AppConfig
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType, PluginURLs, PluginSettings
|
||||
from edx_django_utils.plugins import PluginURLs, PluginSettings
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType
|
||||
|
||||
|
||||
class AnnouncementsConfig(AppConfig):
|
||||
|
||||
@@ -5,7 +5,7 @@ Support for course tool plugins.
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from openedx.core.lib.plugins import PluginManager
|
||||
from edx_django_utils.plugins import PluginManager
|
||||
|
||||
# Stevedore extension point namespace
|
||||
COURSE_TOOLS_NAMESPACE = 'openedx.course_tool'
|
||||
|
||||
Reference in New Issue
Block a user