From 48aea534ad4f42738c891671f1b3ba0c6afc4fad Mon Sep 17 00:00:00 2001 From: Kyle D McCormick Date: Wed, 13 May 2026 21:52:51 -0400 Subject: [PATCH 1/2] refactor: CourseArchiveStatus references CourseRun via FK Replace the CourseKeyField on CourseArchiveStatus with a ForeignKey to openedx_catalog.CourseRun, following the new "use a FK to CourseRun for any model-level course reference" guidance from openedx-core. The public API still identifies courses by their course_key string (never by CourseRun's internal PK), so frontend consumers and the JSON contract are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend-plugin-sample/pyproject.toml | 1 + .../migrations/0001_initial.py | 72 +--- .../src/openedx_plugin_sample/models.py | 17 +- .../src/openedx_plugin_sample/pipeline.py | 5 +- .../src/openedx_plugin_sample/serializers.py | 22 ++ .../src/openedx_plugin_sample/signals.py | 2 +- .../src/openedx_plugin_sample/views.py | 55 ++- backend-plugin-sample/test_settings.py | 2 + backend-plugin-sample/tests/test_api.py | 56 ++- backend-plugin-sample/tests/test_models.py | 34 +- backend-plugin-sample/tests/test_pipeline.py | 16 +- backend-plugin-sample/tests/test_signals.py | 16 +- backend-plugin-sample/uv.lock | 355 +++++++++++++++++- 13 files changed, 537 insertions(+), 116 deletions(-) diff --git a/backend-plugin-sample/pyproject.toml b/backend-plugin-sample/pyproject.toml index 9595618..7bd7a6b 100644 --- a/backend-plugin-sample/pyproject.toml +++ b/backend-plugin-sample/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "djangorestframework", "django-filter", "edx-opaque-keys", + "openedx-core", "openedx-events", "openedx-filters", "openedx-atlas", diff --git a/backend-plugin-sample/src/openedx_plugin_sample/migrations/0001_initial.py b/backend-plugin-sample/src/openedx_plugin_sample/migrations/0001_initial.py index 79a97d3..2f4d98d 100644 --- a/backend-plugin-sample/src/openedx_plugin_sample/migrations/0001_initial.py +++ b/backend-plugin-sample/src/openedx_plugin_sample/migrations/0001_initial.py @@ -1,9 +1,8 @@ -# Generated by Django 4.2.20 on 2025-04-14 12:39 +# Generated by Django 5.2.13 on 2026-05-14 01:13 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import opaque_keys.edx.django.models class Migration(migrations.Migration): @@ -11,68 +10,27 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('openedx_catalog', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name="CourseArchiveStatus", + name='CourseArchiveStatus', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "course_id", - opaque_keys.edx.django.models.CourseKeyField( - db_index=True, - help_text="The unique identifier for the course.", - max_length=255, - ), - ), - ( - "is_archived", - models.BooleanField( - db_index=True, - default=False, - help_text="Whether the course is archived.", - ), - ), - ( - "archive_date", - models.DateTimeField( - blank=True, - help_text="The date and time when the course was archived.", - null=True, - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "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, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_archived', models.BooleanField(db_index=True, default=False, help_text='Whether the course is archived.')), + ('archive_date', models.DateTimeField(blank=True, help_text='The date and time when the course was archived.', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('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')), + ('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)), ], options={ - "verbose_name": "Course Archive Status", - "verbose_name_plural": "Course Archive Statuses", - "ordering": ["-updated_at"], + 'verbose_name': 'Course Archive Status', + 'verbose_name_plural': 'Course Archive Statuses', + 'ordering': ['-updated_at'], + 'constraints': [models.UniqueConstraint(fields=('course_run', 'user'), name='unique_user_course_archive_status')], }, ), - migrations.AddConstraint( - model_name="coursearchivestatus", - constraint=models.UniqueConstraint( - fields=("course_id", "user"), name="unique_user_course_archive_status" - ), - ), ] diff --git a/backend-plugin-sample/src/openedx_plugin_sample/models.py b/backend-plugin-sample/src/openedx_plugin_sample/models.py index 5e80e25..079e4b4 100644 --- a/backend-plugin-sample/src/openedx_plugin_sample/models.py +++ b/backend-plugin-sample/src/openedx_plugin_sample/models.py @@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model from django.db import models -from opaque_keys.edx.django.models import CourseKeyField +from openedx_catalog.models import CourseRun class CourseArchiveStatus(models.Model): @@ -16,8 +16,11 @@ class CourseArchiveStatus(models.Model): .. no_pii: This model does not store PII directly, only references to users via foreign keys. """ - course_id = CourseKeyField( - max_length=255, db_index=True, help_text="The unique identifier for the course." + course_run = models.ForeignKey( + CourseRun, + on_delete=models.CASCADE, + related_name="archive_statuses", + help_text="The course run that this archive status is for.", ) user = models.ForeignKey( @@ -47,7 +50,9 @@ def __str__(self): Return a string representation of the course archive status. """ # pylint: disable=no-member - return f"{self.course_id} - {self.user.username} - {'Archived' if self.is_archived else 'Not Archived'}" + # Identify the course by its course_key string, never by the internal PK. + archived = "Archived" if self.is_archived else "Not Archived" + return f"{self.course_run.course_key} - {self.user.username} - {archived}" class Meta: """ @@ -57,9 +62,9 @@ class Meta: verbose_name = "Course Archive Status" verbose_name_plural = "Course Archive Statuses" ordering = ["-updated_at"] - # Ensure combination of course_id and user is unique + # Ensure combination of course_run and user is unique constraints = [ models.UniqueConstraint( - fields=["course_id", "user"], name="unique_user_course_archive_status" + fields=["course_run", "user"], name="unique_user_course_archive_status" ) ] diff --git a/backend-plugin-sample/src/openedx_plugin_sample/pipeline.py b/backend-plugin-sample/src/openedx_plugin_sample/pipeline.py index 91037f7..9068540 100644 --- a/backend-plugin-sample/src/openedx_plugin_sample/pipeline.py +++ b/backend-plugin-sample/src/openedx_plugin_sample/pipeline.py @@ -35,7 +35,7 @@ - Data transformation and validation - Integration with external systems - Custom business logic implementation -""" # pylint: disable=line-too-long +""" import logging @@ -78,7 +78,8 @@ def run_filter(self, serialized_courserun, **kwargs): # pylint: disable=argumen return serialized_courserun try: is_archived_by_learner = CourseArchiveStatus.objects.get( - user=request.user, course_id=serialized_courserun["courseId"] + user=request.user, + course_run__course_key=serialized_courserun["courseId"], ).is_archived except CourseArchiveStatus.DoesNotExist: is_archived_by_learner = False diff --git a/backend-plugin-sample/src/openedx_plugin_sample/serializers.py b/backend-plugin-sample/src/openedx_plugin_sample/serializers.py index ea98181..d7abde9 100644 --- a/backend-plugin-sample/src/openedx_plugin_sample/serializers.py +++ b/backend-plugin-sample/src/openedx_plugin_sample/serializers.py @@ -3,6 +3,7 @@ """ from django.contrib.auth import get_user_model +from openedx_catalog.models import CourseRun from rest_framework import serializers from openedx_plugin_sample.models import CourseArchiveStatus @@ -21,6 +22,16 @@ class CourseArchiveStatusSerializer(serializers.ModelSerializer): required=False, ) + # The model stores a FK to CourseRun, but APIs should identify courses by + # their full course key string (e.g. "course-v1:edX+DemoX+Demo_Course"), + # never by CourseRun's internal integer PK. The slug field looks up the + # related CourseRun by its `course_key` for both reads and writes. + course_id = serializers.SlugRelatedField( + source="course_run", + slug_field="course_key", + queryset=CourseRun.objects.all(), + ) + class Meta: """ Meta class for CourseArchiveStatusSerializer. @@ -37,3 +48,14 @@ class Meta: "updated_at", ] read_only_fields = ["id", "created_at", "updated_at", "archive_date"] + + def to_representation(self, instance): + """ + Serialize the instance, casting course_id to a string. + + CourseRun.course_key returns a CourseLocator (not a string), which the + default JSON encoder can't serialize, so we coerce to str on output. + """ + data = super().to_representation(instance) + data["course_id"] = str(data["course_id"]) + return data diff --git a/backend-plugin-sample/src/openedx_plugin_sample/signals.py b/backend-plugin-sample/src/openedx_plugin_sample/signals.py index c17ebde..d8ae87e 100644 --- a/backend-plugin-sample/src/openedx_plugin_sample/signals.py +++ b/backend-plugin-sample/src/openedx_plugin_sample/signals.py @@ -54,7 +54,7 @@ def unarchive_on_verified_upgrade( updated = CourseArchiveStatus.objects.filter( user_id=enrollment.user.id, - course_id=enrollment.course.course_key, + course_run__course_key=enrollment.course.course_key, is_archived=True, ).update(is_archived=False, archive_date=None) diff --git a/backend-plugin-sample/src/openedx_plugin_sample/views.py b/backend-plugin-sample/src/openedx_plugin_sample/views.py index d08eaaa..3e59c17 100644 --- a/backend-plugin-sample/src/openedx_plugin_sample/views.py +++ b/backend-plugin-sample/src/openedx_plugin_sample/views.py @@ -5,6 +5,7 @@ import logging from django.utils import timezone +from django_filters import rest_framework as django_filters from django_filters.rest_framework import DjangoFilterBackend from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey @@ -64,6 +65,39 @@ class CourseArchiveStatusThrottle(UserRateThrottle): rate = "60/minute" +class CourseArchiveStatusFilterSet(django_filters.FilterSet): + """ + FilterSet for CourseArchiveStatus. + + The model stores a FK to CourseRun, but the public API filters and orders + by the course_key string (never by the internal CourseRun PK). + """ + + # Map ?course_id=course-v1:... onto the FK's course_key column. + course_id = django_filters.CharFilter(field_name="course_run__course_key") + + # Expose ?ordering=course_id (and other fields) without leaking the + # double-underscore FK lookup path. + ordering = django_filters.OrderingFilter( + fields=( + ("course_run__course_key", "course_id"), + ("user", "user"), + ("is_archived", "is_archived"), + ("archive_date", "archive_date"), + ("created_at", "created_at"), + ("updated_at", "updated_at"), + ) + ) + + class Meta: + """ + FilterSet Meta options for CourseArchiveStatus. + """ + + model = CourseArchiveStatus + fields = ["course_id", "user", "is_archived"] + + class CourseArchiveStatusViewSet(viewsets.ModelViewSet): """ API viewset for CourseArchiveStatus. @@ -81,15 +115,7 @@ class CourseArchiveStatusViewSet(viewsets.ModelViewSet): CourseArchiveStatusThrottle, ] filter_backends = [DjangoFilterBackend, filters.OrderingFilter] - filterset_fields = ["course_id", "user", "is_archived"] - ordering_fields = [ - "course_id", - "user", - "is_archived", - "archive_date", - "created_at", - "updated_at", - ] + filterset_class = CourseArchiveStatusFilterSet ordering = ["-updated_at"] def get_queryset(self): @@ -104,8 +130,9 @@ def get_queryset(self): # Validate query parameters to prevent injection self._validate_query_params() - # Always use select_related to avoid N+1 queries - base_queryset = CourseArchiveStatus.objects.select_related("user") + # Always use select_related to avoid N+1 queries when accessing + # related user and course_run (for course_key) fields. + base_queryset = CourseArchiveStatus.objects.select_related("user", "course_run") if user.is_staff or user.is_superuser: return base_queryset @@ -172,7 +199,7 @@ def perform_create(self, serializer): # Log at debug level for normal operation logger.debug( "CourseArchiveStatus created: course_id=%s, user=%s, is_archived=%s", - instance.course_id, + instance.course_run.course_key, instance.user.username, instance.is_archived, ) @@ -218,7 +245,7 @@ def perform_update(self, serializer): # Log at debug level logger.debug( "CourseArchiveStatus updated: course_id=%s, user=%s, is_archived=%s", - updated_instance.course_id, + updated_instance.course_run.course_key, updated_instance.user.username, updated_instance.is_archived, ) @@ -232,7 +259,7 @@ def perform_destroy(self, instance): # Log at debug level before deletion logger.debug( "CourseArchiveStatus deleted: course_id=%s, user=%s, by=%s", - instance.course_id, + instance.course_run.course_key, instance.user.username, self.request.user.username, ) diff --git a/backend-plugin-sample/test_settings.py b/backend-plugin-sample/test_settings.py index c556490..04ecda1 100644 --- a/backend-plugin-sample/test_settings.py +++ b/backend-plugin-sample/test_settings.py @@ -47,6 +47,8 @@ def root(*args): "django_filters", "edx_django_utils.plugins", "django_extensions", + "organizations", + "openedx_catalog", ] # Dynamically add plugin apps - only using the LMS context for simplicity diff --git a/backend-plugin-sample/tests/test_api.py b/backend-plugin-sample/tests/test_api.py index bc23abd..d0d647c 100644 --- a/backend-plugin-sample/tests/test_api.py +++ b/backend-plugin-sample/tests/test_api.py @@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model from django.urls import reverse from opaque_keys.edx.keys import CourseKey +from openedx_catalog.api import create_course_run_for_modulestore_course_with from rest_framework import status from rest_framework.test import APIClient @@ -70,12 +71,24 @@ def course_key(): @pytest.fixture -def course_archive_status(user, course_key): +def course_run(course_key): + """ + Create and return a test CourseRun (plus its Organization and CatalogCourse) + matching the `course_key` fixture, so that API requests that POST/PATCH the + public `course_id` string can resolve it to this CourseRun. + """ + return create_course_run_for_modulestore_course_with( + course_key, title="Demo Course" + ) + + +@pytest.fixture +def course_archive_status(user, course_run): """ Create and return a test course archive status. """ return CourseArchiveStatus.objects.create( - course_id=course_key, user=user, is_archived=False + course_run=course_run, user=user, is_archived=False ) @@ -93,7 +106,7 @@ def test_list_course_archive_status_authenticated( assert response.status_code == status.HTTP_200_OK assert response.data["count"] == 1 assert response.data["results"][0]["course_id"] == str( - course_archive_status.course_id + course_archive_status.course_run.course_key ) assert response.data["results"][0]["user"] == user.id assert ( @@ -114,17 +127,23 @@ def test_list_course_archive_status_unauthenticated(api_client): @pytest.mark.django_db def test_list_course_archive_status_staff_can_see_all( - api_client, staff_user, user, another_user, course_key + api_client, staff_user, user, another_user, course_run ): """ Test that a staff user can list all course archive statuses. """ + # A second CourseRun (with its own CatalogCourse/Organization) for the second status. + course_run_2 = create_course_run_for_modulestore_course_with( + CourseKey.from_string("course-v1:edX+DemoX+Demo_Course2"), + title="Demo Course 2", + ) + # Create archive statuses for both users CourseArchiveStatus.objects.create( - course_id=course_key, user=user, is_archived=False + course_run=course_run, user=user, is_archived=False ) CourseArchiveStatus.objects.create( - course_id=CourseKey.from_string("course-v1:edX+DemoX+Demo_Course2"), + course_run=course_run_2, user=another_user, is_archived=True, ) @@ -138,7 +157,7 @@ def test_list_course_archive_status_staff_can_see_all( @pytest.mark.django_db -def test_create_course_archive_status(api_client, user, course_key): +def test_create_course_archive_status(api_client, user, course_key, course_run): """ Test that a user can create a course archive status. """ @@ -159,13 +178,14 @@ def test_create_course_archive_status(api_client, user, course_key): # Verify in database course_archive_status = CourseArchiveStatus.objects.get( - course_id=course_key, user=user + course_run=course_run, user=user ) assert course_archive_status.is_archived is True assert course_archive_status.archive_date is not None @pytest.mark.django_db +@pytest.mark.usefixtures("course_run") def test_create_course_archive_status_for_another_user( api_client, user, another_user, course_key ): @@ -185,6 +205,7 @@ def test_create_course_archive_status_for_another_user( @pytest.mark.django_db +@pytest.mark.usefixtures("course_run") def test_staff_create_course_archive_status_for_another_user( api_client, staff_user, user, course_key ): @@ -281,7 +302,9 @@ def test_staff_can_update_other_user_course_archive_status( # New tests for optional user field behavior @pytest.mark.django_db -def test_create_course_archive_status_without_user_field(api_client, user, course_key): +def test_create_course_archive_status_without_user_field( + api_client, user, course_key, course_run +): """ Test that a user can create a course archive status without specifying user field. 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 # Verify in database course_archive_status = CourseArchiveStatus.objects.get( - course_id=course_key, user=user + course_run=course_run, user=user ) assert course_archive_status.is_archived is True assert course_archive_status.user == user @@ -340,7 +363,7 @@ def test_update_course_archive_status_without_user_field(api_client, user, cours @pytest.mark.django_db def test_staff_create_with_explicit_user_override( - api_client, staff_user, user, course_key + api_client, staff_user, user, course_key, course_run ): """ Test that staff can explicitly set user field to override default behavior. @@ -361,7 +384,7 @@ def test_staff_create_with_explicit_user_override( # Verify in database course_archive_status = CourseArchiveStatus.objects.get( - course_id=course_key, user=user + course_run=course_run, user=user ) assert course_archive_status.user == user assert course_archive_status.user != staff_user @@ -369,14 +392,14 @@ def test_staff_create_with_explicit_user_override( @pytest.mark.django_db def test_staff_update_with_explicit_user_override( - api_client, staff_user, user, another_user, course_key + api_client, staff_user, user, another_user, course_run ): """ Test that staff can explicitly change user field when updating. """ # Create initial record for user initial_status = CourseArchiveStatus.objects.create( - course_id=course_key, user=user, is_archived=False + course_run=course_run, user=user, is_archived=False ) api_client.force_authenticate(user=staff_user) @@ -400,6 +423,7 @@ def test_staff_update_with_explicit_user_override( @pytest.mark.django_db +@pytest.mark.usefixtures("course_run") def test_regular_user_cannot_override_user_field_create( api_client, user, another_user, course_key ): @@ -420,7 +444,7 @@ def test_regular_user_cannot_override_user_field_create( @pytest.mark.django_db def test_staff_create_without_user_field_defaults_to_current_user( - api_client, staff_user, course_key + api_client, staff_user, course_key, course_run ): """ 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( # Verify in database course_archive_status = CourseArchiveStatus.objects.get( - course_id=course_key, user=staff_user + course_run=course_run, user=staff_user ) assert course_archive_status.user == staff_user diff --git a/backend-plugin-sample/tests/test_models.py b/backend-plugin-sample/tests/test_models.py index ebc025e..d6bf916 100644 --- a/backend-plugin-sample/tests/test_models.py +++ b/backend-plugin-sample/tests/test_models.py @@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model from django.db.utils import IntegrityError from opaque_keys.edx.keys import CourseKey +from openedx_catalog.api import create_course_run_for_modulestore_course_with from openedx_plugin_sample.models import CourseArchiveStatus @@ -38,24 +39,27 @@ def staff_user(): @pytest.fixture -def course_key(): +def course_run(): """ - Create and return a test course key. + Create and return a test CourseRun (plus its Organization and CatalogCourse). """ - return CourseKey.from_string("course-v1:edX+DemoX+Demo_Course") + return create_course_run_for_modulestore_course_with( + CourseKey.from_string("course-v1:edX+DemoX+Demo_Course"), + title="Demo Course", + ) @pytest.mark.django_db -def test_course_archive_status_creation(user, course_key): +def test_course_archive_status_creation(user, course_run): """ Test that a CourseArchiveStatus can be created with valid data. """ course_archive_status = CourseArchiveStatus.objects.create( - course_id=course_key, user=user, is_archived=False + course_run=course_run, user=user, is_archived=False ) assert course_archive_status.pk is not None - assert course_archive_status.course_id == course_key + assert course_archive_status.course_run == course_run assert course_archive_status.user == user assert course_archive_status.is_archived is False assert course_archive_status.archive_date is None @@ -64,35 +68,35 @@ def test_course_archive_status_creation(user, course_key): @pytest.mark.django_db -def test_course_archive_status_uniqueness(user, course_key): +def test_course_archive_status_uniqueness(user, course_run): """ - Test that a CourseArchiveStatus must be unique per user and course_id. + Test that a CourseArchiveStatus must be unique per user and course_run. """ CourseArchiveStatus.objects.create( - course_id=course_key, user=user, is_archived=False + course_run=course_run, user=user, is_archived=False ) - # Creating another with same user and course_id should raise an IntegrityError + # Creating another with same user and course_run should raise an IntegrityError with pytest.raises(IntegrityError): CourseArchiveStatus.objects.create( - course_id=course_key, user=user, is_archived=True + course_run=course_run, user=user, is_archived=True ) @pytest.mark.django_db -def test_course_archive_status_str_method(user, course_key): +def test_course_archive_status_str_method(user, course_run): """ Test the string representation of CourseArchiveStatus. """ course_archive_status = CourseArchiveStatus.objects.create( - course_id=course_key, user=user, is_archived=True + course_run=course_run, user=user, is_archived=True ) - expected_str = f"{course_key} - {user.username} - Archived" + expected_str = f"{course_run.course_key} - {user.username} - Archived" assert str(course_archive_status) == expected_str course_archive_status.is_archived = False course_archive_status.save() - expected_str = f"{course_key} - {user.username} - Not Archived" + expected_str = f"{course_run.course_key} - {user.username} - Not Archived" assert str(course_archive_status) == expected_str diff --git a/backend-plugin-sample/tests/test_pipeline.py b/backend-plugin-sample/tests/test_pipeline.py index 8454970..3e6ee7b 100644 --- a/backend-plugin-sample/tests/test_pipeline.py +++ b/backend-plugin-sample/tests/test_pipeline.py @@ -9,6 +9,7 @@ import pytest from django.contrib.auth import get_user_model from opaque_keys.edx.keys import CourseKey +from openedx_catalog.api import create_course_run_for_modulestore_course_with from openedx_plugin_sample.models import CourseArchiveStatus from openedx_plugin_sample.pipeline import AddArchiveStatusToLearnerHomeCourseRun @@ -34,6 +35,17 @@ def course_key(): return CourseKey.from_string("course-v1:edX+DemoX+Demo_Course") +@pytest.fixture +def course_run(course_key): + """ + Create and return a test CourseRun (plus its Organization and CatalogCourse) + matching the `course_key` fixture, so CourseArchiveStatus rows can FK to it. + """ + return create_course_run_for_modulestore_course_with( + course_key, title="Demo Course" + ) + + @pytest.fixture def serialized_courserun(course_key): """ @@ -64,14 +76,14 @@ def mock_current_request(user): @pytest.mark.django_db def test_archived_courserun_gets_is_archived_by_learner_true( - user, course_key, serialized_courserun, mock_current_request # pylint: disable=unused-argument + user, course_key, course_run, serialized_courserun, mock_current_request # pylint: disable=unused-argument ): """ Test that the filter adds isArchivedByLearner=True when the learner has archived this course. """ CourseArchiveStatus.objects.create( - course_id=course_key, user=user, is_archived=True + course_run=course_run, user=user, is_archived=True ) result = AddArchiveStatusToLearnerHomeCourseRun( diff --git a/backend-plugin-sample/tests/test_signals.py b/backend-plugin-sample/tests/test_signals.py index a82fe6a..a4d43a9 100644 --- a/backend-plugin-sample/tests/test_signals.py +++ b/backend-plugin-sample/tests/test_signals.py @@ -9,6 +9,7 @@ import pytest from django.contrib.auth import get_user_model from opaque_keys.edx.keys import CourseKey +from openedx_catalog.api import create_course_run_for_modulestore_course_with from openedx_events.learning.data import CourseData, CourseEnrollmentData, UserData, UserPersonalData from openedx_events.learning.signals import COURSE_ENROLLMENT_CHANGED @@ -35,6 +36,17 @@ def course_key(): return CourseKey.from_string("course-v1:edX+DemoX+Demo_Course") +@pytest.fixture +def course_run(course_key): + """ + Create and return a test CourseRun (plus its Organization and CatalogCourse) + matching the `course_key` fixture, so CourseArchiveStatus rows can FK to it. + """ + return create_course_run_for_modulestore_course_with( + course_key, title="Demo Course" + ) + + def _build_enrollment(user, course_key, *, mode, is_active): """ Build a CourseEnrollmentData payload like the platform would emit. @@ -53,14 +65,14 @@ def _build_enrollment(user, course_key, *, mode, is_active): @pytest.mark.django_db -def test_verified_upgrade_unarchives_course(user, course_key): +def test_verified_upgrade_unarchives_course(user, course_key, course_run): """ Test that firing COURSE_ENROLLMENT_CHANGED with is_active=True and mode="verified" flips an existing archived CourseArchiveStatus back to unarchived. """ archive_status = CourseArchiveStatus.objects.create( - course_id=course_key, + course_run=course_run, user=user, is_archived=True, archive_date=datetime.now(timezone.utc), diff --git a/backend-plugin-sample/uv.lock b/backend-plugin-sample/uv.lock index 3c1f086..574fe9f 100644 --- a/backend-plugin-sample/uv.lock +++ b/backend-plugin-sample/uv.lock @@ -42,6 +42,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + [[package]] name = "asgiref" version = "3.11.1" @@ -91,6 +103,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "billiard" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, +] + [[package]] name = "build" version = "1.4.4" @@ -114,6 +135,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/c4/cf76242a5da1410917107ff14551764aa405a5fd10cd10cf9a5ca8fa77f4/cachetools-7.0.6-py3-none-any.whl", hash = "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", size = 13976, upload-time = "2026-04-20T19:02:21.187Z" }, ] +[[package]] +name = "celery" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzlocal" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz", hash = "sha256:177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", size = 1742243, upload-time = "2026-03-26T12:14:51.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl", hash = "sha256:0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", size = 451235, upload-time = "2026-03-26T12:14:49.491Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -296,6 +337,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + [[package]] name = "click-log" version = "0.4.0" @@ -308,6 +361,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" }, ] +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + [[package]] name = "code-annotations" version = "3.0.0" @@ -422,7 +500,7 @@ name = "cryptography" version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'group-21-openedx-plugin-sample-dev' and extra == 'group-21-openedx-plugin-sample-django60') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } wheels = [ @@ -570,6 +648,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" }, ] +[[package]] +name = "django-model-utils" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-dev' or extra != 'group-21-openedx-plugin-sample-django60' or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, + { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/60/5e232c32a2c977cc1af8c70a38ef436598bc649ad89c2c4568454edde2c9/django_model_utils-5.0.0.tar.gz", hash = "sha256:041cdd6230d2fbf6cd943e1969318bce762272077f4ecd333ab2263924b4e5eb", size = 80559, upload-time = "2024-09-04T11:35:22.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/13/87a42048700c54bfce35900a34e2031245132775fb24363fc0e33664aa9c/django_model_utils-5.0.0-py3-none-any.whl", hash = "sha256:fec78e6c323d565a221f7c4edc703f4567d7bb1caeafe1acd16a80c5ff82056b", size = 42630, upload-time = "2024-09-04T11:36:23.166Z" }, +] + +[[package]] +name = "django-simple-history" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-dev' or extra != 'group-21-openedx-plugin-sample-django60' or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, + { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/11/410049f1454b99a78f719d3403fc89437c2a38ee092e939d5ab8d4846738/django_simple_history-3.11.0.tar.gz", hash = "sha256:2c587479cf2c3071e9aa555d0d11b73676994db4910770958f57659ade2deffe", size = 234862, upload-time = "2025-12-11T13:50:55.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c2/e9854a3438cfc80891ab4d3826b7c61a0fe5ba3a4da89104a8f5c9afb5df/django_simple_history-3.11.0-py3-none-any.whl", hash = "sha256:f3c298db49e418ffce7fb709a5e83108452ea2179ec5c4b9232484c25427192a", size = 81868, upload-time = "2025-12-11T13:50:53.71Z" }, +] + [[package]] name = "django-waffle" version = "5.0.0" @@ -629,6 +733,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] +[[package]] +name = "drf-jwt" +version = "1.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-dev' or extra != 'group-21-openedx-plugin-sample-django60' or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, + { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, + { name = "djangorestframework" }, + { name = "pyjwt", extra = ["crypto"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/8e/3c60992bafd0ac51bcfa136ff9f5e1cff65ef45ebbcb393477672e699e82/drf-jwt-1.19.2.tar.gz", hash = "sha256:660bc66f992065cef59832adcbbdf871847e9738671c19e5121971e773768235", size = 38208, upload-time = "2022-01-09T09:23:38.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/48/d72115d8c0d27c5d806fdd6cd61197cef6a7e34df2b8cb63b218ffa674b8/drf_jwt-1.19.2-py2.py3-none-any.whl", hash = "sha256:63c3d4ed61a1013958cd63416e2d5c84467d8ae3e6e1be44b1fb58743dbd1582", size = 21926, upload-time = "2022-01-09T09:23:37.257Z" }, +] + [[package]] name = "edx-ccx-keys" version = "2.0.2" @@ -661,6 +780,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/ae/a8e618fee2be5f1a8ae3e2d1b6ce0b405fbca2726dd11557e1b982cf58ae/edx_django_utils-8.0.1-py2.py3-none-any.whl", hash = "sha256:849c253b5e14ddfbdc47c5e36143fad54147778a53e0b49095c1fecab24edd9e", size = 121130, upload-time = "2025-09-29T18:17:04.628Z" }, ] +[[package]] +name = "edx-drf-extensions" +version = "10.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-dev' or extra != 'group-21-openedx-plugin-sample-django60' or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, + { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, + { name = "django-waffle" }, + { name = "djangorestframework" }, + { name = "drf-jwt" }, + { name = "edx-django-utils" }, + { name = "edx-opaque-keys" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, + { name = "semantic-version" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/b4/24ae933674841abb97332b134ff4d2b6ea099716d5f61935fd2128cb0d38/edx_drf_extensions-10.6.0.tar.gz", hash = "sha256:c633be6aeb615836a5cbb118b52311d87905942273fdb470bfcc1c95fd5f84b7", size = 77425, upload-time = "2025-04-04T11:43:49.648Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/a7/60ce0beb203878005010c396026b3666a4e604e7963608dff94571e0a6f2/edx_drf_extensions-10.6.0-py2.py3-none-any.whl", hash = "sha256:82603edc63f7f34a3d1ab024808320ad0e175e608484f392f3c76e594d7715d1", size = 76298, upload-time = "2025-04-04T11:43:47.913Z" }, +] + [[package]] name = "edx-i18n-tools" version = "2.0.0" @@ -709,6 +849,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/1b/e9edebd12fd461b605691759a01c12e73a78cb50a4cd0b04ab58bb8303d9/edx_opaque_keys-4.0.0-py3-none-any.whl", hash = "sha256:dab0fd985b13d6d6515049606f93684d1eb284207b6d5e8da0721e29efdd0088", size = 77651, upload-time = "2026-04-02T19:00:55.259Z" }, ] +[[package]] +name = "edx-organizations" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-dev' or extra != 'group-21-openedx-plugin-sample-django60' or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, + { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, + { name = "django-model-utils" }, + { name = "django-simple-history" }, + { name = "djangorestframework" }, + { name = "edx-drf-extensions" }, + { name = "edx-opaque-keys" }, + { name = "pillow" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/b0/72abcc14ba2e90181fa789b39c35d7c0c29fcd8788113965c150304db767/edx_organizations-8.0.0.tar.gz", hash = "sha256:3726aef972fa51d1e3e736710c2881432dbb0e87f02876cf4f0dcb52413b9965", size = 43140, upload-time = "2026-03-24T19:38:37.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/75/64acfbcb72f13056535078352b38ff5eef78169cb6c6baf4ce0fd410751c/edx_organizations-8.0.0-py3-none-any.whl", hash = "sha256:3595586d9f6637bd2969b9d2e449f6d620539f91ae3f40a52a67c1ec1483c470", size = 53190, upload-time = "2026-03-24T19:38:36.426Z" }, +] + [[package]] name = "fastavro" version = "1.12.1" @@ -872,6 +1032,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] +[[package]] +name = "kombu" +version = "5.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, +] + [[package]] name = "lxml" version = "6.1.0" @@ -1114,6 +1289,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/1c/868da1caa571d1c01e785a03110f90a0490465fc34a138ef7ce6dc5deed8/openedx_atlas-0.7.0-py3-none-any.whl", hash = "sha256:4884d98e03282181b14f6acefb264cd0843a892ac8ec91b9b53163d1155b4718", size = 22385, upload-time = "2025-04-08T07:51:36.502Z" }, ] +[[package]] +name = "openedx-core" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "celery" }, + { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-dev' or extra != 'group-21-openedx-plugin-sample-django60' or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, + { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, + { name = "djangorestframework" }, + { name = "edx-drf-extensions" }, + { name = "edx-organizations" }, + { name = "openedx-events" }, + { name = "rules" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/cb/2bb4f094e12e9d843ac3795802ee88d1b458fc94aab74b073996beb026e9/openedx_core-1.0.1.tar.gz", hash = "sha256:356e9edbd4ee5bea565d55d9248281ace8ee2746ed38a603e1c0473a65dccfec", size = 232850, upload-time = "2026-04-30T12:55:34.625Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/af156c4451c4eec12a7391a64deb970dfd68876570711f13910777a4456a/openedx_core-1.0.1-py2.py3-none-any.whl", hash = "sha256:2348025586976681c405a27185cf0095ad24776099346ea4d5e9d5e9f905b606", size = 316688, upload-time = "2026-04-30T12:55:33.103Z" }, +] + [[package]] name = "openedx-events" version = "11.2.0" @@ -1158,6 +1354,7 @@ dependencies = [ { name = "djangorestframework" }, { name = "edx-opaque-keys" }, { name = "openedx-atlas" }, + { name = "openedx-core" }, { name = "openedx-events" }, { name = "openedx-filters" }, ] @@ -1239,6 +1436,7 @@ requires-dist = [ { name = "djangorestframework" }, { name = "edx-opaque-keys" }, { name = "openedx-atlas" }, + { name = "openedx-core" }, { name = "openedx-events" }, { name = "openedx-filters" }, ] @@ -1331,6 +1529,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/0f/ddd60b4794cc8a8c086150e13ffbff438dbf306b2739918e65ddb706208f/path-16.16.0-py3-none-any.whl", hash = "sha256:d981989cf87598adc9f5b71ec5192d314a384836e81b4b1f34197138dc4ae659", size = 25531, upload-time = "2024-07-27T09:37:44.312Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "platformdirs" version = "4.9.6" @@ -1358,6 +1625,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/99/45bb1f9926efe370c6dbe324741c749658e44cb060124f28dad201202274/polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d", size = 20634, upload-time = "2023-02-23T17:53:59.919Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + [[package]] name = "psutil" version = "7.2.2" @@ -1443,6 +1722,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pylint" version = "4.0.5" @@ -1645,6 +1938,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-discovery" version = "1.2.2" @@ -1809,6 +2114,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] +[[package]] +name = "rules" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/36/918cf4cc9fd0e38bb9310b2d1a13ae6ebb2b5732d56e7de6feb4a992a6ed/rules-3.5.tar.gz", hash = "sha256:f01336218f4561bab95f53672d22418b4168baea271423d50d9e8490d64cb27a", size = 55504, upload-time = "2024-09-02T16:01:46.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/33/16213dd62ca8ce8749985318a966ac1300ab55c977b2d66632a45b405c99/rules-3.5-py2.py3-none-any.whl", hash = "sha256:0f00fc9ee448b3f82e9aff9334ab0c56c76dce4dfa14f1598f57969f1022acc0", size = 25658, upload-time = "2024-09-02T16:01:44.844Z" }, +] + [[package]] name = "secretstorage" version = "3.5.0" @@ -1822,6 +2136,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + [[package]] name = "setuptools" version = "82.0.1" @@ -2082,6 +2405,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, ] +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32' or (extra == 'group-21-openedx-plugin-sample-dev' and extra == 'group-21-openedx-plugin-sample-django60') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -2117,6 +2452,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/bb/e6bfdea92ed270f3445a5a3c17599d041b3f2dbc5026c09e02830a03bbaf/uv-0.11.7-py3-none-win_arm64.whl", hash = "sha256:6158b7e39464f1aa1e040daa0186cae4749a78b5cd80ac769f32ca711b8976b1", size = 23941816, upload-time = "2026-04-15T21:43:06.732Z" }, ] +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + [[package]] name = "virtualenv" version = "21.2.4" @@ -2131,3 +2475,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133 wheels = [ { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, ] + +[[package]] +name = "wcwidth" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, +] From 176be8c8049969035f1fcc367875d3793a4588d8 Mon Sep 17 00:00:00 2001 From: Kyle D McCormick Date: Wed, 13 May 2026 21:58:01 -0400 Subject: [PATCH 2/2] feat: Django admin page for CourseArchiveStatus Expose the sample plugin's one model in the LMS/CMS admin so operators can inspect and manage records without custom tooling, and so the sample demonstrates the standard ModelAdmin pattern alongside the other plugin interfaces. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../.annotation_safe_list.yml | 8 +++ .../src/openedx_plugin_sample/admin.py | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 backend-plugin-sample/src/openedx_plugin_sample/admin.py diff --git a/backend-plugin-sample/.annotation_safe_list.yml b/backend-plugin-sample/.annotation_safe_list.yml index 62eaaa7..deb98bf 100644 --- a/backend-plugin-sample/.annotation_safe_list.yml +++ b/backend-plugin-sample/.annotation_safe_list.yml @@ -39,3 +39,11 @@ waffle.Sample: ".. no_pii:": "This model has no PII" waffle.Switch: ".. no_pii:": "This model has no PII" +openedx_catalog.CatalogCourse: + ".. no_pii:": "This model has no PII" +openedx_catalog.CourseRun: + ".. no_pii:": "This model has no PII" +organizations.HistoricalOrganization: + ".. no_pii:": "This model has no PII" +organizations.HistoricalOrganizationCourse: + ".. no_pii:": "This model has no PII" diff --git a/backend-plugin-sample/src/openedx_plugin_sample/admin.py b/backend-plugin-sample/src/openedx_plugin_sample/admin.py new file mode 100644 index 0000000..ecf3fc7 --- /dev/null +++ b/backend-plugin-sample/src/openedx_plugin_sample/admin.py @@ -0,0 +1,55 @@ +""" +Django admin configuration for openedx_plugin_sample. + +This module demonstrates how to expose plugin models in the Django admin +site provided by Open edX (LMS and CMS each have their own admin under +``/admin/``). Defining a ``ModelAdmin`` for each model gives operators a +ready-made UI to inspect and manage plugin data without needing custom +tooling. + +Django Documentation: +- ModelAdmin: https://docs.djangoproject.com/en/stable/ref/contrib/admin/ +""" + +from django.contrib import admin + +from openedx_plugin_sample.models import CourseArchiveStatus + + +@admin.register(CourseArchiveStatus) +class CourseArchiveStatusAdmin(admin.ModelAdmin): + """ + Admin configuration for the CourseArchiveStatus model. + """ + + list_display = ( + "course_key", + "user", + "is_archived", + "archive_date", + "updated_at", + ) + list_filter = ("is_archived",) + # Search by the related CourseRun's course_key and the user's username/email. + search_fields = ( + "course_run__course_key", + "user__username", + "user__email", + ) + # FKs use raw id widgets (lookup popup) rather than a