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.
@@ -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,,
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1,5 @@
1
+ [cms.djangoapp]
2
+ openedx_plugin_sample = openedx_plugin_sample.apps:SamplePluginConfig
3
+
4
+ [lms.djangoapp]
5
+ openedx_plugin_sample = openedx_plugin_sample.apps:SamplePluginConfig