diff --git a/common/djangoapps/student/tests/test_filters.py b/common/djangoapps/student/tests/test_filters.py index 6e429fd39a5..a3f79551cfd 100644 --- a/common/djangoapps/student/tests/test_filters.py +++ b/common/djangoapps/student/tests/test_filters.py @@ -4,8 +4,13 @@ from django.http import HttpResponse from django.test import override_settings from django.urls import reverse +from openedx.core.djangoapps.enrollments.data import get_course_enrollments from openedx_filters import PipelineStep -from openedx_filters.learning.filters import DashboardRenderStarted, CourseEnrollmentStarted, CourseUnenrollmentStarted +from openedx_filters.learning.filters import ( + DashboardRenderStarted, + CourseEnrollmentStarted, + CourseUnenrollmentStarted, +) from rest_framework import status from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -110,6 +115,20 @@ def run_filter(self, context, template_name): # pylint: disable=arguments-diffe ) +class TestCourseEnrollmentsPipelineStep(PipelineStep): + """ + Utility function used when getting steps for pipeline. + """ + + def run_filter(self, enrollments): # pylint: disable=arguments-differ + """Pipeline steps that modifies course enrollments when make a queryset request.""" + + enrollments = [enrollment for enrollment in enrollments if enrollment.course_id.org == "demo"] + return { + "enrollments": enrollments, + } + + @skip_unless_lms class EnrollmentFiltersTest(ModuleStoreTestCase): """ @@ -118,17 +137,23 @@ class EnrollmentFiltersTest(ModuleStoreTestCase): This class guarantees that the following filters are triggered during the user's enrollment: - CourseEnrollmentStarted + - CourseEnrollmentQuerysetRequested """ def setUp(self): # pylint: disable=arguments-differ super().setUp() self.course = CourseFactory.create() + demo_course = CourseFactory.create(org='demo') + test_course = CourseFactory.create(org='test') self.user = UserFactory.create( username="test", email="test@example.com", password="password", ) self.user_profile = UserProfileFactory.create(user=self.user, name="Test Example") + CourseEnrollment.enroll(self.user, demo_course.id, mode='audit') + CourseEnrollment.enroll(self.user, test_course.id, mode='audit') + self.enrollment = get_course_enrollments(self.user) @override_settings( OPEN_EDX_FILTERS_CONFIG={ @@ -189,6 +214,46 @@ def test_enrollment_without_filter_configuration(self): self.assertEqual('audit', enrollment.mode) self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) + @override_settings() + def test_enrollment_queryset_filter_unexecuted_data(self): + """ + Test filter enrollment queryset when a request is made. + + Expected result: + - CourseEnrollmentQuerysetRequested is triggered and executes TestCourseEnrollmentsPipelineStep. + - The result is a list of course enrollments queryset filter by org + """ + enrollments = get_course_enrollments(self.user) + + self.assertListEqual(self.enrollment, enrollments) + + @override_settings( + OPEN_EDX_FILTERS_CONFIG={ + "org.openedx.learning.course_enrollment_queryset.requested.v1": { + "pipeline": [ + "common.djangoapps.student.tests.test_filters.TestCourseEnrollmentsPipelineStep", + ], + "fail_silently": False, + }, + }, + ) + def test_enrollment_queryset_filter_executed_data(self): + """ + Test filter enrollment queryset when a request is made. + + Expected result: + - CourseEnrollmentQuerysetRequested is triggered and executes TestCourseEnrollmentsPipelineStep. + - The result is a list of course enrollments queryset filter by org + """ + expected_enrollment = self.enrollment + expected_enrollment = expected_enrollment[0]['course_details']['course_id'] + + enrollments = get_course_enrollments(self.user) + enrollments = enrollments[0]['course_details']['course_id'] + + self.assertEqual(expected_enrollment, enrollments) + self.assertAlmostEqual(len(enrollments), len(expected_enrollment), 1) + @skip_unless_lms class UnenrollmentFiltersTest(ModuleStoreTestCase): diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index a0c00a98df8..b9043f8acd4 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -13,6 +13,7 @@ from django.utils.decorators import method_decorator from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import UsageKey +from openedx_filters.learning.filters import CourseEnrollmentQuerysetRequested from rest_framework import generics, views from rest_framework.decorators import api_view from rest_framework.permissions import SAFE_METHODS @@ -341,6 +342,13 @@ def get_queryset(self): ).order_by('created').reverse() org = self.request.query_params.get('org', None) + try: + ## .. filter_implemented_name: CourseEnrollmentQuerysetRequested + ## .. filter_type: org.openedx.learning.course_enrollment_queryset.requested.v1 + enrollments = CourseEnrollmentQuerysetRequested.run_filter(enrollments=enrollments) + except CourseEnrollmentQuerysetRequested.PreventEnrollmentQuerysetRequest as exc: + raise EnrollmentRequestNotAllowed(str(exc)) from exc + same_org = ( enrollment for enrollment in enrollments if enrollment.course_overview and self.is_org(org, enrollment.course_overview.org) @@ -375,6 +383,14 @@ def list(self, request, *args, **kwargs): return response +class EnrollmentRequestException(Exception): + pass + + +class EnrollmentRequestNotAllowed(EnrollmentRequestException): + pass + + @api_view(["GET"]) @mobile_view() def my_user_info(request, api_version): diff --git a/openedx/core/djangoapps/enrollments/data.py b/openedx/core/djangoapps/enrollments/data.py index fbea06edffa..cdb618fe43f 100644 --- a/openedx/core/djangoapps/enrollments/data.py +++ b/openedx/core/djangoapps/enrollments/data.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.db import transaction from opaque_keys.edx.keys import CourseKey +from openedx_filters.learning.filters import CourseEnrollmentQuerysetRequested from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.enrollments.errors import ( @@ -54,6 +55,13 @@ def get_course_enrollments(username, include_inactive=False): if not include_inactive: qset = qset.filter(is_active=True) + try: + ## .. filter_implemented_name: CourseEnrollmentQuerysetRequested + ## .. filter_type: org.openedx.learning.course_enrollment_queryset.requested.v1 + qset = CourseEnrollmentQuerysetRequested.run_filter(enrollments=qset) + except CourseEnrollmentQuerysetRequested.PreventEnrollmentQuerysetRequest as exc: + raise EnrollmentRequestNotAllowed(str(exc)) from exc + enrollments = CourseEnrollmentSerializer(qset, many=True).data # Find deleted courses and filter them out of the results @@ -76,6 +84,14 @@ def get_course_enrollments(username, include_inactive=False): return valid +class EnrollmentRequestException(Exception): + pass + + +class EnrollmentRequestNotAllowed(EnrollmentRequestException): + pass + + def get_course_enrollment(username, course_id): """Retrieve an object representing all aggregated data for a user's course enrollment.