openedx-plugin-sample 3.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openedx_plugin_sample/__init__.py +7 -0
- openedx_plugin_sample/admin.py +55 -0
- openedx_plugin_sample/apps.py +134 -0
- openedx_plugin_sample/conf/locale/config.yaml +85 -0
- openedx_plugin_sample/migrations/0001_initial.py +36 -0
- openedx_plugin_sample/migrations/__init__.py +0 -0
- openedx_plugin_sample/models.py +70 -0
- openedx_plugin_sample/pipeline.py +91 -0
- openedx_plugin_sample/py.typed +0 -0
- openedx_plugin_sample/serializers.py +61 -0
- openedx_plugin_sample/settings/common.py +136 -0
- openedx_plugin_sample/settings/production.py +16 -0
- openedx_plugin_sample/settings/test.py +17 -0
- openedx_plugin_sample/signals.py +66 -0
- openedx_plugin_sample/templates/openedx_plugin_sample/base.html +26 -0
- openedx_plugin_sample/urls.py +21 -0
- openedx_plugin_sample/views.py +268 -0
- openedx_plugin_sample-3.8.0.dist-info/METADATA +66 -0
- openedx_plugin_sample-3.8.0.dist-info/RECORD +23 -0
- openedx_plugin_sample-3.8.0.dist-info/WHEEL +5 -0
- openedx_plugin_sample-3.8.0.dist-info/entry_points.txt +5 -0
- openedx_plugin_sample-3.8.0.dist-info/licenses/LICENSE.txt +180 -0
- openedx_plugin_sample-3.8.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django admin configuration for openedx_plugin_sample.
|
|
3
|
+
|
|
4
|
+
This module demonstrates how to expose plugin models in the Django admin
|
|
5
|
+
site provided by Open edX (LMS and CMS each have their own admin under
|
|
6
|
+
``/admin/``). Defining a ``ModelAdmin`` for each model gives operators a
|
|
7
|
+
ready-made UI to inspect and manage plugin data without needing custom
|
|
8
|
+
tooling.
|
|
9
|
+
|
|
10
|
+
Django Documentation:
|
|
11
|
+
- ModelAdmin: https://docs.djangoproject.com/en/stable/ref/contrib/admin/
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from django.contrib import admin
|
|
15
|
+
|
|
16
|
+
from openedx_plugin_sample.models import CourseArchiveStatus
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@admin.register(CourseArchiveStatus)
|
|
20
|
+
class CourseArchiveStatusAdmin(admin.ModelAdmin):
|
|
21
|
+
"""
|
|
22
|
+
Admin configuration for the CourseArchiveStatus model.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
list_display = (
|
|
26
|
+
"course_key",
|
|
27
|
+
"user",
|
|
28
|
+
"is_archived",
|
|
29
|
+
"archive_date",
|
|
30
|
+
"updated_at",
|
|
31
|
+
)
|
|
32
|
+
list_filter = ("is_archived",)
|
|
33
|
+
# Search by the related CourseRun's course_key and the user's username/email.
|
|
34
|
+
search_fields = (
|
|
35
|
+
"course_run__course_key",
|
|
36
|
+
"user__username",
|
|
37
|
+
"user__email",
|
|
38
|
+
)
|
|
39
|
+
# FKs use raw id widgets (lookup popup) rather than a <select>, since the
|
|
40
|
+
# CourseRun and User tables can have many thousands of rows on a real
|
|
41
|
+
# Open edX deployment.
|
|
42
|
+
raw_id_fields = ("course_run", "user")
|
|
43
|
+
readonly_fields = ("created_at", "updated_at")
|
|
44
|
+
ordering = ("-updated_at",)
|
|
45
|
+
|
|
46
|
+
@admin.display(description="Course key", ordering="course_run__course_key")
|
|
47
|
+
def course_key(self, obj: CourseArchiveStatus) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Show the course's course_key string in list_display.
|
|
50
|
+
|
|
51
|
+
We never expose CourseRun's internal integer PK in the admin; the
|
|
52
|
+
course_key (e.g. "course-v1:edX+DemoX+Demo_Course") is the identifier
|
|
53
|
+
operators recognize.
|
|
54
|
+
"""
|
|
55
|
+
return str(obj.course_run.course_key)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openedx_plugin_sample Django application initialization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from edx_django_utils.plugins.constants import PluginSettings, PluginURLs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SamplePluginConfig(AppConfig):
|
|
10
|
+
"""
|
|
11
|
+
Django App Plugin configuration for Open edX platform integration.
|
|
12
|
+
|
|
13
|
+
This class demonstrates the complete Django App Plugin pattern, which allows
|
|
14
|
+
you to add new functionality to edx-platform without modifying core code.
|
|
15
|
+
|
|
16
|
+
Key Features Demonstrated:
|
|
17
|
+
- URL configuration for both LMS and CMS
|
|
18
|
+
- Settings integration across environments (common, test, production)
|
|
19
|
+
- Signal handler registration for Open edX Events
|
|
20
|
+
- Proper plugin app structure following Open edX patterns
|
|
21
|
+
|
|
22
|
+
Official Documentation:
|
|
23
|
+
- Plugin Creation: https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html
|
|
24
|
+
- Plugin Overview: https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/readme.html
|
|
25
|
+
- Hooks Framework: https://docs.openedx.org/en/latest/developers/concepts/hooks_extension_framework.html
|
|
26
|
+
|
|
27
|
+
Real-World Usage:
|
|
28
|
+
This pattern is used when you need to:
|
|
29
|
+
- Add new models and database tables
|
|
30
|
+
- Provide new REST API endpoints
|
|
31
|
+
- Integrate with external systems via events
|
|
32
|
+
- Modify platform behavior with filters
|
|
33
|
+
- Add custom business logic
|
|
34
|
+
|
|
35
|
+
Entry Point Configuration:
|
|
36
|
+
This plugin is registered in pyproject.toml as::
|
|
37
|
+
|
|
38
|
+
[project.entry-points."lms.djangoapp"]
|
|
39
|
+
openedx_plugin_sample = "openedx_plugin_sample.apps:SamplePluginConfig"
|
|
40
|
+
|
|
41
|
+
[project.entry-points."cms.djangoapp"]
|
|
42
|
+
openedx_plugin_sample = "openedx_plugin_sample.apps:SamplePluginConfig"
|
|
43
|
+
|
|
44
|
+
The platform automatically discovers and loads plugins registered in these entry points.
|
|
45
|
+
""" # pylint: disable=line-too-long # noqa: E501
|
|
46
|
+
|
|
47
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
48
|
+
name = "openedx_plugin_sample"
|
|
49
|
+
plugin_app = {
|
|
50
|
+
"url_config": {
|
|
51
|
+
"lms.djangoapp": {
|
|
52
|
+
PluginURLs.NAMESPACE: "openedx_plugin_sample",
|
|
53
|
+
PluginURLs.REGEX: r"^sample-plugin/",
|
|
54
|
+
PluginURLs.RELATIVE_PATH: "urls",
|
|
55
|
+
},
|
|
56
|
+
"cms.djangoapp": {
|
|
57
|
+
PluginURLs.NAMESPACE: "openedx_plugin_sample",
|
|
58
|
+
PluginURLs.REGEX: r"^sample-plugin/",
|
|
59
|
+
PluginURLs.RELATIVE_PATH: "urls",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
PluginSettings.CONFIG: {
|
|
63
|
+
"lms.djangoapp": {
|
|
64
|
+
"common": {
|
|
65
|
+
PluginURLs.RELATIVE_PATH: "settings.common",
|
|
66
|
+
},
|
|
67
|
+
"test": {
|
|
68
|
+
PluginURLs.RELATIVE_PATH: "settings.test",
|
|
69
|
+
},
|
|
70
|
+
"production": {
|
|
71
|
+
PluginURLs.RELATIVE_PATH: "settings.production",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
"cms.djangoapp": {
|
|
75
|
+
"common": {
|
|
76
|
+
PluginURLs.RELATIVE_PATH: "settings.common",
|
|
77
|
+
},
|
|
78
|
+
"test": {
|
|
79
|
+
PluginURLs.RELATIVE_PATH: "settings.test",
|
|
80
|
+
},
|
|
81
|
+
"production": {
|
|
82
|
+
PluginURLs.RELATIVE_PATH: "settings.production",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
# Alternative: PluginSignals.CONFIG
|
|
87
|
+
# You can define signal connections here instead of in ready(), but the
|
|
88
|
+
# ready() method approach is more flexible for complex signal handling.
|
|
89
|
+
#
|
|
90
|
+
# Example PluginSignals configuration:
|
|
91
|
+
# PluginSignals.CONFIG: {
|
|
92
|
+
# "lms.djangoapp": {
|
|
93
|
+
# "relative_path": "signals",
|
|
94
|
+
# "receivers": [{
|
|
95
|
+
# "receiver_func_name": "unarchive_on_verified_upgrade",
|
|
96
|
+
# "signal_path": "openedx_events.learning.signals.COURSE_ENROLLMENT_CHANGED",
|
|
97
|
+
# }]
|
|
98
|
+
# }
|
|
99
|
+
# }
|
|
100
|
+
#
|
|
101
|
+
# Documentation:
|
|
102
|
+
# - PluginSignals: https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html#plugin-signals # noqa: E501
|
|
103
|
+
# - Open edX Events: https://docs.openedx.org/projects/openedx-events/en/latest/
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def ready(self):
|
|
107
|
+
"""
|
|
108
|
+
Initialize the plugin when Django starts.
|
|
109
|
+
|
|
110
|
+
This method is called when Django initializes this app. It's the proper
|
|
111
|
+
place to import signal handlers, register filters, and perform other
|
|
112
|
+
startup tasks.
|
|
113
|
+
|
|
114
|
+
Key Responsibilities:
|
|
115
|
+
- Import signal handlers to register Open edX Event receivers
|
|
116
|
+
- Register Open edX Filters (if not done via settings)
|
|
117
|
+
- Initialize any plugin-specific configuration
|
|
118
|
+
- Perform validation checks
|
|
119
|
+
|
|
120
|
+
Django Documentation:
|
|
121
|
+
- AppConfig.ready(): https://docs.djangoproject.com/en/stable/ref/applications/#django.apps.AppConfig.ready
|
|
122
|
+
|
|
123
|
+
Open edX Documentation:
|
|
124
|
+
- Events: https://docs.openedx.org/projects/openedx-events/en/latest/how-tos/consume-an-event.html
|
|
125
|
+
- Filters: https://docs.openedx.org/projects/openedx-filters/en/latest/how-tos/using-filters.html
|
|
126
|
+
|
|
127
|
+
Why Import in ready():
|
|
128
|
+
Signal handlers must be imported for the @receiver decorators to register
|
|
129
|
+
with Django's signal dispatcher. Importing in ready() ensures this happens
|
|
130
|
+
when the app initializes, not when modules are first loaded.
|
|
131
|
+
"""
|
|
132
|
+
# Import signal handlers to register Open edX Event receivers
|
|
133
|
+
# This import registers all @receiver decorated functions in signals.py
|
|
134
|
+
from . import signals # pylint: disable=import-outside-toplevel,unused-import
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Configuration for i18n workflow.
|
|
2
|
+
|
|
3
|
+
locales:
|
|
4
|
+
- en # English - Source Language
|
|
5
|
+
- am # Amharic
|
|
6
|
+
- ar # Arabic
|
|
7
|
+
- az # Azerbaijani
|
|
8
|
+
- bg_BG # Bulgarian (Bulgaria)
|
|
9
|
+
- bn_BD # Bengali (Bangladesh)
|
|
10
|
+
- bn_IN # Bengali (India)
|
|
11
|
+
- bs # Bosnian
|
|
12
|
+
- ca # Catalan
|
|
13
|
+
- ca@valencia # Catalan (Valencia)
|
|
14
|
+
- cs # Czech
|
|
15
|
+
- cy # Welsh
|
|
16
|
+
- da # Danish
|
|
17
|
+
- de_DE # German (Germany)
|
|
18
|
+
- el # Greek
|
|
19
|
+
- en # English
|
|
20
|
+
- en_GB # English (United Kingdom)
|
|
21
|
+
# Don't pull these until we figure out why pages randomly display in these locales,
|
|
22
|
+
# when the user's browser is in English and the user is not logged in.
|
|
23
|
+
# - en@lolcat # LOLCAT English
|
|
24
|
+
# - en@pirate # Pirate English
|
|
25
|
+
- es_419 # Spanish (Latin America)
|
|
26
|
+
- es_AR # Spanish (Argentina)
|
|
27
|
+
- es_EC # Spanish (Ecuador)
|
|
28
|
+
- es_ES # Spanish (Spain)
|
|
29
|
+
- es_MX # Spanish (Mexico)
|
|
30
|
+
- es_PE # Spanish (Peru)
|
|
31
|
+
- et_EE # Estonian (Estonia)
|
|
32
|
+
- eu_ES # Basque (Spain)
|
|
33
|
+
- fa # Persian
|
|
34
|
+
- fa_IR # Persian (Iran)
|
|
35
|
+
- fi_FI # Finnish (Finland)
|
|
36
|
+
- fil # Filipino
|
|
37
|
+
- fr # French
|
|
38
|
+
- gl # Galician
|
|
39
|
+
- gu # Gujarati
|
|
40
|
+
- he # Hebrew
|
|
41
|
+
- hi # Hindi
|
|
42
|
+
- hr # Croatian
|
|
43
|
+
- hu # Hungarian
|
|
44
|
+
- hy_AM # Armenian (Armenia)
|
|
45
|
+
- id # Indonesian
|
|
46
|
+
- it_IT # Italian (Italy)
|
|
47
|
+
- ja_JP # Japanese (Japan)
|
|
48
|
+
- kk_KZ # Kazakh (Kazakhstan)
|
|
49
|
+
- km_KH # Khmer (Cambodia)
|
|
50
|
+
- kn # Kannada
|
|
51
|
+
- ko_KR # Korean (Korea)
|
|
52
|
+
- lt_LT # Lithuanian (Lithuania)
|
|
53
|
+
- ml # Malayalam
|
|
54
|
+
- mn # Mongolian
|
|
55
|
+
- mr # Marathi
|
|
56
|
+
- ms # Malay
|
|
57
|
+
- nb # Norwegian Bokmål
|
|
58
|
+
- ne # Nepali
|
|
59
|
+
- nl_NL # Dutch (Netherlands)
|
|
60
|
+
- or # Oriya
|
|
61
|
+
- pl # Polish
|
|
62
|
+
- pt_BR # Portuguese (Brazil)
|
|
63
|
+
- pt_PT # Portuguese (Portugal)
|
|
64
|
+
- ro # Romanian
|
|
65
|
+
- ru # Russian
|
|
66
|
+
- si # Sinhala
|
|
67
|
+
- sk # Slovak
|
|
68
|
+
- sl # Slovenian
|
|
69
|
+
- sq # Albanian
|
|
70
|
+
- sr # Serbian
|
|
71
|
+
- ta # Tamil
|
|
72
|
+
- te # Telugu
|
|
73
|
+
- th # Thai
|
|
74
|
+
- tr_TR # Turkish (Turkey)
|
|
75
|
+
- uk # Ukrainian
|
|
76
|
+
- ur # Urdu
|
|
77
|
+
- uz # Uzbek
|
|
78
|
+
- vi # Vietnamese
|
|
79
|
+
- zh_CN # Chinese (China)
|
|
80
|
+
- zh_HK # Chinese (Hong Kong)
|
|
81
|
+
- zh_TW # Chinese (Taiwan)
|
|
82
|
+
|
|
83
|
+
# The locales used for fake-accented English, for testing.
|
|
84
|
+
dummy_locales:
|
|
85
|
+
- eo
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Generated by Django 5.2.13 on 2026-05-14 01:13
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
('openedx_catalog', '0001_initial'),
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='CourseArchiveStatus',
|
|
20
|
+
fields=[
|
|
21
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
22
|
+
('is_archived', models.BooleanField(db_index=True, default=False, help_text='Whether the course is archived.')),
|
|
23
|
+
('archive_date', models.DateTimeField(blank=True, help_text='The date and time when the course was archived.', null=True)),
|
|
24
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
25
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
26
|
+
('course_run', models.ForeignKey(help_text='The course run that this archive status is for.', on_delete=django.db.models.deletion.CASCADE, related_name='archive_statuses', to='openedx_catalog.courserun')),
|
|
27
|
+
('user', models.ForeignKey(help_text='The user who this archive status is for.', on_delete=django.db.models.deletion.CASCADE, related_name='course_archive_statuses', to=settings.AUTH_USER_MODEL)),
|
|
28
|
+
],
|
|
29
|
+
options={
|
|
30
|
+
'verbose_name': 'Course Archive Status',
|
|
31
|
+
'verbose_name_plural': 'Course Archive Statuses',
|
|
32
|
+
'ordering': ['-updated_at'],
|
|
33
|
+
'constraints': [models.UniqueConstraint(fields=('course_run', 'user'), name='unique_user_course_archive_status')],
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database models for openedx_plugin_sample.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.db import models
|
|
7
|
+
from openedx_catalog.models import CourseRun
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CourseArchiveStatus(models.Model):
|
|
11
|
+
"""
|
|
12
|
+
Model to track the archive status of a course.
|
|
13
|
+
|
|
14
|
+
Stores information about whether a course has been archived and when it was archived.
|
|
15
|
+
|
|
16
|
+
.. no_pii: This model does not store PII directly, only references to users via foreign keys.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
course_run = models.ForeignKey(
|
|
20
|
+
CourseRun,
|
|
21
|
+
on_delete=models.CASCADE,
|
|
22
|
+
related_name="archive_statuses",
|
|
23
|
+
help_text="The course run that this archive status is for.",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
user = models.ForeignKey(
|
|
27
|
+
get_user_model(),
|
|
28
|
+
on_delete=models.CASCADE,
|
|
29
|
+
related_name="course_archive_statuses",
|
|
30
|
+
help_text="The user who this archive status is for.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
is_archived = models.BooleanField(
|
|
34
|
+
default=False,
|
|
35
|
+
db_index=True, # Add index for performance on this frequently filtered field
|
|
36
|
+
help_text="Whether the course is archived.",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
archive_date = models.DateTimeField(
|
|
40
|
+
null=True,
|
|
41
|
+
blank=True,
|
|
42
|
+
help_text="The date and time when the course was archived.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
46
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
"""
|
|
50
|
+
Return a string representation of the course archive status.
|
|
51
|
+
"""
|
|
52
|
+
# pylint: disable=no-member
|
|
53
|
+
# Identify the course by its course_key string, never by the internal PK.
|
|
54
|
+
archived = "Archived" if self.is_archived else "Not Archived"
|
|
55
|
+
return f"{self.course_run.course_key} - {self.user.username} - {archived}"
|
|
56
|
+
|
|
57
|
+
class Meta:
|
|
58
|
+
"""
|
|
59
|
+
Meta options for the CourseArchiveStatus model.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
verbose_name = "Course Archive Status"
|
|
63
|
+
verbose_name_plural = "Course Archive Statuses"
|
|
64
|
+
ordering = ["-updated_at"]
|
|
65
|
+
# Ensure combination of course_run and user is unique
|
|
66
|
+
constraints = [
|
|
67
|
+
models.UniqueConstraint(
|
|
68
|
+
fields=["course_run", "user"], name="unique_user_course_archive_status"
|
|
69
|
+
)
|
|
70
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Open edX Filters implementation for the openedx_plugin_sample application.
|
|
3
|
+
|
|
4
|
+
This module demonstrates how to use Open edX Filters to modify platform behavior
|
|
5
|
+
without changing core code. Filters are part of the Hooks Extension Framework
|
|
6
|
+
and allow you to intercept and modify data at specific points in the platform.
|
|
7
|
+
|
|
8
|
+
What Are Open edX Filters?
|
|
9
|
+
Filters are functions that can modify application behavior by altering input data
|
|
10
|
+
or halting execution based on specific conditions. Unlike events (which only
|
|
11
|
+
observe), filters can change what happens next in the platform.
|
|
12
|
+
|
|
13
|
+
Key Concepts:
|
|
14
|
+
- Filters receive data and return modified data
|
|
15
|
+
- They run at specific pipeline steps during platform operations
|
|
16
|
+
- Filters can halt execution by raising exceptions
|
|
17
|
+
- Multiple filters can be chained together in a pipeline
|
|
18
|
+
- Filters should be lightweight and handle errors gracefully
|
|
19
|
+
|
|
20
|
+
Official Documentation:
|
|
21
|
+
- Filters Overview: https://docs.openedx.org/projects/openedx-filters/en/latest/
|
|
22
|
+
- Using Filters: https://docs.openedx.org/projects/openedx-filters/en/latest/how-tos/using-filters.html
|
|
23
|
+
- Available Filters: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters.html
|
|
24
|
+
- Filter Tooling: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters-tooling.html
|
|
25
|
+
|
|
26
|
+
Registration Process:
|
|
27
|
+
1. Create filter class inheriting from PipelineStep
|
|
28
|
+
2. Implement run_filter() method with correct signature
|
|
29
|
+
3. Register filter in Django settings OPEN_EDX_FILTERS_CONFIG
|
|
30
|
+
4. Deploy and test the filter behavior
|
|
31
|
+
|
|
32
|
+
Common Use Cases:
|
|
33
|
+
- URL redirection and customization
|
|
34
|
+
- Access control and permission checks
|
|
35
|
+
- Data transformation and validation
|
|
36
|
+
- Integration with external systems
|
|
37
|
+
- Custom business logic implementation
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
|
|
42
|
+
import crum
|
|
43
|
+
from openedx_filters.filters import PipelineStep
|
|
44
|
+
|
|
45
|
+
from .models import CourseArchiveStatus
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AddArchiveStatusToLearnerHomeCourseRun(PipelineStep):
|
|
51
|
+
"""
|
|
52
|
+
Customize each courseRun within a Learner Dashboard's /init API response to include the CourseArchiveStatus.
|
|
53
|
+
""" # noqa: E501
|
|
54
|
+
|
|
55
|
+
def run_filter(self, serialized_courserun, **kwargs): # pylint: disable=arguments-differ
|
|
56
|
+
"""
|
|
57
|
+
Insert `isArchivedByLearner` into one serialized courseRun for the Learner Home /init response.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
serialized_courserun (dict): One courseRun from the serializer. Reads
|
|
61
|
+
`courseId` (a course key string, e.g. "course-v1:edX+DemoX+Demo_Course");
|
|
62
|
+
all other fields are passed through unchanged.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
dict: ``{"serialized_courserun": <updated dict>}``. The updated dict has the
|
|
66
|
+
same keys as the input plus `isArchivedByLearner` (bool) -- True iff a
|
|
67
|
+
CourseArchiveStatus row exists for the current request user and this
|
|
68
|
+
courseId with `is_archived=True`; False otherwise (including when no row
|
|
69
|
+
exists).
|
|
70
|
+
|
|
71
|
+
The current user is read from the active request via `crum`, so this filter only
|
|
72
|
+
runs meaningfully inside a request cycle. Note that `isArchivedByLearner` is
|
|
73
|
+
distinct from `isArchived`, which the platform sets based on whether the course
|
|
74
|
+
run itself has ended.
|
|
75
|
+
""" # noqa: E501
|
|
76
|
+
request = crum.get_current_request()
|
|
77
|
+
if not (request and request.user):
|
|
78
|
+
return serialized_courserun
|
|
79
|
+
try:
|
|
80
|
+
is_archived_by_learner = CourseArchiveStatus.objects.get(
|
|
81
|
+
user=request.user,
|
|
82
|
+
course_run__course_key=serialized_courserun["courseId"],
|
|
83
|
+
).is_archived
|
|
84
|
+
except CourseArchiveStatus.DoesNotExist:
|
|
85
|
+
is_archived_by_learner = False
|
|
86
|
+
return {
|
|
87
|
+
"serialized_courserun": {
|
|
88
|
+
**serialized_courserun,
|
|
89
|
+
"isArchivedByLearner": is_archived_by_learner,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serializers for the openedx_plugin_sample app.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from openedx_catalog.models import CourseRun
|
|
7
|
+
from rest_framework import serializers
|
|
8
|
+
|
|
9
|
+
from openedx_plugin_sample.models import CourseArchiveStatus
|
|
10
|
+
|
|
11
|
+
User = get_user_model()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CourseArchiveStatusSerializer(serializers.ModelSerializer):
|
|
15
|
+
"""
|
|
16
|
+
Serializer for the CourseArchiveStatus model.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
user = serializers.PrimaryKeyRelatedField(
|
|
20
|
+
queryset=User.objects.all(),
|
|
21
|
+
default=serializers.CurrentUserDefault(),
|
|
22
|
+
required=False,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# The model stores a FK to CourseRun, but APIs should identify courses by
|
|
26
|
+
# their full course key string (e.g. "course-v1:edX+DemoX+Demo_Course"),
|
|
27
|
+
# never by CourseRun's internal integer PK. The slug field looks up the
|
|
28
|
+
# related CourseRun by its `course_key` for both reads and writes.
|
|
29
|
+
course_id = serializers.SlugRelatedField(
|
|
30
|
+
source="course_run",
|
|
31
|
+
slug_field="course_key",
|
|
32
|
+
queryset=CourseRun.objects.all(),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
class Meta:
|
|
36
|
+
"""
|
|
37
|
+
Meta class for CourseArchiveStatusSerializer.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
model = CourseArchiveStatus
|
|
41
|
+
fields = [
|
|
42
|
+
"id",
|
|
43
|
+
"course_id",
|
|
44
|
+
"user",
|
|
45
|
+
"is_archived",
|
|
46
|
+
"archive_date",
|
|
47
|
+
"created_at",
|
|
48
|
+
"updated_at",
|
|
49
|
+
]
|
|
50
|
+
read_only_fields = ["id", "created_at", "updated_at", "archive_date"]
|
|
51
|
+
|
|
52
|
+
def to_representation(self, instance):
|
|
53
|
+
"""
|
|
54
|
+
Serialize the instance, casting course_id to a string.
|
|
55
|
+
|
|
56
|
+
CourseRun.course_key returns a CourseLocator (not a string), which the
|
|
57
|
+
default JSON encoder can't serialize, so we coerce to str on output.
|
|
58
|
+
"""
|
|
59
|
+
data = super().to_representation(instance)
|
|
60
|
+
data["course_id"] = str(data["course_id"])
|
|
61
|
+
return data
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common settings for the openedx_plugin_sample application.
|
|
3
|
+
|
|
4
|
+
This module demonstrates how Django App Plugins integrate with the platform's
|
|
5
|
+
settings system. Plugin settings are merged with the main settings during
|
|
6
|
+
platform initialization.
|
|
7
|
+
|
|
8
|
+
Plugin Settings Integration:
|
|
9
|
+
The plugin_settings function is called during Django startup and receives
|
|
10
|
+
the main settings object. You can modify this object to add plugin-specific
|
|
11
|
+
configuration that integrates seamlessly with the platform.
|
|
12
|
+
|
|
13
|
+
Official Documentation:
|
|
14
|
+
- Plugin Settings:
|
|
15
|
+
https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html#plugin-settings
|
|
16
|
+
- Django Settings: https://docs.djangoproject.com/en/stable/topics/settings/
|
|
17
|
+
|
|
18
|
+
Settings Organization:
|
|
19
|
+
- common.py: Settings for all environments
|
|
20
|
+
- production.py: Production-specific overrides
|
|
21
|
+
- test.py: Test environment optimizations
|
|
22
|
+
|
|
23
|
+
Integration Points:
|
|
24
|
+
- OPEN_EDX_FILTERS_CONFIG: Register filters with the platform
|
|
25
|
+
- API rate limiting and throttling configuration
|
|
26
|
+
- Database connection settings for plugin models
|
|
27
|
+
- External service integration parameters
|
|
28
|
+
- Feature flags and environment-specific toggles
|
|
29
|
+
""" # noqa: E501
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def plugin_settings(settings):
|
|
37
|
+
"""
|
|
38
|
+
Configure plugin-specific Django settings.
|
|
39
|
+
|
|
40
|
+
This function is called during Django startup to merge plugin settings
|
|
41
|
+
with the main platform configuration. All settings added here become
|
|
42
|
+
available throughout the Django application.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
settings (dict): Main Django settings object to modify
|
|
46
|
+
|
|
47
|
+
Common Settings Patterns:
|
|
48
|
+
|
|
49
|
+
# Plugin-specific configuration
|
|
50
|
+
settings.SAMPLE_PLUGIN_API_RATE_LIMIT = "60/minute"
|
|
51
|
+
settings.SAMPLE_PLUGIN_ARCHIVE_RETENTION_DAYS = 365
|
|
52
|
+
|
|
53
|
+
# External service integration
|
|
54
|
+
settings.SAMPLE_PLUGIN_EXTERNAL_API_URL = "https://api.example.com"
|
|
55
|
+
settings.SAMPLE_PLUGIN_API_KEY = "your-api-key"
|
|
56
|
+
|
|
57
|
+
# Feature flags
|
|
58
|
+
settings.SAMPLE_PLUGIN_ENABLE_ARCHIVING = True
|
|
59
|
+
settings.SAMPLE_PLUGIN_ENABLE_NOTIFICATIONS = False
|
|
60
|
+
|
|
61
|
+
Environment-Specific Settings:
|
|
62
|
+
Different environment files can override these settings:
|
|
63
|
+
- production.py: Stricter rate limits, external API endpoints
|
|
64
|
+
- test.py: Faster timeouts, mock services, in-memory databases
|
|
65
|
+
- development.py: Debug logging, local service endpoints
|
|
66
|
+
|
|
67
|
+
Security Considerations:
|
|
68
|
+
- Never commit API keys or secrets to version control
|
|
69
|
+
- Use environment variables for sensitive configuration
|
|
70
|
+
- Validate setting values to prevent configuration errors
|
|
71
|
+
"""
|
|
72
|
+
# Plugin is configured but no additional settings needed for this basic example
|
|
73
|
+
# Uncomment and modify the examples below for your use case:
|
|
74
|
+
|
|
75
|
+
# Plugin-specific configuration
|
|
76
|
+
# settings.SAMPLE_PLUGIN_API_RATE_LIMIT = "60/minute"
|
|
77
|
+
# settings.SAMPLE_PLUGIN_ARCHIVE_RETENTION_DAYS = 365
|
|
78
|
+
|
|
79
|
+
# Register Open edX Filters (additive approach)
|
|
80
|
+
_configure_openedx_filters(settings)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _configure_openedx_filters(settings):
|
|
84
|
+
"""
|
|
85
|
+
Configure Open edX Filters for the sample plugin.
|
|
86
|
+
|
|
87
|
+
This function demonstrates the proper way to register filters by:
|
|
88
|
+
1. Preserving existing filter configuration from other plugins
|
|
89
|
+
2. Adding our filter configuration additively
|
|
90
|
+
3. Avoiding duplicate pipeline steps
|
|
91
|
+
4. Logging configuration state for debugging
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
settings (dict): Django settings object
|
|
95
|
+
"""
|
|
96
|
+
# Get existing filter configuration (may be from other plugins or platform)
|
|
97
|
+
filters_config = getattr(settings, 'OPEN_EDX_FILTERS_CONFIG', {})
|
|
98
|
+
|
|
99
|
+
# Filter we want to register
|
|
100
|
+
filter_name = "org.openedx.learning.home.courserun.api.rendered.started.v1"
|
|
101
|
+
our_pipeline_step = "openedx_plugin_sample.pipeline.AddArchiveStatusToLearnerHomeCourseRun"
|
|
102
|
+
|
|
103
|
+
# Check if this filter already has configuration
|
|
104
|
+
if filter_name in filters_config:
|
|
105
|
+
logger.debug(f"Filter {filter_name} already configured, adding our pipeline step")
|
|
106
|
+
|
|
107
|
+
# Get existing pipeline steps
|
|
108
|
+
existing_pipeline = filters_config[filter_name].get("pipeline", [])
|
|
109
|
+
|
|
110
|
+
# Check if our pipeline step is already registered
|
|
111
|
+
if our_pipeline_step in existing_pipeline:
|
|
112
|
+
logger.info(
|
|
113
|
+
f"Pipeline step {our_pipeline_step} already registered for filter {filter_name}. "
|
|
114
|
+
"This may indicate the plugin is being loaded multiple times or another plugin "
|
|
115
|
+
"has registered the same pipeline step."
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
# Add our pipeline step to existing configuration
|
|
119
|
+
existing_pipeline.append(our_pipeline_step)
|
|
120
|
+
filters_config[filter_name]["pipeline"] = existing_pipeline
|
|
121
|
+
logger.debug(f"Added {our_pipeline_step} to existing filter configuration")
|
|
122
|
+
else:
|
|
123
|
+
# Create new filter configuration
|
|
124
|
+
logger.debug(f"Creating new filter configuration for {filter_name}")
|
|
125
|
+
filters_config[filter_name] = {
|
|
126
|
+
"pipeline": [our_pipeline_step],
|
|
127
|
+
"fail_silently": False,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Update the settings object
|
|
131
|
+
settings.OPEN_EDX_FILTERS_CONFIG = filters_config
|
|
132
|
+
|
|
133
|
+
logger.debug(
|
|
134
|
+
f"Final filter configuration for {filter_name}: "
|
|
135
|
+
f"{filters_config.get(filter_name, {})}"
|
|
136
|
+
)
|