Show course home messages for important course dates
LEARNER-2073
This commit is contained in:
@@ -121,3 +121,32 @@
|
||||
color: $btn-brand-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// #UPGRADE
|
||||
// ----------------------------
|
||||
.btn-upgrade {
|
||||
@extend %btn-shims;
|
||||
|
||||
border-color: $btn-upgrade-border-color;
|
||||
background: $btn-upgrade-background;
|
||||
color: $btn-upgrade-color;
|
||||
|
||||
// STATE: hover and focus
|
||||
&:hover,
|
||||
&.is-hovered,
|
||||
&:focus,
|
||||
&.is-focused {
|
||||
border-color: $btn-upgrade-focus-border-color;
|
||||
background-color: $btn-upgrade-focus-background;
|
||||
color: $btn-upgrade-focus-color;
|
||||
}
|
||||
|
||||
// STATE: is disabled
|
||||
&:disabled,
|
||||
&.is-disabled {
|
||||
border-color: $btn-disabled-border-color;
|
||||
background: $btn-brand-disabled-background;
|
||||
color: $btn-upgrade-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,9 +143,8 @@ $error-color: rgb(203, 7, 18) !default;
|
||||
$success-color: rgb(0, 155, 0) !default;
|
||||
$warning-color: rgb(255, 192, 31) !default;
|
||||
$warning-color-accent: rgb(255, 252, 221) !default;
|
||||
$general-color: $uxpl-blue-base !default;;
|
||||
$general-color-accent: $uxpl-blue-base !default
|
||||
|
||||
$general-color: $uxpl-blue-base !default;
|
||||
$general-color-accent: $uxpl-blue-base !default;
|
||||
|
||||
// CAPA correctness color to be consistent with Alert styles above
|
||||
$correct: $success-color !default;
|
||||
@@ -181,6 +180,16 @@ $btn-brand-active-background: $uxpl-blue-base !default;
|
||||
$btn-brand-disabled-background: #f2f3f3 !default;
|
||||
$btn-brand-disabled-color: #676666 !default;
|
||||
|
||||
// Upgrade button
|
||||
$btn-upgrade-border-color: $uxpl-green-base !default;
|
||||
$btn-upgrade-background: $uxpl-green-base !default;
|
||||
$btn-upgrade-color: #fcfcfc !default;
|
||||
$btn-upgrade-focus-color: $btn-upgrade-color !default;
|
||||
$btn-upgrade-focus-border-color: rgb(0, 155, 0) !default;
|
||||
$btn-upgrade-focus-background: rgb(0, 155, 0) !default;
|
||||
$btn-upgrade-active-border-color: $uxpl-green-base !default;
|
||||
$btn-upgrade-active-background: $uxpl-green-base !default;
|
||||
|
||||
// ----------------------------
|
||||
// #SETTINGS
|
||||
// ----------------------------
|
||||
|
||||
@@ -4,6 +4,8 @@ from urlparse import urljoin
|
||||
|
||||
import waffle
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from commerce.models import CommerceConfiguration
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
@@ -93,3 +95,16 @@ class EcommerceService(object):
|
||||
checkout_page_path=self.get_absolute_ecommerce_url(self.config.MULTIPLE_ITEMS_BASKET_PAGE_URL),
|
||||
skus=urlencode({'sku': skus}, doseq=True),
|
||||
)
|
||||
|
||||
def upgrade_url(self, user, course_key):
|
||||
"""
|
||||
Returns the URL for the user to upgrade, or None if not applicable.
|
||||
"""
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course_key)
|
||||
verified_mode = enrollment.verified_mode if enrollment else None
|
||||
if verified_mode:
|
||||
if self.is_enabled(user):
|
||||
return self.get_checkout_page_url(verified_mode.sku)
|
||||
else:
|
||||
return reverse('verify_student_upgrade_and_verify', args=(course_key,))
|
||||
return None
|
||||
|
||||
@@ -3,26 +3,45 @@ This module provides date summary blocks for the Course Info
|
||||
page. Each block gives information about a particular
|
||||
course-run-specific date which will be displayed to the user.
|
||||
"""
|
||||
import crum
|
||||
import datetime
|
||||
|
||||
from babel.dates import format_timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import get_language, to_locale, ugettext_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from lazy import lazy
|
||||
from pytz import timezone, utc
|
||||
from pytz import utc
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.models import CourseMode, get_cosmetic_verified_display_price
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
|
||||
from openedx.core.djangoapps.certificates.api import can_show_certificate_available_date_field
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.course_experience import CourseHomeMessages, UPGRADE_DEADLINE_MESSAGE
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from .context_processor import user_timezone_locale_prefs
|
||||
|
||||
|
||||
class DateSummary(object):
|
||||
"""Base class for all date summary blocks."""
|
||||
|
||||
# A consistent representation of the current time.
|
||||
_current_time = None
|
||||
|
||||
@property
|
||||
def current_time(self):
|
||||
"""
|
||||
Returns a consistent current time.
|
||||
"""
|
||||
if self._current_time is None:
|
||||
self._current_time = datetime.datetime.now(utc)
|
||||
return self._current_time
|
||||
|
||||
@property
|
||||
def css_class(self):
|
||||
"""
|
||||
@@ -41,6 +60,12 @@ class DateSummary(object):
|
||||
"""The detail text displayed by this summary."""
|
||||
return ''
|
||||
|
||||
def register_alerts(self, request, course):
|
||||
"""
|
||||
Registers any relevant course alerts given the current request.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
"""This summary's date."""
|
||||
@@ -64,15 +89,6 @@ class DateSummary(object):
|
||||
"""The text of the link."""
|
||||
return ''
|
||||
|
||||
@property
|
||||
def time_zone(self):
|
||||
"""
|
||||
The time zone in which to display -- defaults to UTC
|
||||
"""
|
||||
return timezone(
|
||||
self.user.preferences.model.get_value(self.user, "time_zone", "UTC")
|
||||
)
|
||||
|
||||
def __init__(self, course, user, course_id=None):
|
||||
self.course = course
|
||||
self.user = user
|
||||
@@ -87,7 +103,7 @@ class DateSummary(object):
|
||||
if self.date is None:
|
||||
return ''
|
||||
locale = to_locale(get_language())
|
||||
delta = self.date - datetime.datetime.now(utc)
|
||||
delta = self.date - self.current_time
|
||||
try:
|
||||
relative_date = format_timedelta(delta, locale=locale)
|
||||
# Babel doesn't have translations for Esperanto, so we get
|
||||
@@ -117,7 +133,7 @@ class DateSummary(object):
|
||||
future.
|
||||
"""
|
||||
if self.date is not None:
|
||||
return datetime.datetime.now(utc).date() <= self.date.date()
|
||||
return self.current_time.date() <= self.date.date()
|
||||
return False
|
||||
|
||||
def deadline_has_passed(self):
|
||||
@@ -126,7 +142,52 @@ class DateSummary(object):
|
||||
Returns False otherwise.
|
||||
"""
|
||||
deadline = self.date
|
||||
return deadline is not None and deadline <= datetime.datetime.now(utc)
|
||||
return deadline is not None and deadline <= self.current_time
|
||||
|
||||
@property
|
||||
def time_remaining_string(self):
|
||||
"""
|
||||
Returns the time remaining as a localized string.
|
||||
"""
|
||||
locale = to_locale(get_language())
|
||||
return format_timedelta(self.date - self.current_time, locale=locale)
|
||||
|
||||
def date_html(self, date_format='shortDate'):
|
||||
"""
|
||||
Returns a representation of the date as HTML.
|
||||
|
||||
Note: this returns a span that will be localized on the client.
|
||||
"""
|
||||
locale = to_locale(get_language())
|
||||
user_timezone = user_timezone_locale_prefs(crum.get_current_request())['user_timezone']
|
||||
return HTML(
|
||||
'<span class="date localized-datetime" data-format="{date_format}" data-datetime="{date_time}"'
|
||||
' data-timezone="{user_timezone}" data-language="{user_language}">'
|
||||
'</span>'
|
||||
).format(
|
||||
date_format=date_format,
|
||||
date_time=self.date,
|
||||
user_timezone=user_timezone,
|
||||
user_language=locale,
|
||||
)
|
||||
|
||||
@property
|
||||
def long_date_html(self):
|
||||
"""
|
||||
Returns a long representation of the date as HTML.
|
||||
|
||||
Note: this returns a span that will be localized on the client.
|
||||
"""
|
||||
return self.date_html(date_format='shortDate')
|
||||
|
||||
@property
|
||||
def short_time_html(self):
|
||||
"""
|
||||
Returns a short representation of the time as HTML.
|
||||
|
||||
Note: this returns a span that will be localized on the client.
|
||||
"""
|
||||
return self.date_html(date_format='shortTime')
|
||||
|
||||
def __repr__(self):
|
||||
return u'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format(
|
||||
@@ -151,7 +212,7 @@ class TodaysDate(DateSummary):
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
return datetime.datetime.now(utc)
|
||||
return self.current_time
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
@@ -169,6 +230,35 @@ class CourseStartDate(DateSummary):
|
||||
def date(self):
|
||||
return self.course.start
|
||||
|
||||
def register_alerts(self, request, course):
|
||||
"""
|
||||
Registers an alert if the course has not started yet.
|
||||
"""
|
||||
is_enrolled = CourseEnrollment.get_enrollment(request.user, course.id)
|
||||
if not course.start or not is_enrolled:
|
||||
return
|
||||
days_until_start = (course.start - self.current_time).days
|
||||
if course.start > self.current_time:
|
||||
if days_until_start > 0:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
"Don't forget to add a calendar reminder!"
|
||||
)),
|
||||
title=Text(_("Course starts in {time_remaining_string} on {course_start_date}.")).format(
|
||||
time_remaining_string=self.time_remaining_string,
|
||||
course_start_date=self.long_date_html,
|
||||
)
|
||||
)
|
||||
else:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_("Course starts in {time_remaining_string} at {course_start_time}.")).format(
|
||||
time_remaining_string=self.time_remaining_string,
|
||||
course_start_time=self.short_time_html,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class CourseEndDate(DateSummary):
|
||||
"""
|
||||
@@ -183,7 +273,7 @@ class CourseEndDate(DateSummary):
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
if datetime.datetime.now(utc) <= self.date:
|
||||
if self.current_time <= self.date:
|
||||
mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id)
|
||||
if is_active and CourseMode.is_eligible_for_certificate(mode):
|
||||
return _('To earn a certificate, you must complete all requirements before this date.')
|
||||
@@ -195,6 +285,35 @@ class CourseEndDate(DateSummary):
|
||||
def date(self):
|
||||
return self.course.end
|
||||
|
||||
def register_alerts(self, request, course):
|
||||
"""
|
||||
Registers an alert if the end date is approaching.
|
||||
"""
|
||||
is_enrolled = CourseEnrollment.get_enrollment(request.user, course.id)
|
||||
if not course.start or self.current_time < course.start or not is_enrolled:
|
||||
return
|
||||
days_until_end = (course.end - self.current_time).days
|
||||
if course.end > self.current_time and days_until_end <= settings.COURSE_MESSAGE_ALERT_DURATION_IN_DAYS:
|
||||
if days_until_end > 0:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(self.description),
|
||||
title=Text(_('This course is ending in {time_remaining_string} on {course_end_date}.')).format(
|
||||
time_remaining_string=self.time_remaining_string,
|
||||
course_end_date=self.long_date_html,
|
||||
)
|
||||
)
|
||||
else:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(self.description),
|
||||
title=Text(_('This course is ending in {time_remaining_string} at {course_end_time}.')).format(
|
||||
time_remaining_string=self.time_remaining_string,
|
||||
course_end_time=self.short_time_html,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
class CertificateAvailableDate(DateSummary):
|
||||
"""
|
||||
@@ -216,7 +335,7 @@ class CertificateAvailableDate(DateSummary):
|
||||
can_show_certificate_available_date_field(self.course) and
|
||||
self.has_certificate_modes and
|
||||
self.date is not None and
|
||||
datetime.datetime.now(utc) <= self.date and
|
||||
self.current_time <= self.date and
|
||||
len(self.active_certificates) > 0
|
||||
)
|
||||
|
||||
@@ -252,13 +371,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
ecommerce_service = EcommerceService()
|
||||
if ecommerce_service.is_enabled(self.user):
|
||||
course_mode = CourseMode.objects.get(
|
||||
course_id=self.course_id, mode_slug=CourseMode.VERIFIED
|
||||
)
|
||||
return ecommerce_service.get_checkout_page_url(course_mode.sku)
|
||||
return reverse('verify_student_upgrade_and_verify', args=(self.course_id,))
|
||||
return EcommerceService().upgrade_url(self.user, self.course_id)
|
||||
|
||||
@cached_property
|
||||
def enrollment(self):
|
||||
@@ -299,6 +412,39 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
|
||||
|
||||
return deadline
|
||||
|
||||
def register_alerts(self, request, course):
|
||||
"""
|
||||
Registers an alert if the verification deadline is approaching.
|
||||
"""
|
||||
upgrade_price = get_cosmetic_verified_display_price(course)
|
||||
if not UPGRADE_DEADLINE_MESSAGE.is_enabled(course.id) or not self.is_enabled or not upgrade_price:
|
||||
return
|
||||
days_left_to_upgrade = (self.date - self.current_time).days
|
||||
if self.date > self.current_time and days_left_to_upgrade <= settings.COURSE_MESSAGE_ALERT_DURATION_IN_DAYS:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
'In order to qualify for a certificate, you must meet all course grading '
|
||||
'requirements, upgrade before the course deadline, and successfully verify '
|
||||
'your identity on {platform_name} if you have not done so already.{button_panel}'
|
||||
)).format(
|
||||
platform_name=settings.PLATFORM_NAME,
|
||||
button_panel=HTML(
|
||||
'<div class="message-actions">'
|
||||
'<a class="btn btn-upgrade" href="{upgrade_url}">{upgrade_label}</a>'
|
||||
'</div>'
|
||||
).format(
|
||||
upgrade_url=self.link,
|
||||
upgrade_label=Text(_('Upgrade ({upgrade_price})')).format(upgrade_price=upgrade_price),
|
||||
)
|
||||
),
|
||||
title=Text(_(
|
||||
"Don't forget, you have {time_remaining_string} left to upgrade to a Verified Certificate."
|
||||
)).format(
|
||||
time_remaining_string=self.time_remaining_string,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VerificationDeadlineDate(DateSummary):
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,9 @@ from datetime import datetime, timedelta
|
||||
|
||||
import ddt
|
||||
import waffle
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import RequestFactory, TestCase
|
||||
from freezegun import freeze_time
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
@@ -31,7 +33,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
|
||||
from openedx.features.course_experience import CourseHomeMessages, UNIFIED_COURSE_TAB_FLAG, UPGRADE_DEADLINE_MESSAGE
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory, TEST_PASSWORD
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -46,20 +48,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
super(CourseDateSummaryTest, self).setUp()
|
||||
SelfPacedConfiguration.objects.create(enable_course_home_improvements=True)
|
||||
|
||||
def create_user(self, verification_status=None):
|
||||
""" Create a new User instance.
|
||||
|
||||
Arguments:
|
||||
verification_status (str): User's verification status. If this value is set an instance of
|
||||
SoftwareSecurePhotoVerification will be created for the user with the specified status.
|
||||
"""
|
||||
user = UserFactory()
|
||||
|
||||
if verification_status is not None:
|
||||
SoftwareSecurePhotoVerificationFactory.create(user=user, status=verification_status)
|
||||
|
||||
return user
|
||||
|
||||
def enable_course_certificates(self, course):
|
||||
""" Enable course certificate configuration """
|
||||
course.certificates = {
|
||||
@@ -74,7 +62,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_course_info_feature_flag(self):
|
||||
SelfPacedConfiguration(enable_course_home_improvements=False).save()
|
||||
course = create_course_run()
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
|
||||
self.client.login(username=user.username, password=TEST_PASSWORD)
|
||||
@@ -144,7 +132,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
@ddt.unpack
|
||||
def test_enabled_block_types(self, course_kwargs, user_kwargs, expected_blocks):
|
||||
course = create_course_run(**course_kwargs)
|
||||
user = self.create_user(**user_kwargs)
|
||||
user = create_user(**user_kwargs)
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
self.assert_block_types(course, user, expected_blocks)
|
||||
|
||||
@@ -160,12 +148,12 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
@ddt.unpack
|
||||
def test_enabled_block_types_without_enrollment(self, course_kwargs, expected_blocks):
|
||||
course = create_course_run(**course_kwargs)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
self.assert_block_types(course, user, expected_blocks)
|
||||
|
||||
def test_enabled_block_types_with_non_upgradeable_course_run(self):
|
||||
course = create_course_run(days_till_start=-10, days_till_verification_deadline=None)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED).delete()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT)
|
||||
self.assert_block_types(course, user, (TodaysDate, CourseEndDate))
|
||||
@@ -177,7 +165,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
"""
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run()
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
block = TodaysDate(course, user)
|
||||
self.assertTrue(block.is_enabled)
|
||||
self.assertEqual(block.date, datetime.now(utc))
|
||||
@@ -191,7 +179,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_todays_date_no_timezone(self, url_name):
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run()
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
self.client.login(username=user.username, password=TEST_PASSWORD)
|
||||
|
||||
html_elements = [
|
||||
@@ -216,7 +204,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_todays_date_timezone(self, url_name):
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run()
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
self.client.login(username=user.username, password=TEST_PASSWORD)
|
||||
set_user_preference(user, 'time_zone', 'America/Los_Angeles')
|
||||
url = reverse(url_name, args=(course.id,))
|
||||
@@ -237,7 +225,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
## Tests Course Start Date
|
||||
def test_course_start_date(self):
|
||||
course = create_course_run()
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
block = CourseStartDate(course, user)
|
||||
self.assertEqual(block.date, course.start)
|
||||
|
||||
@@ -249,7 +237,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_start_date_render(self, url_name):
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run()
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
self.client.login(username=user.username, password=TEST_PASSWORD)
|
||||
url = reverse(url_name, args=(course.id,))
|
||||
response = self.client.get(url, follow=True)
|
||||
@@ -268,7 +256,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_start_date_render_time_zone(self, url_name):
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run()
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
self.client.login(username=user.username, password=TEST_PASSWORD)
|
||||
set_user_preference(user, 'time_zone', 'America/Los_Angeles')
|
||||
url = reverse(url_name, args=(course.id,))
|
||||
@@ -284,7 +272,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
## Tests Course End Date Block
|
||||
def test_course_end_date_for_certificate_eligible_mode(self):
|
||||
course = create_course_run(days_till_start=-1)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
block = CourseEndDate(course, user)
|
||||
self.assertEqual(
|
||||
@@ -294,7 +282,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
|
||||
def test_course_end_date_for_non_certificate_eligible_mode(self):
|
||||
course = create_course_run(days_till_start=-1)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT)
|
||||
block = CourseEndDate(course, user)
|
||||
self.assertEqual(
|
||||
@@ -305,7 +293,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
|
||||
def test_course_end_date_after_course(self):
|
||||
course = create_course_run(days_till_start=-2, days_till_end=-1)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
block = CourseEndDate(course, user)
|
||||
self.assertEqual(
|
||||
@@ -319,7 +307,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
sku = 'TESTSKU'
|
||||
configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
|
||||
course = create_course_run()
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
course_mode = CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED)
|
||||
course_mode.sku = sku
|
||||
course_mode.save()
|
||||
@@ -332,7 +320,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
@waffle.testutils.override_switch('certificates.auto_certificate_generation', True)
|
||||
def test_no_certificate_available_date(self):
|
||||
course = create_course_run(days_till_start=-1)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT)
|
||||
block = CertificateAvailableDate(course, user)
|
||||
self.assertEqual(block.date, None)
|
||||
@@ -342,7 +330,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
@waffle.testutils.override_switch('certificates.auto_certificate_generation', True)
|
||||
def test_no_certificate_available_date_for_self_paced(self):
|
||||
course = create_self_paced_course_run()
|
||||
verified_user = self.create_user()
|
||||
verified_user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=verified_user, mode=CourseMode.VERIFIED)
|
||||
course.certificate_available_date = datetime.now(utc) + timedelta(days=7)
|
||||
course.save()
|
||||
@@ -356,7 +344,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
if the course only has audit mode.
|
||||
"""
|
||||
course = create_course_run()
|
||||
audit_user = self.create_user()
|
||||
audit_user = create_user()
|
||||
|
||||
# Enroll learner in the audit mode and verify the course only has 1 mode (audit)
|
||||
CourseEnrollmentFactory(course_id=course.id, user=audit_user, mode=CourseMode.AUDIT)
|
||||
@@ -376,9 +364,9 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
@waffle.testutils.override_switch('certificates.auto_certificate_generation', True)
|
||||
def test_certificate_available_date_defined(self):
|
||||
course = create_course_run()
|
||||
audit_user = self.create_user()
|
||||
audit_user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=audit_user, mode=CourseMode.AUDIT)
|
||||
verified_user = self.create_user()
|
||||
verified_user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=verified_user, mode=CourseMode.VERIFIED)
|
||||
course.certificate_available_date = datetime.now(utc) + timedelta(days=7)
|
||||
self.enable_course_certificates(course)
|
||||
@@ -391,14 +379,14 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
## VerificationDeadlineDate
|
||||
def test_no_verification_deadline(self):
|
||||
course = create_course_run(days_till_start=-1, days_till_verification_deadline=None)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
block = VerificationDeadlineDate(course, user)
|
||||
self.assertFalse(block.is_enabled)
|
||||
|
||||
def test_no_verified_enrollment(self):
|
||||
course = create_course_run(days_till_start=-1)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT)
|
||||
block = VerificationDeadlineDate(course, user)
|
||||
self.assertFalse(block.is_enabled)
|
||||
@@ -406,7 +394,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_verification_deadline_date_upcoming(self):
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run(days_till_start=-1)
|
||||
user = self.create_user()
|
||||
user = create_user()
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
|
||||
block = VerificationDeadlineDate(course, user)
|
||||
@@ -423,7 +411,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_verification_deadline_date_retry(self):
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run(days_till_start=-1)
|
||||
user = self.create_user(verification_status='denied')
|
||||
user = create_user(verification_status='denied')
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
|
||||
block = VerificationDeadlineDate(course, user)
|
||||
@@ -440,7 +428,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_verification_deadline_date_denied(self):
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run(days_till_start=-10, days_till_verification_deadline=-1)
|
||||
user = self.create_user(verification_status='denied')
|
||||
user = create_user(verification_status='denied')
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
|
||||
block = VerificationDeadlineDate(course, user)
|
||||
@@ -462,13 +450,104 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_render_date_string_past(self, delta, expected_date_string):
|
||||
with freeze_time('2015-01-02'):
|
||||
course = create_course_run(days_till_start=-10, days_till_verification_deadline=delta)
|
||||
user = self.create_user(verification_status='denied')
|
||||
user = create_user(verification_status='denied')
|
||||
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
|
||||
|
||||
block = VerificationDeadlineDate(course, user)
|
||||
self.assertEqual(block.relative_datestring, expected_date_string)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
@ddt.ddt
|
||||
class TestDateAlerts(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Unit tests for date alerts.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestDateAlerts, self).setUp()
|
||||
with freeze_time('2017-07-01 09:00:00'):
|
||||
self.course = create_course_run(days_till_start=0)
|
||||
self.enrollment = CourseEnrollmentFactory(course_id=self.course.id, mode=CourseMode.AUDIT)
|
||||
self.request = RequestFactory().request()
|
||||
self.request.session = {}
|
||||
self.request.user = self.enrollment.user
|
||||
MessageMiddleware().process_request(self.request)
|
||||
|
||||
@ddt.data(
|
||||
['2017-01-01 09:00:00', u'in 6 months on <span class="date localized-datetime" data-format="shortDate"'],
|
||||
['2017-06-17 09:00:00', u'in 2 weeks on <span class="date localized-datetime" data-format="shortDate"'],
|
||||
['2017-06-30 10:00:00', u'in 1 day at <span class="date localized-datetime" data-format="shortTime"'],
|
||||
['2017-07-01 08:00:00', u'in 1 hour at <span class="date localized-datetime" data-format="shortTime"'],
|
||||
['2017-07-01 08:55:00', u'in 5 minutes at <span class="date localized-datetime" data-format="shortTime"'],
|
||||
['2017-07-01 09:00:00', None],
|
||||
['2017-08-01 09:00:00', None],
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_start_date_alert(self, current_time, expected_message_html):
|
||||
"""
|
||||
Verify that course start date alerts are registered.
|
||||
"""
|
||||
with freeze_time(current_time):
|
||||
block = CourseStartDate(self.course, self.request.user)
|
||||
block.register_alerts(self.request, self.course)
|
||||
messages = list(CourseHomeMessages.user_messages(self.request))
|
||||
if expected_message_html:
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertIn(expected_message_html, messages[0].message_html)
|
||||
else:
|
||||
self.assertEqual(len(messages), 0)
|
||||
|
||||
@ddt.data(
|
||||
['2017-06-30 09:00:00', None],
|
||||
['2017-07-01 09:00:00', u'in 2 weeks on <span class="date localized-datetime" data-format="shortDate"'],
|
||||
['2017-07-14 10:00:00', u'in 1 day at <span class="date localized-datetime" data-format="shortTime"'],
|
||||
['2017-07-15 08:00:00', u'in 1 hour at <span class="date localized-datetime" data-format="shortTime"'],
|
||||
['2017-07-15 08:55:00', u'in 5 minutes at <span class="date localized-datetime" data-format="shortTime"'],
|
||||
['2017-07-15 09:00:00', None],
|
||||
['2017-08-15 09:00:00', None],
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_end_date_alert(self, current_time, expected_message_html):
|
||||
"""
|
||||
Verify that course end date alerts are registered.
|
||||
"""
|
||||
with freeze_time(current_time):
|
||||
block = CourseEndDate(self.course, self.request.user)
|
||||
block.register_alerts(self.request, self.course)
|
||||
messages = list(CourseHomeMessages.user_messages(self.request))
|
||||
if expected_message_html:
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertIn(expected_message_html, messages[0].message_html)
|
||||
else:
|
||||
self.assertEqual(len(messages), 0)
|
||||
|
||||
@ddt.data(
|
||||
['2017-06-20 09:00:00', None],
|
||||
['2017-06-21 09:00:00', u'Don't forget, you have 2 weeks left to upgrade to a Verified Certificate.'],
|
||||
['2017-07-04 10:00:00', u'Don't forget, you have 1 day left to upgrade to a Verified Certificate.'],
|
||||
['2017-07-05 08:00:00', u'Don't forget, you have 1 hour left to upgrade to a Verified Certificate.'],
|
||||
['2017-07-05 08:55:00', u'Don't forget, you have 5 minutes left to upgrade to a Verified Certificate.'],
|
||||
['2017-07-05 09:00:00', None],
|
||||
['2017-08-05 09:00:00', None],
|
||||
)
|
||||
@ddt.unpack
|
||||
@override_waffle_flag(UPGRADE_DEADLINE_MESSAGE, active=True)
|
||||
def test_verified_upgrade_deadline_alert(self, current_time, expected_message_html):
|
||||
"""
|
||||
Verify the verified upgrade deadline alerts.
|
||||
"""
|
||||
with freeze_time(current_time):
|
||||
block = VerifiedUpgradeDeadlineDate(self.course, self.request.user)
|
||||
block.register_alerts(self.request, self.course)
|
||||
messages = list(CourseHomeMessages.user_messages(self.request))
|
||||
if expected_message_html:
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertIn(expected_message_html, messages[0].message_html)
|
||||
else:
|
||||
self.assertEqual(len(messages), 0)
|
||||
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class TestScheduleOverrides(SharedModuleStoreTestCase):
|
||||
|
||||
@@ -560,6 +639,21 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
|
||||
self.assertEqual(block.date, expected)
|
||||
|
||||
|
||||
def create_user(verification_status=None):
|
||||
""" Create a new User instance.
|
||||
|
||||
Arguments:
|
||||
verification_status (str): User's verification status. If this value is set an instance of
|
||||
SoftwareSecurePhotoVerification will be created for the user with the specified status.
|
||||
"""
|
||||
user = UserFactory()
|
||||
|
||||
if verification_status is not None:
|
||||
SoftwareSecurePhotoVerificationFactory.create(user=user, status=verification_status)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def create_course_run(
|
||||
days_till_start=1, days_till_end=14, days_till_upgrade_deadline=4, days_till_verification_deadline=14,
|
||||
):
|
||||
|
||||
@@ -213,8 +213,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
NUM_PROBLEMS = 20
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 10, 145),
|
||||
(ModuleStoreEnum.Type.split, 4, 145),
|
||||
(ModuleStoreEnum.Type.mongo, 10, 147),
|
||||
(ModuleStoreEnum.Type.split, 4, 147),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
|
||||
|
||||
@@ -431,6 +431,9 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
|
||||
RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5
|
||||
RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5
|
||||
|
||||
# Deadline message configurations
|
||||
COURSE_MESSAGE_ALERT_DURATION_IN_DAYS = 14
|
||||
|
||||
############################# SET PATH INFORMATION #############################
|
||||
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms
|
||||
REPO_ROOT = PROJECT_ROOT.dirname()
|
||||
@@ -2589,6 +2592,7 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
|
||||
TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC'
|
||||
|
||||
|
||||
|
||||
########################## VIDEO IMAGE STORAGE ############################
|
||||
|
||||
VIDEO_IMAGE_SETTINGS = dict(
|
||||
|
||||
@@ -7,3 +7,7 @@
|
||||
@import 'base/variables';
|
||||
@import 'base/mixins';
|
||||
@import 'base/theme';
|
||||
|
||||
// Pattern Library shims
|
||||
@import 'edx-pattern-library-shims/base/variables';
|
||||
@import 'edx-pattern-library-shims/buttons';
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// Elements
|
||||
@import 'notifications';
|
||||
@import 'elements/controls';
|
||||
@import 'elements-v2/buttons';
|
||||
@import 'elements-v2/pagination';
|
||||
|
||||
// Features
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
// Upgrade button
|
||||
$btn-upgrade-border-color: $uxpl-green-base !default;
|
||||
$btn-upgrade-background: $uxpl-green-base !default;
|
||||
$btn-upgrade-color: #fcfcfc !default;
|
||||
$btn-upgrade-focus-color: $btn-upgrade-color !default;
|
||||
$btn-upgrade-focus-border-color: rgb(0, 155, 0) !default;
|
||||
$btn-upgrade-focus-background: rgb(0, 155, 0) !default;
|
||||
$btn-upgrade-active-border-color: $uxpl-green-base !default;
|
||||
$btn-upgrade-active-background: $uxpl-green-base !default;
|
||||
|
||||
//// Notifications
|
||||
// Upgrade
|
||||
|
||||
@@ -142,31 +132,6 @@ div.info-wrapper {
|
||||
@include margin(0, 0, 0, auto);
|
||||
padding: $baseline/2 $baseline;
|
||||
}
|
||||
|
||||
.btn-upgrade {
|
||||
@extend %btn-shims;
|
||||
|
||||
border-color: $btn-upgrade-border-color;
|
||||
background: $btn-upgrade-background;
|
||||
color: $btn-upgrade-color;
|
||||
// STATE: hover and focus
|
||||
&:hover,
|
||||
&.is-hovered,
|
||||
&:focus,
|
||||
&.is-focused {
|
||||
border-color: $btn-upgrade-focus-border-color;
|
||||
background-color: $btn-upgrade-focus-background;
|
||||
color: $btn-upgrade-focus-color;
|
||||
}
|
||||
|
||||
// STATE: is disabled
|
||||
&:disabled,
|
||||
&.is-disabled {
|
||||
border-color: $btn-disabled-border-color;
|
||||
background: $btn-brand-disabled-background;
|
||||
color: $btn-upgrade-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
lms/static/sass/elements-v2/_buttons.scss
Normal file
42
lms/static/sass/elements-v2/_buttons.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
// ----------------------------
|
||||
// #UPGRADE
|
||||
// ----------------------------
|
||||
$upgrade-color: #009b00 !default;
|
||||
$upgrade-dark-color: #008100 !default;
|
||||
|
||||
.btn-upgrade {
|
||||
@extend %btn;
|
||||
|
||||
border-color: $upgrade-color;
|
||||
background: $upgrade-color;
|
||||
color: palette(primary, x-back);
|
||||
text-decoration: none;
|
||||
|
||||
// STATE: hover and focus
|
||||
&:hover,
|
||||
&.is-hovered,
|
||||
&:focus,
|
||||
&.is-focused {
|
||||
border-color: $upgrade-dark-color;
|
||||
background: $upgrade-dark-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// STATE: is pressed or active
|
||||
&:active,
|
||||
&.is-pressed,
|
||||
&.is-active {
|
||||
border-color: $upgrade-dark-color;
|
||||
background: $upgrade-dark-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// STATE: is disabled
|
||||
&:disabled,
|
||||
&.is-disabled {
|
||||
border-color: $btn-disabled-border-color;
|
||||
background: $btn-disabled-background-color;
|
||||
color: $btn-disabled-text-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
.message-content {
|
||||
@include margin(0, 0, $baseline, $baseline);
|
||||
|
||||
position: relative;
|
||||
border: 1px solid $lms-border-color;
|
||||
padding: $baseline;
|
||||
@@ -60,15 +61,17 @@
|
||||
.message-header {
|
||||
font-weight: $font-semibold;
|
||||
margin-bottom: $baseline/2;
|
||||
width: calc(100% - 40px)
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
a {
|
||||
a:not(.btn) {
|
||||
font-weight: $font-semibold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dismiss {
|
||||
@include right($baseline/4);
|
||||
|
||||
top: $baseline/4;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
@@ -90,6 +93,7 @@
|
||||
|
||||
&.dismissible {
|
||||
@include right($baseline/4);
|
||||
|
||||
position: absolute;
|
||||
top: $baseline/2;
|
||||
font-size: font-size(small);
|
||||
@@ -103,6 +107,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-actions {
|
||||
display: flex;
|
||||
margin-top: $baseline/2;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// Welcome message / Latest Update message
|
||||
|
||||
@@ -111,10 +111,6 @@
|
||||
.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) {
|
||||
& {
|
||||
@@ -142,11 +138,6 @@
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $success-color-hover;
|
||||
border-color: $success-color-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,13 +70,6 @@ $upgrade-message-background-color: $blue-d1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
// Upgrade Button
|
||||
.btn-upgrade {
|
||||
@extend %btn-primary-green;
|
||||
|
||||
background: $uxpl-green-base;
|
||||
}
|
||||
|
||||
// Cert image
|
||||
.vc-hero {
|
||||
@include float(right);
|
||||
|
||||
@@ -28,8 +28,12 @@ SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_t
|
||||
# Waffle flag to enable the setting of course goals.
|
||||
ENABLE_COURSE_GOALS = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'enable_course_goals')
|
||||
|
||||
# Waffle flag to control the display of the hero
|
||||
SHOW_UPGRADE_MSG_ON_COURSE_HOME = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_upgrade_msg_on_course_home')
|
||||
|
||||
# Waffle flag to control the display of the upgrade deadline message
|
||||
UPGRADE_DEADLINE_MESSAGE = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'upgrade_deadline_message')
|
||||
|
||||
# Waffle flag to switch between the 'welcome message' and 'latest update' on the course home page.
|
||||
# Important Admin Note: This is meant to be configured using waffle_utils course
|
||||
# override only. Either do not create the actual waffle flag, or be sure to unset the
|
||||
|
||||
@@ -82,7 +82,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
|
||||
</ul>
|
||||
|
||||
<div class="vc-cta vc-fade vc-polite-only">
|
||||
<a class="btn-upgrade" href="${ upgrade_url }">${_("Upgrade ({price})").format(price='$' + str(upgrade_price))}</a>
|
||||
<a class="btn-upgrade" href="${ upgrade_url }">${_("Upgrade ({price})").format(price=upgrade_price)}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,9 +55,9 @@ from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
|
||||
</div>
|
||||
</div>
|
||||
<img class="mini-cert" src="${static.url('course_experience/images/verified-cert.png')}"/>
|
||||
<a href="/verify_student/upgrade/${course_id}/">
|
||||
<button type="button" class="btn btn-brand stuck-top focusable action-upgrade-certificate">
|
||||
Upgrade Now (${HTML(course_price)})
|
||||
<a href="${upgrade_url}">
|
||||
<button type="button" class="btn btn-upgrade stuck-top focusable action-upgrade-certificate">
|
||||
Upgrade (${HTML(course_price)})
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -173,7 +173,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
course_home_url(self.course)
|
||||
|
||||
# Fetch the view and verify the query counts
|
||||
with self.assertNumQueries(44, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(45, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
@@ -477,11 +477,9 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase):
|
||||
response = self.client.get(self.url)
|
||||
self.assertIn('vc-message', response.content)
|
||||
url = EcommerceService().get_checkout_page_url(self.verified_mode.sku)
|
||||
expected = '<a class="btn-upgrade" href="{url}">Upgrade (${price})</a>'.format(
|
||||
url=url,
|
||||
price=self.verified_mode.min_price
|
||||
)
|
||||
self.assertIn(expected, response.content)
|
||||
self.assertIn('<a class="btn-upgrade"', response.content)
|
||||
self.assertIn(url, response.content)
|
||||
self.assertIn('Upgrade (${price})</a>'.format(price=self.verified_mode.min_price), response.content)
|
||||
|
||||
def test_no_upgrade_message_if_logged_out(self):
|
||||
self.client.logout()
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from commerce.utils import EcommerceService
|
||||
from course_modes.models import get_cosmetic_verified_display_price
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import (
|
||||
can_self_enroll_in_course,
|
||||
@@ -165,15 +166,8 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
|
||||
# TODO Add switch to control deployment
|
||||
if SHOW_UPGRADE_MSG_ON_COURSE_HOME.is_enabled(course_key) and enrollment and enrollment.upgrade_deadline:
|
||||
verified_mode = enrollment.verified_mode
|
||||
if verified_mode:
|
||||
upgrade_price = verified_mode.min_price
|
||||
|
||||
ecommerce_service = EcommerceService()
|
||||
if ecommerce_service.is_enabled(request.user):
|
||||
upgrade_url = ecommerce_service.get_checkout_page_url(verified_mode.sku)
|
||||
else:
|
||||
upgrade_url = reverse('verify_student_upgrade_and_verify', args=(course_key,))
|
||||
upgrade_url = EcommerceService().upgrade_url(request.user, course_key)
|
||||
upgrade_price = get_cosmetic_verified_display_price(course)
|
||||
|
||||
# Render the course home fragment
|
||||
context = {
|
||||
|
||||
@@ -17,7 +17,7 @@ from rest_framework.reverse import reverse
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.courses import get_course_date_blocks, get_course_with_access
|
||||
from lms.djangoapps.course_goals.api import get_course_goal
|
||||
from lms.djangoapps.course_goals.models import GOAL_KEY_CHOICES
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
@@ -64,7 +64,15 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
|
||||
}
|
||||
|
||||
# Register the course home messages to be loaded on the page
|
||||
_register_course_home_messages(request, course_id, user_access, course_start_data)
|
||||
_register_course_home_messages(request, course, user_access, course_start_data)
|
||||
|
||||
# Register course date alerts
|
||||
for course_date_block in get_course_date_blocks(course, request.user):
|
||||
course_date_block.register_alerts(request, course)
|
||||
|
||||
# Register a course goal message, if appropriate
|
||||
if _should_show_course_goal_message(request, course, user_access):
|
||||
_register_course_goal_message(request, course)
|
||||
|
||||
# Grab the relevant messages
|
||||
course_home_messages = list(CourseHomeMessages.user_messages(request))
|
||||
@@ -73,7 +81,7 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
|
||||
goal_api_url = reverse('course_goals_api:v0:course_goal-list', request=request)
|
||||
|
||||
# Grab the logo
|
||||
image_src = "course_experience/images/home_message_author.png"
|
||||
image_src = 'course_experience/images/home_message_author.png'
|
||||
|
||||
context = {
|
||||
'course_home_messages': course_home_messages,
|
||||
@@ -87,24 +95,22 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
|
||||
return Fragment(html)
|
||||
|
||||
|
||||
def _register_course_home_messages(request, course_id, user_access, course_start_data):
|
||||
def _register_course_home_messages(request, course, user_access, course_start_data):
|
||||
"""
|
||||
Register messages to be shown in the course home content page.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
if user_access['is_anonymous']:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
" {sign_in_link} or {register_link} and then enroll in this course."
|
||||
'{sign_in_link} or {register_link} and then enroll in this course.'
|
||||
)).format(
|
||||
sign_in_link=HTML("<a href='/login?next={current_url}'>{sign_in_label}</a>").format(
|
||||
sign_in_label=_("Sign in"),
|
||||
sign_in_link=HTML('<a href="/login?next={current_url}">{sign_in_label}</a>').format(
|
||||
sign_in_label=_('Sign in'),
|
||||
current_url=urlquote_plus(request.path),
|
||||
),
|
||||
register_link=HTML("<a href='/register?next={current_url}'>{register_label}</a>").format(
|
||||
register_label=_("register"),
|
||||
register_link=HTML('<a href="/register?next={current_url}">{register_label}</a>').format(
|
||||
register_label=_('register'),
|
||||
current_url=urlquote_plus(request.path),
|
||||
)
|
||||
),
|
||||
@@ -114,7 +120,7 @@ def _register_course_home_messages(request, course_id, user_access, course_start
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
"{open_enroll_link} Enroll now{close_enroll_link} to access the full course."
|
||||
'{open_enroll_link}Enroll now{close_enroll_link} to access the full course.'
|
||||
)).format(
|
||||
open_enroll_link='',
|
||||
close_enroll_link=''
|
||||
@@ -123,81 +129,97 @@ def _register_course_home_messages(request, course_id, user_access, course_start
|
||||
course_display_name=course.display_name
|
||||
)
|
||||
)
|
||||
if user_access['is_enrolled'] and not course_start_data['already_started']:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
"Don't forget to add a calendar reminder!"
|
||||
)),
|
||||
title=Text(_("Course starts in {days_until_start_string} on {course_start_date}.")).format(
|
||||
days_until_start_string=course_start_data['days_until_start_string'],
|
||||
course_start_date=course_start_data['course_start_date']
|
||||
)
|
||||
)
|
||||
|
||||
# Only show the set course goal message for enrolled, unverified
|
||||
# users that have not yet set a goal in a course that allows for
|
||||
# verified statuses.
|
||||
has_verified_mode = CourseMode.has_verified_mode(CourseMode.modes_for_course_dict(unicode(course.id)))
|
||||
is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)
|
||||
user_goal = get_course_goal(auth.get_user(request), course_key) if not request.user.is_anonymous() else None
|
||||
if user_access['is_enrolled'] and has_verified_mode and not is_already_verified and not user_goal \
|
||||
and ENABLE_COURSE_GOALS.is_enabled(course_key) and settings.FEATURES.get('ENABLE_COURSE_GOALS'):
|
||||
goal_choices_html = Text(_(
|
||||
'To start, set a course goal by selecting the option below that best describes '
|
||||
'your learning plan. {goal_options_container}'
|
||||
)).format(
|
||||
goal_options_container=HTML('<div class="row goal-options-container">')
|
||||
)
|
||||
|
||||
# Add the dismissible option for users that are unsure of their goal
|
||||
goal_choices_html += Text(
|
||||
'{initial_tag}{choice}{closing_tag}'
|
||||
def _should_show_course_goal_message(request, course, user_access):
|
||||
"""
|
||||
Returns true if the current learner should be shown a course goal message.
|
||||
"""
|
||||
course_key = course.id
|
||||
|
||||
# Don't show a message if course goals has not been enabled
|
||||
if not ENABLE_COURSE_GOALS.is_enabled(course_key) or not settings.FEATURES.get('ENABLE_COURSE_GOALS'):
|
||||
return False
|
||||
|
||||
# Don't show a message if the user is not enrolled
|
||||
if not user_access['is_enrolled']:
|
||||
return False
|
||||
|
||||
# Don't show a message if the learner has already specified a goal
|
||||
if get_course_goal(auth.get_user(request), course_key):
|
||||
return False
|
||||
|
||||
# Don't show a message if the course does not have a verified mode
|
||||
if not CourseMode.has_verified_mode(CourseMode.modes_for_course_dict(unicode(course_key))):
|
||||
return False
|
||||
|
||||
# Don't show a message if the learner has already verified
|
||||
if CourseEnrollment.is_enrolled_as_verified(request.user, course_key):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _register_course_goal_message(request, course):
|
||||
"""
|
||||
Register a message to let a learner specify a course goal.
|
||||
"""
|
||||
goal_choices_html = Text(_(
|
||||
'To start, set a course goal by selecting the option below that best describes '
|
||||
'your learning plan. {goal_options_container}'
|
||||
)).format(
|
||||
goal_options_container=HTML('<div class="row goal-options-container">')
|
||||
)
|
||||
|
||||
# Add the dismissible option for users that are unsure of their goal
|
||||
goal_choices_html += Text(
|
||||
'{initial_tag}{choice}{closing_tag}'
|
||||
).format(
|
||||
initial_tag=HTML(
|
||||
'<div tabindex="0" aria-label="{aria_label_choice}" class="goal-option dismissible" '
|
||||
'data-choice="{goal_key}">'
|
||||
).format(
|
||||
goal_key=GOAL_KEY_CHOICES.unsure,
|
||||
aria_label_choice=Text(_("Set goal to: {choice}")).format(
|
||||
choice=GOAL_KEY_CHOICES[GOAL_KEY_CHOICES.unsure]
|
||||
),
|
||||
),
|
||||
choice=Text(_('{choice}')).format(
|
||||
choice=GOAL_KEY_CHOICES[GOAL_KEY_CHOICES.unsure],
|
||||
),
|
||||
closing_tag=HTML('</div>'),
|
||||
)
|
||||
|
||||
# Add the option to set a goal to earn a certificate,
|
||||
# complete the course or explore the course
|
||||
goal_options = [
|
||||
GOAL_KEY_CHOICES.certify,
|
||||
GOAL_KEY_CHOICES.complete,
|
||||
GOAL_KEY_CHOICES.explore
|
||||
]
|
||||
for goal_key in goal_options:
|
||||
goal_text = GOAL_KEY_CHOICES[goal_key]
|
||||
goal_choices_html += HTML(
|
||||
'{initial_tag}{goal_text}{closing_tag}'
|
||||
).format(
|
||||
initial_tag=HTML(
|
||||
'<div tabindex="0" aria-label="{aria_label_choice}" class="goal-option dismissible" '
|
||||
'<div tabindex="0" aria-label="{aria_label_choice}" class="goal-option {col_sel} btn" '
|
||||
'data-choice="{goal_key}">'
|
||||
).format(
|
||||
goal_key=GOAL_KEY_CHOICES.unsure,
|
||||
aria_label_choice=Text(_("Set goal to: {choice}")).format(
|
||||
choice=GOAL_KEY_CHOICES[GOAL_KEY_CHOICES.unsure]
|
||||
goal_key=goal_key,
|
||||
aria_label_choice=Text(_("Set goal to: {goal_text}")).format(
|
||||
goal_text=Text(_(goal_text))
|
||||
),
|
||||
col_sel='col-' + str(int(math.floor(12 / len(goal_options))))
|
||||
),
|
||||
choice=Text(_('{choice}')).format(
|
||||
choice=GOAL_KEY_CHOICES[GOAL_KEY_CHOICES.unsure],
|
||||
),
|
||||
closing_tag=HTML('</div>'),
|
||||
goal_text=goal_text,
|
||||
closing_tag=HTML('</div>')
|
||||
)
|
||||
|
||||
# Add the option to set a goal to earn a certificate,
|
||||
# complete the course or explore the course
|
||||
goal_options = [GOAL_KEY_CHOICES.certify, GOAL_KEY_CHOICES.complete, GOAL_KEY_CHOICES.explore]
|
||||
for goal_key in goal_options:
|
||||
goal_text = GOAL_KEY_CHOICES[goal_key]
|
||||
goal_choices_html += HTML(
|
||||
'{initial_tag}{goal_text}{closing_tag}'
|
||||
).format(
|
||||
initial_tag=HTML(
|
||||
'<div tabindex="0" aria-label="{aria_label_choice}" class="goal-option {col_sel} btn" '
|
||||
'data-choice="{goal_key}">'
|
||||
).format(
|
||||
goal_key=goal_key,
|
||||
aria_label_choice=Text(_("Set goal to: {goal_text}")).format(
|
||||
goal_text=Text(_(goal_text))
|
||||
),
|
||||
col_sel='col-' + str(int(math.floor(12 / len(goal_options))))
|
||||
),
|
||||
goal_text=goal_text,
|
||||
closing_tag=HTML('</div>')
|
||||
)
|
||||
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
HTML('{goal_choices_html}{closing_tag}').format(
|
||||
goal_choices_html=goal_choices_html,
|
||||
closing_tag=HTML('</div>')
|
||||
),
|
||||
title=Text(_('Welcome to {course_display_name}')).format(
|
||||
course_display_name=course.display_name
|
||||
)
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
goal_choices_html,
|
||||
title=Text(_('Welcome to {course_display_name}')).format(
|
||||
course_display_name=course.display_name
|
||||
)
|
||||
)
|
||||
|
||||
@@ -6,10 +6,11 @@ from django.utils.translation import get_language
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from commerce.utils import EcommerceService
|
||||
from course_modes.models import CourseMode, get_cosmetic_verified_display_price
|
||||
from courseware.date_summary import VerifiedUpgradeDeadlineDate
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
|
||||
class CourseSockFragmentView(EdxFragmentView):
|
||||
@@ -44,13 +45,15 @@ class CourseSockFragmentView(EdxFragmentView):
|
||||
not deadline_has_passed and get_language() == 'en'
|
||||
)
|
||||
|
||||
# Get the price of the course and format correctly
|
||||
# Get information about the upgrade
|
||||
course_price = get_cosmetic_verified_display_price(course)
|
||||
upgrade_url = EcommerceService().upgrade_url(request.user, course_key)
|
||||
|
||||
context = {
|
||||
'show_course_sock': show_course_sock,
|
||||
'course_price': course_price,
|
||||
'course_id': course.id
|
||||
'course_id': course.id,
|
||||
'upgrade_url': upgrade_url,
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
Reference in New Issue
Block a user