Skip to content

Commit

Permalink
Merge pull request openedx#16098 from edx/andya/course-dates-messaging
Browse files Browse the repository at this point in the history
Show course home messages for important course dates
  • Loading branch information
andy-armstrong authored Oct 2, 2017
2 parents 1aff33d + 7f45503 commit 39671b8
Show file tree
Hide file tree
Showing 24 changed files with 571 additions and 233 deletions.
29 changes: 29 additions & 0 deletions common/static/sass/edx-pattern-library-shims/_buttons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
15 changes: 12 additions & 3 deletions common/static/sass/edx-pattern-library-shims/base/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
// ----------------------------
Expand Down
15 changes: 15 additions & 0 deletions lms/djangoapps/commerce/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

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
Expand Down Expand Up @@ -93,3 +95,16 @@ def get_checkout_page_url(self, *skus):
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
195 changes: 171 additions & 24 deletions lms/djangoapps/courseware/date_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,45 @@
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):
"""
Expand All @@ -41,6 +60,12 @@ def description(self):
"""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."""
Expand All @@ -64,15 +89,6 @@ def link_text(self):
"""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
Expand All @@ -87,7 +103,7 @@ def relative_datestring(self):
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
Expand Down Expand Up @@ -117,7 +133,7 @@ def is_enabled(self):
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):
Expand All @@ -126,7 +142,52 @@ def deadline_has_passed(self):
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(
Expand All @@ -151,7 +212,7 @@ def get_context(self):

@property
def date(self):
return datetime.datetime.now(utc)
return self.current_time

@property
def title(self):
Expand All @@ -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):
"""
Expand All @@ -183,7 +273,7 @@ def is_enabled(self):

@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.')
Expand All @@ -195,6 +285,34 @@ def description(self):
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):
"""
Expand All @@ -216,7 +334,7 @@ def is_enabled(self):
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
)

Expand Down Expand Up @@ -252,13 +370,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):
Expand Down Expand Up @@ -299,6 +411,41 @@ def date(self):

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"'
'data-creative="original_message" data-position="course_message"'
'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):
"""
Expand Down
Loading

0 comments on commit 39671b8

Please sign in to comment.