Skip to content

Commit 985b2bf

Browse files
committed
feat: Add bits needed to make a django app plugin.
This adds the plugin entry points as well as relevant settings and updates the testing setup to load the plugin using the plugin system so that we're testing that things load correctly.
1 parent b1fbf57 commit 985b2bf

File tree

11 files changed

+338
-152
lines changed

11 files changed

+338
-152
lines changed

backend/sample_plugin/apps.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,69 @@
33
"""
44

55
from django.apps import AppConfig
6+
from edx_django_utils.plugins.constants import PluginSettings, PluginSignals, PluginURLs
67

78

89
class SamplePluginConfig(AppConfig):
10+
# pylint: disable=line-too-long
911
"""
1012
Configuration for the sample_plugin Django application.
11-
"""
12-
default_auto_field = 'django.db.models.BigAutoField'
13-
name = 'sample_plugin'
13+
14+
See https://github.com/openedx/edx-django-utils/blob/master/edx_django_utils/plugins/docs/how_tos/how_to_create_a_plugin_app.rst#manual-setup
15+
for more details and examples.
16+
""" # noqa:
17+
18+
default_auto_field = "django.db.models.BigAutoField"
19+
name = "sample_plugin"
20+
plugin_app = {
21+
"url_config": {
22+
"lms.djangoapp": {
23+
PluginURLs.NAMESPACE: "sample_plugin",
24+
PluginURLs.REGEX: r"^sample-plugin/",
25+
PluginURLs.RELATIVE_PATH: "urls",
26+
},
27+
"cms.djangoapp": {
28+
PluginURLs.NAMESPACE: "sample_plugin",
29+
PluginURLs.REGEX: r"^sample-plugin/",
30+
PluginURLs.RELATIVE_PATH: "urls",
31+
},
32+
},
33+
PluginSettings.CONFIG: {
34+
"lms.djangoapp": {
35+
"common": {
36+
PluginURLs.RELATIVE_PATH: "settings.common",
37+
},
38+
"test": {
39+
PluginURLs.RELATIVE_PATH: "settings.test",
40+
},
41+
"production": {
42+
PluginURLs.RELATIVE_PATH: "settings.production",
43+
},
44+
},
45+
"cms.djangoapp": {
46+
"common": {
47+
PluginURLs.RELATIVE_PATH: "settings.common",
48+
},
49+
"test": {
50+
PluginURLs.RELATIVE_PATH: "settings.test",
51+
},
52+
"production": {
53+
PluginURLs.RELATIVE_PATH: "settings.production",
54+
},
55+
},
56+
},
57+
PluginSignals.CONFIG: {
58+
"lms.djangoapp": {
59+
PluginURLs.RELATIVE_PATH: "signals",
60+
PluginSignals.RECEIVERS: [
61+
# Signals handlers can be registered here
62+
],
63+
},
64+
"cms.djangoapp": {
65+
PluginURLs.RELATIVE_PATH: "signals",
66+
PluginSignals.RECEIVERS: [
67+
# Signals handlers can be registered here
68+
],
69+
},
70+
},
71+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Common settings for the sample_plugin application.
3+
"""
4+
5+
6+
def plugin_settings(settings):
7+
"""
8+
Add plugin settings to main settings object.
9+
10+
Args:
11+
settings (dict): Django settings object
12+
"""
13+
pass
14+
# settings.FOO = 'bar'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
Production settings for the sample_plugin application.
3+
"""
4+
from sample_plugin.settings.common import plugin_settings as common_settings
5+
6+
7+
def plugin_settings(settings):
8+
"""
9+
Set up production-specific settings.
10+
11+
Args:
12+
settings (dict): Django settings object
13+
"""
14+
# Apply common settings
15+
common_settings(settings)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
Test settings for the sample_plugin application.
3+
"""
4+
from sample_plugin.settings.common import plugin_settings as common_settings
5+
6+
7+
def plugin_settings(settings):
8+
"""
9+
Set up test-specific settings.
10+
11+
Args:
12+
settings (dict): Django settings object
13+
"""
14+
15+
# Apply common settings
16+
common_settings(settings)

backend/sample_plugin/signals.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
Signal handlers for the sample_plugin application.
3+
"""
4+
5+
# Signal handlers can be defined here if needed
6+
# For example:
7+
# from django.dispatch import receiver
8+
# from openedx.core.djangoapps.user_api.accounts.signals import USER_RETIRE_LMS_CRITICAL
9+
#
10+
# @receiver(USER_RETIRE_LMS_CRITICAL)
11+
# def _handle_user_retirement(sender, **kwargs):
12+
# """
13+
# Handle user retirement actions for this app.
14+
# """
15+
# pass

backend/setup.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,12 @@ def is_requirement(line):
157157
'Programming Language :: Python :: 3',
158158
'Programming Language :: Python :: 3.12',
159159
],
160+
entry_points={
161+
'lms.djangoapp': [
162+
'sample_plugin = sample_plugin.apps:SamplePluginConfig',
163+
],
164+
'cms.djangoapp': [
165+
'sample_plugin = sample_plugin.apps:SamplePluginConfig',
166+
],
167+
},
160168
)

backend/test_settings.py

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
from os.path import abspath, dirname, join
99

10+
from edx_django_utils.plugins.plugin_apps import get_plugin_apps
11+
from edx_django_utils.plugins.plugin_settings import add_plugins
12+
1013

