diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index df713daf0b98..aff2154abbde 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -30,10 +30,6 @@ from pytz import UTC from rest_framework import status from social_django.models import UserSocialAuth -from xmodule.modulestore.tests.django_utils import ( - TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase, SharedModuleStoreTestCase, -) -from xmodule.modulestore.tests.factories import CourseFactory from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory @@ -47,16 +43,16 @@ ) from common.djangoapps.student.roles import GlobalStaff, SupportStaffRole from common.djangoapps.student.tests.factories import ( - CourseEnrollmentFactory, CourseEnrollmentAttributeFactory, - UserFactory, + CourseEnrollmentFactory, + UserFactory ) from common.djangoapps.third_party_auth.tests.factories import SAMLProviderConfigFactory -from common.test.utils import disable_signal, assert_dict_contains_subset +from common.test.utils import assert_dict_contains_subset, disable_signal from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory from lms.djangoapps.support.models import CourseResetAudit from lms.djangoapps.support.serializers import ProgramEnrollmentSerializer -from lms.djangoapps.support.tests.factories import CourseResetCourseOptInFactory, CourseResetAuditFactory +from lms.djangoapps.support.tests.factories import CourseResetAuditFactory, CourseResetCourseOptInFactory from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.services import IDVerificationService from lms.djangoapps.verify_student.tests.factories import SSOVerificationFactory @@ -64,16 +60,12 @@ from openedx.core.djangoapps.oauth_dispatch.tests import factories from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.models import CourseDurationLimitConfig -from openedx.features.enterprise_support.api import enterprise_is_enabled -from openedx.features.enterprise_support.tests.factories import ( - EnterpriseCourseEnrollmentFactory, - EnterpriseCustomerUserFactory +from xmodule.modulestore.tests.django_utils import ( + TEST_DATA_SPLIT_MODULESTORE, + ModuleStoreTestCase, + SharedModuleStoreTestCase ) - -try: - from consent.models import DataSharingConsent -except ImportError: # pragma: no cover - pass +from xmodule.modulestore.tests.factories import CourseFactory class SupportViewTestCase(ModuleStoreTestCase): @@ -357,7 +349,7 @@ def test_get_enrollments(self, search_string_type): ) assert {CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.HONOR, CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.PROFESSIONAL, CourseMode.CREDIT_MODE} == {mode['slug'] for mode in data[0]['course_modes']} - assert 'enterprise_course_enrollments' not in data[0] + assert data[0]['enterprise_course_enrollments'] == [] assert data[0]['order_number'] == '' assert data[0]['source_system'] == '' @@ -398,52 +390,37 @@ def test_order_source_system_information(self): assert len(data) == 1 assert data[0]['source_system'] == 'commercetools' - @override_settings(FEATURES=dict(ENABLE_ENTERPRISE_INTEGRATION=True)) - @enterprise_is_enabled() - def test_get_enrollments_enterprise_enabled(self): + @patch( + 'lms.djangoapps.support.views.enrollments.SupportEnrollmentDataRequested.run_filter' + ) + def test_get_enrollments_with_enterprise_filter(self, mock_run_filter): + """ + Test that enterprise enrollment data from the filter is included in the response. + """ + course_id = str(self.course.id) + mock_enterprise_enrollment = { + 'course_id': course_id, + 'enterprise_customer_name': 'Test Enterprise', + 'enterprise_customer_user_id': 42, + 'license': None, + 'saved_for_later': False, + 'data_sharing_consent': None, + } + mock_run_filter.return_value = {course_id: [mock_enterprise_enrollment]} + url = reverse( 'support:enrollment_list', kwargs={'username_or_email': self.student.username} ) - - enterprise_customer_user = EnterpriseCustomerUserFactory( - user_id=self.student.id - ) - enterprise_course_enrollment = EnterpriseCourseEnrollmentFactory( - course_id=self.course.id, - enterprise_customer_user=enterprise_customer_user - ) - data_sharing_consent = DataSharingConsent( - course_id=self.course.id, - enterprise_customer=enterprise_customer_user.enterprise_customer, - username=self.student.username, - granted=True - ) - data_sharing_consent.save() - response = self.client.get(url) assert response.status_code == 200 data = json.loads(response.content.decode('utf-8')) assert len(data) == 1 + mock_run_filter.assert_called_once() enterprise_course_enrollments_data = data[0]['enterprise_course_enrollments'] assert len(enterprise_course_enrollments_data) == 1 - expected = { - 'course_id': str(enterprise_course_enrollment.course_id), - 'enterprise_customer_name': enterprise_customer_user.enterprise_customer.name, - 'enterprise_customer_user_id': enterprise_customer_user.id, - 'license': None, - 'saved_for_later': enterprise_course_enrollment.saved_for_later, - 'data_sharing_consent': { - 'username': self.student.username, - 'enterprise_customer_uuid': str(enterprise_customer_user.enterprise_customer_id), - 'exists': data_sharing_consent.exists, - 'consent_provided': data_sharing_consent.granted, - 'consent_required': data_sharing_consent.consent_required(), - 'course_id': str(enterprise_course_enrollment.course_id), - } - } - assert enterprise_course_enrollments_data[0] == expected + assert enterprise_course_enrollments_data[0] == mock_enterprise_enrollment @ddt.data( (True, 'Self Paced'), diff --git a/lms/djangoapps/support/views/contact_us.py b/lms/djangoapps/support/views/contact_us.py index 4ba2d46634aa..c08e4cf756dd 100644 --- a/lms/djangoapps/support/views/contact_us.py +++ b/lms/djangoapps/support/views/contact_us.py @@ -7,11 +7,11 @@ from django.http import Http404 from django.shortcuts import redirect from django.views.generic import View +from openedx_filters.learning.filters import SupportContactContextRequested from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.models import CourseEnrollment from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from openedx.features.enterprise_support import api as enterprise_api class ContactUsView(View): @@ -46,9 +46,9 @@ def get(self, request): # lint-amnesty, pylint: disable=missing-function-docstr if request.user.is_authenticated: context['course_id'] = request.session.get('course_id', '') context['user_enrollments'] = CourseEnrollment.enrollments_for_user_with_overviews_preload(request.user) - enterprise_customer = enterprise_api.enterprise_customer_for_request(request) - if enterprise_customer: - tags.append('enterprise_learner') + tags = SupportContactContextRequested.run_filter( + tags=tags, request=request, user=request.user + ) context['tags'] = tags diff --git a/lms/djangoapps/support/views/enrollments.py b/lms/djangoapps/support/views/enrollments.py index 0e469ec8de78..832c6a63c3a6 100644 --- a/lms/djangoapps/support/views/enrollments.py +++ b/lms/djangoapps/support/views/enrollments.py @@ -2,7 +2,6 @@ Support tool for changing course enrollments. """ import logging -from collections import defaultdict import markupsafe from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user @@ -14,6 +13,7 @@ from django.views.generic import View from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from openedx_filters.learning.filters import SupportEnrollmentDataRequested from rest_framework.generics import GenericAPIView from common.djangoapps.course_modes.models import CourseMode @@ -31,16 +31,9 @@ from lms.djangoapps.support.serializers import ManualEnrollmentSerializer from lms.djangoapps.verify_student.models import VerificationDeadline from openedx.core.djangoapps.credit.email_utils import get_credit_provider_attribute_values -from openedx.core.djangoapps.enrollments.api import get_enrollments, get_enrollment_attributes, update_enrollment +from openedx.core.djangoapps.enrollments.api import get_enrollment_attributes, get_enrollments, update_enrollment from openedx.core.djangoapps.enrollments.errors import CourseModeNotFoundError from openedx.core.djangoapps.enrollments.serializers import ModeSerializer -from openedx.features.enterprise_support.api import ( - enterprise_enabled, - get_data_sharing_consents, - get_enterprise_course_enrollments -) -from openedx.features.enterprise_support.serializers import EnterpriseCourseEnrollmentSerializer - logger = logging.getLogger(__name__) @@ -71,35 +64,6 @@ class EnrollmentSupportListView(GenericAPIView): # does not specify a serializer class. exclude_from_schema = True - def _enterprise_course_enrollments_by_course_id(self, user): - """ - Returns a dict containing enterprise course enrollments data with - course ids as keys. - """ - enterprise_course_enrollments = get_enterprise_course_enrollments(user) - data_sharing_consents_for_user = get_data_sharing_consents(user) - - enterprise_enrollments_by_course_id = defaultdict(list) - consent_by_course_and_enterprise_customer_id = {} - - # Get data sharing consent for each enterprise enrollment - for consent in data_sharing_consents_for_user: - key = f'{consent.course_id}-{consent.enterprise_customer_id}' - consent_by_course_and_enterprise_customer_id[key] = consent.serialize() - - for enterprise_course_enrollment in enterprise_course_enrollments: - serialized_enterprise_course_enrollment = EnterpriseCourseEnrollmentSerializer( - enterprise_course_enrollment - ).data - course_id = enterprise_course_enrollment.course_id - enterprise_customer_id = enterprise_course_enrollment.enterprise_customer_user.enterprise_customer_id - key = f'{course_id}-{enterprise_customer_id}' - consent = consent_by_course_and_enterprise_customer_id.get(key) - serialized_enterprise_course_enrollment['data_sharing_consent'] = consent - enterprise_enrollments_by_course_id[course_id].append(serialized_enterprise_course_enrollment) - - return enterprise_enrollments_by_course_id - @method_decorator(require_support_permission) def get(self, request, username_or_email): """ @@ -127,11 +91,13 @@ def get(self, request, username_or_email): # Add manual enrollment history, if it exists enrollment['manual_enrollment'] = self.manual_enrollment_data(enrollment, course_key) - if enterprise_enabled(): - enterprise_enrollments_by_course_id = self._enterprise_course_enrollments_by_course_id(user) - for enrollment in enrollments: - enterprise_course_enrollments = enterprise_enrollments_by_course_id.get(enrollment['course_id'], []) - enrollment['enterprise_course_enrollments'] = enterprise_course_enrollments + enterprise_enrollments_by_course_id = SupportEnrollmentDataRequested.run_filter( + enrollment_data={}, user=user + ) + for enrollment in enrollments: + enrollment['enterprise_course_enrollments'] = enterprise_enrollments_by_course_id.get( + enrollment['course_id'], [] + ) return JsonResponse(enrollments) diff --git a/lms/envs/common.py b/lms/envs/common.py index 917dd025e96f..fed59cdca727 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3172,3 +3172,54 @@ def _should_send_certificate_events(settings): SSL_AUTH_DN_FORMAT_STRING = ( "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" ) + +# .. setting_name: OPEN_EDX_FILTERS_CONFIG +# .. setting_default: {} +# .. setting_description: Configuration dict for openedx-filters pipeline steps. +# Keys are filter type strings; values are dicts with 'fail_silently' (bool) and +# 'pipeline' (list of dotted-path strings to PipelineStep subclasses). +OPEN_EDX_FILTERS_CONFIG = { + "org.openedx.learning.grade.context.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.grades.GradeEventContextEnricher"], + }, + "org.openedx.learning.account.settings.read_only_fields.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.accounts.AccountSettingsReadOnlyFieldsStep"], + }, + "org.openedx.learning.discount.eligibility.check.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.discounts.DiscountEligibilityStep"], + }, + "org.openedx.learning.courseware.view.redirect_url.requested.v1": { + "fail_silently": True, + "pipeline": [ + "enterprise.filters.courseware.ConsentRedirectStep", + "enterprise.filters.courseware.LearnerPortalRedirectStep", + ], + }, + "org.openedx.learning.logistration.context.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.logistration.LogistrationContextEnricher"], + }, + "org.openedx.learning.auth.post_login.redirect_url.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.logistration.PostLoginEnterpriseRedirect"], + }, + "org.openedx.learning.dashboard.render.started.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.dashboard.DashboardContextEnricher"], + }, + "org.openedx.learning.course_mode.checkout.started.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.course_modes.CourseModeEnterpriseStep"], + }, + "org.openedx.learning.support.contact.context.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.support.SupportContactEnterpriseTagInjector"], + }, + "org.openedx.learning.support.enrollment.data.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.support.SupportEnterpriseEnrollmentDataInjector"], + }, +} diff --git a/lms/envs/production.py b/lms/envs/production.py index 63246821d954..a61704261153 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -84,6 +84,7 @@ def get_env_setting(setting): 'EVENT_BUS_PRODUCER_CONFIG', 'DEFAULT_FILE_STORAGE', 'STATICFILES_STORAGE', + 'OPEN_EDX_FILTERS_CONFIG', ] }) @@ -515,6 +516,19 @@ def get_env_setting(setting): _YAML_TOKENS.get('EVENT_BUS_PRODUCER_CONFIG', {}) ) +# Merge OPEN_EDX_FILTERS_CONFIG from YAML into the default defined in common.py. +# Pipeline steps from YAML are appended after steps defined in common.py. +# The fail_silently value from YAML takes precedence over the one in common.py. +for _filter_type, _filter_config in _YAML_TOKENS.get('OPEN_EDX_FILTERS_CONFIG', {}).items(): + if _filter_type in OPEN_EDX_FILTERS_CONFIG: + OPEN_EDX_FILTERS_CONFIG[_filter_type]['pipeline'].extend( + _filter_config.get('pipeline', []) + ) + if 'fail_silently' in _filter_config: + OPEN_EDX_FILTERS_CONFIG[_filter_type]['fail_silently'] = _filter_config['fail_silently'] + else: + OPEN_EDX_FILTERS_CONFIG[_filter_type] = _filter_config + ####################################################################################################################### # HEY! Don't add anything to the end of this file. # Add your defaults to common.py instead!