openedx-plugin-sample 3.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openedx_plugin_sample/__init__.py +7 -0
- openedx_plugin_sample/apps.py +134 -0
- openedx_plugin_sample/conf/locale/config.yaml +85 -0
- openedx_plugin_sample/migrations/0001_initial.py +78 -0
- openedx_plugin_sample/migrations/__init__.py +0 -0
- openedx_plugin_sample/models.py +65 -0
- openedx_plugin_sample/pipeline.py +156 -0
- openedx_plugin_sample/py.typed +0 -0
- openedx_plugin_sample/serializers.py +39 -0
- openedx_plugin_sample/settings/common.py +135 -0
- openedx_plugin_sample/settings/production.py +16 -0
- openedx_plugin_sample/settings/test.py +17 -0
- openedx_plugin_sample/signals.py +132 -0
- openedx_plugin_sample/templates/openedx_plugin_sample/base.html +26 -0
- openedx_plugin_sample/urls.py +21 -0
- openedx_plugin_sample/views.py +241 -0
- openedx_plugin_sample-3.0.0.dist-info/METADATA +579 -0
- openedx_plugin_sample-3.0.0.dist-info/RECORD +22 -0
- openedx_plugin_sample-3.0.0.dist-info/WHEEL +6 -0
- openedx_plugin_sample-3.0.0.dist-info/entry_points.txt +5 -0
- openedx_plugin_sample-3.0.0.dist-info/licenses/LICENSE.txt +180 -0
- openedx_plugin_sample-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openedx-plugin-sample
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: A sample backend plugin for the Open edX Platform
|
|
5
|
+
Author-email: Open edX Project <oscm@openedx.org>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://openedx.org/openedx/sample-plugin
|
|
8
|
+
Project-URL: Repository, https://openedx.org/openedx/sample-plugin
|
|
9
|
+
Keywords: Python,edx
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 4.2
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE.txt
|
|
20
|
+
Requires-Dist: Django
|
|
21
|
+
Requires-Dist: djangorestframework
|
|
22
|
+
Requires-Dist: django-filter
|
|
23
|
+
Requires-Dist: edx-opaque-keys
|
|
24
|
+
Requires-Dist: openedx-events
|
|
25
|
+
Requires-Dist: openedx-filters
|
|
26
|
+
Requires-Dist: openedx-atlas
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Backend Plugin Implementation Guide
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
## Table of Contents
|
|
34
|
+
|
|
35
|
+
- [Overview](#overview)
|
|
36
|
+
- [Django App Plugin Configuration](#django-app-plugin-configuration)
|
|
37
|
+
- [Models & Database](#models--database)
|
|
38
|
+
- [API Endpoints](#api-endpoints)
|
|
39
|
+
- [Events & Signals](#events--signals)
|
|
40
|
+
- [Filters & Pipeline Steps](#filters--pipeline-steps)
|
|
41
|
+
- [Settings Configuration](#settings-configuration)
|
|
42
|
+
- [Development Setup](#development-setup)
|
|
43
|
+
- [Testing Your Plugin](#testing-your-plugin)
|
|
44
|
+
- [Integration Examples](#integration-examples)
|
|
45
|
+
- [Adapting This Plugin](#adapting-this-plugin)
|
|
46
|
+
|
|
47
|
+
## Overview
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
**What this plugin provides:**
|
|
52
|
+
- **Models**: Course archive status tracking
|
|
53
|
+
- **APIs**: REST endpoints for frontend integration
|
|
54
|
+
- **Events**: React to course catalog changes
|
|
55
|
+
- **Filters**: Modify course about page URLs
|
|
56
|
+
- **Settings**: Plugin configuration management
|
|
57
|
+
|
|
58
|
+
**Official Documentation:**
|
|
59
|
+
- [Django App Plugins Overview](https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/readme.html)
|
|
60
|
+
- [How to create a plugin app](https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html)
|
|
61
|
+
- [Hooks Extension Framework](https://docs.openedx.org/en/latest/developers/concepts/hooks_extension_framework.html)
|
|
62
|
+
|
|
63
|
+
## Django App Plugin Configuration
|
|
64
|
+
|
|
65
|
+
**File**: [`openedx_plugin_sample/apps.py`](./openedx_plugin_sample/apps.py)
|
|
66
|
+
|
|
67
|
+
### Plugin Registration
|
|
68
|
+
|
|
69
|
+
The `SamplePluginConfig` class configures this app as an edx-platform plugin:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
class SamplePluginConfig(AppConfig):
|
|
73
|
+
name = "openedx_plugin_sample"
|
|
74
|
+
plugin_app = {
|
|
75
|
+
"url_config": {
|
|
76
|
+
# Register URLs for both LMS and CMS
|
|
77
|
+
"lms.djangoapp": {
|
|
78
|
+
PluginURLs.NAMESPACE: "openedx_plugin_sample",
|
|
79
|
+
PluginURLs.REGEX: r"^sample-plugin/",
|
|
80
|
+
PluginURLs.RELATIVE_PATH: "urls",
|
|
81
|
+
},
|
|
82
|
+
# ... CMS configuration
|
|
83
|
+
},
|
|
84
|
+
PluginSettings.CONFIG: {
|
|
85
|
+
# Configure settings for different environments
|
|
86
|
+
"lms.djangoapp": {
|
|
87
|
+
"common": {PluginURLs.RELATIVE_PATH: "settings.common"},
|
|
88
|
+
"production": {PluginURLs.RELATIVE_PATH: "settings.production"},
|
|
89
|
+
},
|
|
90
|
+
# ... CMS configuration
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Key Configuration Options
|
|
96
|
+
|
|
97
|
+
| Option | Purpose | Official Docs |
|
|
98
|
+
|--------|---------|---------------|
|
|
99
|
+
| **url_config** | Register plugin URLs with platform | [Plugin URLs](https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html#plugin-urls) |
|
|
100
|
+
| **PluginSettings.CONFIG** | Load plugin settings | [Plugin Settings](https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html#plugin-settings) |
|
|
101
|
+
| **ready() method** | Initialize signal handlers | [Django AppConfig.ready()](https://docs.djangoproject.com/en/stable/ref/applications/#django.apps.AppConfig.ready) |
|
|
102
|
+
|
|
103
|
+
### Entry Points Configuration
|
|
104
|
+
|
|
105
|
+
In [`pyproject.toml`](./pyproject.toml), the plugin registers itself with edx-platform:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
[project.entry-points."lms.djangoapp"]
|
|
109
|
+
openedx_plugin_sample = "openedx_plugin_sample.apps:SamplePluginConfig"
|
|
110
|
+
|
|
111
|
+
[project.entry-points."cms.djangoapp"]
|
|
112
|
+
openedx_plugin_sample = "openedx_plugin_sample.apps:SamplePluginConfig"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Why this works**: The platform automatically discovers and loads any Django app registered in these entry points.
|
|
116
|
+
|
|
117
|
+
## Models & Database
|
|
118
|
+
|
|
119
|
+
**File**: [`openedx_plugin_sample/models.py`](./openedx_plugin_sample/models.py)
|
|
120
|
+
**Official Docs**: [OEP-49: Django App Patterns](https://docs.openedx.org/projects/openedx-proposals/en/latest/best-practices/oep-0049-django-app-patterns.html)
|
|
121
|
+
|
|
122
|
+
### CourseArchiveStatus Model
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
class CourseArchiveStatus(models.Model):
|
|
126
|
+
course_id = CourseKeyField(max_length=255, db_index=True)
|
|
127
|
+
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
|
128
|
+
is_archived = models.BooleanField(default=False, db_index=True)
|
|
129
|
+
archive_date = models.DateTimeField(null=True, blank=True)
|
|
130
|
+
# ... timestamps
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Key Features:**
|
|
134
|
+
- **CourseKeyField**: Uses Open edX's opaque keys for course identification
|
|
135
|
+
- **User Reference**: Links to platform's user model via `get_user_model()`
|
|
136
|
+
- **Database Indexes**: Performance optimization on frequently queried fields
|
|
137
|
+
- **Unique Constraints**: Prevents duplicate records per user-course combination
|
|
138
|
+
|
|
139
|
+
### Database Migration
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# After modifying models.py
|
|
143
|
+
cd backend-plugin-sample
|
|
144
|
+
python manage.py makemigrations openedx_plugin_sample
|
|
145
|
+
python manage.py migrate
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Migration files**: Generated in [`openedx_plugin_sample/migrations/`](./openedx_plugin_sample/migrations/)
|
|
149
|
+
|
|
150
|
+
### PII Annotations
|
|
151
|
+
|
|
152
|
+
The model includes PII documentation:
|
|
153
|
+
```python
|
|
154
|
+
# .. no_pii: This model does not store PII directly, only references to users via foreign keys.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Best Practice**: Always document PII handling for Open edX compliance.
|
|
158
|
+
|
|
159
|
+
## API Endpoints
|
|
160
|
+
|
|
161
|
+
**File**: [`openedx_plugin_sample/views.py`](./openedx_plugin_sample/views.py)
|
|
162
|
+
**URLs**: [`openedx_plugin_sample/urls.py`](./openedx_plugin_sample/urls.py)
|
|
163
|
+
|
|
164
|
+
### REST API Implementation
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
|
|
168
|
+
serializer_class = CourseArchiveStatusSerializer
|
|
169
|
+
permission_classes = [IsOwnerOrStaffSuperuser]
|
|
170
|
+
pagination_class = CourseArchiveStatusPagination
|
|
171
|
+
throttle_classes = [CourseArchiveStatusThrottle]
|
|
172
|
+
# ... filtering and ordering
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### API Features
|
|
176
|
+
|
|
177
|
+
| Feature | Implementation | Why It Matters |
|
|
178
|
+
|---------|----------------|----------------|
|
|
179
|
+
| **Authentication** | `IsOwnerOrStaffSuperuser` permission | Users only see their own data; staff see all |
|
|
180
|
+
| **Pagination** | Custom pagination class | Performance with large datasets |
|
|
181
|
+
| **Throttling** | Rate limiting (60/minute) | Prevents API abuse |
|
|
182
|
+
| **Filtering** | DjangoFilterBackend | Query by course_id, user, archive status |
|
|
183
|
+
| **Validation** | Course ID format checking | Prevents injection attacks |
|
|
184
|
+
|
|
185
|
+
### API Endpoints
|
|
186
|
+
|
|
187
|
+
- **GET** `/sample-plugin/api/v1/course-archive-status/` - List archive statuses
|
|
188
|
+
- **POST** `/sample-plugin/api/v1/course-archive-status/` - Create new status
|
|
189
|
+
- **GET** `/sample-plugin/api/v1/course-archive-status/{id}/` - Get specific status
|
|
190
|
+
- **PUT/PATCH** `/sample-plugin/api/v1/course-archive-status/{id}/` - Update status
|
|
191
|
+
- **DELETE** `/sample-plugin/api/v1/course-archive-status/{id}/` - Delete status
|
|
192
|
+
|
|
193
|
+
### Business Logic
|
|
194
|
+
|
|
195
|
+
The viewset includes custom business logic:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
def perform_create(self, serializer):
|
|
199
|
+
# Set archive_date when creating archived status
|
|
200
|
+
data = {}
|
|
201
|
+
if serializer.validated_data.get("is_archived", False):
|
|
202
|
+
data["archive_date"] = timezone.now()
|
|
203
|
+
instance = serializer.save(**data)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Pattern**: Use `perform_create()` and `perform_update()` for business logic, following the pattern documented in [CLAUDE.md](../CLAUDE.md#api-development-guidelines).
|
|
207
|
+
|
|
208
|
+
## Events & Signals
|
|
209
|
+
|
|
210
|
+
**File**: [`openedx_plugin_sample/signals.py`](./openedx_plugin_sample/signals.py)
|
|
211
|
+
**Official Docs**: [Open edX Events Guide](https://docs.openedx.org/projects/openedx-events/en/latest/)
|
|
212
|
+
|
|
213
|
+
### Event Handler Example
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from openedx_events.content_authoring.signals import COURSE_CATALOG_INFO_CHANGED
|
|
217
|
+
from django.dispatch import receiver
|
|
218
|
+
|
|
219
|
+
@receiver(COURSE_CATALOG_INFO_CHANGED)
|
|
220
|
+
def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs):
|
|
221
|
+
logging.info(f"{catalog_info.course_key} has been updated!")
|
|
222
|
+
# Add your custom business logic here
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Available Events
|
|
226
|
+
|
|
227
|
+
**Event Catalog**: [Open edX Events Reference](https://docs.openedx.org/projects/openedx-events/en/latest/reference/events.html)
|
|
228
|
+
|
|
229
|
+
**Common Events:**
|
|
230
|
+
- `COURSE_CATALOG_INFO_CHANGED` - Course information updated
|
|
231
|
+
- `STUDENT_REGISTRATION_COMPLETED` - New user registered
|
|
232
|
+
- `CERTIFICATE_CREATED` - Certificate generated for learner
|
|
233
|
+
- `ENROLLMENT_CREATED` - Student enrolled in course
|
|
234
|
+
|
|
235
|
+
### Event Data Structure
|
|
236
|
+
|
|
237
|
+
Each event includes specific data. For `COURSE_CATALOG_INFO_CHANGED`:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs):
|
|
241
|
+
# catalog_info contains:
|
|
242
|
+
# - course_key: CourseKey object
|
|
243
|
+
# - name: Course display name
|
|
244
|
+
# - schedule: Course schedule information
|
|
245
|
+
# - hidden: Visibility status
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Key Point**: Check the [event definition](https://docs.openedx.org/projects/openedx-events/en/latest/reference/events.html) to understand what data is available.
|
|
249
|
+
|
|
250
|
+
### Signal Handler Registration
|
|
251
|
+
|
|
252
|
+
Handlers are automatically registered via the `ready()` method in [`apps.py`](./openedx_plugin_sample/apps.py):
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
def ready(self):
|
|
256
|
+
# Import handlers to register signal receivers
|
|
257
|
+
from . import signals
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Real-World Use Cases
|
|
261
|
+
|
|
262
|
+
- **Integration**: Send course updates to external systems
|
|
263
|
+
- **Analytics**: Track course lifecycle events
|
|
264
|
+
- **Notifications**: Email administrators about important changes
|
|
265
|
+
- **Auditing**: Log sensitive operations for compliance
|
|
266
|
+
|
|
267
|
+
## Filters & Pipeline Steps
|
|
268
|
+
|
|
269
|
+
**File**: [`openedx_plugin_sample/pipeline.py`](./openedx_plugin_sample/pipeline.py)
|
|
270
|
+
**Official Docs**: [Using Open edX Filters](https://docs.openedx.org/projects/openedx-filters/en/latest/how-tos/using-filters.html)
|
|
271
|
+
|
|
272
|
+
### Filter Implementation
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from openedx_filters.filters import PipelineStep
|
|
276
|
+
|
|
277
|
+
class ChangeCourseAboutPageUrl(PipelineStep):
|
|
278
|
+
def run_filter(self, url, org, **kwargs):
|
|
279
|
+
# Extract course ID from URL
|
|
280
|
+
pattern = r'(?P<course_id>course-v1:[^/]+)'
|
|
281
|
+
match = re.search(pattern, url)
|
|
282
|
+
|
|
283
|
+
if match:
|
|
284
|
+
course_id = match.group('course_id')
|
|
285
|
+
new_url = f"https://example.com/new_about_page/{course_id}"
|
|
286
|
+
return {"url": new_url, "org": org}
|
|
287
|
+
|
|
288
|
+
# Return original data if no match
|
|
289
|
+
return {"url": url, "org": org}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Filter Requirements
|
|
293
|
+
|
|
294
|
+
**Essential Elements:**
|
|
295
|
+
- Inherit from `PipelineStep`
|
|
296
|
+
- Implement `run_filter()` method
|
|
297
|
+
- Return dictionary with same parameter names as input
|
|
298
|
+
- Handle all possible input scenarios
|
|
299
|
+
|
|
300
|
+
### Available Filters
|
|
301
|
+
|
|
302
|
+
**Filter Catalog**: [Open edX Filters Reference](https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters.html)
|
|
303
|
+
|
|
304
|
+
**Common Filters:**
|
|
305
|
+
- Course enrollment filters
|
|
306
|
+
- Authentication filters
|
|
307
|
+
- Certificate generation filters
|
|
308
|
+
- Course discovery filters
|
|
309
|
+
|
|
310
|
+
### Filter Registration
|
|
311
|
+
|
|
312
|
+
Filters must be registered in Django settings. This happens automatically via the plugin settings system (see [Settings Configuration](#settings-configuration)).
|
|
313
|
+
|
|
314
|
+
### Real-World Use Cases
|
|
315
|
+
|
|
316
|
+
- **URL Redirection**: Send users to custom course pages
|
|
317
|
+
- **Access Control**: Implement custom enrollment restrictions
|
|
318
|
+
- **Data Transformation**: Modify course data before display
|
|
319
|
+
- **Integration**: Add custom fields to API responses
|
|
320
|
+
|
|
321
|
+
## Settings Configuration
|
|
322
|
+
|
|
323
|
+
**Files**: [`openedx_plugin_sample/settings/`](./openedx_plugin_sample/settings/)
|
|
324
|
+
|
|
325
|
+
### Settings Structure
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# settings/common.py
|
|
329
|
+
def plugin_settings(settings):
|
|
330
|
+
"""Add plugin settings to main settings object."""
|
|
331
|
+
# Add your custom settings here
|
|
332
|
+
# settings.SAMPLE_PLUGIN_API_KEY = "your-key"
|
|
333
|
+
pass
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Environment-Specific Settings
|
|
337
|
+
|
|
338
|
+
- **`common.py`**: Settings for all environments
|
|
339
|
+
- **`production.py`**: Production-only settings
|
|
340
|
+
- **`test.py`**: Test-specific settings (faster database, etc.)
|
|
341
|
+
|
|
342
|
+
### Filter Registration via Settings
|
|
343
|
+
|
|
344
|
+
To register the URL filter, add to `common.py`:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
def plugin_settings(settings):
|
|
348
|
+
# Register the course about page URL filter
|
|
349
|
+
settings.OPEN_EDX_FILTERS_CONFIG = {
|
|
350
|
+
"org.openedx.learning.course.about.render.started.v1": {
|
|
351
|
+
"pipeline": [
|
|
352
|
+
"openedx_plugin_sample.pipeline.ChangeCourseAboutPageUrl"
|
|
353
|
+
],
|
|
354
|
+
"fail_silently": False,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Filter Name Discovery**: Filter names are found in the [official filters documentation](https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters.html).
|
|
360
|
+
|
|
361
|
+
### Plugin-Specific Settings
|
|
362
|
+
|
|
363
|
+
Add custom configuration:
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
def plugin_settings(settings):
|
|
367
|
+
# Plugin-specific settings
|
|
368
|
+
settings.SAMPLE_PLUGIN_ARCHIVE_RETENTION_DAYS = 365
|
|
369
|
+
settings.SAMPLE_PLUGIN_API_RATE_LIMIT = "60/minute"
|
|
370
|
+
settings.SAMPLE_PLUGIN_EXTERNAL_API_URL = "https://api.example.com"
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Development Setup
|
|
374
|
+
|
|
375
|
+
### Prerequisites
|
|
376
|
+
|
|
377
|
+
1. **Platform Setup**: [Open edX Development Guide](https://docs.openedx.org/en/latest/developers/how-tos/get-ready-for-python-dev.html)
|
|
378
|
+
2. **Python Environment**: Python 3.8+ with virtual environment
|
|
379
|
+
|
|
380
|
+
### Installation Methods
|
|
381
|
+
|
|
382
|
+
#### Option 1: With Tutor (Recommended)
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# Mount the backend plugin
|
|
386
|
+
tutor mounts add lms:$PWD:/openedx/sample-plugin-backend
|
|
387
|
+
|
|
388
|
+
# Launch and install
|
|
389
|
+
tutor dev launch
|
|
390
|
+
tutor dev exec lms pip install -e ../sample-plugin-backend
|
|
391
|
+
tutor dev exec lms python manage.py lms migrate
|
|
392
|
+
tutor dev restart lms
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
#### Option 2: Direct Installation
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
# In your edx-platform directory
|
|
399
|
+
pip install -e /path/to/sample-plugin/backend-plugin-sample
|
|
400
|
+
|
|
401
|
+
# Run migrations
|
|
402
|
+
python manage.py lms migrate
|
|
403
|
+
python manage.py cms migrate
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Verification Steps
|
|
407
|
+
|
|
408
|
+
1. **Check Installation**:
|
|
409
|
+
```bash
|
|
410
|
+
python manage.py lms shell
|
|
411
|
+
>>> from openedx_plugin_sample.models import CourseArchiveStatus
|
|
412
|
+
>>> print("Plugin installed successfully!")
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
2. **Test API**: Visit `http://localhost:18000/sample-plugin/api/v1/course-archive-status/`
|
|
416
|
+
|
|
417
|
+
3. **Check Admin**: Go to `http://localhost:18000/admin/` and look for "Course Archive Statuses"
|
|
418
|
+
|
|
419
|
+
## Testing Your Plugin
|
|
420
|
+
|
|
421
|
+
### Running Tests
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
cd backend-plugin-sample
|
|
425
|
+
|
|
426
|
+
# Install test dependencies
|
|
427
|
+
make requirements
|
|
428
|
+
|
|
429
|
+
# Run all tests
|
|
430
|
+
make test
|
|
431
|
+
|
|
432
|
+
# Run specific test
|
|
433
|
+
pytest tests/test_models.py::test_course_archive_status_creation
|
|
434
|
+
|
|
435
|
+
# Run with coverage
|
|
436
|
+
make test-coverage
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Test Structure
|
|
440
|
+
|
|
441
|
+
**Test Files:**
|
|
442
|
+
- [`tests/test_models.py`](./tests/test_models.py) - Model functionality
|
|
443
|
+
- [`tests/test_api.py`](./tests/test_api.py) - API endpoint testing
|
|
444
|
+
- [`tests/test_plugin_integration.py`](./tests/test_plugin_integration.py) - Plugin integration
|
|
445
|
+
|
|
446
|
+
### Writing Plugin Tests
|
|
447
|
+
|
|
448
|
+
**Model Testing Pattern:**
|
|
449
|
+
```python
|
|
450
|
+
from django.test import TestCase
|
|
451
|
+
from openedx_plugin_sample.models import CourseArchiveStatus
|
|
452
|
+
|
|
453
|
+
class TestCourseArchiveStatus(TestCase):
|
|
454
|
+
def test_create_archive_status(self):
|
|
455
|
+
# Test model creation and validation
|
|
456
|
+
pass
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
**API Testing Pattern:**
|
|
460
|
+
```python
|
|
461
|
+
from rest_framework.test import APITestCase
|
|
462
|
+
from django.contrib.auth import get_user_model
|
|
463
|
+
|
|
464
|
+
class TestCourseArchiveStatusAPI(APITestCase):
|
|
465
|
+
def setUp(self):
|
|
466
|
+
self.user = get_user_model().objects.create_user(username="testuser")
|
|
467
|
+
|
|
468
|
+
def test_list_archive_statuses(self):
|
|
469
|
+
# Test API endpoints
|
|
470
|
+
pass
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Quality Checks
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# Run linting and quality checks
|
|
477
|
+
make quality
|
|
478
|
+
|
|
479
|
+
# Individual tools
|
|
480
|
+
pylint openedx_plugin_sample/
|
|
481
|
+
isort --check-only openedx_plugin_sample/
|
|
482
|
+
black --check openedx_plugin_sample/
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Integration Examples
|
|
486
|
+
|
|
487
|
+
### Backend + Frontend Integration
|
|
488
|
+
|
|
489
|
+
**API Endpoint** (`views.py`):
|
|
490
|
+
```python
|
|
491
|
+
class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
|
|
492
|
+
# Provides data for frontend consumption
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Frontend Consumption** (see [`../frontend-plugin-sample/src/plugin.jsx`](../frontend-plugin-sample/src/plugin.jsx)):
|
|
496
|
+
```javascript
|
|
497
|
+
const response = await client.get(
|
|
498
|
+
`${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`
|
|
499
|
+
);
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Events + API Integration
|
|
503
|
+
|
|
504
|
+
```python
|
|
505
|
+
@receiver(COURSE_CATALOG_INFO_CHANGED)
|
|
506
|
+
def sync_course_archive_on_change(signal, sender, catalog_info, **kwargs):
|
|
507
|
+
# Update archive statuses when course info changes
|
|
508
|
+
CourseArchiveStatus.objects.filter(
|
|
509
|
+
course_id=catalog_info.course_key
|
|
510
|
+
).update(last_synced=timezone.now())
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Filters + Settings Integration
|
|
514
|
+
|
|
515
|
+
Settings configure filter behavior:
|
|
516
|
+
```python
|
|
517
|
+
# settings/common.py
|
|
518
|
+
def plugin_settings(settings):
|
|
519
|
+
settings.SAMPLE_PLUGIN_REDIRECT_DOMAIN = "custom-domain.com"
|
|
520
|
+
|
|
521
|
+
# pipeline.py - Uses setting
|
|
522
|
+
class ChangeCourseAboutPageUrl(PipelineStep):
|
|
523
|
+
def run_filter(self, url, org, **kwargs):
|
|
524
|
+
redirect_domain = getattr(settings, 'SAMPLE_PLUGIN_REDIRECT_DOMAIN', 'example.com')
|
|
525
|
+
new_url = f"https://{redirect_domain}/course/{course_id}"
|
|
526
|
+
return {"url": new_url, "org": org}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
## Adapting This Plugin
|
|
530
|
+
|
|
531
|
+
### For Your Use Case
|
|
532
|
+
|
|
533
|
+
1. **Models**: Modify [`models.py`](./openedx_plugin_sample/models.py) for your data structure
|
|
534
|
+
2. **APIs**: Update [`views.py`](./openedx_plugin_sample/views.py) and [`serializers.py`](./openedx_plugin_sample/serializers.py)
|
|
535
|
+
3. **Events**: Change event handlers in [`signals.py`](./openedx_plugin_sample/signals.py)
|
|
536
|
+
4. **Filters**: Implement your business logic in [`pipeline.py`](./openedx_plugin_sample/pipeline.py)
|
|
537
|
+
5. **Settings**: Configure plugin behavior in [`settings/`](./openedx_plugin_sample/settings/)
|
|
538
|
+
|
|
539
|
+
### Plugin Development Checklist
|
|
540
|
+
|
|
541
|
+
- [ ] Update `pyproject.toml` with your plugin name and dependencies
|
|
542
|
+
- [ ] Modify `apps.py` with your app configuration
|
|
543
|
+
- [ ] Design your models in `models.py`
|
|
544
|
+
- [ ] Create and run database migrations
|
|
545
|
+
- [ ] Implement API endpoints in `views.py`
|
|
546
|
+
- [ ] Add event handlers in `signals.py`
|
|
547
|
+
- [ ] Create filters in `pipeline.py`
|
|
548
|
+
- [ ] Configure settings in `settings/`
|
|
549
|
+
- [ ] Write comprehensive tests
|
|
550
|
+
- [ ] Update documentation
|
|
551
|
+
|
|
552
|
+
### Common Customization Patterns
|
|
553
|
+
|
|
554
|
+
**Adding New Models:**
|
|
555
|
+
```python
|
|
556
|
+
class YourModel(models.Model):
|
|
557
|
+
# Use Open edX field types when possible
|
|
558
|
+
course_id = CourseKeyField(max_length=255)
|
|
559
|
+
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
|
560
|
+
# ... your fields
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Adding New API Endpoints:**
|
|
564
|
+
```python
|
|
565
|
+
class YourViewSet(viewsets.ModelViewSet):
|
|
566
|
+
# Follow the permission patterns from CourseArchiveStatusViewSet
|
|
567
|
+
permission_classes = [IsOwnerOrStaffSuperuser]
|
|
568
|
+
# ... your implementation
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Adding New Event Handlers:**
|
|
572
|
+
```python
|
|
573
|
+
@receiver(YOUR_CHOSEN_EVENT)
|
|
574
|
+
def handle_your_event(signal, sender, event_data, **kwargs):
|
|
575
|
+
# Your business logic
|
|
576
|
+
pass
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
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.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
openedx_plugin_sample/__init__.py,sha256=E9fmKelIlLZ45iHYCtDbKUrTE3-52gChGfskhKc70TA,154
|
|
2
|
+
openedx_plugin_sample/apps.py,sha256=ofzrHW0mcbXDTt-nMan5AbA75OjTY_E8nrNk8gzkkzE,5600
|
|
3
|
+
openedx_plugin_sample/models.py,sha256=g9FUTFWFs_OsYCSHpfKfcL1-jnxI21KJw-5I8EOpQC8,2030
|
|
4
|
+
openedx_plugin_sample/pipeline.py,sha256=k68QYAE3ATGRkmV6U0CCbAPCEzhcoLxApfaK53tpJIM,6312
|
|
5
|
+
openedx_plugin_sample/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
openedx_plugin_sample/serializers.py,sha256=TMoqkKt5T1oFCr1K5dTKQshfz98STzVcAxxmqtXNIww,937
|
|
7
|
+
openedx_plugin_sample/signals.py,sha256=b-Bj5FIM_DW5u7zWuET7EwFev7_dOnMZdTKS50r2fTA,5487
|
|
8
|
+
openedx_plugin_sample/urls.py,sha256=SY9POVN6X1jPYfR_WmqJAGI3BCX5qvLTnS77-20pXag,517
|
|
9
|
+
openedx_plugin_sample/views.py,sha256=hktpWqQteZyUFyPUsyeD7dpZJpD8OaumS0PlybiRFC0,8162
|
|
10
|
+
openedx_plugin_sample/conf/locale/config.yaml,sha256=Rbk0_bjc9HRZTRQvzO58sKsdMIeP7EmhyeOO30l7dQ8,2281
|
|
11
|
+
openedx_plugin_sample/migrations/0001_initial.py,sha256=0rwgPsSGBhh6VrUZNXsCeNCfuovBim8_oTJ3d__PdQ4,2660
|
|
12
|
+
openedx_plugin_sample/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
openedx_plugin_sample/settings/common.py,sha256=4aC_6JKM1Q05RTtXxoJEZw1lAxhN52cBy52X41WCPnw,5226
|
|
14
|
+
openedx_plugin_sample/settings/production.py,sha256=R1_L0P9cbSjKxhbaezTkcd0dxZox2cEYhpE0v47D34U,364
|
|
15
|
+
openedx_plugin_sample/settings/test.py,sha256=oYi7SuwG6K56IXGtR8GUfrajua7m2_2_sfHrgYYLop0,353
|
|
16
|
+
openedx_plugin_sample/templates/openedx_plugin_sample/base.html,sha256=NHmMV45xJTnPEKQltuhe5Ddw9MDZLIQD_rK8GRXhqrU,873
|
|
17
|
+
openedx_plugin_sample-3.0.0.dist-info/licenses/LICENSE.txt,sha256=_kYizHmx1l2Y2boQzMunfQZ7A1T8r_mzfiD83ObbafY,10177
|
|
18
|
+
openedx_plugin_sample-3.0.0.dist-info/METADATA,sha256=AHymO8seYuVXQw1-Z2QjsFXQUYUD0fkTQVEQKKU8TmQ,19417
|
|
19
|
+
openedx_plugin_sample-3.0.0.dist-info/WHEEL,sha256=TdQ5LtNwLuxTCjgxN51AgdU5w-KkB9ttmLbzjTH02pg,109
|
|
20
|
+
openedx_plugin_sample-3.0.0.dist-info/entry_points.txt,sha256=-h7tq5JL02glSCuvKRpX3lMI9tO8s7_SS6vIE5fxLJU,173
|
|
21
|
+
openedx_plugin_sample-3.0.0.dist-info/top_level.txt,sha256=hRO3yIbde1yryAI7pdB_9dzMyQf138iKO0DBEjQFXUg,22
|
|
22
|
+
openedx_plugin_sample-3.0.0.dist-info/RECORD,,
|