diff --git a/common/static/sass/edx-pattern-library-shims/_buttons.scss b/common/static/sass/edx-pattern-library-shims/_buttons.scss index cb5e5d85dc..d0eaf87b92 100644 --- a/common/static/sass/edx-pattern-library-shims/_buttons.scss +++ b/common/static/sass/edx-pattern-library-shims/_buttons.scss @@ -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; + } +} diff --git a/common/static/sass/edx-pattern-library-shims/base/_variables.scss b/common/static/sass/edx-pattern-library-shims/base/_variables.scss index 3ae121632d..dba1a33ad5 100644 --- a/common/static/sass/edx-pattern-library-shims/base/_variables.scss +++ b/common/static/sass/edx-pattern-library-shims/base/_variables.scss @@ -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 // ---------------------------- diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index 7de0e6d6f1..daf83e4d0b 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -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 diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index d27bb188ef..205edbe3a8 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -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( + '' + '' + ).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( + '
' + ).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): """ diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 069edb6f2b..08c2f320e6 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -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
-
-