Skip to content
2 changes: 1 addition & 1 deletion lms/djangoapps/instructor/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
46 changes: 45 additions & 1 deletion openedx/core/djangoapps/courseware_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,20 @@ def get_progress(self, program):
}


class PrerequisiteCourseSerializer(serializers.Serializer):
"""
Serializer for prerequisite course data with the serialized course key and display name.
"""
key = serializers.CharField()
Comment thread
feanil marked this conversation as resolved.
display = serializers.CharField()


class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for Course objects providing minimal data about the course.
Comment thread
feanil marked this conversation as resolved.
Compare this with CourseDetailSerializer.

For detailed information about what each field is for, see the docstring of the
CoursewareInformation class.
"""

access_expiration = serializers.DictField()
Expand Down Expand Up @@ -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()
Comment thread
feanil marked this conversation as resolved.
is_course_full = serializers.BooleanField()
can_enroll = serializers.BooleanField()
invitation_only = serializers.BooleanField()
is_shib_course = serializers.BooleanField()
allow_anonymous = serializers.BooleanField()
Comment thread
feanil marked this conversation as resolved.
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have DEPR for fields prerequisites and ocw_links, we need to:

  1. ensure that these fields really do not have corresponding BE logic
  2. reply to the comment left in this DEPR
  3. remove unnecessary fields from this serializer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some investigation, I can confirm that there is no BE logic that would populate these two fields. I left a reply in the DEPR and updated this PR accordingly.

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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a new field display_number_with_default

display_number_with_default = serializers.CharField()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add display_org_with_default

org = serializers.CharField(source='display_org_with_default')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved


def __init__(self, *args, **kwargs):
"""
Expand Down
258 changes: 256 additions & 2 deletions openedx/core/djangoapps/courseware_api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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
Expand Down Expand Up @@ -45,7 +45,7 @@
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()

Expand Down Expand Up @@ -611,3 +611,257 @@ def test_masquerade(self):
# 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
Loading
Loading