diff --git a/lms/djangoapps/course_wiki/middleware.py b/lms/djangoapps/course_wiki/middleware.py index 37bef0b74740..802ab9cc3b27 100644 --- a/lms/djangoapps/course_wiki/middleware.py +++ b/lms/djangoapps/course_wiki/middleware.py @@ -7,12 +7,12 @@ from django.http import Http404 from django.shortcuts import redirect from django.utils.deprecation import MiddlewareMixin +from openedx_filters.learning.filters import CoursewareViewStarted from wiki.models import reverse from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import get_course_overview_with_access, get_course_with_access from openedx.core.lib.request_utils import course_id_from_url -from openedx.features.enterprise_support.api import get_enterprise_consent_url from common.djangoapps.student.models import CourseEnrollment from xmodule.modulestore.django import modulestore @@ -96,10 +96,11 @@ def process_view(self, request, view_func, view_args, view_kwargs): # lint-amne # we'll redirect them to the course about page return redirect('about_course', str(course_id)) - # If we need enterprise data sharing consent for this course, then redirect to the form. - consent_url = get_enterprise_consent_url(request, str(course_id), source='WikiAccessMiddleware') - if consent_url: - return redirect(consent_url) + # If a plugin requires a redirect for this course, redirect now. + try: + CoursewareViewStarted.run_filter(course_key=course_id, view_name='WikiAccessMiddleware') + except CoursewareViewStarted.RedirectToUrl as exc: + return redirect(exc.redirect_to) # set the course onto here so that the wiki template can show the course navigation request.course = course diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py index 7821f659d983..7fd4a25f4746 100644 --- a/lms/djangoapps/course_wiki/tests/tests.py +++ b/lms/djangoapps/course_wiki/tests/tests.py @@ -5,15 +5,15 @@ from unittest.mock import patch from django.urls import reverse +from openedx_filters.learning.filters import CoursewareViewStarted from lms.djangoapps.courseware.tests.tests import LoginEnrollmentTestCase from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url -from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order -class WikiRedirectTestCase(EnterpriseTestConsentRequired, LoginEnrollmentTestCase, ModuleStoreTestCase): +class WikiRedirectTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): """ Tests for wiki course redirection. """ @@ -202,27 +202,33 @@ def test_create_wiki_with_long_course_id(self): assert resp.status_code == 200 @patch.dict("django.conf.settings.FEATURES", {'ALLOW_WIKI_ROOT_ACCESS': True}) - @patch('openedx.features.enterprise_support.api.enterprise_customer_for_request') - def test_consent_required(self, mock_enterprise_customer_for_request): + @patch('openedx_filters.learning.filters.CoursewareViewStarted.run_filter') + def test_filter_redirect(self, mock_run_filter): """ - Test that enterprise data sharing consent is required when enabled for the various courseware views. + Test that wiki views redirect when the CoursewareViewStarted filter provides a URL. """ - # ENT-924: Temporary solution to replace sensitive SSO usernames. - mock_enterprise_customer_for_request.return_value = None + redirect_url = 'http://example.com/redirect' + mock_run_filter.side_effect = CoursewareViewStarted.RedirectToUrl(message="redirect", redirect_to=redirect_url) - # Public wikis can be accessed by non-enrolled users, and so direct access is not gated by the consent page + # Public wikis can be accessed by non-enrolled users, and so direct access is not gated by the redirect course = CourseFactory.create() course.allow_public_wiki_access = False course.save() - # However, for private wikis, enrolled users must pass through the consent gate + # However, for private wikis, enrolled users must pass through the filter redirect gate # (Unenrolled users are redirected to course/about) course_id = str(course.id) self.login(self.student, self.password) self.enroll(course) - for (url, status_code) in ( - (reverse('course_wiki', kwargs={'course_id': course_id}), 302), - (f'/courses/{course_id}/wiki/', 200), - ): - self.verify_consent_required(self.client, url, status_code=status_code) # lint-amnesty, pylint: disable=no-value-for-parameter + # The course_wiki view is decorated with courseware_view_hooks which calls the filter + url = reverse('course_wiki', kwargs={'course_id': course_id}) + response = self.client.get(url) + assert response.status_code == 302 + assert response['Location'] == redirect_url + + # The wiki middleware (/courses/.../wiki/) also calls the filter + url = f'/courses/{course_id}/wiki/' + response = self.client.get(url) + assert response.status_code == 302 + assert response['Location'] == redirect_url diff --git a/lms/djangoapps/course_wiki/views.py b/lms/djangoapps/course_wiki/views.py index 955bf4e04268..8c8ddd50ed51 100644 --- a/lms/djangoapps/course_wiki/views.py +++ b/lms/djangoapps/course_wiki/views.py @@ -14,10 +14,10 @@ from wiki.models import Article, URLPath from lms.djangoapps.course_wiki.utils import course_wiki_slug +from lms.djangoapps.courseware.decorators import courseware_view_hooks from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangolib.markup import Text from openedx.core.lib.courses import get_course_by_id -from openedx.features.enterprise_support.api import data_sharing_consent_required log = logging.getLogger(__name__) @@ -31,7 +31,7 @@ def root_create(request): return redirect('wiki:get', path=root.path) -@data_sharing_consent_required +@courseware_view_hooks def course_wiki_redirect(request, course_id, wiki_path=""): """ This redirects to whatever page on the wiki that the course designates diff --git a/lms/djangoapps/courseware/decorators.py b/lms/djangoapps/courseware/decorators.py new file mode 100644 index 000000000000..dbe5c7baf4b0 --- /dev/null +++ b/lms/djangoapps/courseware/decorators.py @@ -0,0 +1,46 @@ +""" +Decorators for courseware views. +""" +import functools + +from django.shortcuts import redirect +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from openedx_filters.learning.filters import CoursewareViewStarted + + +def courseware_view_hooks(view_func): + """ + Decorator that calls the CoursewareViewStarted filter before rendering a courseware view. + + If any pipeline step raises ``CoursewareViewStarted.RedirectToUrl``, the user is + redirected to that URL. Otherwise, the original view is rendered normally. + + Usage:: + + @courseware_view_hooks + def my_view(request, course_id, ...): + ... + + Works with both function-based views and ``method_decorator``-wrapped class-based views. + The wrapped view must accept ``course_id`` as its first argument after ``request``, which + binds whether callers pass it positionally or as a URL keyword argument. + """ + @functools.wraps(view_func) + def _wrapper(request, course_id, *args, **kwargs): + try: + course_key = CourseKey.from_string(course_id) + except InvalidKeyError: + # Skip bad request which contains a malformed course_id; let the view logic raise an error. + return view_func(request, course_id, *args, **kwargs) + + try: + view_name = getattr(view_func, '__name__', '') + CoursewareViewStarted.run_filter(course_key=course_key, view_name=view_name) + except CoursewareViewStarted.RedirectToUrl as exc: + # One of the pipeline steps wants us to block view execution and redirect to a specific URL. + return redirect(exc.redirect_to) + + return view_func(request, course_id, *args, **kwargs) + + return _wrapper diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index f9668853b244..bdb8bf543dbe 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -20,11 +20,10 @@ from common.djangoapps.student.tests.factories import StaffFactory from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.tests.helpers import CourseAccessTestMixin, LoginEnrollmentTestCase -from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory -class TestViewAuth(EnterpriseTestConsentRequired, ModuleStoreTestCase, LoginEnrollmentTestCase): +class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Check that view authentication works properly. """ diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 4a3f16523ce5..98c01570e004 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -26,6 +26,7 @@ from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch from freezegun import freeze_time from opaque_keys.edx.keys import CourseKey, UsageKey +from openedx_filters.learning.filters import CoursewareViewStarted from pytz import UTC from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel from rest_framework import status @@ -111,7 +112,6 @@ EnterpriseCustomerUserFactory, EnterpriseCustomerFactory ) -from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from openedx.features.enterprise_support.api import add_enterprise_customer_to_session from enterprise.api.v1.serializers import EnterpriseCustomerSerializer @@ -1607,6 +1607,19 @@ def mock_certificate_downloadable_status( 'earned_but_not_available': earned_but_not_available, } + @patch('openedx_filters.learning.filters.CoursewareViewStarted.run_filter') + def test_redirects_when_courseware_view_filter_raises(self, mock_run_filter): + """ + Redirects to the URL raised by the CoursewareViewStarted filter on progress page URLs. + """ + redirect_url = 'http://example.com/redirect' + mock_run_filter.side_effect = CoursewareViewStarted.RedirectToUrl(message="redirect", redirect_to=redirect_url) + + resp = self._get_progress_page(expected_status_code=302) + assert resp['Location'] == redirect_url + resp = self._get_student_progress_page(expected_status_code=302) + assert resp['Location'] == redirect_url + @ddt.ddt class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): @@ -2624,34 +2637,6 @@ def course_options(self): return options -class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ModuleStoreTestCase): - """ - Ensure that the Enterprise Data Consent redirects are in place only when consent is required. - """ - def setUp(self): - super().setUp() - self.user = UserFactory.create() - assert self.client.login(username=self.user.username, password=TEST_PASSWORD) - self.course = CourseFactory.create() - CourseOverview.load_from_module_store(self.course.id) - CourseEnrollmentFactory(user=self.user, course_id=self.course.id) - - @patch('openedx.features.enterprise_support.api.enterprise_customer_for_request') - def test_consent_required(self, mock_enterprise_customer_for_request): - """ - Test that enterprise data sharing consent is required when enabled for the various courseware views. - """ - # ENT-924: Temporary solution to replace sensitive SSO usernames. - mock_enterprise_customer_for_request.return_value = None - - course_id = str(self.course.id) - for url in ( - reverse("progress", kwargs=dict(course_id=course_id)), - reverse("student_progress", kwargs=dict(course_id=course_id, student_id=str(self.user.id))), - ): - self.verify_consent_required(self.client, url) # lint-amnesty, pylint: disable=no-value-for-parameter - - @ddt.ddt class AccessUtilsTestCase(ModuleStoreTestCase): """ diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index 1034674b7b41..e9748e60ec5b 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -19,11 +19,11 @@ from xmodule.modulestore.django import modulestore from common.djangoapps.util.views import ensure_valid_course_key +from lms.djangoapps.courseware.decorators import courseware_view_hooks from lms.djangoapps.courseware.exceptions import Redirect from lms.djangoapps.courseware.masquerade import setup_masquerade from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG -from openedx.features.enterprise_support.api import data_sharing_consent_required from ..block_render import get_block_for_descriptor from ..courses import get_course_with_access @@ -44,7 +44,7 @@ def enable_unenrolled_access(self): @method_decorator(ensure_csrf_cookie) @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) @method_decorator(ensure_valid_course_key) - @method_decorator(data_sharing_consent_required) + @method_decorator(courseware_view_hooks) def get(self, request, course_id, section=None, subsection=None, position=None): """ Instead of loading the legacy courseware sequences pages, load the equivalent URL diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index ccee9a0fa729..01a28de1e441 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -93,6 +93,7 @@ sort_by_start_date ) from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link +from lms.djangoapps.courseware.decorators import courseware_view_hooks from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect from lms.djangoapps.courseware.masquerade import is_masquerading_as_specific_student, setup_masquerade from lms.djangoapps.courseware.model_data import FieldDataCache @@ -152,7 +153,6 @@ ) from openedx.features.course_experience.utils import dates_banner_should_display from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML -from openedx.features.enterprise_support.api import data_sharing_consent_required from ..block_render import get_block, get_block_by_usage_id, get_block_for_descriptor from ..tabs import _get_dynamic_tabs @@ -530,7 +530,7 @@ class CourseTabView(EdxFragmentView): """ @method_decorator(ensure_csrf_cookie) @method_decorator(ensure_valid_course_key) - @method_decorator(data_sharing_consent_required) + @method_decorator(courseware_view_hooks) def get(self, request, course_id, tab_type, **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Displays a course tab page that contains a web fragment. @@ -979,7 +979,7 @@ def dates(request, course_id): @login_required @cache_control(no_cache=True, no_store=True, must_revalidate=True) @ensure_valid_course_key -@data_sharing_consent_required +@courseware_view_hooks def progress(request, course_id, student_id=None): """ Display the progress page. """ course_key = CourseKey.from_string(course_id) diff --git a/lms/djangoapps/discussion/tests/test_views_v2.py b/lms/djangoapps/discussion/tests/test_views_v2.py index 0f268e93eb32..022471f747a1 100644 --- a/lms/djangoapps/discussion/tests/test_views_v2.py +++ b/lms/djangoapps/discussion/tests/test_views_v2.py @@ -5,38 +5,28 @@ import json import logging -from datetime import datetime -from unittest import mock -from unittest.mock import ANY, Mock, call, patch +from unittest.mock import patch import ddt import pytest from django.conf import settings from django.http import Http404 -from django.test.client import Client, RequestFactory +from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse -from django.utils import translation -from edx_django_utils.cache import RequestCache from edx_toggles.toggles.testutils import override_waffle_flag from lms.djangoapps.discussion.django_comment_client.tests.mixins import ( MockForumApiMixin, ) -from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ( - TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase, SharedModuleStoreTestCase, ) from xmodule.modulestore.tests.factories import ( CourseFactory, BlockFactory, - check_mongo_calls, ) -from common.djangoapps.course_modes.models import CourseMode -from common.djangoapps.course_modes.tests.factories import CourseModeFactory from common.djangoapps.student.roles import CourseStaffRole, UserBasedRole from common.djangoapps.student.tests.factories import ( AdminFactory, @@ -44,12 +34,7 @@ UserFactory, ) from common.djangoapps.util.testing import EventTestMixin, UrlResetMixin -from lms.djangoapps.courseware.exceptions import CourseAccessRedirect from lms.djangoapps.discussion import views -from lms.djangoapps.discussion.django_comment_client.constants import ( - TYPE_ENTRY, - TYPE_SUBCATEGORY, -) from lms.djangoapps.discussion.django_comment_client.permissions import get_team from lms.djangoapps.discussion.django_comment_client.tests.group_id import ( CohortedTopicGroupIdTestMixinV2, @@ -61,28 +46,16 @@ ) from lms.djangoapps.discussion.django_comment_client.tests.utils import ( CohortedTestCase, - config_course_discussions, - topic_name_to_id, ) from lms.djangoapps.discussion.django_comment_client.utils import strip_none from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE -from lms.djangoapps.discussion.views import ( - _get_discussion_default_topic_id, - course_discussions_settings_handler, -) from lms.djangoapps.teams.tests.factories import ( CourseTeamFactory, CourseTeamMembershipFactory, ) from openedx.core.djangoapps.course_groups.models import CourseUserGroup -from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts -from openedx.core.djangoapps.course_groups.tests.test_views import CohortViewsTestCase -from openedx.core.djangoapps.django_comment_common.comment_client.utils import ( - CommentClientPaginatedResult, -) from openedx.core.djangoapps.django_comment_common.models import ( FORUM_ROLE_STUDENT, - CourseDiscussionSettings, ) from openedx.core.djangoapps.django_comment_common.utils import ( ThreadContext, @@ -91,10 +64,6 @@ from openedx.core.djangoapps.util.testing import ContentGroupTestCase from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.lib.teams_config import TeamsConfig -from openedx.features.content_type_gating.models import ContentTypeGatingConfig -from openedx.features.enterprise_support.tests.mixins.enterprise import ( - EnterpriseTestConsentRequired, -) log = logging.getLogger(__name__) @@ -1071,75 +1040,6 @@ def _test_unicode_data( assert response_data["discussion_data"][0]["body"] == text -class EnterpriseConsentTestCase( - EnterpriseTestConsentRequired, - UrlResetMixin, - ModuleStoreTestCase, - ForumViewsUtilsMixin, -): - """ - Ensure that the Enterprise Data Consent redirects are in place only when consent is required. - """ - - CREATE_USER = False - - @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) - def setUp(self): - # Invoke UrlResetMixin setUp - super().setUp() - username = "foo" - password = "bar" - - self.discussion_id = "dummy_discussion_id" - self.course = CourseFactory.create( - discussion_topics={"dummy discussion": {"id": self.discussion_id}} - ) - self.student = UserFactory.create(username=username, password=password) - CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id) - assert self.client.login(username=username, password=password) - - self.addCleanup(translation.deactivate) - - @classmethod - def setUpClass(cls): - super().setUpClass() - super().setUpClassAndForumMock() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - super().disposeForumMocks() - - @patch("openedx.features.enterprise_support.api.enterprise_customer_for_request") - def test_consent_required(self, mock_enterprise_customer_for_request): - """ - Test that enterprise data sharing consent is required when enabled for the various discussion views. - """ - # ENT-924: Temporary solution to replace sensitive SSO usernames. - mock_enterprise_customer_for_request.return_value = None - - thread_id = "dummy" - course_id = str(self.course.id) - self._configure_mock_responses( - course=self.course, text="dummy", thread_id=thread_id - ) - - for url in ( - reverse("forum_form_discussion", kwargs=dict(course_id=course_id)), - reverse( - "single_thread", - kwargs=dict( - course_id=course_id, - discussion_id=self.discussion_id, - thread_id=thread_id, - ), - ), - ): - self.verify_consent_required( # pylint: disable=no-value-for-parameter - self.client, url - ) - - class InlineDiscussionGroupIdTestCase( # lint-amnesty, pylint: disable=missing-class-docstring CohortedTestCase, CohortedTopicGroupIdTestMixinV2, diff --git a/openedx/core/lib/request_utils.py b/openedx/core/lib/request_utils.py index 36d2b12ec3d5..ba275bbdab59 100644 --- a/openedx/core/lib/request_utils.py +++ b/openedx/core/lib/request_utils.py @@ -66,9 +66,9 @@ def safe_get_host(request): return configuration_helpers.get_value('site_domain', settings.SITE_NAME) -def course_id_from_url(url): +def course_id_from_url(url: str | None) -> CourseKey | None: """ - Extracts the course_id from the given `url`. + Extracts and parses the course_id from the given `url` and returns a CourseKey. """ if not url: return None diff --git a/openedx/features/enterprise_support/api.py b/openedx/features/enterprise_support/api.py index f6c3e229a7ed..fd11b8fe7d95 100644 --- a/openedx/features/enterprise_support/api.py +++ b/openedx/features/enterprise_support/api.py @@ -4,7 +4,6 @@ import logging import traceback -from functools import wraps from urllib.parse import urljoin import requests @@ -14,7 +13,6 @@ from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.contrib.sites.models import Site from django.core.cache import cache -from django.shortcuts import redirect from django.template.loader import render_to_string from django.urls import reverse from django.utils.http import urlencode @@ -340,44 +338,6 @@ def activate_learner_enterprise(request, user, enterprise_customer): return False -def data_sharing_consent_required(view_func): - """ - Decorator which makes a view method redirect to the Data Sharing Consent form if: - - * The wrapped method is passed request, course_id as the first two arguments. - * Enterprise integration is enabled - * Data sharing consent is required before accessing this course view. - * The request.user has not yet given data sharing consent for this course. - - After granting consent, the user will be redirected back to the original request.path. - - """ - - @wraps(view_func) - def inner(request, course_id, *args, **kwargs): - """ - Redirect to the consent page if the request.user must consent to data sharing before viewing course_id. - - Otherwise, just call the wrapped view function. - """ - # Redirect to the consent URL, if consent is required. - source = getattr(view_func, '__name__', '') - consent_url = get_enterprise_consent_url(request, course_id, enrollment_exists=True, source=source) - if consent_url: - real_user = getattr(request.user, 'real_user', request.user) - LOGGER.info( - 'User %s cannot access the course %s because they have not granted consent', - real_user, - course_id, - ) - return redirect(consent_url) - - # Otherwise, drop through to wrapped view - return view_func(request, course_id, *args, **kwargs) - - return inner - - def enterprise_enabled(): """ Determines whether the Enterprise app is installed diff --git a/openedx/features/enterprise_support/tests/mixins/enterprise.py b/openedx/features/enterprise_support/tests/mixins/enterprise.py index 8b8f420dddfb..355a190bef2f 100644 --- a/openedx/features/enterprise_support/tests/mixins/enterprise.py +++ b/openedx/features/enterprise_support/tests/mixins/enterprise.py @@ -5,14 +5,9 @@ import json -from unittest import mock - import httpretty from django.conf import settings from django.core.cache import cache -from django.test import SimpleTestCase -from django.urls import reverse -from openedx.features.enterprise_support.tests import FAKE_ENTERPRISE_CUSTOMER class EnterpriseServiceMockMixin: @@ -282,62 +277,3 @@ def mock_enterprise_learner_api( body=enterprise_learner_api_response_json, content_type='application/json' ) - - -class EnterpriseTestConsentRequired(SimpleTestCase): - """ - Mixin to help test the data_sharing_consent_required decorator. - """ - - @mock.patch('openedx.features.enterprise_support.utils.get_enterprise_learner_generic_name') - @mock.patch('openedx.features.enterprise_support.api.enterprise_customer_from_api') - @mock.patch('openedx.features.enterprise_support.api.enterprise_customer_uuid_for_request') - @mock.patch('openedx.features.enterprise_support.api.reverse') - @mock.patch('openedx.features.enterprise_support.api.enterprise_enabled') - @mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course') - def verify_consent_required( - self, - client, - url, - mock_consent_necessary, - mock_enterprise_enabled, - mock_reverse, - mock_enterprise_customer_uuid_for_request, - mock_enterprise_customer_from_api, - mock_get_enterprise_learner_generic_name, - status_code=200, - ): - """ - Verify that the given URL redirects to the consent page when consent is required, - and doesn't redirect to the consent page when consent is not required. - """ - - def mock_consent_reverse(*args, **kwargs): - if args[0] == 'grant_data_sharing_permissions': - return '/enterprise/grant_data_sharing_permissions' - return reverse(*args, **kwargs) - - # ENT-924: Temporary solution to replace sensitive SSO usernames. - mock_get_enterprise_learner_generic_name.return_value = '' - - mock_reverse.side_effect = mock_consent_reverse - mock_enterprise_enabled.return_value = True - mock_enterprise_customer_uuid_for_request.return_value = 'fake-uuid' - mock_enterprise_customer_from_api.return_value = FAKE_ENTERPRISE_CUSTOMER - # Ensure that when consent is necessary, the user is redirected to the consent page. - mock_consent_necessary.return_value = True - response = client.get(url) - while(response.status_code == 302 and 'grant_data_sharing_permissions' not in response.url): - response = client.get(response.url) - assert response.status_code == 302 - assert 'grant_data_sharing_permissions' in response.url - - # Ensure that when consent is not necessary, the user continues through to the requested page. - mock_consent_necessary.return_value = False - response = client.get(url) - assert response.status_code == status_code - - # If we were expecting a redirect, ensure it's not to the data sharing permission page - if status_code == 302: - assert 'grant_data_sharing_permissions' not in response.url - return response diff --git a/openedx/features/enterprise_support/tests/test_api.py b/openedx/features/enterprise_support/tests/test_api.py index 80e83d8a2721..e09fed23f5ec 100644 --- a/openedx/features/enterprise_support/tests/test_api.py +++ b/openedx/features/enterprise_support/tests/test_api.py @@ -11,7 +11,6 @@ from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core.cache import cache -from django.http import HttpResponseRedirect from django.test.utils import override_settings from django.urls import reverse from edx_django_utils.cache import TieredCache, get_cache_key @@ -32,7 +31,6 @@ activate_learner_enterprise, add_enterprise_customer_to_session, consent_needed_for_course, - data_sharing_consent_required, enterprise_customer_for_request, enterprise_customer_from_api, enterprise_customer_from_session, @@ -819,84 +817,6 @@ def test_enterprise_customer_for_request_with_session(self): assert mock_enterprise_customer_from_api.called is False assert mock_enterprise_customer_from_session.called is True - def check_data_sharing_consent(self, consent_required=False, consent_url=None): - """ - Used to test the data_sharing_consent_required view decorator. - """ - - # Test by wrapping a function that has the expected signature - @data_sharing_consent_required - def view_func(request, course_id, *args, **kwargs): - """ - Return the function arguments, so they can be tested. - """ - return ((request, course_id,) + args, kwargs) - - # Call the wrapped function - args = (mock.MagicMock(), 'course-id', 'another arg', 'and another') - kwargs = dict(a=1, b=2, c=3) - response = view_func(*args, **kwargs) - - # If consent required, then the response should be a redirect to the consent URL, and the view function would - # not be called. - if consent_required: - assert isinstance(response, HttpResponseRedirect) - assert response.url == consent_url # pylint: disable=no-member - - # Otherwise, the view function should have been called with the expected arguments. - else: - assert response == (args, kwargs) - - @mock.patch('openedx.features.enterprise_support.api.enterprise_enabled') - @mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course') - def test_data_consent_required_enterprise_disabled(self, - mock_consent_necessary, - mock_enterprise_enabled): - """ - Verify that the wrapped view is called directly when enterprise integration is disabled, - without checking for course consent necessary. - """ - mock_enterprise_enabled.return_value = False - - self.check_data_sharing_consent(consent_required=False) - - mock_enterprise_enabled.assert_called_once() - mock_consent_necessary.assert_not_called() - - @mock.patch('openedx.features.enterprise_support.api.enterprise_enabled') - @mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course') - def test_no_course_data_consent_required(self, - mock_consent_necessary, - mock_enterprise_enabled): - """ - Verify that the wrapped view is called directly when enterprise integration is enabled, - and no course consent is required. - """ - mock_enterprise_enabled.return_value = True - mock_consent_necessary.return_value = False - - self.check_data_sharing_consent(consent_required=False) - - mock_enterprise_enabled.assert_called_once() - mock_consent_necessary.assert_called_once() - - @mock.patch('openedx.features.enterprise_support.api.enterprise_enabled') - @mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course') - @mock.patch('openedx.features.enterprise_support.api.get_enterprise_consent_url') - def test_data_consent_required(self, mock_get_consent_url, mock_consent_necessary, mock_enterprise_enabled): - """ - Verify that the wrapped function returns a redirect to the consent URL when enterprise integration is enabled, - and course consent is required. - """ - mock_enterprise_enabled.return_value = True - mock_consent_necessary.return_value = True - consent_url = '/abc/def' - mock_get_consent_url.return_value = consent_url - - self.check_data_sharing_consent(consent_required=True, consent_url=consent_url) - - mock_get_consent_url.assert_called_once() - @ddt.data(True, False) @httpretty.activate @mock.patch('openedx.features.enterprise_support.api.enterprise_customer_uuid_for_request') diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 459143e839c5..785e6f3b084a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -42,7 +42,7 @@ django-stubs<6 # The team that owns this package will manually bump this package rather than having it pulled in automatically. # This is to allow them to better control its deployment and to do it in a process that works better # for them. -edx-enterprise==8.0.19 +edx-enterprise==8.1.0 # Date: 2023-07-26 # Our legacy Sass code is incompatible with anything except this ancient libsass version. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 44e5821715e7..3f17745b77dd 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -473,7 +473,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.0.19 +edx-enterprise==8.1.0 # via # -c requirements/constraints.txt # -r requirements/edx/kernel.in @@ -829,7 +829,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.3.0 +openedx-filters==3.5.0 # via # -r requirements/edx/kernel.in # edx-enterprise diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index e87a2efdb558..88a28e07d325 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -747,7 +747,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.0.19 +edx-enterprise==8.1.0 # via # -c requirements/constraints.txt # -r requirements/edx/doc.txt @@ -1375,7 +1375,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.3.0 +openedx-filters==3.5.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 351bb2c992f5..dd53c957d475 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -557,7 +557,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.0.19 +edx-enterprise==8.1.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt @@ -1001,7 +1001,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.3.0 +openedx-filters==3.5.0 # via # -r requirements/edx/base.txt # edx-enterprise diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 00255a359dc1..832ab8585b78 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -578,7 +578,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.0.19 +edx-enterprise==8.1.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt @@ -1046,7 +1046,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.3.0 +openedx-filters==3.5.0 # via # -r requirements/edx/base.txt # edx-enterprise