diff --git a/backend-plugin-sample/src/openedx_plugin_sample/pipeline.py b/backend-plugin-sample/src/openedx_plugin_sample/pipeline.py index d159377..91037f7 100644 --- a/backend-plugin-sample/src/openedx_plugin_sample/pipeline.py +++ b/backend-plugin-sample/src/openedx_plugin_sample/pipeline.py @@ -38,119 +38,53 @@ """ # pylint: disable=line-too-long import logging -import re +import crum from openedx_filters.filters import PipelineStep +from .models import CourseArchiveStatus + logger = logging.getLogger(__name__) -class ChangeCourseAboutPageUrl(PipelineStep): +class AddArchiveStatusToLearnerHomeCourseRun(PipelineStep): """ - Filter to customize course about page URLs. - - This filter demonstrates how to intercept and modify course about page URLs, - redirecting them to external sites or custom implementations. - - Filter Hook Point: - This filter hooks into the course about page URL rendering process. - Register it for the filter: org.openedx.learning.course.about.render.started.v1 - - Registration Example (in settings/common.py):: - - def plugin_settings(settings): - settings.OPEN_EDX_FILTERS_CONFIG = { - "org.openedx.learning.course.about.render.started.v1": { - "pipeline": [ - "openedx_plugin_sample.pipeline.ChangeCourseAboutPageUrl" - ], - "fail_silently": False, - } - } - - Filter Documentation: - - Available Filters: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters.html - - PipelineStep: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters-tooling.html#openedx_filters.filters.PipelineStep - - Real-World Use Cases: - - Redirect to marketing site course pages - - Implement custom course discovery interfaces - - Add tracking parameters to URLs - - Route different course types to different platforms - - Implement A/B testing for course pages + Customize each courseRun within a Learner Dashboard's /init API response to include the CourseArchiveStatus. """ # noqa: E501 - def run_filter(self, url, org, **kwargs): # pylint: disable=arguments-differ + def run_filter(self, serialized_courserun, **kwargs): # pylint: disable=arguments-differ """ - Modify the course about page URL. - - This method intercepts course about page URL generation and can modify - the destination URL based on business logic. + Insert `isArchivedByLearner` into one serialized courseRun for the Learner Home /init response. Args: - url (str): The original course about page URL - org (str): The organization/institution identifier - **kwargs: Additional context data from the platform + serialized_courserun (dict): One courseRun from the serializer. Reads + `courseId` (a course key string, e.g. "course-v1:edX+DemoX+Demo_Course"); + all other fields are passed through unchanged. Returns: - dict: Dictionary with same parameter names as input - - url (str): Modified or original URL - - org (str): Organization identifier (usually unchanged) - - Raises: - FilterException: If processing should be halted - - Filter Requirements: - - Must return dictionary with keys matching input parameters - - Return None to skip this filter (let other filters run) - - Raise FilterException to halt pipeline execution - - Handle all input scenarios gracefully - - URL Pattern Matching: - This implementation looks for Open edX course keys in the format: - course-v1:ORG+COURSE+RUN (e.g., course-v1:edX+DemoX+Demo_Course) - - Documentation: - - run_filter method: https://docs.openedx.org/projects/openedx-filters/en/latest/reference/filters-tooling.html#openedx_filters.filters.PipelineStep.run_filter + dict: ``{"serialized_courserun": }``. The updated dict has the + same keys as the input plus `isArchivedByLearner` (bool) -- True iff a + CourseArchiveStatus row exists for the current request user and this + courseId with `is_archived=True`; False otherwise (including when no row + exists). + + The current user is read from the active request via `crum`, so this filter only + runs meaningfully inside a request cycle. Note that `isArchivedByLearner` is + distinct from `isArchived`, which the platform sets based on whether the course + run itself has ended. """ # noqa: E501 - # Extract course ID using Open edX course key pattern - # Course keys follow the format: course-v1:ORG+COURSE+RUN - pattern = r'(?Pcourse-v1:[^/]+)' - - match = re.search(pattern, url) - if match: - course_id = match.group('course_id') - - # Example: Redirect to external marketing site - new_url = f"https://example.com/new_about_page/{course_id}" - - logger.debug( - f"Redirecting course about page for {course_id} from {url} to {new_url}" - ) - - # Return modified data - return {"url": new_url, "org": org} - - # No course ID found - return original data unchanged - logger.debug(f"No course ID found in URL {url}, leaving unchanged") - return {"url": url, "org": org} - - # Alternative patterns for different business logic: - - # Organization-based routing: - # if org == "special_org": - # new_url = f"https://special-site.com/courses/{course_id}" - # return {"url": new_url, "org": org} - - # Course type-based routing: - # if "MicroMasters" in course_id: - # new_url = f"https://micromasters.example.com/{course_id}" - # return {"url": new_url, "org": org} - - # A/B testing implementation: - # import random - # if random.choice([True, False]): - # new_url = f"https://variant-a.example.com/{course_id}" - # else: - # new_url = f"https://variant-b.example.com/{course_id}" - # return {"url": new_url, "org": org} + request = crum.get_current_request() + if not (request and request.user): + return serialized_courserun + try: + is_archived_by_learner = CourseArchiveStatus.objects.get( + user=request.user, course_id=serialized_courserun["courseId"] + ).is_archived + except CourseArchiveStatus.DoesNotExist: + is_archived_by_learner = False + return { + "serialized_courserun": { + **serialized_courserun, + "isArchivedByLearner": is_archived_by_learner, + }, + } diff --git a/backend-plugin-sample/src/openedx_plugin_sample/settings/common.py b/backend-plugin-sample/src/openedx_plugin_sample/settings/common.py index acf817c..627ec3c 100644 --- a/backend-plugin-sample/src/openedx_plugin_sample/settings/common.py +++ b/backend-plugin-sample/src/openedx_plugin_sample/settings/common.py @@ -11,7 +11,8 @@ configuration that integrates seamlessly with the platform. Official Documentation: -- Plugin Settings: https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html#plugin-settings +- Plugin Settings: + https://docs.openedx.org/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html#plugin-settings - Django Settings: https://docs.djangoproject.com/en/stable/topics/settings/ Settings Organization: @@ -96,8 +97,8 @@ def _configure_openedx_filters(settings): filters_config = getattr(settings, 'OPEN_EDX_FILTERS_CONFIG', {}) # Filter we want to register - filter_name = "org.openedx.learning.course_about.page.url.requested.v1" - our_pipeline_step = "openedx_plugin_sample.pipeline.ChangeCourseAboutPageUrl" + filter_name = "org.openedx.learning.home.courserun.api.rendered.started.v1" + our_pipeline_step = "openedx_plugin_sample.pipeline.AddArchiveStatusToLearnerHomeCourseRun" # Check if this filter already has configuration if filter_name in filters_config: diff --git a/backend-plugin-sample/tests/test_pipeline.py b/backend-plugin-sample/tests/test_pipeline.py new file mode 100644 index 0000000..8454970 --- /dev/null +++ b/backend-plugin-sample/tests/test_pipeline.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# pylint: disable=redefined-outer-name +""" +Tests for the `sample-plugin` Open edX Filters pipeline steps. +""" + +from unittest.mock import MagicMock, patch + +import pytest +from django.contrib.auth import get_user_model +from opaque_keys.edx.keys import CourseKey + +from openedx_plugin_sample.models import CourseArchiveStatus +from openedx_plugin_sample.pipeline import AddArchiveStatusToLearnerHomeCourseRun + +User = get_user_model() + + +@pytest.fixture +def user(): + """ + Create and return a test user. + """ + return User.objects.create_user( + username="testuser", email="testuser@example.com", password="password123" + ) + + +@pytest.fixture +def course_key(): + """ + Create and return a test course key. + """ + return CourseKey.from_string("course-v1:edX+DemoX+Demo_Course") + + +@pytest.fixture +def serialized_courserun(course_key): + """ + Return a minimal courseRun dict like the learner home /init API would emit. + """ + return { + "courseId": str(course_key), + "courseNumber": "DemoX", + } + + +@pytest.fixture +def mock_current_request(user): + """ + Patch crum.get_current_request so the filter sees `user` as the requester. + + The filter relies on `crum` to find the current user, which is set by middleware + in a real request cycle. In unit tests we stub it directly. + """ + request = MagicMock() + request.user = user + with patch( + "openedx_plugin_sample.pipeline.crum.get_current_request", + return_value=request, + ): + yield request + + +@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 +): + """ + 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 + ) + + result = AddArchiveStatusToLearnerHomeCourseRun( + filter_type="org.openedx.learning.home.courserun.api.rendering.started.v1", + running_pipeline=[], + ).run_filter(serialized_courserun=serialized_courserun) + + assert result["serialized_courserun"]["isArchivedByLearner"] is True + # Existing fields on the courseRun are preserved. + assert result["serialized_courserun"]["courseId"] == str(course_key) + assert result["serialized_courserun"]["courseNumber"] == "DemoX" + + +@pytest.mark.django_db +def test_courserun_with_no_archive_record_defaults_to_false( + serialized_courserun, mock_current_request # pylint: disable=unused-argument +): + """ + Test that the filter defaults isArchivedByLearner to False when the learner + has no CourseArchiveStatus row for the course. + """ + result = AddArchiveStatusToLearnerHomeCourseRun( + filter_type="org.openedx.learning.home.courserun.api.rendering.started.v1", + running_pipeline=[], + ).run_filter(serialized_courserun=serialized_courserun) + + assert result["serialized_courserun"]["isArchivedByLearner"] is False diff --git a/backend-plugin-sample/uv.lock b/backend-plugin-sample/uv.lock index 9f09c99..3c1f086 100644 --- a/backend-plugin-sample/uv.lock +++ b/backend-plugin-sample/uv.lock @@ -2,17 +2,17 @@ version = 1 revision = 3 requires-python = ">=3.12" conflicts = [[ - { package = "platform-plugin-sample", group = "django60" }, - { package = "platform-plugin-sample", group = "test" }, + { package = "openedx-plugin-sample", group = "django60" }, + { package = "openedx-plugin-sample", group = "test" }, ], [ - { package = "platform-plugin-sample", group = "django60" }, - { package = "platform-plugin-sample", group = "doc" }, + { package = "openedx-plugin-sample", group = "django60" }, + { package = "openedx-plugin-sample", group = "doc" }, ], [ - { package = "platform-plugin-sample", group = "django60" }, - { package = "platform-plugin-sample", group = "quality" }, + { package = "openedx-plugin-sample", group = "django60" }, + { package = "openedx-plugin-sample", group = "quality" }, ], [ - { package = "platform-plugin-sample", group = "dev" }, - { package = "platform-plugin-sample", group = "django60" }, + { package = "openedx-plugin-sample", group = "dev" }, + { package = "openedx-plugin-sample", group = "django60" }, ]] [manifest] @@ -128,7 +128,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy' or (extra == 'group-22-platform-plugin-sample-dev' and extra == 'group-22-platform-plugin-sample-django60') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, + { name = "pycparser", marker = "implementation_name != '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/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -289,7 +289,7 @@ name = "click" version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-22-platform-plugin-sample-dev' and extra == 'group-22-platform-plugin-sample-django60') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, + { name = "colorama", 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/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ @@ -508,9 +508,9 @@ name = "django" version = "5.2.13" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref", marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "sqlparse", marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-22-platform-plugin-sample-dev') or (sys_platform == 'win32' and extra != 'group-22-platform-plugin-sample-django60') or (extra == 'group-22-platform-plugin-sample-dev' and extra == 'group-22-platform-plugin-sample-django60') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, + { name = "asgiref", 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 = "sqlparse", 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 = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-21-openedx-plugin-sample-dev') or (sys_platform == 'win32' and extra != 'group-21-openedx-plugin-sample-django60') 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/1f/c5/c69e338eb2959f641045802e5ea87ca4bf5ac90c5fd08953ca10742fad51/django-5.2.13.tar.gz", hash = "sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4", size = 10890368, upload-time = "2026-04-07T14:02:15.072Z" } wheels = [ @@ -522,9 +522,9 @@ name = "django" version = "6.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref", marker = "extra == 'group-22-platform-plugin-sample-django60'" }, - { name = "sqlparse", marker = "extra == 'group-22-platform-plugin-sample-django60'" }, - { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-22-platform-plugin-sample-django60') or (extra == 'group-22-platform-plugin-sample-dev' and extra == 'group-22-platform-plugin-sample-django60') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, + { name = "asgiref", marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, + { name = "sqlparse", marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, + { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-21-openedx-plugin-sample-django60') 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/60/b9/4155091ad1788b38563bd77a7258c0834e8c12a7f56f6975deaf54f8b61d/django-6.0.4.tar.gz", hash = "sha256:8cfa2572b3f2768b2e84983cf3c4811877a01edb64e817986ec5d60751c113ac", size = 10907407, upload-time = "2026-04-07T13:55:44.961Z" } wheels = [ @@ -536,8 +536,8 @@ name = "django-crum" version = "0.7.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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/34/1d/c56588f67130aeef8828e47535e8551337d2ae02f91f1414da61bc5e49fb/django-crum-0.7.9.tar.gz", hash = "sha256:65e9bc0f070a663fafc4d9e357f45fd4e6f01838b20a9e2fb7670f5706754288", size = 5168, upload-time = "2020-11-10T17:15:35.124Z" } wheels = [ @@ -549,8 +549,8 @@ name = "django-extensions" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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/6d/b3/ed0f54ed706ec0b54fd251cc0364a249c6cd6c6ec97f04dc34be5e929eac/django_extensions-4.1.tar.gz", hash = "sha256:7b70a4d28e9b840f44694e3f7feb54f55d495f8b3fa6c5c0e5e12bcb2aa3cdeb", size = 283078, upload-time = "2025-04-11T01:15:39.617Z" } wheels = [ @@ -562,8 +562,8 @@ name = "django-filter" version = "25.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" } wheels = [ @@ -575,8 +575,8 @@ name = "django-waffle" 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-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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/22/e1/6f533da0d4ac89f427dfd9410e39bfc14ae3a23335ecd549d76be4b2a834/django_waffle-5.0.0.tar.gz", hash = "sha256:62f9d00eedf68dafb82657beab56e601bddedc1ea1ccfef91d83df8658708509", size = 37761, upload-time = "2025-06-12T07:38:54.895Z" } wheels = [ @@ -588,8 +588,8 @@ name = "djangorestframework" version = "3.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" } wheels = [ @@ -648,8 +648,8 @@ version = "8.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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-crum" }, { name = "django-waffle" }, { name = "psutil" }, @@ -667,7 +667,7 @@ version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" } }, - { name = "lxml", extra = ["html-clean"], marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, + { name = "lxml", extra = ["html-clean"], 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 = "path" }, { name = "polib" }, { name = "pyyaml" }, @@ -1120,8 +1120,8 @@ version = "11.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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 = "edx-ccx-keys" }, { name = "edx-django-utils" }, { name = "edx-opaque-keys" }, @@ -1138,8 +1138,8 @@ name = "openedx-filters" version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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 = "edx-opaque-keys" }, { name = "setuptools" }, ] @@ -1149,29 +1149,11 @@ wheels = [ ] [[package]] -name = "packaging" -version = "26.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, -] - -[[package]] -name = "path" -version = "16.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/1c/3950c87aa25437af5f1663cc8627d44ff26f8c5117a5053c9fc3f641027c/path-16.16.0.tar.gz", hash = "sha256:a6a6d916c910dc17e0ddc883358756c5a33d1b6dbdf5d6de86554f399053af58", size = 50905, upload-time = "2024-07-27T09:37:45.926Z" } -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 = "platform-plugin-sample" +name = "openedx-plugin-sample" source = { editable = "." } dependencies = [ - { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-dev' or extra != 'group-22-platform-plugin-sample-django60' or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, - { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-22-platform-plugin-sample-django60'" }, + { 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-filter" }, { name = "djangorestframework" }, { name = "edx-opaque-keys" }, @@ -1331,6 +1313,24 @@ test-base = [ { name = "pytest-django" }, ] +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "path" +version = "16.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/1c/3950c87aa25437af5f1663cc8627d44ff26f8c5117a5053c9fc3f641027c/path-16.16.0.tar.gz", hash = "sha256:a6a6d916c910dc17e0ddc883358756c5a33d1b6dbdf5d6de86554f399053af58", size = 50905, upload-time = "2024-07-27T09:37:45.926Z" } +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 = "platformdirs" version = "4.9.6" @@ -1552,7 +1552,7 @@ name = "pynacl" version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'group-22-platform-plugin-sample-dev' and extra == 'group-22-platform-plugin-sample-django60') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, + { 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/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } wheels = [ @@ -1608,7 +1608,7 @@ name = "pytest" version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-22-platform-plugin-sample-dev' and extra == 'group-22-platform-plugin-sample-django60') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-doc') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-quality') or (extra == 'group-22-platform-plugin-sample-django60' and extra == 'group-22-platform-plugin-sample-test')" }, + { name = "colorama", 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')" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, diff --git a/frontend-plugin-sample/README.md b/frontend-plugin-sample/README.md index fc94641..4092fe1 100644 --- a/frontend-plugin-sample/README.md +++ b/frontend-plugin-sample/README.md @@ -90,33 +90,35 @@ const courses = courseListData.visibleList; **Slot Props**: Each slot provides specific data. For CourseListSlot, see the [slot documentation](https://github.com/openedx/frontend-app-learner-dashboard/tree/master/src/plugin-slots/CourseListSlot#plugin-props). -#### 2. Backend API Integration +#### 2. Backend Data via the Filter Pipeline -```jsx -useEffect(() => { - const fetchArchivedCourses = async () => { - const client = getAuthenticatedHttpClient(); - const lmsBaseUrl = getConfig().LMS_BASE_URL; - - const response = await client.get( - `${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`, - { params: { is_archived: true } } - ); - - const archivedCourseIds = new Set( - response.data.results.map((item) => item.course_id) - ); - setArchivedCourses(archivedCourseIds); - }; +Rather than firing an extra GET to `course-archive-status/` on every dashboard +load, the initial archive state is read directly off the slot props. The backend +plugin uses an Open edX filter (see [`pipeline.py`](../backend-plugin-sample/src/openedx_plugin_sample/pipeline.py)) +to inject `isArchivedByLearner` into each courseRun in the Learner Home `/init` +API response, so it arrives alongside the rest of the course data: - fetchArchivedCourses(); -}, []); +```jsx +const [archivedCourses, setArchivedCourses] = useState(() => { + const initial = new Set(); + (courseListData?.visibleList || []).forEach((courseData) => { + if (courseData.courseRun?.isArchivedByLearner) { + initial.add(courseData.courseRun.courseId); + } + }); + return initial; +}); ``` +**Why this pattern**: One fewer round-trip per dashboard load, and the archive +state is consistent with the rest of the course data from the same response. +The REST API is still used for writes (archive/unarchive) — see the toggle +handler below. + **Key Patterns:** -- **Authentication**: `getAuthenticatedHttpClient()` handles Open edX auth +- **Filter-injected data**: Read `courseRun.isArchivedByLearner` straight from slot props +- **Authentication** (for writes): `getAuthenticatedHttpClient()` handles Open edX auth - **Configuration**: `getConfig().LMS_BASE_URL` gets platform URLs -- **Error Handling**: Try/catch blocks for API failures #### 3. Open edX UI Components diff --git a/frontend-plugin-sample/src/plugin.jsx b/frontend-plugin-sample/src/plugin.jsx index 64e770c..b6cd646 100644 --- a/frontend-plugin-sample/src/plugin.jsx +++ b/frontend-plugin-sample/src/plugin.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { getConfig } from "@edx/frontend-platform"; import { getAuthenticatedHttpClient } from "@edx/frontend-platform/auth"; import { @@ -17,97 +17,37 @@ import { import { Archive, Unarchive, MoreVert } from "@openedx/paragon/icons"; const CourseList = ({ courseListData }) => { - const [archivedCourses, setArchivedCourses] = useState(new Set()); + // Seed the archived-course set from `courseRun.isArchivedByLearner`, which the + // backend plugin's filter pipeline injects into each courseRun in the Learner + // Home /init API response. This avoids a separate GET to course-archive-status + // on every dashboard load. Local toggles below keep this set in sync without + // a refetch. + const [archivedCourses, setArchivedCourses] = useState(() => { + const initial = new Set(); + (courseListData?.visibleList || []).forEach((courseData) => { + if (courseData.courseRun?.isArchivedByLearner) { + initial.add(courseData.courseRun.courseId); + } + }); + return initial; + }); const [loadingStates, setLoadingStates] = useState(new Map()); - // Safety check for courseListData if (!courseListData || !courseListData.visibleList) { - console.log("DEBUG: courseListData not available yet"); return
Loading courses...
; } - // Extract the "visibleList" const courses = courseListData.visibleList; - // Log the full course data structure for debugging - console.log("DEBUG: Full courseListData structure:", courseListData); - console.log("DEBUG: First course data object:", courses?.[0]); - - useEffect(() => { - const fetchArchivedCourses = async () => { - try { - const client = getAuthenticatedHttpClient(); - const lmsBaseUrl = getConfig().LMS_BASE_URL; - console.log( - "DEBUG: Fetching archived courses from:", - `${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`, - ); - - const response = await client.get( - `${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`, - { - params: { is_archived: true }, - }, - ); - - console.log("DEBUG: Archived courses API response:", response.data); - - const archivedCourseIds = new Set( - response.data.results.map((item) => item.course_id), - ); - - console.log( - "DEBUG: Archived course IDs from API:", - Array.from(archivedCourseIds), - ); - setArchivedCourses(archivedCourseIds); - } catch (error) { - console.error("Failed to fetch archived courses:", error); - } - }; - - fetchArchivedCourses(); - }, []); - - // Log course IDs we're trying to match against - console.log("DEBUG: Course IDs from course data:"); - courses.forEach((courseData, index) => { - console.log(`Course ${index}:`, { - cardId: courseData.cardId, - "courseRun.courseId": courseData.courseRun?.courseId, - "courseRun object keys": Object.keys(courseData.courseRun || {}), - "full courseRun object": courseData.courseRun, - }); - }); - - console.log( - "DEBUG: Archived course IDs to match:", - Array.from(archivedCourses), + const activeCourses = courses.filter( + (courseData) => !archivedCourses.has(courseData.courseRun?.courseId), ); - // Separate courses into active and archived - const activeCourses = courses.filter((courseData) => { - const courseId = courseData.courseRun?.courseId; - const isArchived = archivedCourses.has(courseId); - console.log( - `DEBUG: Course "${courseData.course?.courseName}" (ID: ${courseId}) - archived: ${isArchived}`, - ); - return !isArchived; - }); - - const archivedCoursesList = courses.filter((courseData) => { - const courseId = courseData.courseRun?.courseId; - return archivedCourses.has(courseId); - }); - - console.log("DEBUG: Active courses count:", activeCourses.length); - console.log("DEBUG: Archived courses count:", archivedCoursesList.length); + const archivedCoursesList = courses.filter((courseData) => + archivedCourses.has(courseData.courseRun?.courseId), + ); const handleArchiveToggle = async (courseId, isCurrentlyArchived) => { - console.log( - `DEBUG: Toggling archive for course ${courseId}, currently archived: ${isCurrentlyArchived}`, - ); - setLoadingStates((prev) => new Map(prev).set(courseId, true)); try { @@ -115,74 +55,36 @@ const CourseList = ({ courseListData }) => { const lmsBaseUrl = getConfig().LMS_BASE_URL; const url = `${lmsBaseUrl}/sample-plugin/api/v1/course-archive-status/`; - if (isCurrentlyArchived) { - // Unarchive: Find existing record and update it - const listResponse = await client.get(url, { - params: { course_id: courseId }, - }); - - if (listResponse.data.results.length > 0) { - const existingRecord = listResponse.data.results[0]; - await client.patch(`${url}${existingRecord.id}/`, { - course_id: courseId, - is_archived: false, - }); - } + const listResponse = await client.get(url, { + params: { course_id: courseId }, + }); - // Update local state - setArchivedCourses((prev) => { - const newSet = new Set(prev); - newSet.delete(courseId); - return newSet; + if (listResponse.data.results.length > 0) { + const existingRecord = listResponse.data.results[0]; + await client.patch(`${url}${existingRecord.id}/`, { + is_archived: !isCurrentlyArchived, }); } else { - // Archive: Check if record exists first, then create or update - const listResponse = await client.get(url, { - params: { course_id: courseId }, + await client.post(url, { + course_id: courseId, + is_archived: !isCurrentlyArchived, }); + } - if (listResponse.data.results.length > 0) { - // Update existing record - const existingRecord = listResponse.data.results[0]; - await client.patch(`${url}${existingRecord.id}/`, { - is_archived: true, - }); + setArchivedCourses((prev) => { + const newSet = new Set(prev); + if (isCurrentlyArchived) { + newSet.delete(courseId); } else { - // Create new record - console.log( - `DEBUG: Creating new archive record for course ${courseId}`, - ); - const createResponse = await client.post(url, { - course_id: courseId, - is_archived: true, - }); - console.log(`DEBUG: Create response:`, createResponse.data); + newSet.add(courseId); } - - // Update local state - console.log(`DEBUG: Adding course ${courseId} to archived set`); - setArchivedCourses((prev) => { - const newSet = new Set(prev).add(courseId); - console.log(`DEBUG: New archived courses set:`, Array.from(newSet)); - return newSet; - }); - } - - console.log( - `DEBUG: Successfully ${isCurrentlyArchived ? "unarchived" : "archived"} course ${courseId}`, - ); + return newSet; + }); } catch (error) { console.error( `Failed to ${isCurrentlyArchived ? "unarchive" : "archive"} course:`, error, ); - console.error("Error details:", { - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - message: error.message, - }); - // Could add toast notification here } finally { setLoadingStates((prev) => { const newMap = new Map(prev);