Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 67 additions & 47 deletions lms/djangoapps/support/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,13 @@
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,
)
from xmodule.modulestore.tests.factories import CourseFactory

try:
from consent.models import DataSharingConsent
except ImportError: # pragma: no cover
pass


class SupportViewTestCase(ModuleStoreTestCase):
"""
Expand All @@ -96,6 +86,49 @@ def setUp(self):
assert success, 'Could not log in'


class ContactUsViewTests(SupportViewTestCase):
"""
Tests for ContactUsView.
"""

@override_settings(ZENDESK_URL='https://example.zendesk.com')
@patch('lms.djangoapps.support.views.contact_us.SupportContactContextRequested.run_filter')
def test_tags_run_through_filter_for_authenticated_user(self, mock_run_filter):
"""
For an authenticated user, the tags list is passed through the
SupportContactContextRequested filter, and the filter's return value is used
as the final tags list in the rendered context.

The enterprise-specific behavior of the filter's pipeline step (edx-enterprise's
SupportContactEnterpriseTagInjector) is covered by edx-enterprise's own test suite.
This view only needs to verify it wires the filter's return value through correctly.
"""
mock_run_filter.return_value = ['LMS', 'enterprise_learner']

response = self.client.get(reverse('support:contact_us'))

assert response.status_code == 200
mock_run_filter.assert_called_once()
_, call_kwargs = mock_run_filter.call_args
assert call_kwargs['tags'] == ['LMS']
assert call_kwargs['user'] == self.user
assert b'enterprise_learner' in response.content

def test_filter_not_called_for_anonymous_user(self):
"""
Anonymous users never reach the enterprise-tagging branch.
"""
self.client.logout()
with override_settings(ZENDESK_URL='https://example.zendesk.com'):
with patch(
'lms.djangoapps.support.views.contact_us.SupportContactContextRequested.run_filter'
) as mock_run_filter:
response = self.client.get(reverse('support:contact_us'))

assert response.status_code == 200
mock_run_filter.assert_not_called()


class SupportViewManageUserTests(SupportViewTestCase):
"""
Base class for support view tests.
Expand Down Expand Up @@ -359,7 +392,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'] == ''

Expand Down Expand Up @@ -400,52 +433,39 @@ def test_order_source_system_information(self):
assert len(data) == 1
assert data[0]['source_system'] == 'commercetools'

@override_settings(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):
"""
Enterprise enrollment data returned by the SupportEnrollmentDataRequested filter
is threaded into each enrollment's 'enterprise_course_enrollments' key.

The enterprise-specific behavior of the filter's pipeline step (edx-enterprise's
SupportEnterpriseEnrollmentDataInjector) is covered by edx-enterprise's own test suite.
This view only needs to verify it wires the filter's return value through correctly.
"""
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': {'consent_provided': True},
}
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_with(enrollment_data={}, user=self.student)
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 == [mock_enterprise_enrollment]

@ddt.data(
(True, 'Self Paced'),
Expand Down
6 changes: 2 additions & 4 deletions lms/djangoapps/support/views/contact_us.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 marketing_link, 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):
Expand Down Expand Up @@ -47,9 +47,7 @@ def get(self, request): # pylint: disable=missing-function-docstring
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

Expand Down
49 changes: 8 additions & 41 deletions lms/djangoapps/support/views/enrollments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 # pylint: disable=imported-auth-user
Expand All @@ -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
Expand All @@ -34,12 +34,6 @@
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__)

Expand Down Expand Up @@ -70,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):
"""
Expand Down Expand Up @@ -126,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)

Expand Down
16 changes: 16 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3305,3 +3305,19 @@ 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.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"],
},
}
14 changes: 14 additions & 0 deletions lms/envs/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def get_env_setting(setting):
'EVENT_BUS_PRODUCER_CONFIG',
'DEFAULT_FILE_STORAGE',
'STATICFILES_STORAGE',
'OPEN_EDX_FILTERS_CONFIG',
]
})

Expand Down Expand Up @@ -519,6 +520,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: # noqa: F405
OPEN_EDX_FILTERS_CONFIG[_filter_type]['pipeline'].extend( # noqa: F405
_filter_config.get('pipeline', [])
)
if 'fail_silently' in _filter_config:
OPEN_EDX_FILTERS_CONFIG[_filter_type]['fail_silently'] = _filter_config['fail_silently'] # noqa: F405
else:
OPEN_EDX_FILTERS_CONFIG[_filter_type] = _filter_config # noqa: F405

#######################################################################################################################
# HEY! Don't add anything to the end of this file.
# Add your defaults to common.py instead!
Expand Down
Loading