From 040b25319b7c2017079e2be4424f3d561a5fa14b Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Tue, 21 Oct 2025 15:30:55 -0400 Subject: [PATCH 1/3] docs: Update the catalog-info.yaml --- catalog-info.yaml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/catalog-info.yaml b/catalog-info.yaml index 4e23637..0ee171f 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -2,10 +2,10 @@ # https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html apiVersion: backstage.io/v1alpha1 -kind: "" +kind: "Component" metadata: name: 'sample-plugin' - description: "A sample backend plugin for the Open edX Platform" + description: "A set of examples for plugging into the Open edX Platform" annotations: # The openedx.org/release key is described in OEP-10: # https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html @@ -21,18 +21,10 @@ metadata: spec: # (Required) This can be a group(`group:` or a user(`user:`) - owner: "" + owner: "user:feanil" # (Required) Acceptable Type Values: service, website, library type: '' # (Required) Acceptable Lifecycle Values: experimental, production, deprecated lifecycle: 'experimental' - - # (Optional) The value can be the name of any known component. - subcomponentOf: '' - - # (Optional) An array of different components or resources. - dependsOn: - - '' - - '' From bc42fa31ed0508a020794c8a588b2d9bd3821857 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Wed, 22 Oct 2025 10:07:49 -0400 Subject: [PATCH 2/3] docs: Update docstrings to valid rst syntax. --- backend/sample_plugin/apps.py | 13 +++++------ backend/sample_plugin/pipeline.py | 37 +++++++++++++++---------------- backend/sample_plugin/signals.py | 37 +++++++++++++++---------------- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/backend/sample_plugin/apps.py b/backend/sample_plugin/apps.py index 4f074cd..3774c71 100644 --- a/backend/sample_plugin/apps.py +++ b/backend/sample_plugin/apps.py @@ -33,13 +33,12 @@ class SamplePluginConfig(AppConfig): - Add custom business logic Entry Point Configuration: - This plugin is registered in setup.py as: - ```python - entry_points={ - "lms.djangoapp": ["sample_plugin = sample_plugin.apps:SamplePluginConfig"], - "cms.djangoapp": ["sample_plugin = sample_plugin.apps:SamplePluginConfig"], - } - ``` + This plugin is registered in setup.py as:: + + entry_points={ + "lms.djangoapp": ["sample_plugin = sample_plugin.apps:SamplePluginConfig"], + "cms.djangoapp": ["sample_plugin = sample_plugin.apps:SamplePluginConfig"], + } The platform automatically discovers and loads plugins registered in these entry points. """ # noqa: diff --git a/backend/sample_plugin/pipeline.py b/backend/sample_plugin/pipeline.py index cfedba7..9894422 100644 --- a/backend/sample_plugin/pipeline.py +++ b/backend/sample_plugin/pipeline.py @@ -12,7 +12,7 @@ Key Concepts: - Filters receive data and return modified data -- They run at specific pipeline steps during platform operations +- They run at specific pipeline steps during platform operations - Filters can halt execution by raising exceptions - Multiple filters can be chained together in a pipeline - Filters should be lightweight and handle errors gracefully @@ -56,18 +56,17 @@ class ChangeCourseAboutPageUrl(PipelineStep): This filter hooks into the course about page URL rendering process. Register it for the filter: org.openedx.learning.course.about.render.started.v1 - Registration Example (in settings/common.py): - ```python - def plugin_settings(settings): - settings.OPEN_EDX_FILTERS_CONFIG = { - "org.openedx.learning.course.about.render.started.v1": { - "pipeline": [ - "sample_plugin.pipeline.ChangeCourseAboutPageUrl" - ], - "fail_silently": False, + Registration Example (in settings/common.py):: + + def plugin_settings(settings): + settings.OPEN_EDX_FILTERS_CONFIG = { + "org.openedx.learning.course.about.render.started.v1": { + "pipeline": [ + "sample_plugin.pipeline.ChangeCourseAboutPageUrl" + ], + "fail_silently": False, + } } - } - ``` Filter Documentation: - Available Filters: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters.html @@ -100,7 +99,7 @@ def run_filter(self, url, org, **kwargs): Raises: FilterException: If processing should be halted - + Filter Requirements: - Must return dictionary with keys matching input parameters - Return None to skip this filter (let other filters run) @@ -121,14 +120,14 @@ def run_filter(self, url, org, **kwargs): match = re.search(pattern, url) if match: course_id = match.group('course_id') - + # Example: Redirect to external marketing site new_url = f"https://example.com/new_about_page/{course_id}" - + logger.debug( f"Redirecting course about page for {course_id} from {url} to {new_url}" ) - + # Return modified data return {"url": new_url, "org": org} @@ -137,17 +136,17 @@ def run_filter(self, url, org, **kwargs): return {"url": url, "org": org} # Alternative patterns for different business logic: - + # Organization-based routing: # if org == "special_org": # new_url = f"https://special-site.com/courses/{course_id}" # return {"url": new_url, "org": org} - + # Course type-based routing: # if "MicroMasters" in course_id: # new_url = f"https://micromasters.example.com/{course_id}" # return {"url": new_url, "org": org} - + # A/B testing implementation: # import random # if random.choice([True, False]): diff --git a/backend/sample_plugin/signals.py b/backend/sample_plugin/signals.py index ca9e45c..4b4529a 100644 --- a/backend/sample_plugin/signals.py +++ b/backend/sample_plugin/signals.py @@ -85,22 +85,21 @@ def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **k - Log changes for audit and compliance - Update analytics dashboards with new course information - Example Implementation: - ```python - # Send to external CRM system - external_api.update_course( - course_id=str(catalog_info.course_key), - name=catalog_info.name, - is_hidden=catalog_info.hidden - ) - - # Update internal tracking - CourseChangeLog.objects.create( - course_key=catalog_info.course_key, - change_type='catalog_updated', - timestamp=timezone.now() - ) - ``` + Example Implementation:: + + # Send to external CRM system + external_api.update_course( + course_id=str(catalog_info.course_key), + name=catalog_info.name, + is_hidden=catalog_info.hidden + ) + + # Update internal tracking + CourseChangeLog.objects.create( + course_key=catalog_info.course_key, + change_type='catalog_updated', + timestamp=timezone.now() + ) Performance Considerations: - Keep processing lightweight (events should not block platform operations) @@ -108,11 +107,11 @@ def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **k - Handle exceptions gracefully to prevent platform disruption """ logging.info(f"Course catalog updated: {catalog_info.course_key}") - + # Access available data from the event logging.debug(f"Course name: {catalog_info.name}") logging.debug(f"Course hidden: {catalog_info.hidden}") - + # Example: Integrate with external systems # try: # # Send to external system @@ -123,7 +122,7 @@ def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **k # ) # except Exception as e: # logging.error(f"Failed to notify external system: {e}") - + # Example: Update internal tracking # from .models import CourseArchiveStatus # CourseArchiveStatus.objects.filter( From e538c8935ecae854c35b6d0d964c1992e28e2926 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Wed, 22 Oct 2025 12:33:27 -0400 Subject: [PATCH 3/3] style: Fix/Ignore linting issues. --- backend/sample_plugin/apps.py | 6 ++--- backend/sample_plugin/pipeline.py | 8 +++---- backend/sample_plugin/settings/common.py | 28 ++++++++++++------------ backend/sample_plugin/signals.py | 10 +++++---- backend/tests/__init__.py | 0 backend/tests/test_api.py | 6 ++--- 6 files changed, 30 insertions(+), 28 deletions(-) delete mode 100644 backend/tests/__init__.py diff --git a/backend/sample_plugin/apps.py b/backend/sample_plugin/apps.py index 3774c71..00ad310 100644 --- a/backend/sample_plugin/apps.py +++ b/backend/sample_plugin/apps.py @@ -41,7 +41,7 @@ class SamplePluginConfig(AppConfig): } The platform automatically discovers and loads plugins registered in these entry points. - """ # noqa: + """ # pylint: disable=line-too-long # noqa: E501 default_auto_field = "django.db.models.BigAutoField" name = "sample_plugin" @@ -98,7 +98,7 @@ class SamplePluginConfig(AppConfig): # } # # Documentation: - # - PluginSignals: https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html#plugin-signals + # - 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 # - Open edX Events: https://docs.openedx.org/projects/openedx-events/en/latest/ } @@ -130,4 +130,4 @@ def ready(self): """ # Import signal handlers to register Open edX Event receivers # This import registers all @receiver decorated functions in signals.py - from . import signals # noqa: F401 + from . import signals # pylint: disable=import-outside-toplevel,unused-import diff --git a/backend/sample_plugin/pipeline.py b/backend/sample_plugin/pipeline.py index 9894422..8b5dc3d 100644 --- a/backend/sample_plugin/pipeline.py +++ b/backend/sample_plugin/pipeline.py @@ -35,7 +35,7 @@ - Data transformation and validation - Integration with external systems - Custom business logic implementation -""" +""" # pylint: disable=line-too-long import logging import re @@ -78,9 +78,9 @@ def plugin_settings(settings): - Add tracking parameters to URLs - Route different course types to different platforms - Implement A/B testing for course pages - """ + """ # noqa: E501 - def run_filter(self, url, org, **kwargs): + def run_filter(self, url, org, **kwargs): # pylint: disable=arguments-differ """ Modify the course about page URL. @@ -112,7 +112,7 @@ def run_filter(self, url, org, **kwargs): Documentation: - run_filter method: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters-tooling.html#openedx_filters.filters.PipelineStep.run_filter - """ + """ # noqa: E501 # Extract course ID using Open edX course key pattern # Course keys follow the format: course-v1:ORG+COURSE+RUN pattern = r'(?Pcourse-v1:[^/]+)' diff --git a/backend/sample_plugin/settings/common.py b/backend/sample_plugin/settings/common.py index 823d614..a935984 100644 --- a/backend/sample_plugin/settings/common.py +++ b/backend/sample_plugin/settings/common.py @@ -16,7 +16,7 @@ Settings Organization: - common.py: Settings for all environments -- production.py: Production-specific overrides +- production.py: Production-specific overrides - test.py: Test environment optimizations Integration Points: @@ -25,7 +25,7 @@ - Database connection settings for plugin models - External service integration parameters - Feature flags and environment-specific toggles -""" +""" # noqa: E501 import logging @@ -48,11 +48,11 @@ def plugin_settings(settings): # Plugin-specific configuration settings.SAMPLE_PLUGIN_API_RATE_LIMIT = "60/minute" settings.SAMPLE_PLUGIN_ARCHIVE_RETENTION_DAYS = 365 - + # External service integration settings.SAMPLE_PLUGIN_EXTERNAL_API_URL = "https://api.example.com" settings.SAMPLE_PLUGIN_API_KEY = "your-api-key" - + # Feature flags settings.SAMPLE_PLUGIN_ENABLE_ARCHIVING = True settings.SAMPLE_PLUGIN_ENABLE_NOTIFICATIONS = False @@ -70,11 +70,11 @@ def plugin_settings(settings): """ # Plugin is configured but no additional settings needed for this basic example # Uncomment and modify the examples below for your use case: - + # Plugin-specific configuration # settings.SAMPLE_PLUGIN_API_RATE_LIMIT = "60/minute" # settings.SAMPLE_PLUGIN_ARCHIVE_RETENTION_DAYS = 365 - + # Register Open edX Filters (additive approach) _configure_openedx_filters(settings) @@ -82,30 +82,30 @@ def plugin_settings(settings): def _configure_openedx_filters(settings): """ Configure Open edX Filters for the sample plugin. - + This function demonstrates the proper way to register filters by: 1. Preserving existing filter configuration from other plugins 2. Adding our filter configuration additively 3. Avoiding duplicate pipeline steps 4. Logging configuration state for debugging - + Args: settings (dict): Django settings object """ # Get existing filter configuration (may be from other plugins or platform) filters_config = getattr(settings, 'OPEN_EDX_FILTERS_CONFIG', {}) - + # Filter we want to register filter_name = "org.openedx.learning.course_about.page.url.requested.v1" our_pipeline_step = "sample_plugin.pipeline.ChangeCourseAboutPageUrl" - + # Check if this filter already has configuration if filter_name in filters_config: logger.debug(f"Filter {filter_name} already configured, adding our pipeline step") - + # Get existing pipeline steps existing_pipeline = filters_config[filter_name].get("pipeline", []) - + # Check if our pipeline step is already registered if our_pipeline_step in existing_pipeline: logger.info( @@ -125,10 +125,10 @@ def _configure_openedx_filters(settings): "pipeline": [our_pipeline_step], "fail_silently": False, } - + # Update the settings object settings.OPEN_EDX_FILTERS_CONFIG = filters_config - + logger.debug( f"Final filter configuration for {filter_name}: " f"{filters_config.get(filter_name, {})}" diff --git a/backend/sample_plugin/signals.py b/backend/sample_plugin/signals.py index 4b4529a..0e707e5 100644 --- a/backend/sample_plugin/signals.py +++ b/backend/sample_plugin/signals.py @@ -42,15 +42,17 @@ - Synchronizing data with external databases """ -from openedx_events.content_authoring.signals import COURSE_CATALOG_INFO_CHANGED -from openedx_events.content_authoring.data import CourseCatalogData -from django.dispatch import receiver import logging +from django.dispatch import receiver +from openedx_events.content_authoring.data import CourseCatalogData +from openedx_events.content_authoring.signals import COURSE_CATALOG_INFO_CHANGED + logger = logging.getLogger(__name__) + @receiver(COURSE_CATALOG_INFO_CHANGED) -def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs): +def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs): # pylint: disable=unused-argument # noqa: E501 """ Handle course catalog information changes. diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 139e5db..0dc1b56 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -298,7 +298,7 @@ def test_create_course_archive_status_without_user_field(api_client, user, cours if response.status_code != status.HTTP_201_CREATED: print(f"Response status: {response.status_code}") print(f"Response data: {response.data}") - + assert response.status_code == status.HTTP_201_CREATED assert response.data["course_id"] == str(course_key) assert response.data["user"] == user.id @@ -378,7 +378,7 @@ def test_staff_update_with_explicit_user_override( initial_status = CourseArchiveStatus.objects.create( course_id=course_key, user=user, is_archived=False ) - + api_client.force_authenticate(user=staff_user) url = reverse( "sample_plugin:course-archive-status-detail", args=[initial_status.id] @@ -418,7 +418,7 @@ def test_regular_user_cannot_override_user_field_create( assert response.status_code == status.HTTP_403_FORBIDDEN -@pytest.mark.django_db +@pytest.mark.django_db def test_staff_create_without_user_field_defaults_to_current_user( api_client, staff_user, course_key ):