This project has been archived by its maintainers, and is no longer receiving any updates.
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.
Release files for openedx-sample-plugin 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| openedx_sample_plugin-2.0.0.tar.gz | 143.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| openedx_sample_plugin-2.0.0-py2.py3-none-any.whl | Python 2, Python 3 | none | any | Details |
Total release size: 171.5 kB
Release files / openedx_sample_plugin-2.0.0.tar.gz
| Download URL | openedx_sample_plugin-2.0.0.tar.gz |
|---|---|
| Size | 143.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a849e09c8f6cad89a45928b98604734b871454857ce29a4b20d4d8d4fc4ea366
|
|
BLAKE2b-256 checksum How to use checksums |
abd1767c2d76b58edf8ff5d2b9db8f81851410067aa9e8404c59762b73bae846
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / openedx_sample_plugin-2.0.0-py2.py3-none-any.whl
| Download URL | openedx_sample_plugin-2.0.0-py2.py3-none-any.whl |
|---|---|
| Size | 28.3 kB |
| Tags | Python 2 Python 3 |
|
SHA-256 checksum How to use checksums |
497cd46de2b251687610979da3577cf23ee471cda8b9439f7b692b7850007df4
|
|
BLAKE2b-256 checksum How to use checksums |
8c43abd383c3932edd5fb995c470cd75a95a52c507417406dfbbec64eb6d1736
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|