openedx-plugin-sample 3.1.0__tar.gz → 3.3.0__tar.gz
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-3.1.0 → openedx_plugin_sample-3.3.0}/.annotation_safe_list.yml +8 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/PKG-INFO +2 -1
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/pyproject.toml +1 -0
- openedx_plugin_sample-3.3.0/src/openedx_plugin_sample/admin.py +55 -0
- openedx_plugin_sample-3.3.0/src/openedx_plugin_sample/migrations/0001_initial.py +36 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/models.py +11 -6
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/pipeline.py +3 -2
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/serializers.py +22 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/signals.py +1 -1
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/views.py +41 -14
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample.egg-info/PKG-INFO +2 -1
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample.egg-info/SOURCES.txt +1 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample.egg-info/requires.txt +1 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/test_settings.py +2 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/tests/test_api.py +40 -16
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/tests/test_models.py +19 -15
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/tests/test_pipeline.py +14 -2
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/tests/test_signals.py +14 -2
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/uv.lock +354 -1
- openedx_plugin_sample-3.1.0/src/openedx_plugin_sample/migrations/0001_initial.py +0 -78
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/.coveragerc +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/.editorconfig +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/.gitignore +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/.pii_annotations.yml +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/.readthedocs.yaml +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/LICENSE.txt +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/Makefile +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/README.md +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/codecov.yml +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/Makefile +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/_static/theme_overrides.css +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/concepts/index.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/conf.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/decisions/0001-purpose-of-this-repo.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/decisions/README.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/decisions.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/getting_started.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/how-tos/index.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/index.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/internationalization.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/make.bat +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/openedx_plugin_sample.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/quickstarts/index.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/references/index.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/docs/testing.rst +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/manage.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/pylintrc +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/pylintrc_tweaks +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/setup.cfg +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/__init__.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/apps.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/conf/locale/config.yaml +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/migrations/__init__.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/py.typed +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/settings/common.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/settings/production.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/settings/test.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/templates/openedx_plugin_sample/base.html +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/urls.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample.egg-info/dependency_links.txt +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample.egg-info/entry_points.txt +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample.egg-info/top_level.txt +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/test_utils/__init__.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/tests/test_plugin_integration.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/tests/urls.py +0 -0
- {openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/tox.ini +0 -0
|
@@ -39,3 +39,11 @@ waffle.Sample:
|
|
|
39
39
|
".. no_pii:": "This model has no PII"
|
|
40
40
|
waffle.Switch:
|
|
41
41
|
".. no_pii:": "This model has no PII"
|
|
42
|
+
openedx_catalog.CatalogCourse:
|
|
43
|
+
".. no_pii:": "This model has no PII"
|
|
44
|
+
openedx_catalog.CourseRun:
|
|
45
|
+
".. no_pii:": "This model has no PII"
|
|
46
|
+
organizations.HistoricalOrganization:
|
|
47
|
+
".. no_pii:": "This model has no PII"
|
|
48
|
+
organizations.HistoricalOrganizationCourse:
|
|
49
|
+
".. no_pii:": "This model has no PII"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openedx-plugin-sample
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: A sample backend plugin for the Open edX Platform
|
|
5
5
|
Author-email: Open edX Project <oscm@openedx.org>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,6 +21,7 @@ Requires-Dist: Django
|
|
|
21
21
|
Requires-Dist: djangorestframework
|
|
22
22
|
Requires-Dist: django-filter
|
|
23
23
|
Requires-Dist: edx-opaque-keys
|
|
24
|
+
Requires-Dist: openedx-core
|
|
24
25
|
Requires-Dist: openedx-events
|
|
25
26
|
Requires-Dist: openedx-filters
|
|
26
27
|
Requires-Dist: openedx-atlas
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django admin configuration for openedx_plugin_sample.
|
|
3
|
+
|
|
4
|
+
This module demonstrates how to expose plugin models in the Django admin
|
|
5
|
+
site provided by Open edX (LMS and CMS each have their own admin under
|
|
6
|
+
``/admin/``). Defining a ``ModelAdmin`` for each model gives operators a
|
|
7
|
+
ready-made UI to inspect and manage plugin data without needing custom
|
|
8
|
+
tooling.
|
|
9
|
+
|
|
10
|
+
Django Documentation:
|
|
11
|
+
- ModelAdmin: https://docs.djangoproject.com/en/stable/ref/contrib/admin/
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from django.contrib import admin
|
|
15
|
+
|
|
16
|
+
from openedx_plugin_sample.models import CourseArchiveStatus
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@admin.register(CourseArchiveStatus)
|
|
20
|
+
class CourseArchiveStatusAdmin(admin.ModelAdmin):
|
|
21
|
+
"""
|
|
22
|
+
Admin configuration for the CourseArchiveStatus model.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
list_display = (
|
|
26
|
+
"course_key",
|
|
27
|
+
"user",
|
|
28
|
+
"is_archived",
|
|
29
|
+
"archive_date",
|
|
30
|
+
"updated_at",
|
|
31
|
+
)
|
|
32
|
+
list_filter = ("is_archived",)
|
|
33
|
+
# Search by the related CourseRun's course_key and the user's username/email.
|
|
34
|
+
search_fields = (
|
|
35
|
+
"course_run__course_key",
|
|
36
|
+
"user__username",
|
|
37
|
+
"user__email",
|
|
38
|
+
)
|
|
39
|
+
# FKs use raw id widgets (lookup popup) rather than a <select>, since the
|
|
40
|
+
# CourseRun and User tables can have many thousands of rows on a real
|
|
41
|
+
# Open edX deployment.
|
|
42
|
+
raw_id_fields = ("course_run", "user")
|
|
43
|
+
readonly_fields = ("created_at", "updated_at")
|
|
44
|
+
ordering = ("-updated_at",)
|
|
45
|
+
|
|
46
|
+
@admin.display(description="Course key", ordering="course_run__course_key")
|
|
47
|
+
def course_key(self, obj: CourseArchiveStatus) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Show the course's course_key string in list_display.
|
|
50
|
+
|
|
51
|
+
We never expose CourseRun's internal integer PK in the admin; the
|
|
52
|
+
course_key (e.g. "course-v1:edX+DemoX+Demo_Course") is the identifier
|
|
53
|
+
operators recognize.
|
|
54
|
+
"""
|
|
55
|
+
return str(obj.course_run.course_key)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Generated by Django 5.2.13 on 2026-05-14 01:13
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
('openedx_catalog', '0001_initial'),
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='CourseArchiveStatus',
|
|
20
|
+
fields=[
|
|
21
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
22
|
+
('is_archived', models.BooleanField(db_index=True, default=False, help_text='Whether the course is archived.')),
|
|
23
|
+
('archive_date', models.DateTimeField(blank=True, help_text='The date and time when the course was archived.', null=True)),
|
|
24
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
25
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
26
|
+
('course_run', models.ForeignKey(help_text='The course run that this archive status is for.', on_delete=django.db.models.deletion.CASCADE, related_name='archive_statuses', to='openedx_catalog.courserun')),
|
|
27
|
+
('user', models.ForeignKey(help_text='The user who this archive status is for.', on_delete=django.db.models.deletion.CASCADE, related_name='course_archive_statuses', to=settings.AUTH_USER_MODEL)),
|
|
28
|
+
],
|
|
29
|
+
options={
|
|
30
|
+
'verbose_name': 'Course Archive Status',
|
|
31
|
+
'verbose_name_plural': 'Course Archive Statuses',
|
|
32
|
+
'ordering': ['-updated_at'],
|
|
33
|
+
'constraints': [models.UniqueConstraint(fields=('course_run', 'user'), name='unique_user_course_archive_status')],
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
]
|
{openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/models.py
RENAMED
|
@@ -4,7 +4,7 @@ Database models for openedx_plugin_sample.
|
|
|
4
4
|
|
|
5
5
|
from django.contrib.auth import get_user_model
|
|
6
6
|
from django.db import models
|
|
7
|
-
from
|
|
7
|
+
from openedx_catalog.models import CourseRun
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class CourseArchiveStatus(models.Model):
|
|
@@ -16,8 +16,11 @@ class CourseArchiveStatus(models.Model):
|
|
|
16
16
|
.. no_pii: This model does not store PII directly, only references to users via foreign keys.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
course_run = models.ForeignKey(
|
|
20
|
+
CourseRun,
|
|
21
|
+
on_delete=models.CASCADE,
|
|
22
|
+
related_name="archive_statuses",
|
|
23
|
+
help_text="The course run that this archive status is for.",
|
|
21
24
|
)
|
|
22
25
|
|
|
23
26
|
user = models.ForeignKey(
|
|
@@ -47,7 +50,9 @@ class CourseArchiveStatus(models.Model):
|
|
|
47
50
|
Return a string representation of the course archive status.
|
|
48
51
|
"""
|
|
49
52
|
# pylint: disable=no-member
|
|
50
|
-
|
|
53
|
+
# Identify the course by its course_key string, never by the internal PK.
|
|
54
|
+
archived = "Archived" if self.is_archived else "Not Archived"
|
|
55
|
+
return f"{self.course_run.course_key} - {self.user.username} - {archived}"
|
|
51
56
|
|
|
52
57
|
class Meta:
|
|
53
58
|
"""
|
|
@@ -57,9 +62,9 @@ class CourseArchiveStatus(models.Model):
|
|
|
57
62
|
verbose_name = "Course Archive Status"
|
|
58
63
|
verbose_name_plural = "Course Archive Statuses"
|
|
59
64
|
ordering = ["-updated_at"]
|
|
60
|
-
# Ensure combination of
|
|
65
|
+
# Ensure combination of course_run and user is unique
|
|
61
66
|
constraints = [
|
|
62
67
|
models.UniqueConstraint(
|
|
63
|
-
fields=["
|
|
68
|
+
fields=["course_run", "user"], name="unique_user_course_archive_status"
|
|
64
69
|
)
|
|
65
70
|
]
|
{openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/pipeline.py
RENAMED
|
@@ -35,7 +35,7 @@ Common Use Cases:
|
|
|
35
35
|
- Data transformation and validation
|
|
36
36
|
- Integration with external systems
|
|
37
37
|
- Custom business logic implementation
|
|
38
|
-
"""
|
|
38
|
+
"""
|
|
39
39
|
|
|
40
40
|
import logging
|
|
41
41
|
|
|
@@ -78,7 +78,8 @@ class AddArchiveStatusToLearnerHomeCourseRun(PipelineStep):
|
|
|
78
78
|
return serialized_courserun
|
|
79
79
|
try:
|
|
80
80
|
is_archived_by_learner = CourseArchiveStatus.objects.get(
|
|
81
|
-
user=request.user,
|
|
81
|
+
user=request.user,
|
|
82
|
+
course_run__course_key=serialized_courserun["courseId"],
|
|
82
83
|
).is_archived
|
|
83
84
|
except CourseArchiveStatus.DoesNotExist:
|
|
84
85
|
is_archived_by_learner = False
|
{openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/serializers.py
RENAMED
|
@@ -3,6 +3,7 @@ Serializers for the openedx_plugin_sample app.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from django.contrib.auth import get_user_model
|
|
6
|
+
from openedx_catalog.models import CourseRun
|
|
6
7
|
from rest_framework import serializers
|
|
7
8
|
|
|
8
9
|
from openedx_plugin_sample.models import CourseArchiveStatus
|
|
@@ -21,6 +22,16 @@ class CourseArchiveStatusSerializer(serializers.ModelSerializer):
|
|
|
21
22
|
required=False,
|
|
22
23
|
)
|
|
23
24
|
|
|
25
|
+
# The model stores a FK to CourseRun, but APIs should identify courses by
|
|
26
|
+
# their full course key string (e.g. "course-v1:edX+DemoX+Demo_Course"),
|
|
27
|
+
# never by CourseRun's internal integer PK. The slug field looks up the
|
|
28
|
+
# related CourseRun by its `course_key` for both reads and writes.
|
|
29
|
+
course_id = serializers.SlugRelatedField(
|
|
30
|
+
source="course_run",
|
|
31
|
+
slug_field="course_key",
|
|
32
|
+
queryset=CourseRun.objects.all(),
|
|
33
|
+
)
|
|
34
|
+
|
|
24
35
|
class Meta:
|
|
25
36
|
"""
|
|
26
37
|
Meta class for CourseArchiveStatusSerializer.
|
|
@@ -37,3 +48,14 @@ class CourseArchiveStatusSerializer(serializers.ModelSerializer):
|
|
|
37
48
|
"updated_at",
|
|
38
49
|
]
|
|
39
50
|
read_only_fields = ["id", "created_at", "updated_at", "archive_date"]
|
|
51
|
+
|
|
52
|
+
def to_representation(self, instance):
|
|
53
|
+
"""
|
|
54
|
+
Serialize the instance, casting course_id to a string.
|
|
55
|
+
|
|
56
|
+
CourseRun.course_key returns a CourseLocator (not a string), which the
|
|
57
|
+
default JSON encoder can't serialize, so we coerce to str on output.
|
|
58
|
+
"""
|
|
59
|
+
data = super().to_representation(instance)
|
|
60
|
+
data["course_id"] = str(data["course_id"])
|
|
61
|
+
return data
|
{openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/signals.py
RENAMED
|
@@ -54,7 +54,7 @@ def unarchive_on_verified_upgrade(
|
|
|
54
54
|
|
|
55
55
|
updated = CourseArchiveStatus.objects.filter(
|
|
56
56
|
user_id=enrollment.user.id,
|
|
57
|
-
|
|
57
|
+
course_run__course_key=enrollment.course.course_key,
|
|
58
58
|
is_archived=True,
|
|
59
59
|
).update(is_archived=False, archive_date=None)
|
|
60
60
|
|
{openedx_plugin_sample-3.1.0 → openedx_plugin_sample-3.3.0}/src/openedx_plugin_sample/views.py
RENAMED
|
@@ -5,6 +5,7 @@ Views for the openedx_plugin_sample app.
|
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
7
|
from django.utils import timezone
|
|
8
|
+
from django_filters import rest_framework as django_filters
|
|
8
9
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
9
10
|
from opaque_keys import InvalidKeyError
|
|
10
11
|
from opaque_keys.edx.keys import CourseKey
|
|
@@ -64,6 +65,39 @@ class CourseArchiveStatusThrottle(UserRateThrottle):
|
|
|
64
65
|
rate = "60/minute"
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
class CourseArchiveStatusFilterSet(django_filters.FilterSet):
|
|
69
|
+
"""
|
|
70
|
+
FilterSet for CourseArchiveStatus.
|
|
71
|
+
|
|
72
|
+
The model stores a FK to CourseRun, but the public API filters and orders
|
|
73
|
+
by the course_key string (never by the internal CourseRun PK).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
# Map ?course_id=course-v1:... onto the FK's course_key column.
|
|
77
|
+
course_id = django_filters.CharFilter(field_name="course_run__course_key")
|
|
78
|
+
|
|
79
|
+
# Expose ?ordering=course_id (and other fields) without leaking the
|
|
80
|
+
# double-underscore FK lookup path.
|
|
81
|
+
ordering = django_filters.OrderingFilter(
|
|
82
|
+
fields=(
|
|
83
|
+
("course_run__course_key", "course_id"),
|
|
84
|
+
("user", "user"),
|
|
85
|
+
("is_archived", "is_archived"),
|
|
86
|
+
("archive_date", "archive_date"),
|
|
87
|
+
("created_at", "created_at"),
|
|
88
|
+
("updated_at", "updated_at"),
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
class Meta:
|
|
93
|
+
"""
|
|
94
|
+
FilterSet Meta options for CourseArchiveStatus.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
model = CourseArchiveStatus
|
|
98
|
+
fields = ["course_id", "user", "is_archived"]
|
|
99
|
+
|
|
100
|
+
|
|
67
101
|
class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
|
|
68
102
|
"""
|
|
69
103
|
API viewset for CourseArchiveStatus.
|
|
@@ -81,15 +115,7 @@ class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
|
|
|
81
115
|
CourseArchiveStatusThrottle,
|
|
82
116
|
]
|
|
83
117
|
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
|
|
84
|
-
|
|
85
|
-
ordering_fields = [
|
|
86
|
-
"course_id",
|
|
87
|
-
"user",
|
|
88
|
-
"is_archived",
|
|
89
|
-
"archive_date",
|
|
90
|
-
"created_at",
|
|
91
|
-
"updated_at",
|
|
92
|
-
]
|
|
118
|
+
filterset_class = CourseArchiveStatusFilterSet
|
|
93
119
|
ordering = ["-updated_at"]
|
|
94
120
|
|
|
95
121
|
def get_queryset(self):
|
|
@@ -104,8 +130,9 @@ class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
|
|
|
104
130
|
# Validate query parameters to prevent injection
|
|
105
131
|
self._validate_query_params()
|
|
106
132
|
|
|
107
|
-
# Always use select_related to avoid N+1 queries
|
|
108
|
-
|
|
133
|
+
# Always use select_related to avoid N+1 queries when accessing
|
|
134
|
+
# related user and course_run (for course_key) fields.
|
|
135
|
+
base_queryset = CourseArchiveStatus.objects.select_related("user", "course_run")
|
|
109
136
|
|
|
110
137
|
if user.is_staff or user.is_superuser:
|
|
111
138
|
return base_queryset
|
|
@@ -172,7 +199,7 @@ class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
|
|
|
172
199
|
# Log at debug level for normal operation
|
|
173
200
|
logger.debug(
|
|
174
201
|
"CourseArchiveStatus created: course_id=%s, user=%s, is_archived=%s",
|
|
175
|
-
instance.
|
|
202
|
+
instance.course_run.course_key,
|
|
176
203
|
instance.user.username,
|
|
177
204
|
instance.is_archived,
|
|
178
205
|
)
|
|
@@ -218,7 +245,7 @@ class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
|
|
|
218
245
|
# Log at debug level
|
|
219
246
|
logger.debug(
|
|
220
247
|
"CourseArchiveStatus updated: course_id=%s, user=%s, is_archived=%s",
|
|
221
|
-
updated_instance.
|
|
248
|
+
updated_instance.course_run.course_key,
|
|
222
249
|
updated_instance.user.username,
|
|
223
250
|
updated_instance.is_archived,
|
|
224
251
|
)
|
|
@@ -232,7 +259,7 @@ class CourseArchiveStatusViewSet(viewsets.ModelViewSet):
|
|
|
232
259
|
# Log at debug level before deletion
|
|
233
260
|
logger.debug(
|
|
234
261
|
"CourseArchiveStatus deleted: course_id=%s, user=%s, by=%s",
|
|
235
|
-
instance.
|
|
262
|
+
instance.course_run.course_key,
|
|
236
263
|
instance.user.username,
|
|
237
264
|
self.request.user.username,
|
|
238
265
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openedx-plugin-sample
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: A sample backend plugin for the Open edX Platform
|
|
5
5
|
Author-email: Open edX Project <oscm@openedx.org>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,6 +21,7 @@ Requires-Dist: Django
|
|
|
21
21
|
Requires-Dist: djangorestframework
|
|
22
22
|
Requires-Dist: django-filter
|
|
23
23
|
Requires-Dist: edx-opaque-keys
|
|
24
|
+
Requires-Dist: openedx-core
|
|
24
25
|
Requires-Dist: openedx-events
|
|
25
26
|
Requires-Dist: openedx-filters
|
|
26
27
|
Requires-Dist: openedx-atlas
|
|
@@ -33,6 +33,7 @@ docs/how-tos/index.rst
|
|
|
33
33
|
docs/quickstarts/index.rst
|
|
34
34
|
docs/references/index.rst
|
|
35
35
|
src/openedx_plugin_sample/__init__.py
|
|
36
|
+
src/openedx_plugin_sample/admin.py
|
|
36
37
|
src/openedx_plugin_sample/apps.py
|
|
37
38
|
src/openedx_plugin_sample/models.py
|
|
38
39
|
src/openedx_plugin_sample/pipeline.py
|
|
@@ -8,6 +8,7 @@ import pytest
|
|
|
8
8
|
from django.contrib.auth import get_user_model
|
|
9
9
|
from django.urls import reverse
|
|
10
10
|
from opaque_keys.edx.keys import CourseKey
|
|
11
|
+
from openedx_catalog.api import create_course_run_for_modulestore_course_with
|
|
11
12
|
from rest_framework import status
|
|
12
13
|
from rest_framework.test import APIClient
|
|
13
14
|
|
|
@@ -70,12 +71,24 @@ def course_key():
|
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
@pytest.fixture
|
|
73
|
-
def
|
|
74
|
+
def course_run(course_key):
|
|
75
|
+
"""
|
|
76
|
+
Create and return a test CourseRun (plus its Organization and CatalogCourse)
|
|
77
|
+
matching the `course_key` fixture, so that API requests that POST/PATCH the
|
|
78
|
+
public `course_id` string can resolve it to this CourseRun.
|
|
79
|
+
"""
|
|
80
|
+
return create_course_run_for_modulestore_course_with(
|
|
81
|
+
course_key, title="Demo Course"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.fixture
|
|
86
|
+
def course_archive_status(user, course_run):
|
|
74
87
|
"""
|
|
75
88
|
Create and return a test course archive status.
|
|
76
89
|
"""
|
|
77
90
|
return CourseArchiveStatus.objects.create(
|
|
78
|
-
|
|
91
|
+
course_run=course_run, user=user, is_archived=False
|
|
79
92
|
)
|
|
80
93
|
|
|
81
94
|
|
|
@@ -93,7 +106,7 @@ def test_list_course_archive_status_authenticated(
|
|
|
93
106
|
assert response.status_code == status.HTTP_200_OK
|
|
94
107
|
assert response.data["count"] == 1
|
|
95
108
|
assert response.data["results"][0]["course_id"] == str(
|
|
96
|
-
course_archive_status.
|
|
109
|
+
course_archive_status.course_run.course_key
|
|
97
110
|
)
|
|
98
111
|
assert response.data["results"][0]["user"] == user.id
|
|
99
112
|
assert (
|
|
@@ -114,17 +127,23 @@ def test_list_course_archive_status_unauthenticated(api_client):
|
|
|
114
127
|
|
|
115
128
|
@pytest.mark.django_db
|
|
116
129
|
def test_list_course_archive_status_staff_can_see_all(
|
|
117
|
-
api_client, staff_user, user, another_user,
|
|
130
|
+
api_client, staff_user, user, another_user, course_run
|
|
118
131
|
):
|
|
119
132
|
"""
|
|
120
133
|
Test that a staff user can list all course archive statuses.
|
|
121
134
|
"""
|
|
135
|
+
# A second CourseRun (with its own CatalogCourse/Organization) for the second status.
|
|
136
|
+
course_run_2 = create_course_run_for_modulestore_course_with(
|
|
137
|
+
CourseKey.from_string("course-v1:edX+DemoX+Demo_Course2"),
|
|
138
|
+
title="Demo Course 2",
|
|
139
|
+
)
|
|
140
|
+
|
|
122
141
|
# Create archive statuses for both users
|
|
123
142
|
CourseArchiveStatus.objects.create(
|
|
124
|
-
|
|
143
|
+
course_run=course_run, user=user, is_archived=False
|
|
125
144
|
)
|
|
126
145
|
CourseArchiveStatus.objects.create(
|
|
127
|
-
|
|
146
|
+
course_run=course_run_2,
|
|
128
147
|
user=another_user,
|
|
129
148
|
is_archived=True,
|
|
130
149
|
)
|
|
@@ -138,7 +157,7 @@ def test_list_course_archive_status_staff_can_see_all(
|
|
|
138
157
|
|
|
139
158
|
|
|
140
159
|
@pytest.mark.django_db
|
|
141
|
-
def test_create_course_archive_status(api_client, user, course_key):
|
|
160
|
+
def test_create_course_archive_status(api_client, user, course_key, course_run):
|
|
142
161
|
"""
|
|
143
162
|
Test that a user can create a course archive status.
|
|
144
163
|
"""
|
|
@@ -159,13 +178,14 @@ def test_create_course_archive_status(api_client, user, course_key):
|
|
|
159
178
|
|
|
160
179
|
# Verify in database
|
|
161
180
|
course_archive_status = CourseArchiveStatus.objects.get(
|
|
162
|
-
|
|
181
|
+
course_run=course_run, user=user
|
|
163
182
|
)
|
|
164
183
|
assert course_archive_status.is_archived is True
|
|
165
184
|
assert course_archive_status.archive_date is not None
|
|
166
185
|
|
|
167
186
|
|
|
168
187
|
@pytest.mark.django_db
|
|
188
|
+
@pytest.mark.usefixtures("course_run")
|
|
169
189
|
def test_create_course_archive_status_for_another_user(
|
|
170
190
|
api_client, user, another_user, course_key
|
|
171
191
|
):
|
|
@@ -185,6 +205,7 @@ def test_create_course_archive_status_for_another_user(
|
|
|
185
205
|
|
|
186
206
|
|
|
187
207
|
@pytest.mark.django_db
|
|
208
|
+
@pytest.mark.usefixtures("course_run")
|
|
188
209
|
def test_staff_create_course_archive_status_for_another_user(
|
|
189
210
|
api_client, staff_user, user, course_key
|
|
190
211
|
):
|
|
@@ -281,7 +302,9 @@ def test_staff_can_update_other_user_course_archive_status(
|
|
|
281
302
|
|
|
282
303
|
# New tests for optional user field behavior
|
|
283
304
|
@pytest.mark.django_db
|
|
284
|
-
def test_create_course_archive_status_without_user_field(
|
|
305
|
+
def test_create_course_archive_status_without_user_field(
|
|
306
|
+
api_client, user, course_key, course_run
|
|
307
|
+
):
|
|
285
308
|
"""
|
|
286
309
|
Test that a user can create a course archive status without specifying user field.
|
|
287
310
|
The user field should default to the current user.
|
|
@@ -307,7 +330,7 @@ def test_create_course_archive_status_without_user_field(api_client, user, cours
|
|
|
307
330
|
|
|
308
331
|
# Verify in database
|
|
309
332
|
course_archive_status = CourseArchiveStatus.objects.get(
|
|
310
|
-
|
|
333
|
+
course_run=course_run, user=user
|
|
311
334
|
)
|
|
312
335
|
assert course_archive_status.is_archived is True
|
|
313
336
|
assert course_archive_status.user == user
|
|
@@ -340,7 +363,7 @@ def test_update_course_archive_status_without_user_field(api_client, user, cours
|
|
|
340
363
|
|
|
341
364
|
@pytest.mark.django_db
|
|
342
365
|
def test_staff_create_with_explicit_user_override(
|
|
343
|
-
api_client, staff_user, user, course_key
|
|
366
|
+
api_client, staff_user, user, course_key, course_run
|
|
344
367
|
):
|
|
345
368
|
"""
|
|
346
369
|
Test that staff can explicitly set user field to override default behavior.
|
|
@@ -361,7 +384,7 @@ def test_staff_create_with_explicit_user_override(
|
|
|
361
384
|
|
|
362
385
|
# Verify in database
|
|
363
386
|
course_archive_status = CourseArchiveStatus.objects.get(
|
|
364
|
-
|
|
387
|
+
course_run=course_run, user=user
|
|
365
388
|
)
|
|
366
389
|
assert course_archive_status.user == user
|
|
367
390
|
assert course_archive_status.user != staff_user
|
|
@@ -369,14 +392,14 @@ def test_staff_create_with_explicit_user_override(
|
|
|
369
392
|
|
|
370
393
|
@pytest.mark.django_db
|
|
371
394
|
def test_staff_update_with_explicit_user_override(
|
|
372
|
-
api_client, staff_user, user, another_user,
|
|
395
|
+
api_client, staff_user, user, another_user, course_run
|
|
373
396
|
):
|
|
374
397
|
"""
|
|
375
398
|
Test that staff can explicitly change user field when updating.
|
|
376
399
|
"""
|
|
377
400
|
# Create initial record for user
|
|
378
401
|
initial_status = CourseArchiveStatus.objects.create(
|
|
379
|
-
|
|
402
|
+
course_run=course_run, user=user, is_archived=False
|
|
380
403
|
)
|
|
381
404
|
|
|
382
405
|
api_client.force_authenticate(user=staff_user)
|
|
@@ -400,6 +423,7 @@ def test_staff_update_with_explicit_user_override(
|
|
|
400
423
|
|
|
401
424
|
|
|
402
425
|
@pytest.mark.django_db
|
|
426
|
+
@pytest.mark.usefixtures("course_run")
|
|
403
427
|
def test_regular_user_cannot_override_user_field_create(
|
|
404
428
|
api_client, user, another_user, course_key
|
|
405
429
|
):
|
|
@@ -420,7 +444,7 @@ def test_regular_user_cannot_override_user_field_create(
|
|
|
420
444
|
|
|
421
445
|
@pytest.mark.django_db
|
|
422
446
|
def test_staff_create_without_user_field_defaults_to_current_user(
|
|
423
|
-
api_client, staff_user, course_key
|
|
447
|
+
api_client, staff_user, course_key, course_run
|
|
424
448
|
):
|
|
425
449
|
"""
|
|
426
450
|
Test that even staff users get records created for themselves when no user specified.
|
|
@@ -441,6 +465,6 @@ def test_staff_create_without_user_field_defaults_to_current_user(
|
|
|
441
465
|
|
|
442
466
|
# Verify in database
|
|
443
467
|
course_archive_status = CourseArchiveStatus.objects.get(
|
|
444
|
-
|
|
468
|
+
course_run=course_run, user=staff_user
|
|
445
469
|
)
|
|
446
470
|
assert course_archive_status.user == staff_user
|
|
@@ -8,6 +8,7 @@ import pytest
|
|
|
8
8
|
from django.contrib.auth import get_user_model
|
|
9
9
|
from django.db.utils import IntegrityError
|
|
10
10
|
from opaque_keys.edx.keys import CourseKey
|
|
11
|
+
from openedx_catalog.api import create_course_run_for_modulestore_course_with
|
|
11
12
|
|
|
12
13
|
from openedx_plugin_sample.models import CourseArchiveStatus
|
|
13
14
|
|
|
@@ -38,24 +39,27 @@ def staff_user():
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
@pytest.fixture
|
|
41
|
-
def
|
|
42
|
+
def course_run():
|
|
42
43
|
"""
|
|
43
|
-
Create and return a test
|
|
44
|
+
Create and return a test CourseRun (plus its Organization and CatalogCourse).
|
|
44
45
|
"""
|
|
45
|
-
return
|
|
46
|
+
return create_course_run_for_modulestore_course_with(
|
|
47
|
+
CourseKey.from_string("course-v1:edX+DemoX+Demo_Course"),
|
|
48
|
+
title="Demo Course",
|
|
49
|
+
)
|
|
46
50
|
|
|
47
51
|
|
|
48
52
|
@pytest.mark.django_db
|
|
49
|
-
def test_course_archive_status_creation(user,
|
|
53
|
+
def test_course_archive_status_creation(user, course_run):
|
|
50
54
|
"""
|
|
51
55
|
Test that a CourseArchiveStatus can be created with valid data.
|
|
52
56
|
"""
|
|
53
57
|
course_archive_status = CourseArchiveStatus.objects.create(
|
|
54
|
-
|
|
58
|
+
course_run=course_run, user=user, is_archived=False
|
|
55
59
|
)
|
|
56
60
|
|
|
57
61
|
assert course_archive_status.pk is not None
|
|
58
|
-
assert course_archive_status.
|
|
62
|
+
assert course_archive_status.course_run == course_run
|
|
59
63
|
assert course_archive_status.user == user
|
|
60
64
|
assert course_archive_status.is_archived is False
|
|
61
65
|
assert course_archive_status.archive_date is None
|
|
@@ -64,35 +68,35 @@ def test_course_archive_status_creation(user, course_key):
|
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
@pytest.mark.django_db
|
|
67
|
-
def test_course_archive_status_uniqueness(user,
|
|
71
|
+
def test_course_archive_status_uniqueness(user, course_run):
|
|
68
72
|
"""
|
|
69
|
-
Test that a CourseArchiveStatus must be unique per user and
|
|
73
|
+
Test that a CourseArchiveStatus must be unique per user and course_run.
|
|
70
74
|
"""
|
|
71
75
|
CourseArchiveStatus.objects.create(
|
|
72
|
-
|
|
76
|
+
course_run=course_run, user=user, is_archived=False
|
|
73
77
|
)
|
|
74
78
|
|
|
75
|
-
# Creating another with same user and
|
|
79
|
+
# Creating another with same user and course_run should raise an IntegrityError
|
|
76
80
|
with pytest.raises(IntegrityError):
|
|
77
81
|
CourseArchiveStatus.objects.create(
|
|
78
|
-
|
|
82
|
+
course_run=course_run, user=user, is_archived=True
|
|
79
83
|
)
|
|
80
84
|
|
|
81
85
|
|
|
82
86
|
@pytest.mark.django_db
|
|
83
|
-
def test_course_archive_status_str_method(user,
|
|
87
|
+
def test_course_archive_status_str_method(user, course_run):
|
|
84
88
|
"""
|
|
85
89
|
Test the string representation of CourseArchiveStatus.
|
|
86
90
|
"""
|
|
87
91
|
course_archive_status = CourseArchiveStatus.objects.create(
|
|
88
|
-
|
|
92
|
+
course_run=course_run, user=user, is_archived=True
|
|
89
93
|
)
|
|
90
94
|
|
|
91
|
-
expected_str = f"{course_key} - {user.username} - Archived"
|
|
95
|
+
expected_str = f"{course_run.course_key} - {user.username} - Archived"
|
|
92
96
|
assert str(course_archive_status) == expected_str
|
|
93
97
|
|
|
94
98
|
course_archive_status.is_archived = False
|
|
95
99
|
course_archive_status.save()
|
|
96
100
|
|
|
97
|
-
expected_str = f"{course_key} - {user.username} - Not Archived"
|
|
101
|
+
expected_str = f"{course_run.course_key} - {user.username} - Not Archived"
|
|
98
102
|
assert str(course_archive_status) == expected_str
|