Skip to main content

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

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 updated
  • STUDENT_REGISTRATION_COMPLETED - New user registered
  • CERTIFICATE_CREATED - Certificate generated for learner
  • ENROLLMENT_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 environments
  • production.py: Production-only settings
  • test.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

  1. Platform Setup: Open edX Development Guide
  2. 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

  1. Check Installation:

    python manage.py lms shell
    >>> from sample_plugin.models import CourseArchiveStatus
    >>> print("Plugin installed successfully!")
    
  2. Test API: Visit http://localhost:18000/sample-plugin/api/v1/course-archive-status/

  3. 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:

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

  1. Models: Modify models.py for your data structure
  2. APIs: Update views.py and serializers.py
  3. Events: Change event handlers in signals.py
  4. Filters: Implement your business logic in pipeline.py
  5. Settings: Configure plugin behavior in settings/

Plugin Development Checklist

  • Update pyproject.toml with your plugin name and dependencies
  • Modify apps.py with 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

openedx_sample_plugin-1.3.0.tar.gz (62.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

openedx_sample_plugin-1.3.0-py2.py3-none-any.whl (28.3 kB view details)

Uploaded Python 2Python 3

File details

Details for the file openedx_sample_plugin-1.3.0.tar.gz.

File metadata

  • Download URL: openedx_sample_plugin-1.3.0.tar.gz
  • Upload date:
  • Size: 62.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for openedx_sample_plugin-1.3.0.tar.gz
Algorithm Hash digest
SHA256 1a7d4928d0b44a5d1aaaa880f63e4d31b70cbe9c2588da35302b8b83a0fc1dca
MD5 50557eb9b9b754675cd349a6c9f86833
BLAKE2b-256 92bd92f4eda3a7304f779a171a1361ee735c1332416f8078173a2e803e243d55

See more details on using hashes here.

File details

Details for the file openedx_sample_plugin-1.3.0-py2.py3-none-any.whl.

File metadata

File hashes

Hashes for openedx_sample_plugin-1.3.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 8fc1da29c5794642a0e8b5bf165ded2692d0c5da5deffbb18ab22e41e4fb5e13
MD5 e1c0b1299dc557727f0c443c558bf62c
BLAKE2b-256 36377fbbadd106e4a50bcf7ea8852e01db0f20a4f8361ede8b456cf2b47456a2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page