A sample backend plugin for the Open edX Platform
Project description
Backend Plugin Implementation Guide
This directory contains a comprehensive Django app plugin that demonstrates all major backend plugin interfaces available in Open edX. The plugin implements a course archiving system to show real-world usage patterns.
Table of Contents
- Overview
- Django App Plugin Configuration
- Models & Database
- API Endpoints
- Events & Signals
- Filters & Pipeline Steps
- Settings Configuration
- Development Setup
- Testing Your Plugin
- Integration Examples
- Adapting This Plugin
Overview
This backend plugin demonstrates the Open edX Django App Plugin pattern, which allows you to add new functionality to edx-platform without modifying core platform code.
What this plugin provides:
- Models: Course archive status tracking
- APIs: REST endpoints for frontend integration
- Events: React to course catalog changes
- Filters: Modify course about page URLs
- Settings: Plugin configuration management
Official Documentation:
Django App Plugin Configuration
File: sample_plugin/apps.py
Plugin Registration
The SamplePluginConfig class configures this app as an edx-platform plugin:
class SamplePluginConfig(AppConfig):
name = "sample_plugin"
plugin_app = {
"url_config": {
# Register URLs for both LMS and CMS
"lms.djangoapp": {
PluginURLs.NAMESPACE: "sample_plugin",
PluginURLs.REGEX: r"^sample-plugin/",
PluginURLs.RELATIVE_PATH: "urls",
},
# ... CMS configuration
},
PluginSettings.CONFIG: {
# Configure settings for different environments
"lms.djangoapp": {
"common": {PluginURLs.RELATIVE_PATH: "settings.common"},
"production": {PluginURLs.RELATIVE_PATH: "settings.production"},
},
# ... CMS configuration
}
}
Key Configuration Options
| Option | Purpose | Official Docs |
|---|---|---|
| url_config | Register plugin URLs with platform | Plugin URLs |
| PluginSettings.CONFIG | Load plugin settings | Plugin Settings |
| ready() method | Initialize signal handlers | Django AppConfig.ready() |
Entry Points Configuration
In pyproject.toml, the plugin registers itself with edx-platform:
[project.entry-points."lms.djangoapp"]
sample_plugin = "sample_plugin.apps:SamplePluginConfig"
[project.entry-points."cms.djangoapp"]
sample_plugin = "sample_plugin.apps:SamplePluginConfig"
Why this works: The platform automatically discovers and loads any Django app registered in these entry points.
Models & Database
File: sample_plugin/models.py
Official Docs: OEP-49: Django App Patterns
CourseArchiveStatus Model
class CourseArchiveStatus(models.Model):
course_id = CourseKeyField(max_length=255, db_index=True)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
is_archived = models.BooleanField(default=False, db_index=True)
archive_date = models.DateTimeField(null=True, blank=True)
# ... timestamps
Key Features:
- CourseKeyField: Uses Open edX's opaque keys for course identification
- User Reference: Links to platform's user model via
get_user_model() - Database Indexes: Performance optimization on frequently queried fields
- Unique Constraints: Prevents duplicate records per user-course combination
Database Migration
# After modifying models.py
cd backend
python manage.py makemigrations sample_plugin
python manage.py migrate
Migration files: Generated in sample_plugin/migrations/
PII Annotations
The model includes PII documentation:
# .. no_pii: This model does not store PII directly, only references to users via foreign keys.
Best Practice: Always document PII handling for Open edX compliance.
API Endpoints
File: sample_plugin/views.py
URLs: sample_plugin/urls.py
REST API Implementation
class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
serializer_class = CourseArchiveStatusSerializer
permission_classes = [IsOwnerOrStaffSuperuser]
pagination_class = CourseArchiveStatusPagination
throttle_classes = [CourseArchiveStatusThrottle]
# ... filtering and ordering
API Features
| Feature | Implementation | Why It Matters |
|---|---|---|
| Authentication | IsOwnerOrStaffSuperuser permission |
Users only see their own data; staff see all |
| Pagination | Custom pagination class | Performance with large datasets |
| Throttling | Rate limiting (60/minute) | Prevents API abuse |
| Filtering | DjangoFilterBackend | Query by course_id, user, archive status |
| Validation | Course ID format checking | Prevents injection attacks |
API Endpoints
- GET
/sample-plugin/api/v1/course-archive-status/- List archive statuses - POST
/sample-plugin/api/v1/course-archive-status/- Create new status - GET
/sample-plugin/api/v1/course-archive-status/{id}/- Get specific status - PUT/PATCH
/sample-plugin/api/v1/course-archive-status/{id}/- Update status - DELETE
/sample-plugin/api/v1/course-archive-status/{id}/- Delete status
Business Logic
The viewset includes custom business logic:
def perform_create(self, serializer):
# Set archive_date when creating archived status
data = {}
if serializer.validated_data.get("is_archived", False):
data["archive_date"] = timezone.now()
instance = serializer.save(**data)
Pattern: Use perform_create() and perform_update() for business logic, following the pattern documented in CLAUDE.md.
Events & Signals
File: sample_plugin/signals.py
Official Docs: Open edX Events Guide
Event Handler Example
from openedx_events.content_authoring.signals import COURSE_CATALOG_INFO_CHANGED
from django.dispatch import receiver
@receiver(COURSE_CATALOG_INFO_CHANGED)
def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs):
logging.info(f"{catalog_info.course_key} has been updated!")
# Add your custom business logic here
Available Events
Event Catalog: Open edX Events Reference
Common Events:
COURSE_CATALOG_INFO_CHANGED- Course information updatedSTUDENT_REGISTRATION_COMPLETED- New user registeredCERTIFICATE_CREATED- Certificate generated for learnerENROLLMENT_CREATED- Student enrolled in course
Event Data Structure
Each event includes specific data. For COURSE_CATALOG_INFO_CHANGED:
def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs):
# catalog_info contains:
# - course_key: CourseKey object
# - name: Course display name
# - schedule: Course schedule information
# - hidden: Visibility status
Key Point: Check the event definition to understand what data is available.
Signal Handler Registration
Handlers are automatically registered via the ready() method in apps.py:
def ready(self):
# Import handlers to register signal receivers
from . import signals
Real-World Use Cases
- Integration: Send course updates to external systems
- Analytics: Track course lifecycle events
- Notifications: Email administrators about important changes
- Auditing: Log sensitive operations for compliance
Filters & Pipeline Steps
File: sample_plugin/pipeline.py
Official Docs: Using Open edX Filters
Filter Implementation
from openedx_filters.filters import PipelineStep
class ChangeCourseAboutPageUrl(PipelineStep):
def run_filter(self, url, org, **kwargs):
# Extract course ID from URL
pattern = r'(?P<course_id>course-v1:[^/]+)'
match = re.search(pattern, url)
if match:
course_id = match.group('course_id')
new_url = f"https://example.com/new_about_page/{course_id}"
return {"url": new_url, "org": org}
# Return original data if no match
return {"url": url, "org": org}
Filter Requirements
Essential Elements:
- Inherit from
PipelineStep - Implement
run_filter()method - Return dictionary with same parameter names as input
- Handle all possible input scenarios
Available Filters
Filter Catalog: Open edX Filters Reference
Common Filters:
- Course enrollment filters
- Authentication filters
- Certificate generation filters
- Course discovery filters
Filter Registration
Filters must be registered in Django settings. This happens automatically via the plugin settings system (see Settings Configuration).
Real-World Use Cases
- URL Redirection: Send users to custom course pages
- Access Control: Implement custom enrollment restrictions
- Data Transformation: Modify course data before display
- Integration: Add custom fields to API responses
Settings Configuration
Files: sample_plugin/settings/
Settings Structure
# settings/common.py
def plugin_settings(settings):
"""Add plugin settings to main settings object."""
# Add your custom settings here
# settings.SAMPLE_PLUGIN_API_KEY = "your-key"
pass
Environment-Specific Settings
common.py: Settings for all environmentsproduction.py: Production-only settingstest.py: Test-specific settings (faster database, etc.)
Filter Registration via Settings
To register the URL filter, add to common.py:
def plugin_settings(settings):
# Register the course about page URL filter
settings.OPEN_EDX_FILTERS_CONFIG = {
"org.openedx.learning.course.about.render.started.v1": {
"pipeline": [
"sample_plugin.pipeline.ChangeCourseAboutPageUrl"
],
"fail_silently": False,
}
}
Filter Name Discovery: Filter names are found in the official filters documentation.
Plugin-Specific Settings
Add custom configuration:
def plugin_settings(settings):
# Plugin-specific settings
settings.SAMPLE_PLUGIN_ARCHIVE_RETENTION_DAYS = 365
settings.SAMPLE_PLUGIN_API_RATE_LIMIT = "60/minute"
settings.SAMPLE_PLUGIN_EXTERNAL_API_URL = "https://api.example.com"
Development Setup
Prerequisites
- Platform Setup: Open edX Development Guide
- Python Environment: Python 3.8+ with virtual environment
Installation Methods
Option 1: With Tutor (Recommended)
# Mount the backend plugin
tutor mounts add lms:$PWD:/openedx/sample-plugin-backend
# Launch and install
tutor dev launch
tutor dev exec lms pip install -e ../sample-plugin-backend
tutor dev exec lms python manage.py lms migrate
tutor dev restart lms
Option 2: Direct Installation
# In your edx-platform directory
pip install -e /path/to/sample-plugin/backend
# Run migrations
python manage.py lms migrate
python manage.py cms migrate
Verification Steps
-
Check Installation:
python manage.py lms shell >>> from sample_plugin.models import CourseArchiveStatus >>> print("Plugin installed successfully!")
-
Test API: Visit
http://localhost:18000/sample-plugin/api/v1/course-archive-status/ -
Check Admin: Go to
http://localhost:18000/admin/and look for "Course Archive Statuses"
Testing Your Plugin
Running Tests
cd backend
# Install test dependencies
make requirements
# Run all tests
make test
# Run specific test
pytest tests/test_models.py::test_course_archive_status_creation
# Run with coverage
make test-coverage
Test Structure
Test Files:
tests/test_models.py- Model functionalitytests/test_api.py- API endpoint testingtests/test_plugin_integration.py- Plugin integration
Writing Plugin Tests
Model Testing Pattern:
from django.test import TestCase
from sample_plugin.models import CourseArchiveStatus
class TestCourseArchiveStatus(TestCase):
def test_create_archive_status(self):
# Test model creation and validation
pass
API Testing Pattern:
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
class TestCourseArchiveStatusAPI(APITestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(username="testuser")
def test_list_archive_statuses(self):
# Test API endpoints
pass
Quality Checks
# Run linting and quality checks
make quality
# Individual tools
pylint sample_plugin/
isort --check-only sample_plugin/
black --check sample_plugin/
Integration Examples
Backend + Frontend Integration
API Endpoint (views.py):
class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
# Provides data for frontend consumption
Frontend Consumption (see ../frontend/src/plugin.jsx):
const response = await client.get(
`${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`
);
Events + API Integration
@receiver(COURSE_CATALOG_INFO_CHANGED)
def sync_course_archive_on_change(signal, sender, catalog_info, **kwargs):
# Update archive statuses when course info changes
CourseArchiveStatus.objects.filter(
course_id=catalog_info.course_key
).update(last_synced=timezone.now())
Filters + Settings Integration
Settings configure filter behavior:
# settings/common.py
def plugin_settings(settings):
settings.SAMPLE_PLUGIN_REDIRECT_DOMAIN = "custom-domain.com"
# pipeline.py - Uses setting
class ChangeCourseAboutPageUrl(PipelineStep):
def run_filter(self, url, org, **kwargs):
redirect_domain = getattr(settings, 'SAMPLE_PLUGIN_REDIRECT_DOMAIN', 'example.com')
new_url = f"https://{redirect_domain}/course/{course_id}"
return {"url": new_url, "org": org}
Adapting This Plugin
For Your Use Case
- Models: Modify
models.pyfor your data structure - APIs: Update
views.pyandserializers.py - Events: Change event handlers in
signals.py - Filters: Implement your business logic in
pipeline.py - Settings: Configure plugin behavior in
settings/
Plugin Development Checklist
- Update
pyproject.tomlwith your plugin name and dependencies - Modify
apps.pywith your app configuration - Design your models in
models.py - Create and run database migrations
- Implement API endpoints in
views.py - Add event handlers in
signals.py - Create filters in
pipeline.py - Configure settings in
settings/ - Write comprehensive tests
- Update documentation
Common Customization Patterns
Adding New Models:
class YourModel(models.Model):
# Use Open edX field types when possible
course_id = CourseKeyField(max_length=255)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
# ... your fields
Adding New API Endpoints:
class YourViewSet(viewsets.ModelViewSet):
# Follow the permission patterns from CourseArchiveStatusViewSet
permission_classes = [IsOwnerOrStaffSuperuser]
# ... your implementation
Adding New Event Handlers:
@receiver(YOUR_CHOSEN_EVENT)
def handle_your_event(signal, sender, event_data, **kwargs):
# Your business logic
pass
This backend plugin provides a solid foundation for any Open edX extension. Focus on adapting the business logic while keeping the proven patterns for authentication, permissions, and integration.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openedx_sample_plugin-1.4.0.tar.gz.
File metadata
- Download URL: openedx_sample_plugin-1.4.0.tar.gz
- Upload date:
- Size: 62.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a70cea006b78dedccdb742fcefed1c6a418880a20051c6ecf733cdf1fdef96e1
|
|
| MD5 |
428a61c2043b1b4156de31c445247bb8
|
|
| BLAKE2b-256 |
9e11b438dbc4161c9639837eb2e548cf1366363592ad408590ef14b05ee93cdd
|
File details
Details for the file openedx_sample_plugin-1.4.0-py2.py3-none-any.whl.
File metadata
- Download URL: openedx_sample_plugin-1.4.0-py2.py3-none-any.whl
- Upload date:
- Size: 28.3 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
65ceaf9d82ee758800d7b044b92fbbbdedc690110778884b57b503efc040822d
|
|
| MD5 |
33b84e28fe11b6c6afcb12e249869503
|
|
| BLAKE2b-256 |
5ae47aea8ebc8e1ec5b407d48890d291bed86505b0251419cc7f751b77122b40
|