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))
55 changes: 55 additions & 0 deletions openedx/core/djangoapps/courseware_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ def get_progress(self, program):
}


class PrerequisiteCourseSerializer(serializers.Serializer):
"""
Serializer for prerequisite course data with key and display name.
"""
key = serializers.CharField()
Comment thread
feanil marked this conversation as resolved.
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.
Resolved
Expand Down Expand Up @@ -115,6 +123,53 @@ 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.
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.
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,
)
sidebar_html_enabled = serializers.BooleanField()
Comment thread
feanil marked this conversation as resolved.
Resolved
course_about_section_html = 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

display_number_with_default = serializers.CharField()
org = serializers.CharField(source='display_org_with_default')
Comment thread
feanil marked this conversation as resolved.
Resolved
studio_url = serializers.CharField()
Comment thread
feanil marked this conversation as resolved.
Resolved
is_cosmetic_price_enabled = serializers.BooleanField(default=False)
Comment thread
feanil marked this conversation as resolved.
Resolved
overview = serializers.CharField(
allow_blank=True,
allow_null=True,
default=None,
)
ocw_links = serializers.ListField(
child=serializers.CharField(),
allow_empty=True,
default=list,
)
prerequisites = serializers.ListField(
child=serializers.CharField(),
allow_empty=True,
default=list,
)

def __init__(self, *args, **kwargs):
"""
Expand Down
236 changes: 235 additions & 1 deletion openedx/core/djangoapps/courseware_api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
from completion.test_utils import CompletionWaffleTestMixin, submit_completions_for_testing
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
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
Expand Down Expand Up @@ -45,7 +46,14 @@
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.core.lib import ensure_lms
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML

try:
ensure_lms()
Comment thread
feanil marked this conversation as resolved.
Resolved
from openedx.core.djangoapps.courseware_api.views import CoursewareMeta
except ImproperlyConfigured:
pass

User = get_user_model()

Expand Down Expand Up @@ -611,3 +619,229 @@ 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
Comment thread
feanil marked this conversation as resolved.
Resolved
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"""
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)
def test_sidebar_html_enabled_property(self, waffle_enabled):
"""Test sidebar_html_enabled property with different waffle settings"""
with override_waffle_switch(ENABLE_COURSE_ABOUT_SIDEBAR_HTML, active=waffle_enabled):
meta = self.create_courseware_meta()
assert meta.sidebar_html_enabled == waffle_enabled

@ddt.data(True, False)
@mock.patch(
'openedx.core.djangoapps.courseware_api.views.get_course_about_section', new_callable=mock.PropertyMock
)
def test_course_about_section_html_property(self, waffle_enabled, mock_get_course_about_section):
"""Test course_about_section_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.course_about_section_html == '<div>About Course</div>'
else:
assert meta.course_about_section_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)
def test_api_sidebar_html_enabled_with_waffle(self, waffle_enabled):
"""Test API returns correct sidebar_html_enabled value based on waffle flag"""
with override_waffle_switch(ENABLE_COURSE_ABOUT_SIDEBAR_HTML, active=waffle_enabled):
response = self.client.get(self.url)
assert response.status_code == 200
assert 'sidebar_html_enabled' in response.data
assert response.data['sidebar_html_enabled'] == waffle_enabled

@ddt.data(True, False)
@mock.patch(
'openedx.core.djangoapps.courseware_api.views.get_course_about_section', new_callable=mock.PropertyMock
)
def test_api_course_about_section_html_with_waffle(self, waffle_enabled, mock_get_course_about_section):
"""Test API returns correct course_about_section_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 'course_about_section_html' in response.data
if waffle_enabled:
assert response.data['course_about_section_html'] == '<div>About Course</div>'
else:
assert response.data['course_about_section_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