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.
@@ -0,0 +1,7 @@
1
+ """
2
+ A sample backend plugin for the Open edX Platform.
3
+ """
4
+
5
+ from importlib.metadata import version as get_version
6
+
7
+ __version__ = get_version(__package__)
@@ -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"]