diff --git a/lms/djangoapps/course_home_api/course_metadata/tests/test_views.py b/lms/djangoapps/course_home_api/course_metadata/tests/test_views.py index 8e9760f0ca..3e58ecd646 100644 --- a/lms/djangoapps/course_home_api/course_metadata/tests/test_views.py +++ b/lms/djangoapps/course_home_api/course_metadata/tests/test_views.py @@ -99,32 +99,51 @@ class CourseHomeMetadataTests(BaseCourseHomeTests): 'enroll_user': True, 'instructor_role': False, 'masquerade_role': None, + 'dsc_required': False, 'expect_course_access': True, + 'error_code': None, }, { # Un-enrolled learners should NOT have access. 'enroll_user': False, 'instructor_role': False, 'masquerade_role': None, + 'dsc_required': False, 'expect_course_access': False, + 'error_code': 'enrollment_required' }, { # Un-enrolled instructors should have access. 'enroll_user': False, 'instructor_role': True, 'masquerade_role': None, + 'dsc_required': False, 'expect_course_access': True, + 'error_code': None }, { # Un-enrolled instructors masquerading as students should have access. 'enroll_user': False, 'instructor_role': True, 'masquerade_role': 'student', + 'dsc_required': False, 'expect_course_access': True, + 'error_code': None }, + { + # Data sharing Consent required learners should Not have access. + 'enroll_user': True, + 'instructor_role': False, + 'masquerade_role': None, + 'dsc_required': True, + 'expect_course_access': False, + 'error_code': 'data_sharing_access_required' + } ) @ddt.unpack - def test_course_access(self, enroll_user, instructor_role, masquerade_role, expect_course_access): + def test_course_access( + self, enroll_user, instructor_role, masquerade_role, dsc_required, expect_course_access, error_code + ): """ Test that course_access is calculated correctly based on access to MFE and access to the course itself. @@ -136,13 +155,12 @@ class CourseHomeMetadataTests(BaseCourseHomeTests): if masquerade_role: self.update_masquerade(role=masquerade_role) - response = self.client.get(self.url) + consent_url = 'dump/consent/url' if dsc_required else None + with mock.patch('openedx.features.enterprise_support.api.get_enterprise_consent_url', return_value=consent_url): + response = self.client.get(self.url) assert response.status_code == 200 - if expect_course_access: - assert response.data['course_access']['has_access'] - else: - assert not response.data['course_access']['has_access'] - + assert response.data['course_access']['has_access'] == expect_course_access + assert response.data['course_access']['error_code'] == error_code # Start date is used when handling some errors, so make sure it is present too assert response.data['start'] == self.course.start.isoformat() + 'Z' diff --git a/lms/djangoapps/course_home_api/course_metadata/views.py b/lms/djangoapps/course_home_api/course_metadata/views.py index 2e570bc399..23de19fe56 100644 --- a/lms/djangoapps/course_home_api/course_metadata/views.py +++ b/lms/djangoapps/course_home_api/course_metadata/views.py @@ -88,6 +88,7 @@ class CourseHomeMetadataView(RetrieveAPIView): 'load', check_if_enrolled=True, check_if_authenticated=True, + check_if_dsc_required=True, ) _, request.user = setup_masquerade( diff --git a/lms/djangoapps/courseware/access_response.py b/lms/djangoapps/courseware/access_response.py index 674003d6c8..9885b4169f 100644 --- a/lms/djangoapps/courseware/access_response.py +++ b/lms/djangoapps/courseware/access_response.py @@ -227,6 +227,17 @@ class EnrollmentRequiredAccessError(AccessError): super().__init__(error_code, developer_message, user_message) +class DataSharingConsentRequiredAccessError(AccessError): + """ + Access denied because the user must give Data sharing consent before access it. + """ + def __init__(self, consent_url): + error_code = "data_sharing_access_required" + developer_message = consent_url + user_message = _("You must give Data Sharing Consent for the course") + super().__init__(error_code, developer_message, user_message) + + class AuthenticationRequiredAccessError(AccessError): """ Access denied because the user must be authenticated to see it diff --git a/lms/djangoapps/courseware/access_utils.py b/lms/djangoapps/courseware/access_utils.py index 9fa9c76203..ab82296716 100644 --- a/lms/djangoapps/courseware/access_utils.py +++ b/lms/djangoapps/courseware/access_utils.py @@ -7,25 +7,23 @@ It allows us to share code between access.py and block transformers. from datetime import datetime, timedelta from logging import getLogger +from crum import get_current_request from django.conf import settings from pytz import UTC -from lms.djangoapps.courseware.access_response import ( - AccessResponse, - StartDateError, - EnrollmentRequiredAccessError, - AuthenticationRequiredAccessError, -) -from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_student -from openedx.core.djangoapps.util.user_messages import PageLevelMessages # lint-amnesty, pylint: disable=unused-import -from openedx.core.djangolib.markup import HTML # lint-amnesty, pylint: disable=unused-import -from openedx.features.course_experience import ( - COURSE_PRE_START_ACCESS_FLAG, - COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, -) + from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.roles import CourseBetaTesterRole -from xmodule.util.xmodule_django import get_current_request_hostname # lint-amnesty, pylint: disable=wrong-import-order +from lms.djangoapps.courseware.access_response import ( + AccessResponse, + AuthenticationRequiredAccessError, + DataSharingConsentRequiredAccessError, + EnrollmentRequiredAccessError, + StartDateError +) +from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_student +from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, COURSE_PRE_START_ACCESS_FLAG from xmodule.course_module import COURSE_VISIBILITY_PUBLIC # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.util.xmodule_django import get_current_request_hostname # lint-amnesty, pylint: disable=wrong-import-order DEBUG_ACCESS = False log = getLogger(__name__) @@ -168,3 +166,23 @@ def check_public_access(course, visibilities): return ACCESS_GRANTED return ACCESS_DENIED + + +def check_data_sharing_consent(course_id): + """ + Grants access if the user is do not need DataSharing consent, otherwise returns data sharing link. + + Returns: + AccessResponse: Either ACCESS_GRANTED or DataSharingConsentRequiredAccessError + """ + from openedx.features.enterprise_support.api import get_enterprise_consent_url + consent_url = get_enterprise_consent_url( + request=get_current_request(), + course_id=course_id, + return_to='courseware', + enrollment_exists=True, + source='CoursewareAccess' + ) + if consent_url: + return DataSharingConsentRequiredAccessError(consent_url=consent_url) + return ACCESS_GRANTED diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index bf14f7e2fc..b46aaba2ba 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -19,17 +19,21 @@ from fs.errors import ResourceNotFound from opaque_keys.edx.keys import UsageKey from path import Path as path -from openedx.core.lib.cache_utils import request_cached - +from common.djangoapps.edxmako.shortcuts import render_to_string +from common.djangoapps.static_replace import replace_static_urls +from common.djangoapps.util.date_utils import strftime_localized from lms.djangoapps import branding +from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.access_response import ( AuthenticationRequiredAccessError, EnrollmentRequiredAccessError, MilestoneAccessError, OldMongoAccessError, - StartDateError, + StartDateError ) +from lms.djangoapps.courseware.access_utils import check_authentication, check_data_sharing_consent, check_enrollment +from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException from lms.djangoapps.courseware.date_summary import ( CertificateAvailableDate, CourseAssignmentDate, @@ -40,29 +44,20 @@ from lms.djangoapps.courseware.date_summary import ( VerificationDeadlineDate, VerifiedUpgradeDeadlineDate ) -from lms.djangoapps.courseware.exceptions import CourseRunNotFound +from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, CourseRunNotFound from lms.djangoapps.courseware.masquerade import check_content_start_date_for_masquerade_user from lms.djangoapps.courseware.model_data import FieldDataCache from lms.djangoapps.courseware.module_render import get_module -from common.djangoapps.edxmako.shortcuts import render_to_string -from lms.djangoapps.courseware.access_utils import ( - check_authentication, - check_enrollment, -) -from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException -from lms.djangoapps.courseware.exceptions import CourseAccessRedirect -from lms.djangoapps.course_blocks.api import get_course_blocks +from lms.djangoapps.survey.utils import SurveyRequiredAccessError, check_survey_required_and_unanswered from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.enrollments.api import get_course_enrollment_details from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.lib.api.view_utils import LazySequence +from openedx.core.lib.cache_utils import request_cached from openedx.core.lib.courses import get_course_by_id from openedx.features.course_duration_limits.access import AuditExpiredError from openedx.features.course_experience import RELATIVE_DATES_FLAG from openedx.features.course_experience.utils import is_block_structure_complete_for_assignments -from common.djangoapps.static_replace import replace_static_urls -from lms.djangoapps.survey.utils import SurveyRequiredAccessError, check_survey_required_and_unanswered -from common.djangoapps.util.date_utils import strftime_localized from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.x_module import STUDENT_VIEW # lint-amnesty, pylint: disable=wrong-import-order @@ -135,7 +130,15 @@ def get_course_overview_with_access(user, action, course_key, check_if_enrolled= return course_overview -def check_course_access(course, user, action, check_if_enrolled=False, check_survey_complete=True, check_if_authenticated=False): # lint-amnesty, pylint: disable=line-too-long +def check_course_access( + course, + user, + action, + check_if_enrolled=False, + check_survey_complete=True, + check_if_authenticated=False, + check_if_dsc_required=False, +): """ Check that the user has the access to perform the specified action on the course (CourseBlock|CourseOverview). @@ -161,6 +164,11 @@ def check_course_access(course, user, action, check_if_enrolled=False, check_sur if not enrollment_access_response: return enrollment_access_response + if check_if_dsc_required: + data_sharing_consent_response = check_data_sharing_consent(course) + if not data_sharing_consent_response: + return data_sharing_consent_response + # Redirect if the user must answer a survey before entering the course. if check_survey_complete and action == 'load': survey_access_response = check_survey_required_and_unanswered(user, course)