Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions backend/sample_plugin/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,15 @@ 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:
""" # pylint: disable=line-too-long # noqa: E501

default_auto_field = "django.db.models.BigAutoField"
name = "sample_plugin"
Expand Down Expand Up @@ -99,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/
}

Expand Down Expand Up @@ -131,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
45 changes: 22 additions & 23 deletions backend/sample_plugin/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -79,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.

Expand All @@ -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)
Expand All @@ -113,22 +112,22 @@ 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'(?P<course_id>course-v1:[^/]+)'

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}

Expand All @@ -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]):
Expand Down
28 changes: 14 additions & 14 deletions backend/sample_plugin/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -25,7 +25,7 @@
- Database connection settings for plugin models
- External service integration parameters
- Feature flags and environment-specific toggles
"""
""" # noqa: E501

import logging

Expand All @@ -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
Expand All @@ -70,42 +70,42 @@ 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)


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(
Expand All @@ -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, {})}"
Expand Down
47 changes: 24 additions & 23 deletions backend/sample_plugin/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -85,34 +87,33 @@ 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)
- Use asynchronous tasks for heavy processing (Celery, etc.)
- 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
Expand All @@ -123,7 +124,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(
Expand Down
Empty file removed backend/tests/__init__.py
Empty file.
6 changes: 3 additions & 3 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
):
Expand Down
14 changes: 3 additions & 11 deletions catalog-info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,18 +21,10 @@ metadata:
spec:

# (Required) This can be a group(`group:<group_name>` or a user(`user:<github_username>`)
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: '<name_of_a_component>'

# (Optional) An array of different components or resources.
dependsOn:
- '<component_or_resource>'
- '<another_component_or_resource>'