diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index 23d0ce3d3f..85eb5bac1c 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -623,4 +623,4 @@ def uses_shib(course): Returns a boolean indicating if Shibboleth authentication is set for this course. """ - return course.enrollment_domain and course.enrollment_domain.startswith(settings.SHIBBOLETH_DOMAIN_PREFIX) + return bool(course.enrollment_domain and course.enrollment_domain.startswith(settings.SHIBBOLETH_DOMAIN_PREFIX)) diff --git a/openedx/core/djangoapps/courseware_api/serializers.py b/openedx/core/djangoapps/courseware_api/serializers.py index 191d832c5f..d24e89cec4 100644 --- a/openedx/core/djangoapps/courseware_api/serializers.py +++ b/openedx/core/djangoapps/courseware_api/serializers.py @@ -73,10 +73,20 @@ class CourseProgramSerializer(serializers.Serializer): # lint-amnesty, pylint: } +class PrerequisiteCourseSerializer(serializers.Serializer): + """ + Serializer for prerequisite course data with the serialized course key and display name. + """ + key = serializers.CharField() + display = serializers.CharField() + + class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-method """ Serializer for Course objects providing minimal data about the course. - Compare this with CourseDetailSerializer. + + For detailed information about what each field is for, see the docstring of the + CoursewareInformation class. """ access_expiration = serializers.DictField() @@ -115,6 +125,40 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract- is_integrity_signature_enabled = serializers.BooleanField() user_needs_integrity_signature = serializers.BooleanField() learning_assistant_enabled = serializers.BooleanField() + show_courseware_link = serializers.BooleanField() + is_course_full = serializers.BooleanField() + can_enroll = serializers.BooleanField() + invitation_only = serializers.BooleanField() + is_shib_course = serializers.BooleanField() + allow_anonymous = serializers.BooleanField() + ecommerce_checkout = serializers.BooleanField() + single_paid_mode = serializers.DictField() + ecommerce_checkout_link = AbsoluteURLField() + course_image_urls = serializers.ListField( + child=serializers.CharField(), + allow_empty=True, + default=list, + ) + start_date_is_still_default = serializers.BooleanField() + advertised_start = serializers.CharField() + course_price = serializers.CharField() + pre_requisite_courses = serializers.ListField( + child=PrerequisiteCourseSerializer(), + allow_empty=True, + default=list, + ) + about_sidebar_html = serializers.CharField( + allow_blank=True, + allow_null=True, + default=None, + ) + display_number_with_default = serializers.CharField() + display_org_with_default = serializers.CharField() + overview = serializers.CharField( + allow_blank=True, + allow_null=True, + default=None, + ) def __init__(self, *args, **kwargs): """ diff --git a/openedx/core/djangoapps/courseware_api/tests/test_views.py b/openedx/core/djangoapps/courseware_api/tests/test_views.py index ef2f9ef247..1606d245c0 100644 --- a/openedx/core/djangoapps/courseware_api/tests/test_views.py +++ b/openedx/core/djangoapps/courseware_api/tests/test_views.py @@ -15,7 +15,7 @@ from django.test import override_settings from django.test.client import RequestFactory from edx_django_utils.cache import TieredCache -from edx_toggles.toggles.testutils import override_waffle_flag +from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch from xmodule.data import CertificatesDisplayBehaviors from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase @@ -45,7 +45,7 @@ from common.djangoapps.student.roles import CourseInstructorRole from common.djangoapps.student.tests.factories import CourseEnrollmentCelebrationFactory, UserFactory from openedx.core.djangoapps.agreements.api import create_integrity_signature from openedx.core.djangolib.testing.utils import skip_unless_lms - +from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML User = get_user_model() @@ -611,3 +611,257 @@ class CelebrationApiTestViews(BaseCoursewareTests, MasqueradeMixin): # make sure they didn't change during masquerade attempt assert celebration.celebrate_first_section assert not celebration.celebrate_weekly_goal + + +@ddt.ddt +@skip_unless_lms # If run in CMS, the tests fail as the courseware_api.views module contains imports from the LMS. +class CoursewareMetaTestViews(BaseCoursewareTests): + """ + Tests for the CoursewareMeta class + """ + + def setUp(self): + super().setUp() + self.course_enrollment = CourseEnrollment.enroll(self.user, self.course.id, 'audit') + self.request = RequestFactory().get(self.url) + + def create_courseware_meta(self, user=None): + """ + Helper method to create CoursewareMeta instance + """ + from openedx.core.djangoapps.courseware_api.views import CoursewareMeta + + user = user or self.user + self.request.user = user + return CoursewareMeta(self.course.id, self.request, username=user.username) + + @ddt.data(True, False) + def test_is_course_full_property(self, is_course_full): + """ + Test is_course_full property + """ + with mock.patch( + 'openedx.core.djangoapps.courseware_api.views.CourseEnrollment.objects.is_course_full' + ) as mock_is_course_full: + mock_is_course_full.return_value = is_course_full + meta = self.create_courseware_meta() + assert meta.is_course_full is is_course_full + + @ddt.data(True, False) + def test_invitation_only_property(self, invitation_only): + """ + Test invitation_only property + """ + with override_settings(COURSES_INVITE_ONLY=invitation_only): + meta = self.create_courseware_meta() + assert meta.invitation_only is invitation_only + + @ddt.data(True, False) + @mock.patch( + 'openedx.core.djangoapps.courseware_api.views.get_course_about_section', new_callable=mock.PropertyMock + ) + def test_about_sidebar_html_property(self, waffle_enabled, mock_get_course_about_section): + """ + Test about_sidebar_html property with different waffle settings + """ + mock_get_course_about_section.return_value = '
About Course
' + with override_waffle_switch(ENABLE_COURSE_ABOUT_SIDEBAR_HTML, active=waffle_enabled): + meta = self.create_courseware_meta() + if waffle_enabled: + assert meta.about_sidebar_html == '
About Course
' + else: + assert meta.about_sidebar_html is None + + +@ddt.ddt +@skip_unless_lms +class CoursewareMetaAPIResponseTestViews(BaseCoursewareTests): + """ + Tests for API response fields returned by CoursewareMeta through the API endpoint + """ + + def setUp(self): + super().setUp() + CourseEnrollment.enroll(self.user, self.course.id, 'audit') + + def test_api_returns_show_courseware_link_field(self): + """ + Test that API response contains show_courseware_link field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'show_courseware_link' in response.data + assert isinstance(response.data['show_courseware_link'], bool) + + def test_api_returns_is_course_full_field(self): + """ + Test that API response contains is_course_full field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'is_course_full' in response.data + assert isinstance(response.data['is_course_full'], bool) + + def test_api_returns_can_enroll_field(self): + """ + Test that API response contains can_enroll field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'can_enroll' in response.data + assert isinstance(response.data['can_enroll'], bool) + + def test_api_returns_invitation_only_field(self): + """ + Test that API response contains invitation_only field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'invitation_only' in response.data + assert isinstance(response.data['invitation_only'], bool) + + def test_api_returns_is_shib_course_field(self): + """ + Test that API response contains is_shib_course field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'is_shib_course' in response.data + assert isinstance(response.data['is_shib_course'], bool) + + def test_api_returns_allow_anonymous_field(self): + """ + Test that API response contains allow_anonymous field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'allow_anonymous' in response.data + assert isinstance(response.data['allow_anonymous'], bool) + + def test_api_returns_ecommerce_checkout_field(self): + """ + Test that API response contains ecommerce_checkout field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'ecommerce_checkout' in response.data + assert isinstance(response.data['ecommerce_checkout'], bool) + + def test_api_returns_single_paid_mode_field(self): + """ + Test that API response contains single_paid_mode field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'single_paid_mode' in response.data + assert isinstance(response.data['single_paid_mode'], dict) + + def test_api_returns_ecommerce_checkout_link_field(self): + """ + Test that API response contains ecommerce_checkout_link field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'ecommerce_checkout_link' in response.data + checkout_link = response.data['ecommerce_checkout_link'] + assert isinstance(checkout_link, str) or checkout_link is None + + def test_api_returns_course_image_urls_field(self): + """ + Test that API response contains course_image_urls field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'course_image_urls' in response.data + assert isinstance(response.data['course_image_urls'], list) + + def test_api_returns_start_date_is_still_default_field(self): + """ + Test that API response contains start_date_is_still_default field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'start_date_is_still_default' in response.data + assert isinstance(response.data['start_date_is_still_default'], bool) + + def test_api_returns_advertised_start_field(self): + """ + Test that API response contains advertised_start field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'advertised_start' in response.data + advertised_start = response.data['advertised_start'] + assert isinstance(advertised_start, str) or advertised_start is None + + def test_api_returns_course_price_field(self): + """ + Test that API response contains course_price field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'course_price' in response.data + assert isinstance(response.data['course_price'], str) + + def test_api_returns_pre_requisite_courses_field(self): + """ + Test that API response contains pre_requisite_courses field + """ + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'pre_requisite_courses' in response.data + assert isinstance(response.data['pre_requisite_courses'], list) + + @ddt.data(True, False) + @mock.patch( + 'openedx.core.djangoapps.courseware_api.views.get_course_about_section', new_callable=mock.PropertyMock + ) + def test_api_about_sidebar_html_with_waffle(self, waffle_enabled, mock_get_course_about_section): + """ + Test API returns correct about_sidebar_html value based on waffle flag + """ + with override_waffle_switch(ENABLE_COURSE_ABOUT_SIDEBAR_HTML, active=waffle_enabled): + mock_get_course_about_section.return_value = '
About Course
' + response = self.client.get(self.url) + assert response.status_code == 200 + assert 'about_sidebar_html' in response.data + if waffle_enabled: + assert response.data['about_sidebar_html'] == '
About Course
' + else: + assert response.data['about_sidebar_html'] is None + + +@ddt.ddt +@skip_unless_lms +class CoursewareMetaIntegrationTestViews(BaseCoursewareTests): + """ + Integration tests for CoursewareMeta with different user states and course configurations + """ + + @ddt.data( + ('audit', False), + ('verified', True), + ('honor', True), + ('professional', True), + ) + @ddt.unpack + def test_enrollment_mode_affects_can_access_proctored_exams(self, enrollment_mode, expected_access): + """ + Test that enrollment mode affects proctored exam access in API response + """ + CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) + + response = self.client.get(self.url) + assert response.status_code == 200 + assert response.data['can_access_proctored_exams'] == expected_access + + @mock.patch('openedx.core.djangoapps.courseware_api.views.check_public_access') + def test_public_course_affects_allow_anonymous(self, mock_check_public_access): + """ + Test that course visibility settings affect allow_anonymous field + """ + mock_check_public_access.return_value = ACCESS_GRANTED + + response = self.client.get(self.url) + assert response.status_code == 200 + assert response.data['allow_anonymous'] is True diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index 6b5e12257b..10dde8264b 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -25,15 +25,22 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from xmodule.modulestore.search import path_to_location from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW -from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.course_modes.models import CourseMode, get_course_prices from common.djangoapps.util.views import expose_header from lms.djangoapps.edxnotes.helpers import is_feature_enabled from lms.djangoapps.certificates.api import get_certificate_url from lms.djangoapps.certificates.models import GeneratedCertificate +from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.course_api.api import course_detail from lms.djangoapps.course_goals.models import UserActivity from lms.djangoapps.course_goals.api import get_course_goal from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.access_utils import check_public_access +from lms.djangoapps.courseware.courses import ( + get_course_about_section, + get_course_with_access, + get_permission_for_course_about, +) from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs from lms.djangoapps.courseware.entrance_exams import course_has_entrance_exam, user_has_passed_entrance_exam @@ -43,19 +50,24 @@ from lms.djangoapps.courseware.masquerade import ( is_masquerading_as_non_audit_enrollment, ) from lms.djangoapps.courseware.models import LastSeenCoursewareTimezone +from lms.djangoapps.courseware.permissions import VIEW_COURSEWARE from lms.djangoapps.courseware.block_render import get_block_by_usage_id -from lms.djangoapps.courseware.toggles import course_exit_page_is_active +from lms.djangoapps.courseware.toggles import course_exit_page_is_active, course_is_invitation_only from lms.djangoapps.courseware.views.views import get_cert_data from lms.djangoapps.gating.api import get_entrance_exam_score, get_entrance_exam_usage_key from lms.djangoapps.grades.api import CourseGradeFactory +from lms.djangoapps.instructor.enrollment import uses_shib +from common.djangoapps.util.milestones_helpers import get_prerequisite_courses_display from lms.djangoapps.verify_student.services import IDVerificationService from openedx.core.djangoapps.agreements.api import get_integrity_signature from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict +from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE from openedx.core.djangoapps.programs.utils import ProgramProgressMeter from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin from openedx.core.lib.courses import get_course_by_id from openedx.features.course_experience import ENABLE_COURSE_GOALS +from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.access import get_access_expiration_data from openedx.features.discounts.utils import generate_offer_data @@ -64,6 +76,10 @@ from common.djangoapps.student.models import ( CourseEnrollmentCelebration, LinkedInAddToProfileConfiguration ) +from xmodule.course_block import ( + COURSE_VISIBILITY_PUBLIC, + COURSE_VISIBILITY_PUBLIC_OUTLINE, +) from .serializers import CourseInfoSerializer @@ -75,13 +91,13 @@ class CoursewareMeta: def __init__(self, course_key, request, username=''): self.request = request - self.overview = course_detail( + self.course_overview = course_detail( self.request, username or self.request.user.username, course_key, ) - original_user_is_staff = has_access(self.request.user, 'staff', self.overview).has_access + original_user_is_staff = has_access(self.request.user, 'staff', self.course_overview).has_access self.original_user_is_global_staff = self.request.user.is_staff self.course_key = course_key self.course = get_course_by_id(self.course_key) @@ -91,12 +107,13 @@ class CoursewareMeta: staff_access=original_user_is_staff, ) self.request.user = self.effective_user - self.overview.bind_course_for_student(self.request) + self.course_overview.bind_course_for_student(self.request) self.enrollment_object = CourseEnrollment.get_enrollment(self.effective_user, self.course_key, select_related=['celebration', 'user__celebration']) + self.ecomm_service = EcommerceService() def __getattr__(self, name): - return getattr(self.overview, name) + return getattr(self.course_overview, name) @property def enrollment(self): @@ -113,11 +130,11 @@ class CoursewareMeta: @property def access_expiration(self): - return get_access_expiration_data(self.effective_user, self.overview) + return get_access_expiration_data(self.effective_user, self.course_overview) @property def offer(self): - return generate_offer_data(self.effective_user, self.overview) + return generate_offer_data(self.effective_user, self.course_overview) @property def content_type_gating_enabled(self): @@ -140,8 +157,8 @@ class CoursewareMeta: Return whether edxnotes is enabled and visible. """ return { - 'enabled': is_feature_enabled(self.overview, self.effective_user), - 'visible': self.overview.edxnotes_visibility, + 'enabled': is_feature_enabled(self.course_overview, self.effective_user), + 'visible': self.course_overview.edxnotes_visibility, } @property @@ -214,12 +231,12 @@ class CoursewareMeta: """ return { 'entrance_exam_current_score': get_entrance_exam_score( - self.course_grade, get_entrance_exam_usage_key(self.overview), + self.course_grade, get_entrance_exam_usage_key(self.course_overview), ), - 'entrance_exam_enabled': course_has_entrance_exam(self.overview), - 'entrance_exam_id': self.overview.entrance_exam_id, - 'entrance_exam_minimum_score_pct': self.overview.entrance_exam_minimum_score_pct, - 'entrance_exam_passed': user_has_passed_entrance_exam(self.effective_user, self.overview), + 'entrance_exam_enabled': course_has_entrance_exam(self.course_overview), + 'entrance_exam_id': self.course_overview.entrance_exam_id, + 'entrance_exam_minimum_score_pct': self.course_overview.entrance_exam_minimum_score_pct, + 'entrance_exam_passed': user_has_passed_entrance_exam(self.effective_user, self.course_overview), } @property @@ -271,7 +288,7 @@ class CoursewareMeta: get_certificate_url(course_id=self.course_key, uuid=user_certificate.verify_uuid) ) return linkedin_config.add_to_profile_url( - self.overview.display_name, user_certificate.mode, cert_url, certificate=user_certificate, + self.course_overview.display_name, user_certificate.mode, cert_url, certificate=user_certificate, ) @property @@ -369,6 +386,139 @@ class CoursewareMeta: """ return getattr(settings, 'LEARNING_ASSISTANT_AVAILABLE', False) + @property + def show_courseware_link(self): + """ + Returns a boolean representing whether the courseware link should be shown in the course details page. + """ + with modulestore().bulk_operations(self.course_key): + permission = get_permission_for_course_about() + course_with_access = get_course_with_access(self.request.user, permission, self.course_key) + return bool( + self.request.user.has_perm(VIEW_COURSEWARE, course_with_access) + or settings.FEATURES.get('ENABLE_LMS_MIGRATION') + ) + + @property + def is_course_full(self): + """ + Returns a boolean representing whether the course is full. + """ + return CourseEnrollment.objects.is_course_full(self.course) + + @property + def can_enroll(self): + """ + Returns a boolean representing whether the user can enroll in the course. + """ + return bool(self.request.user.has_perm(ENROLL_IN_COURSE, self.course)) + + @property + def invitation_only(self): + """ + Returns a boolean representing whether the course is invitation only. + """ + return course_is_invitation_only(self.course) + + @property + def is_shib_course(self): + """ + Returns a boolean representing whether the course is a Shibboleth course. + """ + return uses_shib(self.course) + + @property + def allow_anonymous(self): + """ + Returns a boolean representing whether the course allows anonymous access. + """ + return bool(check_public_access(self.course, [COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE])) + + @property + def ecommerce_checkout(self): + """ + Returns a boolean representing whether the course has an ecommerce checkout. + """ + return self.ecomm_service.is_enabled(self.request.user) + + @property + def single_paid_mode(self): + """ + Returns a dict representing the single paid mode for the course, if it exists. + """ + modes = CourseMode.modes_for_course_dict(self.course_key) + single_paid_mode = {} + if self.ecommerce_checkout: + if len(modes) == 1 and list(modes.values())[0].min_price: + single_paid_mode = list(modes.values())[0] + else: + # have professional ignore other modes for historical reasons + single_paid_mode = modes.get(CourseMode.PROFESSIONAL) + return single_paid_mode + + @property + def ecommerce_checkout_link(self): + """ + Returns the ecommerce checkout link for the course. + """ + if self.single_paid_mode and self.single_paid_mode.sku: + return self.ecomm_service.get_checkout_page_url( + self.single_paid_mode.sku, course_run_keys=[self.course_key] + ) + return None + + @property + def course_image_urls(self): + """ + Returns a list of course image URLs. + """ + return self.course_overview.image_urls + + @property + def start_date_is_still_default(self): + """ + Returns a boolean indicating whether the course start date is still the default value. + """ + return self.course_overview.start_date_is_still_default + + @property + def advertised_start(self): + """ + Returns the advertised start date of the course. + """ + return self.course_overview.advertised_start + + @property + def course_price(self): + """ + Returns the course price, formatted with the currency symbol. + """ + _, course_price = get_course_prices(self.course) + return course_price + + @property + def pre_requisite_courses(self): + """ + Returns a list of pre-requisite courses for the course. + """ + return get_prerequisite_courses_display(self.course) + + @property + def about_sidebar_html(self): + """ + Returns the HTML content for the course about section. + """ + if ENABLE_COURSE_ABOUT_SIDEBAR_HTML.is_enabled(): + return get_course_about_section(self.request, self.course, "about_sidebar_html") + return None + + @property + def overview(self): + """ + Returns the overview HTML content for the course. + """ + return get_course_about_section(self.request, self.course, "overview") + @method_decorator(transaction.non_atomic_requests, name='dispatch') class CoursewareInformation(RetrieveAPIView): @@ -458,9 +608,46 @@ class CoursewareInformation(RetrieveAPIView): * certificate_data: data regarding the effective user's certificate for the given course * verify_identity_url: URL for a learner to verify their identity. Only returned for learners enrolled in a verified mode. Will update to reverify URL if necessary. + * verification_status: The verification status of the effective user in the course. Possible values: + * 'none': No verification has been created for the user + * 'expired': The verification has expired + * 'approved': The verification has been approved + * 'pending': The verification is pending + * 'must_reverify': The user must reverify their identity * linkedin_add_to_profile_url: URL to add the effective user's certificate to a LinkedIn Profile. * user_needs_integrity_signature: Whether the user needs to sign the integrity agreement for the course * learning_assistant_enabled: Whether the Xpert Learning Assistant is enabled for the requesting user + * show_courseware_link: Whether the courseware link should be shown in the course details page + * is_course_full: Whether the course is full + * can_enroll: Whether the user can enroll in the course + * invitation_only: Whether the course is invitation only + * is_shib_course: Whether the course is a Shibboleth course + * allow_anonymous: Whether the course allows anonymous access + * ecommerce_checkout: Whether the course has an ecommerce checkout + * single_paid_mode: An object representing the single paid mode for the course, if it exists + * sku: (str) The SKU for the single paid mode + * name: (str) The name of the single paid mode + * min_price: (str) The minimum price for the single paid mode, formatted with the currency symbol + * description: (str) The description of the single paid mode + * is_discounted: (bool) Whether the single paid mode is discounted + * ecommerce_checkout_link: The ecommerce checkout link for the course, if it exists + * course_image_urls: A list of course image URLs + * start_date_is_still_default: Whether the course start date is still the default value + * advertised_start: The advertised start date of the course + * course_price: The course price, formatted with the currency symbol + * pre_requisite_courses: A list of pre-requisite courses for the course + * about_sidebar_html: The HTML content for the course about section, if enabled + * display_number_with_default: The course number with the org name, if set + * display_org_with_default: The org name with the course number, if set + * content_type_gating_enabled: Whether the content type gating is enabled for the course + * show_calculator: Whether the calculator should be shown in the course details page + * can_access_proctored_exams: Whether the user is eligible to access proctored exams + * notes: An object containing note settings for the course + * enabled: Boolean indicating whether edxnotes feature is enabled for the course + * visible: Boolean indicating whether notes are visible in the course + * marketing_url: The marketing URL for the course + * overview: The overview HTML content for the course + * license: The license for the course **Parameters:**