Merge pull request #36845 from raccoongang/rg/axm-course-catalog-extend-courseware-api
feat: [FC-86] extend courseware api with new fields
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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 = '<div>About Course</div>'
|
||||
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 == '<div>About Course</div>'
|
||||
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 = '<div>About Course</div>'
|
||||
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'] == '<div>About Course</div>'
|
||||
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
|
||||
|
||||
@@ -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:**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user