feat!: Remove the course sock and related APIs.
DEPR: https://github.com/openedx/edx-platform/issues/36429 This change removes the course_sock and related API data. The UI it removes is on the Legacy Courseware pages which have also been replaced and have their own [deprecation ticket](https://github.com/openedx/edx-platform/issues/35803) Before this can be merged, we will need to update the frontend-app-learning MFE to no longer consume the `can_show_upgrade_sock` attribute. BREAKING CHANGE: CourseHomeMetadata, ProgressTab, OutlineTab and VerifiedMode APIs will no longer have a `can_show_upgrade_sock` attribute.
This commit is contained in:
@@ -31,7 +31,6 @@ from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactor
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience import (
|
||||
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
|
||||
DISPLAY_COURSE_SOCK_FLAG,
|
||||
ENABLE_COURSE_GOALS
|
||||
)
|
||||
from openedx.features.discounts.applicability import (
|
||||
@@ -362,12 +361,6 @@ class OutlineTabTestViews(BaseCourseHomeTests):
|
||||
assert (data['access_expiration'] is not None) == show_enrolled
|
||||
assert (data['resume_course']['url'] is not None) == show_enrolled
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_can_show_upgrade_sock(self, sock_enabled):
|
||||
with override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=sock_enabled):
|
||||
response = self.client.get(self.url)
|
||||
assert response.data['can_show_upgrade_sock'] == sock_enabled
|
||||
|
||||
def test_verified_mode(self):
|
||||
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
|
||||
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
|
||||
|
||||
@@ -9,7 +9,6 @@ from rest_framework import serializers
|
||||
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
|
||||
from openedx.core.djangoapps.courseware_api.utils import serialize_upgrade_info
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
|
||||
from openedx.features.course_experience.utils import dates_banner_should_display
|
||||
|
||||
|
||||
@@ -59,13 +58,8 @@ class VerifiedModeSerializer(ReadOnlySerializer):
|
||||
|
||||
Requires 'course_overview', 'enrollment', and 'request' from self.context.
|
||||
"""
|
||||
can_show_upgrade_sock = serializers.SerializerMethodField()
|
||||
verified_mode = serializers.SerializerMethodField()
|
||||
|
||||
def get_can_show_upgrade_sock(self, _):
|
||||
course_overview = self.context['course_overview']
|
||||
return DISPLAY_COURSE_SOCK_FLAG.is_enabled(course_overview.id)
|
||||
|
||||
def get_verified_mode(self, _):
|
||||
"""Return verified mode information, or None."""
|
||||
course_overview = self.context['course_overview']
|
||||
|
||||
@@ -341,7 +341,7 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
self.client.login(username=self.user.username, password=self.user_password)
|
||||
CourseEnrollment.enroll(self.user, course.id)
|
||||
|
||||
with self.assertNumQueries(154, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
|
||||
with self.assertNumQueries(152, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
|
||||
with check_mongo_calls(3):
|
||||
url = reverse(
|
||||
'courseware_section',
|
||||
|
||||
@@ -48,7 +48,6 @@ from openedx.features.course_experience import (
|
||||
default_course_url
|
||||
)
|
||||
from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url
|
||||
from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
|
||||
from openedx.features.enterprise_support.api import data_sharing_consent_required
|
||||
|
||||
from ..access import has_access
|
||||
@@ -454,9 +453,6 @@ class CoursewareIndex(View):
|
||||
table_of_contents['chapters'],
|
||||
)
|
||||
|
||||
courseware_context['course_sock_fragment'] = CourseSockFragmentView().render_to_fragment(
|
||||
request, course=self.course)
|
||||
|
||||
# entrance exam data
|
||||
self._add_entrance_exam_to_context(courseware_context)
|
||||
|
||||
|
||||
@@ -66,6 +66,3 @@
|
||||
// responsive
|
||||
@import 'base/layouts'; // temporary spot for responsive course
|
||||
@import 'header';
|
||||
|
||||
// features
|
||||
@import 'features/course-sock';
|
||||
|
||||
@@ -28,7 +28,6 @@ $static-path: '../..';
|
||||
@import 'features/bookmarks';
|
||||
@import 'features/course-experience';
|
||||
@import 'features/course-search';
|
||||
@import 'features/course-sock';
|
||||
@import 'features/course-upgrade-message';
|
||||
@import 'features/course-duration-limits';
|
||||
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
.verification-sock {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: map-get($container-max-widths, xl);
|
||||
margin: $baseline auto 0;
|
||||
-webkit-transition: all 0.4s ease-out;
|
||||
-moz-transition: all 0.4s ease-out;
|
||||
-o-transition: all 0.4s ease-out;
|
||||
-ms-transition: all 0.4s ease-out;
|
||||
transition: all 0.4s ease-out;
|
||||
|
||||
.action-toggle-verification-sock {
|
||||
@include left(50%);
|
||||
@include margin-left(-1 * $baseline * 15/2);
|
||||
|
||||
position: absolute;
|
||||
top: (-1 * $baseline);
|
||||
width: ($baseline * 15);
|
||||
color: theme-color("inverse");
|
||||
background-color: theme-color("success");
|
||||
border-color: theme-color("success");
|
||||
background-image: none;
|
||||
box-shadow: none;
|
||||
-webkit-transition: background-color 0.5s;
|
||||
transition: background-color 0.5s;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: theme-color("success");
|
||||
background-color: theme-color("inverse");
|
||||
border-color: theme-color("success");
|
||||
background-image: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.verification-main-panel {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid $border-color;
|
||||
padding: ($baseline * 5/2) ($baseline * 2);
|
||||
-webkit-transition: height ease-out;
|
||||
transition: height ease-out;
|
||||
|
||||
.verification-desc-panel {
|
||||
color: $black-t3;
|
||||
position: relative;
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.mini-cert {
|
||||
display: none;
|
||||
border: 1px solid $black-t0;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-cert {
|
||||
@include right($baseline);
|
||||
|
||||
position: absolute;
|
||||
top: $baseline;
|
||||
width: ($baseline * 13);
|
||||
}
|
||||
|
||||
.learner-story-container {
|
||||
display: flex;
|
||||
max-width: 630px;
|
||||
|
||||
.student-image {
|
||||
margin: ($baseline / 4) $baseline 0 0;
|
||||
height: ($baseline * 5/2);
|
||||
width: ($baseline * 5/2);
|
||||
}
|
||||
|
||||
.story-quote > .author {
|
||||
display: block;
|
||||
margin-top: ($baseline / 4);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: ($baseline * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.action-upgrade-certificate {
|
||||
position: absolute;
|
||||
right: $baseline;
|
||||
background-color: theme-color("success");
|
||||
border-color: theme-color("success");
|
||||
color: theme-color("inverse");
|
||||
background-image: none;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: theme-color("inverse");
|
||||
color: theme-color("success");
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
& {
|
||||
position: relative;
|
||||
margin-top: ($baseline * 2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
&.stuck-top {
|
||||
bottom: auto;
|
||||
top: $baseline * (52 / 5);
|
||||
}
|
||||
|
||||
&.stuck-bottom {
|
||||
top: auto;
|
||||
bottom: $baseline * (-1 * 3/2);
|
||||
}
|
||||
|
||||
&.attached {
|
||||
@include right($baseline);
|
||||
|
||||
position: fixed;
|
||||
bottom: $baseline;
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides for the courseware page.
|
||||
.view-courseware {
|
||||
.verification-sock {
|
||||
margin-top: 0;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
|
||||
.action-toggle-verification-sock {
|
||||
top: (-1 * $baseline * 5/4);
|
||||
|
||||
&:not(.active) {
|
||||
color: theme-color("inverse");
|
||||
background-color: theme-color("success");
|
||||
box-shadow: none;
|
||||
border: 1px solid theme-color("success");
|
||||
|
||||
&:hover {
|
||||
background-color: $success-color-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.verification-main-panel {
|
||||
border-top: 0;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,7 +290,6 @@ ${HTML(fragment.foot_html())}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
${HTML(course_sock_fragment.body_html())}
|
||||
</div>
|
||||
<div class="container-footer">
|
||||
% if settings.FEATURES.get("LICENSING", False):
|
||||
@@ -317,36 +316,3 @@ ${HTML(fragment.foot_html())}
|
||||
% endif
|
||||
</nav>
|
||||
% endif
|
||||
|
||||
<%static:require_module_async module_name="js/commerce/track_ecommerce_events" class_name="TrackECommerceEvents">
|
||||
|
||||
var fbeLink = $("#FBE_banner");
|
||||
var welcomeLink = $("#welcome");
|
||||
var accessDeniedUpsellLink = $("#accessDeniedUpsell");
|
||||
var sockLink = $("#sock");
|
||||
|
||||
TrackECommerceEvents.trackUpsellClick(fbeLink, 'in_course_audit_access_expires', {
|
||||
pageName: "in_course",
|
||||
linkType: "link",
|
||||
linkCategory: "FBE_banner"
|
||||
});
|
||||
|
||||
TrackECommerceEvents.trackUpsellClick(welcomeLink, 'in_course_welcome', {
|
||||
pageName: "in_course",
|
||||
linkType: "link",
|
||||
linkCategory: "welcome"
|
||||
});
|
||||
|
||||
TrackECommerceEvents.trackUpsellClick(accessDeniedUpsellLink, 'in_course_upgrade', {
|
||||
pageName: "in_course",
|
||||
linkType: "link",
|
||||
linkCategory: "(none)"
|
||||
});
|
||||
|
||||
TrackECommerceEvents.trackUpsellClick(sockLink, 'in_course_sock', {
|
||||
pageName: "in_course",
|
||||
linkType: "button",
|
||||
linkCategory: "green_upgrade"
|
||||
});
|
||||
|
||||
</%static:require_module_async>
|
||||
|
||||
@@ -80,7 +80,6 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-
|
||||
"""
|
||||
|
||||
access_expiration = serializers.DictField()
|
||||
can_show_upgrade_sock = serializers.BooleanField()
|
||||
content_type_gating_enabled = serializers.BooleanField()
|
||||
course_goals = CourseGoalsSerializer()
|
||||
effort = serializers.CharField()
|
||||
|
||||
@@ -55,7 +55,6 @@ 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 DISPLAY_COURSE_SOCK_FLAG
|
||||
from openedx.features.course_experience import ENABLE_COURSE_GOALS
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.course_duration_limits.access import get_access_expiration_data
|
||||
@@ -127,10 +126,6 @@ class CoursewareMeta:
|
||||
course_key=self.course_key,
|
||||
)
|
||||
|
||||
@property
|
||||
def can_show_upgrade_sock(self):
|
||||
return DISPLAY_COURSE_SOCK_FLAG.is_enabled(self.course_key)
|
||||
|
||||
@property
|
||||
def license(self):
|
||||
return self.course.license
|
||||
|
||||
@@ -18,9 +18,6 @@ DISABLE_COURSE_OUTLINE_PAGE_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: di
|
||||
f'{WAFFLE_FLAG_NAMESPACE}.disable_course_outline_page', __name__
|
||||
)
|
||||
|
||||
# Waffle flag to enable the sock on the footer of the home and courseware pages.
|
||||
DISPLAY_COURSE_SOCK_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.display_course_sock', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
|
||||
|
||||
# Waffle flag to let learners access a course before its start date.
|
||||
COURSE_PRE_START_ACCESS_FLAG = WaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.pre_start_access', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/* globals Logger */
|
||||
|
||||
export class CourseSock { // eslint-disable-line import/prefer-default-export
|
||||
constructor() {
|
||||
// eslint-disable-next-line no-undef
|
||||
const $toggleActionButton = $('.action-toggle-verification-sock');
|
||||
// eslint-disable-next-line no-undef
|
||||
const $verificationSock = $('.verification-sock .verification-main-panel');
|
||||
// eslint-disable-next-line no-undef
|
||||
const $upgradeToVerifiedButton = $('.verification-sock .action-upgrade-certificate');
|
||||
// eslint-disable-next-line no-undef
|
||||
const $miniCert = $('.mini-cert');
|
||||
const pageLocation = window.location.href.indexOf('courseware') > -1
|
||||
? 'Course Content Page' : 'Home Page';
|
||||
|
||||
// Behavior to fix button to bottom of screen on scroll
|
||||
const fixUpgradeButton = () => {
|
||||
if (!$upgradeToVerifiedButton.is(':visible')) { return; }
|
||||
|
||||
// Grab the current scroll location
|
||||
// eslint-disable-next-line no-undef
|
||||
const documentBottom = $(window).scrollTop() + $(window).height();
|
||||
|
||||
// Establish a sliding window in which the button is fixed
|
||||
const startFixed = $verificationSock.offset().top + 320;
|
||||
const endFixed = (startFixed + $verificationSock.height()) - 220;
|
||||
|
||||
// Ensure update button stays in sock even when max-width is exceeded
|
||||
const distRight = window.outerWidth - ($miniCert.offset().left + $miniCert.width());
|
||||
|
||||
// Update positioning when scrolling is in fixed window and screen width is sufficient
|
||||
if ((documentBottom > startFixed && documentBottom < endFixed
|
||||
// eslint-disable-next-line no-undef
|
||||
&& $(window).width() > 960)) {
|
||||
$upgradeToVerifiedButton.addClass('attached');
|
||||
$upgradeToVerifiedButton.css('right', `${distRight}px`);
|
||||
} else {
|
||||
// If outside sliding window, reset to un-attached state
|
||||
$upgradeToVerifiedButton.removeClass('attached');
|
||||
$upgradeToVerifiedButton.css('right', '20px');
|
||||
|
||||
// Add class to define absolute location
|
||||
if (documentBottom < startFixed) {
|
||||
$upgradeToVerifiedButton.addClass('stuck-top');
|
||||
$upgradeToVerifiedButton.removeClass('stuck-bottom');
|
||||
} else if (documentBottom > endFixed) {
|
||||
$upgradeToVerifiedButton.addClass('stuck-bottom');
|
||||
$upgradeToVerifiedButton.removeClass('stuck-top');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fix the sock to the screen on scroll and resize events
|
||||
if ($upgradeToVerifiedButton.length) {
|
||||
// eslint-disable-next-line no-undef
|
||||
$(window).scroll(fixUpgradeButton).resize(fixUpgradeButton);
|
||||
}
|
||||
|
||||
// Open the sock when user clicks to Learn More
|
||||
$toggleActionButton.on('click', () => {
|
||||
const toggleSpeed = 400;
|
||||
$toggleActionButton.toggleClass('active');
|
||||
$verificationSock.slideToggle(toggleSpeed, fixUpgradeButton);
|
||||
|
||||
// Toggle aria-expanded attribute
|
||||
const newAriaExpandedState = $toggleActionButton.attr('aria-expanded') === 'false';
|
||||
$toggleActionButton.attr('aria-expanded', newAriaExpandedState);
|
||||
|
||||
// Log open and close events
|
||||
const isOpening = $toggleActionButton.hasClass('active');
|
||||
const logMessage = isOpening ? 'edx.bi.course.sock.toggle_opened'
|
||||
: 'edx.bi.course.sock.toggle_closed';
|
||||
window.analytics.track(
|
||||
logMessage,
|
||||
{
|
||||
from_page: pageLocation,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
$upgradeToVerifiedButton.on('click', () => {
|
||||
Logger.log(
|
||||
'edx.course.enrollment.upgrade.clicked',
|
||||
{
|
||||
location: 'sock',
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%!
|
||||
from django.utils.translation import gettext as _
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
|
||||
%>
|
||||
|
||||
<%block name="content">
|
||||
% if show_course_sock:
|
||||
<div class="verification-sock"
|
||||
% if not DISPLAY_COURSE_SOCK_FLAG.is_enabled(course_id):
|
||||
style="display: none"
|
||||
%endif
|
||||
>
|
||||
<button type="button" aria-expanded="false" class="btn btn-primary focusable action-toggle-verification-sock">
|
||||
${_('Learn About Verified Certificates')}
|
||||
</button>
|
||||
<div class="verification-main-panel">
|
||||
<div class="verification-desc-panel content-main">
|
||||
<h2>${_('{platform_name} Verified Certificate').format(platform_name=settings.PLATFORM_NAME)}</h2>
|
||||
<h3>${_('Why upgrade?')}</h3>
|
||||
<ul>
|
||||
<li>${_('Official proof of completion')}</li>
|
||||
<li>${_('Easily shareable certificate')}</li>
|
||||
<li>${_('Proven motivator to complete the course')}</li>
|
||||
<li>${_('Certificate purchases help {platform_name} continue to offer free courses').format(platform_name=settings.PLATFORM_NAME)}</li>
|
||||
</ul>
|
||||
<h3>${_('How it works')}</h3>
|
||||
<ul>
|
||||
<li>${_('Pay the Verified Certificate upgrade fee')}</li>
|
||||
<li>${_('Verify your identity with a webcam and government-issued ID')}</li>
|
||||
<li>${_('Study hard and pass the course')}</li>
|
||||
<li>${_('Share your certificate with friends, employers, and others')}</li>
|
||||
</ul>
|
||||
% if settings.PLATFORM_NAME == 'edX':
|
||||
<h3>${_('edX Learner Stories')}</h3>
|
||||
<div class="learner-story-container">
|
||||
<img class="student-image" alt="Student Image" src="${static.url('course_experience/images/learner-quote.png')}" />
|
||||
<div class="story-quote">
|
||||
${_('My certificate has helped me showcase my knowledge on my \
|
||||
resumé - I feel like this certificate could really help me land \
|
||||
my dream job!')}
|
||||
<span class="author">- ${_('{learner_name}, edX Learner').format(learner_name='Christina Fong')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="learner-story-container">
|
||||
<img class="student-image" alt="Student Image" src="${static.url('course_experience/images/learner-quote2.png')}" />
|
||||
<div class="story-quote">
|
||||
${_('I wanted to include a verified certificate on my resumé and my profile to \
|
||||
illustrate that I am working towards this goal I have and that I have \
|
||||
achieved something while I was unemployed.')}<br/>
|
||||
<span class="author">- ${_('{learner_name}, edX Learner').format(learner_name='Cheryl Troell')}</span>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<img class="mini-cert" alt="Example Certificate Image" src="${static.url('course_experience/images/verified-cert.png')}"/>
|
||||
<a id="sock" href="${upgrade_url}">
|
||||
<div class="btn btn-upgrade stuck-top focusable action-upgrade-certificate" data-creative="original_sock" data-position="sock">
|
||||
${Text(_('Upgrade ({course_price})')).format(course_price=HTML(course_price))}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
</%block>
|
||||
|
||||
<%static:webpack entry="CourseSock">
|
||||
new CourseSock({
|
||||
el:'.verification-sock'
|
||||
});
|
||||
</%static:webpack>
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
Tests for course verification sock
|
||||
"""
|
||||
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.courseware.tests.helpers import set_preview_mode
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from .helpers import add_course_mode
|
||||
|
||||
TEST_PASSWORD = 'Password1234'
|
||||
TEST_VERIFICATION_SOCK_LOCATOR = '<div class="verification-sock"'
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@set_preview_mode(True)
|
||||
class TestCourseSockView(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the course verification sock fragment view.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Create four courses
|
||||
cls.standard_course = CourseFactory.create()
|
||||
cls.verified_course = CourseFactory.create()
|
||||
cls.verified_course_update_expired = CourseFactory.create()
|
||||
cls.verified_course_already_enrolled = CourseFactory.create()
|
||||
|
||||
# Assign each verifiable course an upgrade deadline
|
||||
add_course_mode(cls.verified_course, upgrade_deadline_expired=False)
|
||||
add_course_mode(cls.verified_course_update_expired, upgrade_deadline_expired=True)
|
||||
add_course_mode(cls.verified_course_already_enrolled, upgrade_deadline_expired=False)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = UserFactory.create(is_staff=True)
|
||||
|
||||
# Enroll the user in the four courses
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.standard_course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.verified_course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.verified_course_update_expired.id)
|
||||
CourseEnrollmentFactory.create(
|
||||
user=self.user, course_id=self.verified_course_already_enrolled.id, mode=CourseMode.VERIFIED
|
||||
)
|
||||
|
||||
CommerceConfiguration.objects.create(enabled=True, checkout_on_ecommerce_service=True)
|
||||
|
||||
# Log the user in
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
|
||||
def get_courseware(self, course):
|
||||
return self.client.get(reverse('courseware', kwargs={'course_id': str(course.id)}))
|
||||
|
||||
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
|
||||
def test_standard_course(self):
|
||||
"""
|
||||
Ensure that a course that cannot be verified does
|
||||
not have a visible verification sock.
|
||||
"""
|
||||
response = self.get_courseware(self.standard_course)
|
||||
self.assert_verified_sock_is_not_visible(self.standard_course, response)
|
||||
|
||||
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
|
||||
def test_verified_course(self):
|
||||
"""
|
||||
Ensure that a course that can be verified has a
|
||||
visible verification sock.
|
||||
"""
|
||||
response = self.get_courseware(self.verified_course)
|
||||
self.assert_verified_sock_is_visible(self.verified_course, response)
|
||||
|
||||
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
|
||||
def test_verified_course_updated_expired(self):
|
||||
"""
|
||||
Ensure that a course that has an expired upgrade
|
||||
date does not display the verification sock.
|
||||
"""
|
||||
response = self.get_courseware(self.verified_course_update_expired)
|
||||
self.assert_verified_sock_is_not_visible(self.verified_course_update_expired, response)
|
||||
|
||||
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
|
||||
def test_verified_course_user_already_upgraded(self):
|
||||
"""
|
||||
Ensure that a user that has already upgraded to a
|
||||
verified status cannot see the verification sock.
|
||||
"""
|
||||
response = self.get_courseware(self.verified_course_already_enrolled)
|
||||
self.assert_verified_sock_is_not_visible(self.verified_course_already_enrolled, response)
|
||||
|
||||
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
|
||||
@mock.patch(
|
||||
'openedx.features.course_experience.views.course_sock.format_strikeout_price',
|
||||
mock.Mock(return_value=(HTML("<span>DISCOUNT_PRICE</span>"), True))
|
||||
)
|
||||
def test_upgrade_message_discount(self):
|
||||
response = self.get_courseware(self.verified_course)
|
||||
self.assertContains(response, "<span>DISCOUNT_PRICE</span>")
|
||||
|
||||
def assert_verified_sock_is_visible(self, course, response): # lint-amnesty, pylint: disable=unused-argument
|
||||
return self.assertContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
|
||||
|
||||
def assert_verified_sock_is_not_visible(self, course, response): # lint-amnesty, pylint: disable=unused-argument
|
||||
return self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
|
||||
@@ -2,11 +2,7 @@
|
||||
Tests for masquerading functionality on course_experience
|
||||
"""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin, set_preview_mode
|
||||
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
|
||||
from common.djangoapps.student.roles import CourseStaffRole
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -15,7 +11,6 @@ from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID # lint-
|
||||
from xmodule.partitions.partitions_service import PartitionService # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from .helpers import add_course_mode
|
||||
from .test_course_sock import TEST_VERIFICATION_SOCK_LOCATOR
|
||||
|
||||
TEST_PASSWORD = 'Password1234'
|
||||
|
||||
@@ -59,40 +54,3 @@ class MasqueradeTestBase(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
if group.name == mode_name:
|
||||
return group.id
|
||||
return None
|
||||
|
||||
|
||||
@set_preview_mode(True)
|
||||
class TestVerifiedUpgradesWithMasquerade(MasqueradeTestBase):
|
||||
"""
|
||||
Tests for the course verification upgrade messages while the user is being masqueraded.
|
||||
"""
|
||||
|
||||
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
|
||||
def test_masquerade_as_student(self):
|
||||
# Elevate the staff user to be student
|
||||
self.update_masquerade(course=self.verified_course, user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
|
||||
response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.verified_course.id)}))
|
||||
self.assertContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
|
||||
|
||||
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
|
||||
def test_masquerade_as_verified_student(self):
|
||||
user_group_id = self.get_group_id_by_course_mode_name(
|
||||
self.verified_course.id,
|
||||
'Verified Certificate'
|
||||
)
|
||||
self.update_masquerade(course=self.verified_course, group_id=user_group_id,
|
||||
user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
|
||||
response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.verified_course.id)}))
|
||||
self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
|
||||
|
||||
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
|
||||
def test_masquerade_as_masters_student(self):
|
||||
user_group_id = self.get_group_id_by_course_mode_name(
|
||||
self.masters_course.id,
|
||||
'Masters'
|
||||
)
|
||||
self.update_masquerade(course=self.masters_course, group_id=user_group_id,
|
||||
user_partition_id=ENROLLMENT_TRACK_PARTITION_ID)
|
||||
response = self.client.get(reverse('courseware', kwargs={'course_id': str(self.masters_course.id)}))
|
||||
|
||||
self.assertNotContains(response, TEST_VERIFICATION_SOCK_LOCATOR, html=False)
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
"""
|
||||
Fragment for rendering the course's sock and associated toggle button.
|
||||
"""
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from lms.djangoapps.courseware.utils import (
|
||||
can_show_verified_upgrade,
|
||||
verified_upgrade_deadline_link
|
||||
)
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.features.discounts.utils import format_strikeout_price
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
|
||||
|
||||
class CourseSockFragmentView(EdxFragmentView):
|
||||
"""
|
||||
A fragment to provide extra functionality in a dropdown sock.
|
||||
"""
|
||||
def render_to_fragment(self, request, course, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Render the course's sock fragment.
|
||||
"""
|
||||
context = self.get_verification_context(request, course)
|
||||
html = render_to_string('course_experience/course-sock-fragment.html', context)
|
||||
return Fragment(html)
|
||||
|
||||
@staticmethod
|
||||
def get_verification_context(request, course): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
enrollment = CourseEnrollment.get_enrollment(request.user, course.id)
|
||||
show_course_sock = can_show_verified_upgrade(request.user, enrollment, course)
|
||||
if show_course_sock:
|
||||
upgrade_url = verified_upgrade_deadline_link(request.user, course=course)
|
||||
course_price, _ = format_strikeout_price(request.user, course)
|
||||
else:
|
||||
upgrade_url = ''
|
||||
course_price = ''
|
||||
|
||||
context = {
|
||||
'show_course_sock': show_course_sock,
|
||||
'course_price': course_price,
|
||||
'course_id': course.id,
|
||||
'upgrade_url': upgrade_url,
|
||||
}
|
||||
|
||||
return context
|
||||
@@ -130,7 +130,6 @@ module.exports = Merge.smart({
|
||||
CompletionOnViewService: './lms/static/completion/js/CompletionOnViewService.js',
|
||||
|
||||
// Features
|
||||
CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js',
|
||||
Currency: './openedx/features/course_experience/static/course_experience/js/currency.js',
|
||||
|
||||
AnnouncementsView: './openedx/features/announcements/static/announcements/jsx/Announcements.jsx',
|
||||
@@ -194,19 +193,19 @@ module.exports = Merge.smart({
|
||||
multiple: [
|
||||
{ search: defineHeader, replace: '' },
|
||||
{ search: defineFooter, replace: '' },
|
||||
{
|
||||
{
|
||||
search: /(\/\* RequireJS) \*\//g,
|
||||
replace(match, p1, offset, string) {
|
||||
return p1;
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
search: /\/\* Webpack/g,
|
||||
replace(match, p1, offset, string) {
|
||||
return match + ' */';
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
search: /text!(.*?\.underscore)/g,
|
||||
replace(match, p1, offset, string) {
|
||||
return p1;
|
||||
@@ -657,13 +656,13 @@ module.exports = Merge.smart({
|
||||
// We used to have node: { fs: 'empty' } in this file,
|
||||
// that is no longer supported. Adding this based on the recommendation in
|
||||
// https://stackoverflow.com/questions/64361940/webpack-error-configuration-node-has-an-unknown-property-fs
|
||||
//
|
||||
//
|
||||
// With this uncommented tests fail
|
||||
// Tests failed in the following suites:
|
||||
// * lms javascript
|
||||
// * xmodule-webpack javascript
|
||||
// Error: define cannot be used indirect
|
||||
//
|
||||
//
|
||||
// fallback: {
|
||||
// fs: false
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user