1114
def root(*args):
1215
"""
@@ -16,70 +19,103 @@ def root(*args):
1619

1720

1821
DATABASES = {
19-
'default': {
20-
'ENGINE': 'django.db.backends.sqlite3',
21-
'NAME': 'default.db',
22-
'USER': '',
23-
'PASSWORD': '',
24-
'HOST': '',
25-
'PORT': '',
22+
"default": {
23+
"ENGINE": "django.db.backends.sqlite3",
24+
"NAME": "default.db",
25+
"USER": "",
26+
"PASSWORD": "",
27+
"HOST": "",
28+
"PORT": "",
2629
}
2730
}
2831

29-
INSTALLED_APPS = (
30-
'django.contrib.admin',
31-
'django.contrib.auth',
32-
'django.contrib.contenttypes',
33-
'django.contrib.messages',
34-
'django.contrib.sessions',
35-
'rest_framework',
36-
'django_filters',
37-
'sample_plugin',
38-
)
32+
# Plugin Settings
33+
ENABLE_PLUGINS = True
34+
# Define both contexts for reference, but we'll only use one for testing
35+
PLUGIN_CONTEXTS = ["lms.djangoapp", "cms.djangoapp"]
36+
# We only use the LMS context for testing as the plugin is configured similarly for both
37+
# Could use CMS context instead by changing the index to 1
38+
39+
# Base INSTALLED_APPS before plugin discovery
40+
INSTALLED_APPS = [
41+
"django.contrib.admin",
42+
"django.contrib.auth",
43+
"django.contrib.contenttypes",
44+
"django.contrib.messages",
45+
"django.contrib.sessions",
46+
"rest_framework",
47+
"django_filters",
48+
"edx_django_utils.plugins",
49+
"django_extensions",
50+
]
51+
52+
# Dynamically add plugin apps - only using the LMS context for simplicity
53+
plugin_apps = get_plugin_apps(PLUGIN_CONTEXTS[0])
54+
INSTALLED_APPS.extend(plugin_apps)
3955

4056
LOCALE_PATHS = [
41-
root('sample_plugin', 'conf', 'locale'),
57+
root("sample_plugin", "conf", "locale"),
4258
]
4359

44-
ROOT_URLCONF = 'sample_plugin.urls'
60+
ROOT_URLCONF = "tests.urls"
4561

46-
SECRET_KEY = 'insecure-secret-key'
62+
SECRET_KEY = "insecure-secret-key"
4763

4864
MIDDLEWARE = (
49-
'django.contrib.sessions.middleware.SessionMiddleware',
50-
'django.contrib.auth.middleware.AuthenticationMiddleware',
51-
'django.contrib.messages.middleware.MessageMiddleware',
65+
"django.contrib.sessions.middleware.SessionMiddleware",
66+
"django.contrib.auth.middleware.AuthenticationMiddleware",
67+
"django.contrib.messages.middleware.MessageMiddleware",
5268
)
5369

54-
TEMPLATES = [{
55-
'BACKEND': 'django.template.backends.django.DjangoTemplates',
56-
'APP_DIRS': False,
57-
'OPTIONS': {
58-
'context_processors': [
59-
'django.contrib.auth.context_processors.auth', # this is required for admin
60-
'django.template.context_processors.request', # this is also required for admin navigation sidebar
61-
'django.contrib.messages.context_processors.messages', # this is required for admin
62-
],
63-
},
64-
}]
70+
TEMPLATES = [
71+
{
72+
"BACKEND": "django.template.backends.django.DjangoTemplates",
73+
"APP_DIRS": False,
74+
"OPTIONS": {
75+
"context_processors": [
76+
"django.contrib.auth.context_processors.auth", # this is required for admin
77+
"django.template.context_processors.request", # this is also required for admin navigation sidebar
78+
"django.contrib.messages.context_processors.messages", # this is required for admin
79+
],
80+
},
81+
}
82+
]
6583

6684
REST_FRAMEWORK = {
67-
'DEFAULT_PERMISSION_CLASSES': [
68-
'rest_framework.permissions.IsAuthenticated',
85+
"DEFAULT_PERMISSION_CLASSES": [
86+
"rest_framework.permissions.IsAuthenticated",
6987
],
70-
'DEFAULT_AUTHENTICATION_CLASSES': [
71-
'rest_framework.authentication.SessionAuthentication',
88+
"DEFAULT_AUTHENTICATION_CLASSES": [
89+
"rest_framework.authentication.SessionAuthentication",
7290
],
73-
'DEFAULT_FILTER_BACKENDS': [
74-
'django_filters.rest_framework.DjangoFilterBackend',
75-
'rest_framework.filters.OrderingFilter',
91+
"DEFAULT_FILTER_BACKENDS": [
92+
"django_filters.rest_framework.DjangoFilterBackend",
93+
"rest_framework.filters.OrderingFilter",
7694
],
77-
'DEFAULT_THROTTLE_CLASSES': [
78-
'rest_framework.throttling.AnonRateThrottle',
79-
'rest_framework.throttling.UserRateThrottle',
95+
"DEFAULT_THROTTLE_CLASSES": [
96+
"rest_framework.throttling.AnonRateThrottle",
97+
"rest_framework.throttling.UserRateThrottle",
8098
],
81-
'DEFAULT_THROTTLE_RATES': {
82-
'anon': '20/hour',
83-
'user': '100/hour',
99+
"DEFAULT_THROTTLE_RATES": {
100+
"anon": "20/hour",
101+
"user": "100/hour",
102+
},
103+
}
104+
105+
LOGGING = {
106+
"version": 1,
107+
"disable_existing_loggers": False,
108+
"handlers": {
109+
"console": {
110+
"class": "logging.StreamHandler",
111+
},
112+
},
113+
"root": {
114+
"handlers": ["console"],
115+
"level": "DEBUG",
84116
},
85117
}
118+
# Apply plugin settings - must be done after base settings are defined
119+
# Only using the LMS context for simplicity
120+
# Third parameter is the settings_type which should match the keys in settings_config
121+
add_plugins(__name__, PLUGIN_CONTEXTS[0], "test")

0 commit comments

Comments
 (0)