Commit 7dfe12a1 by Andy Armstrong

Show course home messages for important course dates

LEARNER-2073
parent 1aff33dc
...@@ -121,3 +121,32 @@ ...@@ -121,3 +121,32 @@
color: $btn-brand-disabled-color; 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; ...@@ -143,9 +143,8 @@ $error-color: rgb(203, 7, 18) !default;
$success-color: rgb(0, 155, 0) !default; $success-color: rgb(0, 155, 0) !default;
$warning-color: rgb(255, 192, 31) !default; $warning-color: rgb(255, 192, 31) !default;
$warning-color-accent: rgb(255, 252, 221) !default; $warning-color-accent: rgb(255, 252, 221) !default;
$general-color: $uxpl-blue-base !default;; $general-color: $uxpl-blue-base !default;
$general-color-accent: $uxpl-blue-base !default $general-color-accent: $uxpl-blue-base !default;
// CAPA correctness color to be consistent with Alert styles above // CAPA correctness color to be consistent with Alert styles above
$correct: $success-color !default; $correct: $success-color !default;
...@@ -181,6 +180,16 @@ $btn-brand-active-background: $uxpl-blue-base !default; ...@@ -181,6 +180,16 @@ $btn-brand-active-background: $uxpl-blue-base !default;
$btn-brand-disabled-background: #f2f3f3 !default; $btn-brand-disabled-background: #f2f3f3 !default;
$btn-brand-disabled-color: #676666 !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 // #SETTINGS
// ---------------------------- // ----------------------------
......
...@@ -4,6 +4,8 @@ from urlparse import urljoin ...@@ -4,6 +4,8 @@ from urlparse import urljoin
import waffle import waffle
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from student.models import CourseEnrollment
from commerce.models import CommerceConfiguration from commerce.models import CommerceConfiguration
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
...@@ -93,3 +95,16 @@ class EcommerceService(object): ...@@ -93,3 +95,16 @@ class EcommerceService(object):
checkout_page_path=self.get_absolute_ecommerce_url(self.config.MULTIPLE_ITEMS_BASKET_PAGE_URL), checkout_page_path=self.get_absolute_ecommerce_url(self.config.MULTIPLE_ITEMS_BASKET_PAGE_URL),
skus=urlencode({'sku': skus}, doseq=True), 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 ...@@ -3,26 +3,45 @@ This module provides date summary blocks for the Course Info
page. Each block gives information about a particular page. Each block gives information about a particular
course-run-specific date which will be displayed to the user. course-run-specific date which will be displayed to the user.
""" """
import crum
import datetime import datetime
from babel.dates import format_timedelta from babel.dates import format_timedelta
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import get_language, to_locale, ugettext_lazy from django.utils.translation import get_language, to_locale, ugettext_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from lazy import lazy 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.commerce.utils import EcommerceService
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline 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.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 student.models import CourseEnrollment
from .context_processor import user_timezone_locale_prefs
class DateSummary(object): class DateSummary(object):
"""Base class for all date summary blocks.""" """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 @property
def css_class(self): def css_class(self):
""" """
...@@ -41,6 +60,12 @@ class DateSummary(object): ...@@ -41,6 +60,12 @@ class DateSummary(object):
"""The detail text displayed by this summary.""" """The detail text displayed by this summary."""
return '' return ''
def register_alerts(self, request, course):
"""
Registers any relevant course alerts given the current request.
"""
pass
@property @property
def date(self): def date(self):
"""This summary's date.""" """This summary's date."""
...@@ -64,15 +89,6 @@ class DateSummary(object): ...@@ -64,15 +89,6 @@ class DateSummary(object):
"""The text of the link.""" """The text of the link."""
return '' 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): def __init__(self, course, user, course_id=None):
self.course = course self.course = course
self.user = user self.user = user
...@@ -87,7 +103,7 @@ class DateSummary(object): ...@@ -87,7 +103,7 @@ class DateSummary(object):
if self.date is None: if self.date is None:
return '' return ''
locale = to_locale(get_language()) locale = to_locale(get_language())
delta = self.date - datetime.datetime.now(utc) delta = self.date - self.current_time
try: try:
relative_date = format_timedelta(delta, locale=locale) relative_date = format_timedelta(delta, locale=locale)
# Babel doesn't have translations for Esperanto, so we get # Babel doesn't have translations for Esperanto, so we get
...@@ -117,7 +133,7 @@ class DateSummary(object): ...@@ -117,7 +133,7 @@ class DateSummary(object):
future. future.
""" """
if self.date is not None: 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 return False
def deadline_has_passed(self): def deadline_has_passed(self):
...@@ -126,7 +142,52 @@ class DateSummary(object): ...@@ -126,7 +142,52 @@ class DateSummary(object):
Returns False otherwise. Returns False otherwise.
""" """
deadline = self.date 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): def __repr__(self):
return u'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format( return u'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format(
...@@ -151,7 +212,7 @@ class TodaysDate(DateSummary): ...@@ -151,7 +212,7 @@ class TodaysDate(DateSummary):
@property @property
def date(self): def date(self):
return datetime.datetime.now(utc) return self.current_time
@property @property
def title(self): def title(self):
...@@ -169,6 +230,35 @@ class CourseStartDate(DateSummary): ...@@ -169,6 +230,35 @@ class CourseStartDate(DateSummary):
def date(self): def date(self):
return self.course.start 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): class CourseEndDate(DateSummary):
""" """
...@@ -183,7 +273,7 @@ class CourseEndDate(DateSummary): ...@@ -183,7 +273,7 @@ class CourseEndDate(DateSummary):
@property @property
def description(self): 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) mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id)
if is_active and CourseMode.is_eligible_for_certificate(mode): if is_active and CourseMode.is_eligible_for_certificate(mode):
return _('To earn a certificate, you must complete all requirements before this date.') return _('To earn a certificate, you must complete all requirements before this date.')
...@@ -195,6 +285,35 @@ class CourseEndDate(DateSummary): ...@@ -195,6 +285,35 @@ class CourseEndDate(DateSummary):
def date(self): def date(self):
return self.course.end 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): class CertificateAvailableDate(DateSummary):
""" """
...@@ -216,7 +335,7 @@ class CertificateAvailableDate(DateSummary): ...@@ -216,7 +335,7 @@ class CertificateAvailableDate(DateSummary):
can_show_certificate_available_date_field(self.course) and can_show_certificate_available_date_field(self.course) and
self.has_certificate_modes and self.has_certificate_modes and
self.date is not None 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 len(self.active_certificates) > 0
) )
...@@ -252,13 +371,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary): ...@@ -252,13 +371,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
@property @property
def link(self): def link(self):
ecommerce_service = EcommerceService() return EcommerceService().upgrade_url(self.user, self.course_id)
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,))
@cached_property @cached_property
def enrollment(self): def enrollment(self):
...@@ -299,6 +412,39 @@ class VerifiedUpgradeDeadlineDate(DateSummary): ...@@ -299,6 +412,39 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
return deadline 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): class VerificationDeadlineDate(DateSummary):
""" """
......
...@@ -4,7 +4,9 @@ from datetime import datetime, timedelta ...@@ -4,7 +4,9 @@ from datetime import datetime, timedelta
import ddt import ddt
import waffle import waffle
from django.contrib.messages.middleware import MessageMiddleware
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import RequestFactory, TestCase
from freezegun import freeze_time from freezegun import freeze_time
from mock import patch from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
...@@ -31,7 +33,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration ...@@ -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.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference 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.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 student.tests.factories import CourseEnrollmentFactory, UserFactory, TEST_PASSWORD
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
...@@ -46,20 +48,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -46,20 +48,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
super(CourseDateSummaryTest, self).setUp() super(CourseDateSummaryTest, self).setUp()
SelfPacedConfiguration.objects.create(enable_course_home_improvements=True) 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): def enable_course_certificates(self, course):
""" Enable course certificate configuration """ """ Enable course certificate configuration """
course.certificates = { course.certificates = {
...@@ -74,7 +62,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -74,7 +62,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_course_info_feature_flag(self): def test_course_info_feature_flag(self):
SelfPacedConfiguration(enable_course_home_improvements=False).save() SelfPacedConfiguration(enable_course_home_improvements=False).save()
course = create_course_run() course = create_course_run()
user = self.create_user() user = create_user()
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
self.client.login(username=user.username, password=TEST_PASSWORD) self.client.login(username=user.username, password=TEST_PASSWORD)
...@@ -144,7 +132,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -144,7 +132,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@ddt.unpack @ddt.unpack
def test_enabled_block_types(self, course_kwargs, user_kwargs, expected_blocks): def test_enabled_block_types(self, course_kwargs, user_kwargs, expected_blocks):
course = create_course_run(**course_kwargs) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
self.assert_block_types(course, user, expected_blocks) self.assert_block_types(course, user, expected_blocks)
...@@ -160,12 +148,12 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -160,12 +148,12 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@ddt.unpack @ddt.unpack
def test_enabled_block_types_without_enrollment(self, course_kwargs, expected_blocks): def test_enabled_block_types_without_enrollment(self, course_kwargs, expected_blocks):
course = create_course_run(**course_kwargs) course = create_course_run(**course_kwargs)
user = self.create_user() user = create_user()
self.assert_block_types(course, user, expected_blocks) self.assert_block_types(course, user, expected_blocks)
def test_enabled_block_types_with_non_upgradeable_course_run(self): def test_enabled_block_types_with_non_upgradeable_course_run(self):
course = create_course_run(days_till_start=-10, days_till_verification_deadline=None) 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() CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED).delete()
CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT)
self.assert_block_types(course, user, (TodaysDate, CourseEndDate)) self.assert_block_types(course, user, (TodaysDate, CourseEndDate))
...@@ -177,7 +165,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -177,7 +165,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
""" """
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run() course = create_course_run()
user = self.create_user() user = create_user()
block = TodaysDate(course, user) block = TodaysDate(course, user)
self.assertTrue(block.is_enabled) self.assertTrue(block.is_enabled)
self.assertEqual(block.date, datetime.now(utc)) self.assertEqual(block.date, datetime.now(utc))
...@@ -191,7 +179,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -191,7 +179,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_todays_date_no_timezone(self, url_name): def test_todays_date_no_timezone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run() course = create_course_run()
user = self.create_user() user = create_user()
self.client.login(username=user.username, password=TEST_PASSWORD) self.client.login(username=user.username, password=TEST_PASSWORD)
html_elements = [ html_elements = [
...@@ -216,7 +204,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -216,7 +204,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_todays_date_timezone(self, url_name): def test_todays_date_timezone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run() course = create_course_run()
user = self.create_user() user = create_user()
self.client.login(username=user.username, password=TEST_PASSWORD) self.client.login(username=user.username, password=TEST_PASSWORD)
set_user_preference(user, 'time_zone', 'America/Los_Angeles') set_user_preference(user, 'time_zone', 'America/Los_Angeles')
url = reverse(url_name, args=(course.id,)) url = reverse(url_name, args=(course.id,))
...@@ -237,7 +225,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -237,7 +225,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
## Tests Course Start Date ## Tests Course Start Date
def test_course_start_date(self): def test_course_start_date(self):
course = create_course_run() course = create_course_run()
user = self.create_user() user = create_user()
block = CourseStartDate(course, user) block = CourseStartDate(course, user)
self.assertEqual(block.date, course.start) self.assertEqual(block.date, course.start)
...@@ -249,7 +237,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -249,7 +237,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_start_date_render(self, url_name): def test_start_date_render(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run() course = create_course_run()
user = self.create_user() user = create_user()
self.client.login(username=user.username, password=TEST_PASSWORD) self.client.login(username=user.username, password=TEST_PASSWORD)
url = reverse(url_name, args=(course.id,)) url = reverse(url_name, args=(course.id,))
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
...@@ -268,7 +256,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -268,7 +256,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_start_date_render_time_zone(self, url_name): def test_start_date_render_time_zone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run() course = create_course_run()
user = self.create_user() user = create_user()
self.client.login(username=user.username, password=TEST_PASSWORD) self.client.login(username=user.username, password=TEST_PASSWORD)
set_user_preference(user, 'time_zone', 'America/Los_Angeles') set_user_preference(user, 'time_zone', 'America/Los_Angeles')
url = reverse(url_name, args=(course.id,)) url = reverse(url_name, args=(course.id,))
...@@ -284,7 +272,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -284,7 +272,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
## Tests Course End Date Block ## Tests Course End Date Block
def test_course_end_date_for_certificate_eligible_mode(self): def test_course_end_date_for_certificate_eligible_mode(self):
course = create_course_run(days_till_start=-1) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
block = CourseEndDate(course, user) block = CourseEndDate(course, user)
self.assertEqual( self.assertEqual(
...@@ -294,7 +282,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -294,7 +282,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_course_end_date_for_non_certificate_eligible_mode(self): def test_course_end_date_for_non_certificate_eligible_mode(self):
course = create_course_run(days_till_start=-1) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT)
block = CourseEndDate(course, user) block = CourseEndDate(course, user)
self.assertEqual( self.assertEqual(
...@@ -305,7 +293,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -305,7 +293,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_course_end_date_after_course(self): def test_course_end_date_after_course(self):
course = create_course_run(days_till_start=-2, days_till_end=-1) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
block = CourseEndDate(course, user) block = CourseEndDate(course, user)
self.assertEqual( self.assertEqual(
...@@ -319,7 +307,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -319,7 +307,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
sku = 'TESTSKU' sku = 'TESTSKU'
configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True) configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
course = create_course_run() 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 = CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED)
course_mode.sku = sku course_mode.sku = sku
course_mode.save() course_mode.save()
...@@ -332,7 +320,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -332,7 +320,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@waffle.testutils.override_switch('certificates.auto_certificate_generation', True) @waffle.testutils.override_switch('certificates.auto_certificate_generation', True)
def test_no_certificate_available_date(self): def test_no_certificate_available_date(self):
course = create_course_run(days_till_start=-1) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT)
block = CertificateAvailableDate(course, user) block = CertificateAvailableDate(course, user)
self.assertEqual(block.date, None) self.assertEqual(block.date, None)
...@@ -342,7 +330,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -342,7 +330,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@waffle.testutils.override_switch('certificates.auto_certificate_generation', True) @waffle.testutils.override_switch('certificates.auto_certificate_generation', True)
def test_no_certificate_available_date_for_self_paced(self): def test_no_certificate_available_date_for_self_paced(self):
course = create_self_paced_course_run() 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) CourseEnrollmentFactory(course_id=course.id, user=verified_user, mode=CourseMode.VERIFIED)
course.certificate_available_date = datetime.now(utc) + timedelta(days=7) course.certificate_available_date = datetime.now(utc) + timedelta(days=7)
course.save() course.save()
...@@ -356,7 +344,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -356,7 +344,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
if the course only has audit mode. if the course only has audit mode.
""" """
course = create_course_run() 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) # 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) CourseEnrollmentFactory(course_id=course.id, user=audit_user, mode=CourseMode.AUDIT)
...@@ -376,9 +364,9 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -376,9 +364,9 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@waffle.testutils.override_switch('certificates.auto_certificate_generation', True) @waffle.testutils.override_switch('certificates.auto_certificate_generation', True)
def test_certificate_available_date_defined(self): def test_certificate_available_date_defined(self):
course = create_course_run() course = create_course_run()
audit_user = self.create_user() audit_user = create_user()
CourseEnrollmentFactory(course_id=course.id, user=audit_user, mode=CourseMode.AUDIT) 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) CourseEnrollmentFactory(course_id=course.id, user=verified_user, mode=CourseMode.VERIFIED)
course.certificate_available_date = datetime.now(utc) + timedelta(days=7) course.certificate_available_date = datetime.now(utc) + timedelta(days=7)
self.enable_course_certificates(course) self.enable_course_certificates(course)
...@@ -391,14 +379,14 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -391,14 +379,14 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
## VerificationDeadlineDate ## VerificationDeadlineDate
def test_no_verification_deadline(self): def test_no_verification_deadline(self):
course = create_course_run(days_till_start=-1, days_till_verification_deadline=None) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
block = VerificationDeadlineDate(course, user) block = VerificationDeadlineDate(course, user)
self.assertFalse(block.is_enabled) self.assertFalse(block.is_enabled)
def test_no_verified_enrollment(self): def test_no_verified_enrollment(self):
course = create_course_run(days_till_start=-1) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT)
block = VerificationDeadlineDate(course, user) block = VerificationDeadlineDate(course, user)
self.assertFalse(block.is_enabled) self.assertFalse(block.is_enabled)
...@@ -406,7 +394,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -406,7 +394,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_verification_deadline_date_upcoming(self): def test_verification_deadline_date_upcoming(self):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run(days_till_start=-1) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
block = VerificationDeadlineDate(course, user) block = VerificationDeadlineDate(course, user)
...@@ -423,7 +411,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -423,7 +411,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_verification_deadline_date_retry(self): def test_verification_deadline_date_retry(self):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run(days_till_start=-1) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
block = VerificationDeadlineDate(course, user) block = VerificationDeadlineDate(course, user)
...@@ -440,7 +428,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -440,7 +428,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_verification_deadline_date_denied(self): def test_verification_deadline_date_denied(self):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run(days_till_start=-10, days_till_verification_deadline=-1) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
block = VerificationDeadlineDate(course, user) block = VerificationDeadlineDate(course, user)
...@@ -462,7 +450,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -462,7 +450,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_render_date_string_past(self, delta, expected_date_string): def test_render_date_string_past(self, delta, expected_date_string):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
course = create_course_run(days_till_start=-10, days_till_verification_deadline=delta) 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) CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED)
block = VerificationDeadlineDate(course, user) block = VerificationDeadlineDate(course, user)
...@@ -470,6 +458,97 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -470,6 +458,97 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@attr(shard=1) @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&#39;t forget, you have 2 weeks left to upgrade to a Verified Certificate.'],
['2017-07-04 10:00:00', u'Don&#39;t forget, you have 1 day left to upgrade to a Verified Certificate.'],
['2017-07-05 08:00:00', u'Don&#39;t forget, you have 1 hour left to upgrade to a Verified Certificate.'],
['2017-07-05 08:55:00', u'Don&#39;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): class TestScheduleOverrides(SharedModuleStoreTestCase):
def setUp(self): def setUp(self):
...@@ -560,6 +639,21 @@ class TestScheduleOverrides(SharedModuleStoreTestCase): ...@@ -560,6 +639,21 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
self.assertEqual(block.date, expected) 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( def create_course_run(
days_till_start=1, days_till_end=14, days_till_upgrade_deadline=4, days_till_verification_deadline=14, 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): ...@@ -213,8 +213,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20 NUM_PROBLEMS = 20
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 145), (ModuleStoreEnum.Type.mongo, 10, 147),
(ModuleStoreEnum.Type.split, 4, 145), (ModuleStoreEnum.Type.split, 4, 147),
) )
@ddt.unpack @ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): 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 ...@@ -431,6 +431,9 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5 RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5
RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5 RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5
# Deadline message configurations
COURSE_MESSAGE_ALERT_DURATION_IN_DAYS = 14
############################# SET PATH INFORMATION ############################# ############################# SET PATH INFORMATION #############################
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms
REPO_ROOT = PROJECT_ROOT.dirname() REPO_ROOT = PROJECT_ROOT.dirname()
...@@ -2589,6 +2592,7 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60 ...@@ -2589,6 +2592,7 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC' TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC'
########################## VIDEO IMAGE STORAGE ############################ ########################## VIDEO IMAGE STORAGE ############################
VIDEO_IMAGE_SETTINGS = dict( VIDEO_IMAGE_SETTINGS = dict(
......
...@@ -7,3 +7,7 @@ ...@@ -7,3 +7,7 @@
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/theme'; @import 'base/theme';
// Pattern Library shims
@import 'edx-pattern-library-shims/base/variables';
@import 'edx-pattern-library-shims/buttons';
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
// Elements // Elements
@import 'notifications'; @import 'notifications';
@import 'elements/controls'; @import 'elements/controls';
@import 'elements-v2/buttons';
@import 'elements-v2/pagination'; @import 'elements-v2/pagination';
// Features // Features
......
// 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 //// Notifications
// Upgrade // Upgrade
...@@ -142,31 +132,6 @@ div.info-wrapper { ...@@ -142,31 +132,6 @@ div.info-wrapper {
@include margin(0, 0, 0, auto); @include margin(0, 0, 0, auto);
padding: $baseline/2 $baseline; 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;
}
}
} }
} }
......
// ----------------------------
// #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 @@ ...@@ -16,6 +16,7 @@
.message-content { .message-content {
@include margin(0, 0, $baseline, $baseline); @include margin(0, 0, $baseline, $baseline);
position: relative; position: relative;
border: 1px solid $lms-border-color; border: 1px solid $lms-border-color;
padding: $baseline; padding: $baseline;
...@@ -60,15 +61,17 @@ ...@@ -60,15 +61,17 @@
.message-header { .message-header {
font-weight: $font-semibold; font-weight: $font-semibold;
margin-bottom: $baseline/2; margin-bottom: $baseline/2;
width: calc(100% - 40px) width: calc(100% - 40px);
} }
a { a:not(.btn) {
font-weight: $font-semibold; font-weight: $font-semibold;
text-decoration: underline; text-decoration: underline;
} }
.dismiss { .dismiss {
@include right($baseline/4); @include right($baseline/4);
top: $baseline/4; top: $baseline/4;
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
...@@ -90,6 +93,7 @@ ...@@ -90,6 +93,7 @@
&.dismissible { &.dismissible {
@include right($baseline/4); @include right($baseline/4);
position: absolute; position: absolute;
top: $baseline/2; top: $baseline/2;
font-size: font-size(small); font-size: font-size(small);
...@@ -103,6 +107,12 @@ ...@@ -103,6 +107,12 @@
} }
} }
} }
.message-actions {
display: flex;
margin-top: $baseline/2;
justify-content: flex-end;
}
} }
// Welcome message / Latest Update message // Welcome message / Latest Update message
......
...@@ -111,10 +111,6 @@ ...@@ -111,10 +111,6 @@
.action-upgrade-certificate { .action-upgrade-certificate {
position: absolute; position: absolute;
right: $baseline; right: $baseline;
background-color: $success-color;
border-color: $success-color;
background-image: none;
box-shadow: none;
@media (max-width: 960px) { @media (max-width: 960px) {
& { & {
...@@ -142,11 +138,6 @@ ...@@ -142,11 +138,6 @@
top: auto; top: auto;
} }
} }
&:hover {
background-color: $success-color-hover;
border-color: $success-color-hover;
}
} }
} }
} }
......
...@@ -70,13 +70,6 @@ $upgrade-message-background-color: $blue-d1; ...@@ -70,13 +70,6 @@ $upgrade-message-background-color: $blue-d1;
color: $white; color: $white;
} }
// Upgrade Button
.btn-upgrade {
@extend %btn-primary-green;
background: $uxpl-green-base;
}
// Cert image // Cert image
.vc-hero { .vc-hero {
@include float(right); @include float(right);
......
...@@ -28,8 +28,12 @@ SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_t ...@@ -28,8 +28,12 @@ SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_t
# Waffle flag to enable the setting of course goals. # Waffle flag to enable the setting of course goals.
ENABLE_COURSE_GOALS = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'enable_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') 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. # 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 # 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 # 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 ...@@ -82,7 +82,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
</ul> </ul>
<div class="vc-cta vc-fade vc-polite-only"> <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> </div>
</div> </div>
......
...@@ -55,9 +55,9 @@ from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG ...@@ -55,9 +55,9 @@ from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
</div> </div>
</div> </div>
<img class="mini-cert" src="${static.url('course_experience/images/verified-cert.png')}"/> <img class="mini-cert" src="${static.url('course_experience/images/verified-cert.png')}"/>
<a href="/verify_student/upgrade/${course_id}/"> <a href="${upgrade_url}">
<button type="button" class="btn btn-brand stuck-top focusable action-upgrade-certificate"> <button type="button" class="btn btn-upgrade stuck-top focusable action-upgrade-certificate">
Upgrade Now (${HTML(course_price)}) Upgrade (${HTML(course_price)})
</button> </button>
</a> </a>
</div> </div>
......
...@@ -173,7 +173,7 @@ class TestCourseHomePage(CourseHomePageTestCase): ...@@ -173,7 +173,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
course_home_url(self.course) course_home_url(self.course)
# Fetch the view and verify the query counts # 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): with check_mongo_calls(4):
url = course_home_url(self.course) url = course_home_url(self.course)
self.client.get(url) self.client.get(url)
...@@ -477,11 +477,9 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase): ...@@ -477,11 +477,9 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase):
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertIn('vc-message', response.content) self.assertIn('vc-message', response.content)
url = EcommerceService().get_checkout_page_url(self.verified_mode.sku) url = EcommerceService().get_checkout_page_url(self.verified_mode.sku)
expected = '<a class="btn-upgrade" href="{url}">Upgrade (${price})</a>'.format( self.assertIn('<a class="btn-upgrade"', response.content)
url=url, self.assertIn(url, response.content)
price=self.verified_mode.min_price self.assertIn('Upgrade (${price})</a>'.format(price=self.verified_mode.min_price), response.content)
)
self.assertIn(expected, response.content)
def test_no_upgrade_message_if_logged_out(self): def test_no_upgrade_message_if_logged_out(self):
self.client.logout() self.client.logout()
......
...@@ -10,6 +10,7 @@ from django.views.decorators.cache import cache_control ...@@ -10,6 +10,7 @@ from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from commerce.utils import EcommerceService from commerce.utils import EcommerceService
from course_modes.models import get_cosmetic_verified_display_price
from courseware.access import has_access from courseware.access import has_access
from courseware.courses import ( from courseware.courses import (
can_self_enroll_in_course, can_self_enroll_in_course,
...@@ -165,15 +166,8 @@ class CourseHomeFragmentView(EdxFragmentView): ...@@ -165,15 +166,8 @@ class CourseHomeFragmentView(EdxFragmentView):
# TODO Add switch to control deployment # TODO Add switch to control deployment
if SHOW_UPGRADE_MSG_ON_COURSE_HOME.is_enabled(course_key) and enrollment and enrollment.upgrade_deadline: if SHOW_UPGRADE_MSG_ON_COURSE_HOME.is_enabled(course_key) and enrollment and enrollment.upgrade_deadline:
verified_mode = enrollment.verified_mode upgrade_url = EcommerceService().upgrade_url(request.user, course_key)
if verified_mode: upgrade_price = get_cosmetic_verified_display_price(course)
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,))
# Render the course home fragment # Render the course home fragment
context = { context = {
......
...@@ -17,7 +17,7 @@ from rest_framework.reverse import reverse ...@@ -17,7 +17,7 @@ from rest_framework.reverse import reverse
from web_fragments.fragment import Fragment from web_fragments.fragment import Fragment
from course_modes.models import CourseMode 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.api import get_course_goal
from lms.djangoapps.course_goals.models import GOAL_KEY_CHOICES from lms.djangoapps.course_goals.models import GOAL_KEY_CHOICES
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
...@@ -64,7 +64,15 @@ class CourseHomeMessageFragmentView(EdxFragmentView): ...@@ -64,7 +64,15 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
} }
# Register the course home messages to be loaded on the page # 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 # Grab the relevant messages
course_home_messages = list(CourseHomeMessages.user_messages(request)) course_home_messages = list(CourseHomeMessages.user_messages(request))
...@@ -73,7 +81,7 @@ class CourseHomeMessageFragmentView(EdxFragmentView): ...@@ -73,7 +81,7 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
goal_api_url = reverse('course_goals_api:v0:course_goal-list', request=request) goal_api_url = reverse('course_goals_api:v0:course_goal-list', request=request)
# Grab the logo # Grab the logo
image_src = "course_experience/images/home_message_author.png" image_src = 'course_experience/images/home_message_author.png'
context = { context = {
'course_home_messages': course_home_messages, 'course_home_messages': course_home_messages,
...@@ -87,24 +95,22 @@ class CourseHomeMessageFragmentView(EdxFragmentView): ...@@ -87,24 +95,22 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
return Fragment(html) 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. 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']: if user_access['is_anonymous']:
CourseHomeMessages.register_info_message( CourseHomeMessages.register_info_message(
request, request,
Text(_( 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( )).format(
sign_in_link=HTML("<a href='/login?next={current_url}'>{sign_in_label}</a>").format( sign_in_link=HTML('<a href="/login?next={current_url}">{sign_in_label}</a>').format(
sign_in_label=_("Sign in"), sign_in_label=_('Sign in'),
current_url=urlquote_plus(request.path), current_url=urlquote_plus(request.path),
), ),
register_link=HTML("<a href='/register?next={current_url}'>{register_label}</a>").format( register_link=HTML('<a href="/register?next={current_url}">{register_label}</a>').format(
register_label=_("register"), register_label=_('register'),
current_url=urlquote_plus(request.path), current_url=urlquote_plus(request.path),
) )
), ),
...@@ -114,7 +120,7 @@ def _register_course_home_messages(request, course_id, user_access, course_start ...@@ -114,7 +120,7 @@ def _register_course_home_messages(request, course_id, user_access, course_start
CourseHomeMessages.register_info_message( CourseHomeMessages.register_info_message(
request, request,
Text(_( 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( )).format(
open_enroll_link='', open_enroll_link='',
close_enroll_link='' close_enroll_link=''
...@@ -123,26 +129,41 @@ def _register_course_home_messages(request, course_id, user_access, course_start ...@@ -123,26 +129,41 @@ def _register_course_home_messages(request, course_id, user_access, course_start
course_display_name=course.display_name 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 def _should_show_course_goal_message(request, course, user_access):
# verified statuses. """
has_verified_mode = CourseMode.has_verified_mode(CourseMode.modes_for_course_dict(unicode(course.id))) Returns true if the current learner should be shown a course goal message.
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 course_key = course.id
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'): # 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(_( goal_choices_html = Text(_(
'To start, set a course goal by selecting the option below that best describes ' 'To start, set a course goal by selecting the option below that best describes '
'your learning plan. {goal_options_container}' 'your learning plan. {goal_options_container}'
...@@ -171,7 +192,11 @@ def _register_course_home_messages(request, course_id, user_access, course_start ...@@ -171,7 +192,11 @@ def _register_course_home_messages(request, course_id, user_access, course_start
# Add the option to set a goal to earn a certificate, # Add the option to set a goal to earn a certificate,
# complete the course or explore the course # complete the course or explore the course
goal_options = [GOAL_KEY_CHOICES.certify, GOAL_KEY_CHOICES.complete, GOAL_KEY_CHOICES.explore] goal_options = [
GOAL_KEY_CHOICES.certify,
GOAL_KEY_CHOICES.complete,
GOAL_KEY_CHOICES.explore
]
for goal_key in goal_options: for goal_key in goal_options:
goal_text = GOAL_KEY_CHOICES[goal_key] goal_text = GOAL_KEY_CHOICES[goal_key]
goal_choices_html += HTML( goal_choices_html += HTML(
...@@ -193,10 +218,7 @@ def _register_course_home_messages(request, course_id, user_access, course_start ...@@ -193,10 +218,7 @@ def _register_course_home_messages(request, course_id, user_access, course_start
CourseHomeMessages.register_info_message( CourseHomeMessages.register_info_message(
request, request,
HTML('{goal_choices_html}{closing_tag}').format( goal_choices_html,
goal_choices_html=goal_choices_html,
closing_tag=HTML('</div>')
),
title=Text(_('Welcome to {course_display_name}')).format( title=Text(_('Welcome to {course_display_name}')).format(
course_display_name=course.display_name course_display_name=course.display_name
) )
......
...@@ -6,10 +6,11 @@ from django.utils.translation import get_language ...@@ -6,10 +6,11 @@ from django.utils.translation import get_language
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment 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 course_modes.models import CourseMode, get_cosmetic_verified_display_price
from courseware.date_summary import VerifiedUpgradeDeadlineDate from courseware.date_summary import VerifiedUpgradeDeadlineDate
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from student.models import CourseEnrollment
class CourseSockFragmentView(EdxFragmentView): class CourseSockFragmentView(EdxFragmentView):
...@@ -44,13 +45,15 @@ class CourseSockFragmentView(EdxFragmentView): ...@@ -44,13 +45,15 @@ class CourseSockFragmentView(EdxFragmentView):
not deadline_has_passed and get_language() == 'en' 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) course_price = get_cosmetic_verified_display_price(course)
upgrade_url = EcommerceService().upgrade_url(request.user, course_key)
context = { context = {
'show_course_sock': show_course_sock, 'show_course_sock': show_course_sock,
'course_price': course_price, 'course_price': course_price,
'course_id': course.id 'course_id': course.id,
'upgrade_url': upgrade_url,
} }
return context return context
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment