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.
@@ -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,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
+ )