openedx-plugin-sample 3.0.0__py2.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/apps.py +134 -0
- openedx_plugin_sample/conf/locale/config.yaml +85 -0
- openedx_plugin_sample/migrations/0001_initial.py +78 -0
- openedx_plugin_sample/migrations/__init__.py +0 -0
- openedx_plugin_sample/models.py +65 -0
- openedx_plugin_sample/pipeline.py +156 -0
- openedx_plugin_sample/py.typed +0 -0
- openedx_plugin_sample/serializers.py +39 -0
- openedx_plugin_sample/settings/common.py +135 -0
- openedx_plugin_sample/settings/production.py +16 -0
- openedx_plugin_sample/settings/test.py +17 -0
- openedx_plugin_sample/signals.py +132 -0
- openedx_plugin_sample/templates/openedx_plugin_sample/base.html +26 -0
- openedx_plugin_sample/urls.py +21 -0
- openedx_plugin_sample/views.py +241 -0
- openedx_plugin_sample-3.0.0.dist-info/METADATA +579 -0
- openedx_plugin_sample-3.0.0.dist-info/RECORD +22 -0
- openedx_plugin_sample-3.0.0.dist-info/WHEEL +6 -0
- openedx_plugin_sample-3.0.0.dist-info/entry_points.txt +5 -0
- openedx_plugin_sample-3.0.0.dist-info/licenses/LICENSE.txt +180 -0
- openedx_plugin_sample-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -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": "log_course_info_changed",
|
|
96
|
+
# "signal_path": "openedx_events.content_authoring.signals.COURSE_CATALOG_INFO_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,78 @@
|
|
|
1
|
+
# Generated by Django 4.2.20 on 2025-04-14 12:39
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
import opaque_keys.edx.django.models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name="CourseArchiveStatus",
|
|
20
|
+
fields=[
|
|
21
|
+
(
|
|
22
|
+
"id",
|
|
23
|
+
models.BigAutoField(
|
|
24
|
+
auto_created=True,
|
|
25
|
+
primary_key=True,
|
|
26
|
+
serialize=False,
|
|
27
|
+
verbose_name="ID",
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
(
|
|
31
|
+
"course_id",
|
|
32
|
+
opaque_keys.edx.django.models.CourseKeyField(
|
|
33
|
+
db_index=True,
|
|
34
|
+
help_text="The unique identifier for the course.",
|
|
35
|
+
max_length=255,
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
(
|
|
39
|
+
"is_archived",
|
|
40
|
+
models.BooleanField(
|
|
41
|
+
db_index=True,
|
|
42
|
+
default=False,
|
|
43
|
+
help_text="Whether the course is archived.",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"archive_date",
|
|
48
|
+
models.DateTimeField(
|
|
49
|
+
blank=True,
|
|
50
|
+
help_text="The date and time when the course was archived.",
|
|
51
|
+
null=True,
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
55
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
56
|
+
(
|
|
57
|
+
"user",
|
|
58
|
+
models.ForeignKey(
|
|
59
|
+
help_text="The user who this archive status is for.",
|
|
60
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
61
|
+
related_name="course_archive_statuses",
|
|
62
|
+
to=settings.AUTH_USER_MODEL,
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
options={
|
|
67
|
+
"verbose_name": "Course Archive Status",
|
|
68
|
+
"verbose_name_plural": "Course Archive Statuses",
|
|
69
|
+
"ordering": ["-updated_at"],
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
migrations.AddConstraint(
|
|
73
|
+
model_name="coursearchivestatus",
|
|
74
|
+
constraint=models.UniqueConstraint(
|
|
75
|
+
fields=("course_id", "user"), name="unique_user_course_archive_status"
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
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 opaque_keys.edx.django.models import CourseKeyField
|
|
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_id = CourseKeyField(
|
|
20
|
+
max_length=255, db_index=True, help_text="The unique identifier for the course."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
user = models.ForeignKey(
|
|
24
|
+
get_user_model(),
|
|
25
|
+
on_delete=models.CASCADE,
|
|
26
|
+
related_name="course_archive_statuses",
|
|
27
|
+
help_text="The user who this archive status is for.",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
is_archived = models.BooleanField(
|
|
31
|
+
default=False,
|
|
32
|
+
db_index=True, # Add index for performance on this frequently filtered field
|
|
33
|
+
help_text="Whether the course is archived.",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
archive_date = models.DateTimeField(
|
|
37
|
+
null=True,
|
|
38
|
+
blank=True,
|
|
39
|
+
help_text="The date and time when the course was archived.",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
43
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
44
|
+
|
|
45
|
+
def __str__(self):
|
|
46
|
+
"""
|
|
47
|
+
Return a string representation of the course archive status.
|
|
48
|
+
"""
|
|
49
|
+
# pylint: disable=no-member
|
|
50
|
+
return f"{self.course_id} - {self.user.username} - {'Archived' if self.is_archived else 'Not Archived'}"
|
|
51
|
+
|
|
52
|
+
class Meta:
|
|
53
|
+
"""
|
|
54
|
+
Meta options for the CourseArchiveStatus model.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
verbose_name = "Course Archive Status"
|
|
58
|
+
verbose_name_plural = "Course Archive Statuses"
|
|
59
|
+
ordering = ["-updated_at"]
|
|
60
|
+
# Ensure combination of course_id and user is unique
|
|
61
|
+
constraints = [
|
|
62
|
+
models.UniqueConstraint(
|
|
63
|
+
fields=["course_id", "user"], name="unique_user_course_archive_status"
|
|
64
|
+
)
|
|
65
|
+
]
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
""" # pylint: disable=line-too-long
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
import re
|
|
42
|
+
|
|
43
|
+
from openedx_filters.filters import PipelineStep
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ChangeCourseAboutPageUrl(PipelineStep):
|
|
49
|
+
"""
|
|
50
|
+
Filter to customize course about page URLs.
|
|
51
|
+
|
|
52
|
+
This filter demonstrates how to intercept and modify course about page URLs,
|
|
53
|
+
redirecting them to external sites or custom implementations.
|
|
54
|
+
|
|
55
|
+
Filter Hook Point:
|
|
56
|
+
This filter hooks into the course about page URL rendering process.
|
|
57
|
+
Register it for the filter: org.openedx.learning.course.about.render.started.v1
|
|
58
|
+
|
|
59
|
+
Registration Example (in settings/common.py)::
|
|
60
|
+
|
|
61
|
+
def plugin_settings(settings):
|
|
62
|
+
settings.OPEN_EDX_FILTERS_CONFIG = {
|
|
63
|
+
"org.openedx.learning.course.about.render.started.v1": {
|
|
64
|
+
"pipeline": [
|
|
65
|
+
"openedx_plugin_sample.pipeline.ChangeCourseAboutPageUrl"
|
|
66
|
+
],
|
|
67
|
+
"fail_silently": False,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Filter Documentation:
|
|
72
|
+
- Available Filters: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters.html
|
|
73
|
+
- PipelineStep: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters-tooling.html#openedx_filters.filters.PipelineStep
|
|
74
|
+
|
|
75
|
+
Real-World Use Cases:
|
|
76
|
+
- Redirect to marketing site course pages
|
|
77
|
+
- Implement custom course discovery interfaces
|
|
78
|
+
- Add tracking parameters to URLs
|
|
79
|
+
- Route different course types to different platforms
|
|
80
|
+
- Implement A/B testing for course pages
|
|
81
|
+
""" # noqa: E501
|
|
82
|
+
|
|
83
|
+
def run_filter(self, url, org, **kwargs): # pylint: disable=arguments-differ
|
|
84
|
+
"""
|
|
85
|
+
Modify the course about page URL.
|
|
86
|
+
|
|
87
|
+
This method intercepts course about page URL generation and can modify
|
|
88
|
+
the destination URL based on business logic.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
url (str): The original course about page URL
|
|
92
|
+
org (str): The organization/institution identifier
|
|
93
|
+
**kwargs: Additional context data from the platform
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
dict: Dictionary with same parameter names as input
|
|
97
|
+
- url (str): Modified or original URL
|
|
98
|
+
- org (str): Organization identifier (usually unchanged)
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
FilterException: If processing should be halted
|
|
102
|
+
|
|
103
|
+
Filter Requirements:
|
|
104
|
+
- Must return dictionary with keys matching input parameters
|
|
105
|
+
- Return None to skip this filter (let other filters run)
|
|
106
|
+
- Raise FilterException to halt pipeline execution
|
|
107
|
+
- Handle all input scenarios gracefully
|
|
108
|
+
|
|
109
|
+
URL Pattern Matching:
|
|
110
|
+
This implementation looks for Open edX course keys in the format:
|
|
111
|
+
course-v1:ORG+COURSE+RUN (e.g., course-v1:edX+DemoX+Demo_Course)
|
|
112
|
+
|
|
113
|
+
Documentation:
|
|
114
|
+
- run_filter method: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters-tooling.html#openedx_filters.filters.PipelineStep.run_filter
|
|
115
|
+
""" # noqa: E501
|
|
116
|
+
# Extract course ID using Open edX course key pattern
|
|
117
|
+
# Course keys follow the format: course-v1:ORG+COURSE+RUN
|
|
118
|
+
pattern = r'(?P<course_id>course-v1:[^/]+)'
|
|
119
|
+
|
|
120
|
+
match = re.search(pattern, url)
|
|
121
|
+
if match:
|
|
122
|
+
course_id = match.group('course_id')
|
|
123
|
+
|
|
124
|
+
# Example: Redirect to external marketing site
|
|
125
|
+
new_url = f"https://example.com/new_about_page/{course_id}"
|
|
126
|
+
|
|
127
|
+
logger.debug(
|
|
128
|
+
f"Redirecting course about page for {course_id} from {url} to {new_url}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Return modified data
|
|
132
|
+
return {"url": new_url, "org": org}
|
|
133
|
+
|
|
134
|
+
# No course ID found - return original data unchanged
|
|
135
|
+
logger.debug(f"No course ID found in URL {url}, leaving unchanged")
|
|
136
|
+
return {"url": url, "org": org}
|
|
137
|
+
|
|
138
|
+
# Alternative patterns for different business logic:
|
|
139
|
+
|
|
140
|
+
# Organization-based routing:
|
|
141
|
+
# if org == "special_org":
|
|
142
|
+
# new_url = f"https://special-site.com/courses/{course_id}"
|
|
143
|
+
# return {"url": new_url, "org": org}
|
|
144
|
+
|
|
145
|
+
# Course type-based routing:
|
|
146
|
+
# if "MicroMasters" in course_id:
|
|
147
|
+
# new_url = f"https://micromasters.example.com/{course_id}"
|
|
148
|
+
# return {"url": new_url, "org": org}
|
|
149
|
+
|
|
150
|
+
# A/B testing implementation:
|
|
151
|
+
# import random
|
|
152
|
+
# if random.choice([True, False]):
|
|
153
|
+
# new_url = f"https://variant-a.example.com/{course_id}"
|
|
154
|
+
# else:
|
|
155
|
+
# new_url = f"https://variant-b.example.com/{course_id}"
|
|
156
|
+
# return {"url": new_url, "org": org}
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serializers for the openedx_plugin_sample app.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from rest_framework import serializers
|
|
7
|
+
|
|
8
|
+
from openedx_plugin_sample.models import CourseArchiveStatus
|
|
9
|
+
|
|
10
|
+
User = get_user_model()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CourseArchiveStatusSerializer(serializers.ModelSerializer):
|
|
14
|
+
"""
|
|
15
|
+
Serializer for the CourseArchiveStatus model.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
user = serializers.PrimaryKeyRelatedField(
|
|
19
|
+
queryset=User.objects.all(),
|
|
20
|
+
default=serializers.CurrentUserDefault(),
|
|
21
|
+
required=False,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
class Meta:
|
|
25
|
+
"""
|
|
26
|
+
Meta class for CourseArchiveStatusSerializer.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
model = CourseArchiveStatus
|
|
30
|
+
fields = [
|
|
31
|
+
"id",
|
|
32
|
+
"course_id",
|
|
33
|
+
"user",
|
|
34
|
+
"is_archived",
|
|
35
|
+
"archive_date",
|
|
36
|
+
"created_at",
|
|
37
|
+
"updated_at",
|
|
38
|
+
]
|
|
39
|
+
read_only_fields = ["id", "created_at", "updated_at", "archive_date"]
|