diff --git a/common/static/sass/edx-pattern-library-shims/_buttons.scss b/common/static/sass/edx-pattern-library-shims/_buttons.scss
index 08eb3361dd..19bcdea1e9 100644
--- a/common/static/sass/edx-pattern-library-shims/_buttons.scss
+++ b/common/static/sass/edx-pattern-library-shims/_buttons.scss
@@ -10,6 +10,8 @@
// ----------------------------
%btn-shims {
display: inline-block;
+ background-color: transparent;
+ background-image: none;
border-style: $btn-border-style;
border-radius: $btn-border-radius;
border-width: $btn-border-size;
diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py
index 2e0b2dc007..c0b552659c 100644
--- a/lms/djangoapps/courseware/date_summary.py
+++ b/lms/djangoapps/courseware/date_summary.py
@@ -118,6 +118,14 @@ class DateSummary(object):
return datetime.now(utc) <= self.date
return False
+ def deadline_has_passed(self):
+ """
+ Return True if a deadline (the date) exists, and has already passed.
+ Returns False otherwise.
+ """
+ deadline = self.date
+ return deadline is not None and deadline <= datetime.now(utc)
+
def __repr__(self):
return u'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format(
title=self.title,
@@ -313,13 +321,6 @@ class VerificationDeadlineDate(DateSummary):
"""Return the verification status for this user."""
return SoftwareSecurePhotoVerification.user_status(self.user)[0]
- def deadline_has_passed(self):
- """
- Return True if a verification deadline exists, and has already passed.
- """
- deadline = self.date
- return deadline is not None and deadline <= datetime.now(utc)
-
def must_retry(self):
"""Return True if the user must re-submit verification, False otherwise."""
return self.verification_status == 'must_reverify'
diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index f186277f71..5c8fdce518 100644
--- a/lms/djangoapps/courseware/tests/test_views.py
+++ b/lms/djangoapps/courseware/tests/test_views.py
@@ -209,8 +209,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20
@ddt.data(
- (ModuleStoreEnum.Type.mongo, 10, 147),
- (ModuleStoreEnum.Type.split, 4, 147),
+ (ModuleStoreEnum.Type.mongo, 10, 149),
+ (ModuleStoreEnum.Type.split, 4, 149),
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py
index 9709c6ad50..62e8593081 100644
--- a/lms/djangoapps/courseware/views/index.py
+++ b/lms/djangoapps/courseware/views/index.py
@@ -33,6 +33,7 @@ from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG, default_course_url_name
from openedx.features.enterprise_support.api import data_sharing_consent_required
+from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
from request_cache.middleware import RequestCache
from shoppingcart.models import CourseRegistrationCode
from student.views import is_course_blocked
@@ -367,6 +368,9 @@ 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)
diff --git a/lms/static/sass/_build-course.scss b/lms/static/sass/_build-course.scss
index d073a2c27b..c23b8dad17 100644
--- a/lms/static/sass/_build-course.scss
+++ b/lms/static/sass/_build-course.scss
@@ -68,3 +68,6 @@
// responsive
@import 'base/layouts'; // temporary spot for responsive course
+
+// features
+@import 'features/course-sock';
diff --git a/lms/static/sass/_build-lms-v2.scss b/lms/static/sass/_build-lms-v2.scss
index 3c1e078fdc..595cfd3542 100644
--- a/lms/static/sass/_build-lms-v2.scss
+++ b/lms/static/sass/_build-lms-v2.scss
@@ -27,3 +27,4 @@
@import 'features/bookmarks';
@import 'features/course-experience';
@import 'features/course-search';
+@import 'features/course-sock';
diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss
index 0337bf41d3..a702fe5858 100644
--- a/lms/static/sass/features/_course-experience.scss
+++ b/lms/static/sass/features/_course-experience.scss
@@ -107,6 +107,10 @@
border: 1px solid $lms-active-color;
}
}
+
+ &:last-child {
+ border-bottom: none;
+ }
}
}
}
@@ -186,4 +190,3 @@
}
}
}
-
diff --git a/lms/static/sass/features/_course-sock.scss b/lms/static/sass/features/_course-sock.scss
new file mode 100644
index 0000000000..a457033a45
--- /dev/null
+++ b/lms/static/sass/features/_course-sock.scss
@@ -0,0 +1,180 @@
+.verification-sock {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ margin-top: $baseline;
+ max-width: $lms-max-width;
+ 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: $button-bg-hover-color;
+ background-color: $success-color;
+ border-color: $success-color;
+ background-image: none;
+ box-shadow: none;
+ -webkit-transition: background-color 0.5s;
+ transition: background-color 0.5s;
+
+ &.active {
+ color: $success-color;
+ background-color: $button-bg-hover-color;
+ border-color: $success-color;
+ background-image: none;
+ box-shadow: none;
+
+ &:hover {
+ color: $button-bg-hover-color;
+ background-color: $success-color-hover;
+ border-color: $success-color-hover;
+ background-image: none;
+ box-shadow: none;
+ }
+ }
+
+ &:hover {
+ color: $button-bg-hover-color;
+ background-color: $success-color-hover;
+ border-color: $success-color-hover;
+ background-image: none;
+ box-shadow: none;
+ }
+ }
+
+ .verification-main-panel {
+ display: none;
+ overflow: hidden;
+ border-top: 1px solid $lms-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;
+
+ @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);
+ }
+
+ h2 {
+ font-size: 1.5rem;
+ font-weight: 700;
+ }
+
+ h4 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ }
+
+ .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: $success-color;
+ border-color: $success-color;
+ background-image: none;
+ box-shadow: none;
+
+ @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;
+ }
+ }
+
+ &:hover {
+ background-color: $success-color-hover;
+ border-color: $success-color-hover;
+ }
+ }
+ }
+ }
+}
+
+// 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: $button-bg-hover-color;
+ background-color: $success-color;
+ box-shadow: none;
+ border: 1px solid $success-color;
+
+ &:hover {
+ background-color: $success-color-hover;
+ }
+ }
+ }
+
+ .verification-main-panel {
+ border-top: 0;
+ border-bottom: 1px solid $lms-border-color;
+ }
+ }
+}
diff --git a/lms/static/sass/partials/base/_variables.scss b/lms/static/sass/partials/base/_variables.scss
index 76becc6145..00aa6d412f 100644
--- a/lms/static/sass/partials/base/_variables.scss
+++ b/lms/static/sass/partials/base/_variables.scss
@@ -36,7 +36,7 @@ $fg-gutter: $gw-gutter !default;
$fg-max-columns: 12 !default;
$fg-max-width: 1400px !default;
$fg-min-width: 810px !default;
-
+$lms-max-width: 1180px !default;
// ----------------------------
// #COLORS
@@ -218,7 +218,7 @@ $active-color: $blue !default;
$highlight-color: rgb(255,255,0) !default;
$alert-color: rgb(212, 64, 64) !default;
$success-color: rgb(0, 155, 0) !default;
-
+$success-color-hover: rgb(0, 129, 0) !default;
// ----------------------------
// #COLORS- EDX-SPECIFIC
diff --git a/lms/static/sass/shared-v2/_variables.scss b/lms/static/sass/shared-v2/_variables.scss
index 503efd066b..34a2cd7b9a 100644
--- a/lms/static/sass/shared-v2/_variables.scss
+++ b/lms/static/sass/shared-v2/_variables.scss
@@ -9,27 +9,38 @@
// ----------------------------
// #GRID
// ----------------------------
-$lms-max-width: 1180px;
+$lms-max-width: 1180px !default;
// ----------------------------
// #COLORS
// ----------------------------
-$lms-gray: palette(grayscale, base);
-$lms-background-color: palette(grayscale, x-back);
-$lms-container-background-color: $white;
-$lms-border-color: palette(grayscale, back);
-$lms-label-color: palette(grayscale, black);
-$lms-active-color: palette(primary, base);
-$lms-preview-menu-color: #c8c8c8;
-$white-transparent: rgba(255, 255, 255, 0);
-$white-opacity-40: rgba(255, 255, 255, 0.4);
-$white-opacity-60: rgba(255, 255, 255, 0.6);
-$white-opacity-70: rgba(255, 255, 255, 0.7);
-$white-opacity-80: rgba(255, 255, 255, 0.8);
+$lms-gray: palette(grayscale, base) !default;
+$lms-background-color: palette(grayscale, x-back) !default;
+$lms-container-background-color: $white !default;
+$lms-border-color: palette(grayscale, back) !default;
+$lms-label-color: palette(grayscale, black) !default;
+$lms-active-color: palette(primary, base) !default;
+$lms-preview-menu-color: #c8c8c8 !default;
+$success-color: palette(success, accent) !default;
+$success-color-hover: palette(success, text) !default;
-$light-grey-transparent: rgba(200,200,200, 0);
-$light-grey-solid: rgba(200,200,200, 1);
+$button-bg-hover-color: $white !default;
+
+$white-transparent: rgba(255, 255, 255, 0) !default;
+$white-opacity-40: rgba(255, 255, 255, 0.4) !default;
+$white-opacity-60: rgba(255, 255, 255, 0.6) !default;
+$white-opacity-70: rgba(255, 255, 255, 0.7) !default;
+$white-opacity-80: rgba(255, 255, 255, 0.8) !default;
+
+$black: rgb(0,0,0) !default;
+$black-t0: rgba($black, 0.125) !default;
+$black-t1: rgba($black, 0.25) !default;
+$black-t2: rgba($black, 0.5) !default;
+$black-t3: rgba($black, 0.75) !default;
+
+$light-grey-transparent: rgba(200,200,200, 0) !default;
+$light-grey-solid: rgba(200,200,200, 1) !default;
// ----------------------------
// #TYPOGRAPHY
@@ -42,9 +53,10 @@ $font-bold: 700 !default;
// ----------------------------
// #ICONS
// ----------------------------
-$lms-dark-icon-color: $white;
-$lms-dark-icon-background-color: palette(grayscale, black);
+// Icons
+$lms-dark-icon-color: $white !default;
+$lms-dark-icon-background-color: palette(grayscale, black) !default;
-$site-status-color: rgb(182,37,103);
+$site-status-color: rgb(182,37,103) !default;
$shadow-l1: rgba(0,0,0,0.1) !default;
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index b7e9ed4e8c..08973de439 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -226,6 +226,7 @@ ${HTML(fragment.foot_html())}
+ ${HTML(course_sock_fragment.body_html())}
+ ${HTML(course_sock_fragment.body_html())}
%block>
diff --git a/openedx/features/course_experience/templates/course_experience/course-sock-fragment.html b/openedx/features/course_experience/templates/course_experience/course-sock-fragment.html
new file mode 100644
index 0000000000..9b3f89b599
--- /dev/null
+++ b/openedx/features/course_experience/templates/course_experience/course-sock-fragment.html
@@ -0,0 +1,69 @@
+## mako
+
+<%page expression_filter="h"/>
+<%namespace name='static' file='../static_content.html'/>
+
+<%!
+from openedx.core.djangolib.markup import HTML
+from openedx.features.course_experience import DISPLAY_COURSE_SOCK
+%>
+
+<%block name="content">
+ % if show_course_sock and DISPLAY_COURSE_SOCK.is_enabled(course_id):
+
+
+ Learn About Verified Certificate
+
+
+
+
edX Verified Certificate
+
Why upgrade?
+
+ Official proof of completion
+ Easily shareable certificate
+ Proven motivator to complete the course
+ Certificate purchases help edX continue to offer free courses
+
+
How it works
+
+ Pay the Verified Certificate upgrade fee
+ Verify your identity with a webcam and government-issued ID
+ Study hard and pass the course
+ Share your certificate with friends, employers, and others
+
+
edX Learner Stories
+
+
+
+ My certificate has helped me showcase my knowledge on my
+ resume - I feel like this certificate could really help me land
+ my dream job!
+ - Christina Fong, edX Learner
+
+
+
+
+
+ I wanted to include a verified certificate on my resume and my profile to
+ illustrate that I am working towards this goal I have and that I have
+ achieved something while I was unemployed.
+ - Cheryl Troell, edX Learner
+
+
+
+
+
+ Upgrade Now (${HTML(course_price)})
+
+
+
+
+
+ % endif
+%block>
+
+<%static:webpack entry="CourseSock">
+ new CourseSock({
+ el:'.verification-sock'
+ });
+%static:webpack>
diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py
index 9b4eaee9ef..491afbe37d 100644
--- a/openedx/features/course_experience/tests/views/test_course_home.py
+++ b/openedx/features/course_experience/tests/views/test_course_home.py
@@ -89,7 +89,7 @@ class TestCourseHomePage(SharedModuleStoreTestCase):
course_home_url(self.course)
# Fetch the view and verify the query counts
- with self.assertNumQueries(45):
+ with self.assertNumQueries(47):
with check_mongo_calls(5):
url = course_home_url(self.course)
self.client.get(url)
diff --git a/openedx/features/course_experience/tests/views/test_course_sock.py b/openedx/features/course_experience/tests/views/test_course_sock.py
new file mode 100644
index 0000000000..6da4e05fb2
--- /dev/null
+++ b/openedx/features/course_experience/tests/views/test_course_sock.py
@@ -0,0 +1,115 @@
+"""
+Tests for course verification sock
+"""
+
+import datetime
+import ddt
+
+from course_modes.models import CourseMode
+from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
+from openedx.features.course_experience import DISPLAY_COURSE_SOCK
+from student.tests.factories import UserFactory, CourseEnrollmentFactory
+from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
+from xmodule.modulestore.tests.factories import CourseFactory
+from .test_course_home import course_home_url
+
+TEST_PASSWORD = 'test'
+TEST_VERIFICATION_SOCK_LOCATOR = ''
+TEST_COURSE_PRICE = 50
+
+
+@ddt.ddt
+class TestCourseSockView(SharedModuleStoreTestCase):
+ """
+ Tests for the course verification sock fragment view.
+ """
+ @classmethod
+ def setUpClass(cls):
+ super(TestCourseSockView, cls).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 a upgrade deadline
+ cls._add_course_mode(cls.verified_course, upgrade_deadline_expired=False)
+ cls._add_course_mode(cls.verified_course_update_expired, upgrade_deadline_expired=True)
+ cls._add_course_mode(cls.verified_course_already_enrolled, upgrade_deadline_expired=False)
+
+ def setUp(self):
+ super(TestCourseSockView, self).setUp()
+ self.user = UserFactory.create()
+
+ # 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)
+
+ # Log the user in
+ self.client.login(username=self.user.username, password=TEST_PASSWORD)
+
+ @override_waffle_flag(DISPLAY_COURSE_SOCK, active=True)
+ def test_standard_course(self):
+ """
+ Assure that a course that cannot be verified does
+ not have a visible verification sock.
+ """
+ response = self.client.get(course_home_url(self.standard_course))
+ self.assertEqual(self.is_verified_sock_visible(response), False,
+ 'Student should not be able to see sock in a unverifiable course.')
+
+ @override_waffle_flag(DISPLAY_COURSE_SOCK, active=True)
+ def test_verified_course(self):
+ """
+ Assure that a course that can be verified has a
+ visible verification sock.
+ """
+ response = self.client.get(course_home_url(self.verified_course))
+ self.assertEqual(self.is_verified_sock_visible(response), True,
+ 'Student should be able to see sock in a verifiable course.')
+
+ @override_waffle_flag(DISPLAY_COURSE_SOCK, active=True)
+ def test_verified_course_updated_expired(self):
+ """
+ Assure that a course that has an expired upgrade
+ date does not display the verification sock.
+ """
+ response = self.client.get(course_home_url(self.verified_course_update_expired))
+ self.assertEqual(self.is_verified_sock_visible(response), False,
+ 'Student should be able to see sock in a verifiable course if the update expiration date has passed.')
+
+ @override_waffle_flag(DISPLAY_COURSE_SOCK, active=True)
+ def test_verified_course_user_already_upgraded(self):
+ """
+ Assure that a user that has already upgraded to a
+ verified status cannot see the verification sock.
+ """
+ response = self.client.get(course_home_url(self.verified_course_already_enrolled))
+ self.assertEqual(self.is_verified_sock_visible(response), False,
+ 'Student should be able to see sock if they have already upgraded to verified mode.')
+
+ @classmethod
+ def is_verified_sock_visible(cls, response):
+ return TEST_VERIFICATION_SOCK_LOCATOR in response.content
+
+ @classmethod
+ def _add_course_mode(cls, course, upgrade_deadline_expired=False):
+ """
+ Adds a course mode to the test course.
+ """
+ upgrade_exp_date = datetime.datetime.now()
+ if upgrade_deadline_expired:
+ upgrade_exp_date = upgrade_exp_date - datetime.timedelta(days=21)
+ else:
+ upgrade_exp_date = upgrade_exp_date + datetime.timedelta(days=21)
+
+ CourseMode(
+ course_id=course.id,
+ mode_slug=CourseMode.VERIFIED,
+ mode_display_name="Verified Certificate",
+ min_price=TEST_COURSE_PRICE,
+ _expiration_datetime=upgrade_exp_date, # pylint: disable=protected-access
+ ).save()
diff --git a/openedx/features/course_experience/urls.py b/openedx/features/course_experience/urls.py
index 42372fb9f8..39d5ecf833 100644
--- a/openedx/features/course_experience/urls.py
+++ b/openedx/features/course_experience/urls.py
@@ -8,6 +8,7 @@ from views.course_home import CourseHomeFragmentView, CourseHomeView
from views.course_outline import CourseOutlineFragmentView
from views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView
from views.welcome_message import WelcomeMessageFragmentView
+from views.course_sock import CourseSockFragmentView
urlpatterns = [
url(
@@ -40,4 +41,9 @@ urlpatterns = [
WelcomeMessageFragmentView.as_view(),
name='openedx.course_experience.welcome_message_fragment_view',
),
+ url(
+ r'course_sock_fragment$',
+ CourseSockFragmentView.as_view(),
+ name='openedx.course_experience.course_sock_fragment_view',
+ ),
]
diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py
index aa3922fc26..aa6a43fbf6 100644
--- a/openedx/features/course_experience/views/course_home.py
+++ b/openedx/features/course_experience/views/course_home.py
@@ -20,6 +20,7 @@ from ..utils import get_course_outline_block_tree
from .course_dates import CourseDatesFragmentView
from .course_outline import CourseOutlineFragmentView
from .welcome_message import WelcomeMessageFragmentView
+from .course_sock import CourseSockFragmentView
class CourseHomeView(CourseTabView):
@@ -105,6 +106,9 @@ class CourseHomeFragmentView(EdxFragmentView):
# TODO: Use get_course_overview_with_access and blocks api
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
+ # Render the verification sock as a fragment
+ course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs)
+
# Get the handouts
handouts_html = get_course_info_section(request, request.user, course, 'handouts')
@@ -119,6 +123,7 @@ class CourseHomeFragmentView(EdxFragmentView):
'resume_course_url': resume_course_url,
'dates_fragment': dates_fragment,
'welcome_message_fragment': welcome_message_fragment,
+ 'course_sock_fragment': course_sock_fragment,
'disable_courseware_js': True,
'uses_pattern_library': True,
}
diff --git a/openedx/features/course_experience/views/course_sock.py b/openedx/features/course_experience/views/course_sock.py
new file mode 100644
index 0000000000..57863ed3a3
--- /dev/null
+++ b/openedx/features/course_experience/views/course_sock.py
@@ -0,0 +1,56 @@
+"""
+Fragment for rendering the course's sock and associated toggle button.
+"""
+from datetime import datetime
+
+from django.conf import settings
+from django.template.loader import render_to_string
+from opaque_keys.edx.keys import CourseKey
+from web_fragments.fragment import Fragment
+
+from student.models import CourseEnrollment
+from course_modes.models import CourseMode
+from courseware.date_summary import VerifiedUpgradeDeadlineDate
+from courseware.courses import get_course_with_access
+from courseware.views.views import get_course_prices
+from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
+
+
+class CourseSockFragmentView(EdxFragmentView):
+ """
+ A fragment to provide extra functionality in a dropdown sock.
+ """
+ def render_to_fragment(self, request, course, **kwargs):
+ """
+ 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)
+
+ def get_verification_context(self, request, course):
+ course_key = CourseKey.from_string(unicode(course.id))
+
+ # Establish whether the course has a verified mode
+ available_modes = CourseMode.modes_for_course_dict(unicode(course.id))
+ has_verified_mode = CourseMode.has_verified_mode(available_modes)
+
+ # Establish whether the user is already enrolled
+ is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user.id, course_key)
+
+ # Establish whether the verification deadline has already passed
+ verification_deadline = VerifiedUpgradeDeadlineDate(course, request.user)
+ deadline_has_passed = verification_deadline.deadline_has_passed()
+
+ show_course_sock = has_verified_mode and not is_already_verified and not deadline_has_passed
+
+ # Get the price of the course and format correctly
+ course_prices = get_course_prices(course)
+
+ context = {
+ 'show_course_sock': show_course_sock,
+ 'course_price': course_prices[1],
+ 'course_id': course.id
+ }
+
+ return context
diff --git a/webpack.config.js b/webpack.config.js
index 5a5800a3f2..6719071a23 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -19,6 +19,7 @@ var wpconfig = {
entry: {
CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js',
+ CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js',
Import: './cms/static/js/features/import/factories/import.js'
},