Merge pull request #10430 from edx/peter-fogg/date-summary-blocks
Date summary blocks.
This commit is contained in:
@@ -19,6 +19,13 @@ from xmodule.x_module import STUDENT_VIEW
|
||||
from microsite_configuration import microsite
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.date_summary import (
|
||||
CourseEndDate,
|
||||
CourseStartDate,
|
||||
TodaysDate,
|
||||
VerificationDeadlineDate,
|
||||
VerifiedUpgradeDeadlineDate,
|
||||
)
|
||||
from courseware.model_data import FieldDataCache
|
||||
from courseware.module_render import get_module
|
||||
from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException
|
||||
@@ -27,6 +34,7 @@ import branding
|
||||
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -301,6 +309,34 @@ def get_course_info_section(request, course, section_key):
|
||||
return html
|
||||
|
||||
|
||||
def get_course_date_summary(course, user):
|
||||
"""
|
||||
Return the snippet of HTML to be included on the course info page
|
||||
in the 'Date Summary' section.
|
||||
"""
|
||||
blocks = _get_course_date_summary_blocks(course, user)
|
||||
return '\n'.join(
|
||||
b.render() for b in blocks
|
||||
)
|
||||
|
||||
|
||||
def _get_course_date_summary_blocks(course, user):
|
||||
"""
|
||||
Return the list of blocks to display on the course info page,
|
||||
sorted by date.
|
||||
"""
|
||||
block_classes = (
|
||||
CourseEndDate,
|
||||
CourseStartDate,
|
||||
TodaysDate,
|
||||
VerificationDeadlineDate,
|
||||
VerifiedUpgradeDeadlineDate,
|
||||
)
|
||||
|
||||
blocks = (cls(course, user) for cls in block_classes)
|
||||
return sorted((b for b in blocks if b.is_enabled), key=lambda b: b.date)
|
||||
|
||||
|
||||
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
|
||||
# arjun will address this by the end of October if no one does so prior to
|
||||
# then.
|
||||
|
||||
248
lms/djangoapps/courseware/date_summary.py
Normal file
248
lms/djangoapps/courseware/date_summary.py
Normal file
@@ -0,0 +1,248 @@
|
||||
# pylint: disable=missing-docstring
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from lazy import lazy
|
||||
import pytz
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
|
||||
class DateSummary(object):
|
||||
"""Base class for all date summary blocks."""
|
||||
|
||||
# The CSS class of this summary. Indicates the type of information
|
||||
# this summary block contains, and its urgency.
|
||||
css_class = ''
|
||||
|
||||
# The title of this summary.
|
||||
title = ''
|
||||
|
||||
# The detail text displayed by this summary.
|
||||
description = ''
|
||||
|
||||
# This summary's date.
|
||||
date = None
|
||||
|
||||
# The format to display this date in. By default, displays like Jan 01, 2015.
|
||||
date_format = '%b %d, %Y'
|
||||
|
||||
# The location to link to for more information.
|
||||
link = ''
|
||||
|
||||
# The text of the link.
|
||||
link_text = ''
|
||||
|
||||
def __init__(self, course, user):
|
||||
self.course = course
|
||||
self.user = user
|
||||
|
||||
def get_context(self):
|
||||
"""Return the template context used to render this summary block."""
|
||||
date = ''
|
||||
if self.date is not None:
|
||||
date = self.date.strftime(self.date_format)
|
||||
return {
|
||||
'title': self.title,
|
||||
'date': date,
|
||||
'description': self.description,
|
||||
'css_class': self.css_class,
|
||||
'link': self.link,
|
||||
'link_text': self.link_text,
|
||||
}
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Return an HTML representation of this summary block.
|
||||
"""
|
||||
return render_to_string('courseware/date_summary.html', self.get_context())
|
||||
|
||||
@property
|
||||
def is_enabled(self):
|
||||
"""
|
||||
Whether or not this summary block should be shown.
|
||||
|
||||
By default, the summary is only shown if its date is in the
|
||||
future.
|
||||
"""
|
||||
if self.date is not None:
|
||||
return datetime.now(pytz.UTC) <= self.date
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return 'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format(
|
||||
title=self.title,
|
||||
date=self.date,
|
||||
is_enabled=self.is_enabled
|
||||
)
|
||||
|
||||
|
||||
class TodaysDate(DateSummary):
|
||||
"""
|
||||
Displays today's date.
|
||||
"""
|
||||
css_class = 'todays-date'
|
||||
is_enabled = True
|
||||
|
||||
# The date is shown in the title, no need to display it again.
|
||||
def get_context(self):
|
||||
context = super(TodaysDate, self).get_context()
|
||||
context['date'] = ''
|
||||
return context
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
return datetime.now(pytz.UTC)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return _('Today is {date}').format(date=datetime.now(pytz.UTC).strftime(self.date_format))
|
||||
|
||||
|
||||
class CourseStartDate(DateSummary):
|
||||
"""
|
||||
Displays the start date of the course.
|
||||
"""
|
||||
css_class = 'start-date'
|
||||
title = _('Course Starts')
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
return self.course.start
|
||||
|
||||
|
||||
class CourseEndDate(DateSummary):
|
||||
"""
|
||||
Displays the end date of the course.
|
||||
"""
|
||||
css_class = 'end-date'
|
||||
title = _('Course End')
|
||||
is_enabled = True
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
if datetime.now(pytz.UTC) <= self.date:
|
||||
return _('To earn a certificate, you must complete all requirements before this date.')
|
||||
return _('This course is archived, which means you can review course content but it is no longer active.')
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
return self.course.end
|
||||
|
||||
|
||||
class VerifiedUpgradeDeadlineDate(DateSummary):
|
||||
"""
|
||||
Displays the date before which learners must upgrade to the
|
||||
Verified track.
|
||||
"""
|
||||
css_class = 'verified-upgrade-deadline'
|
||||
title = _('Verification Upgrade Deadline')
|
||||
description = _('You are still eligible to upgrade to a Verified Certificate!')
|
||||
link_text = _('Upgrade to Verified Certificate')
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
return reverse('verify_student_upgrade_and_verify', args=(self.course.id,))
|
||||
|
||||
@lazy
|
||||
def date(self):
|
||||
try:
|
||||
verified_mode = CourseMode.objects.get(
|
||||
course_id=self.course.id, mode_slug=CourseMode.VERIFIED
|
||||
)
|
||||
return verified_mode.expiration_datetime
|
||||
except CourseMode.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class VerificationDeadlineDate(DateSummary):
|
||||
"""
|
||||
Displays the date by which the user must complete the verification
|
||||
process.
|
||||
"""
|
||||
|
||||
@property
|
||||
def css_class(self):
|
||||
base_state = 'verification-deadline'
|
||||
if self.deadline_has_passed():
|
||||
return base_state + '-passed'
|
||||
elif self.must_retry():
|
||||
return base_state + '-retry'
|
||||
else:
|
||||
return base_state + '-upcoming'
|
||||
|
||||
@property
|
||||
def link_text(self):
|
||||
return self.link_table[self.css_class][0]
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
return self.link_table[self.css_class][1]
|
||||
|
||||
@property
|
||||
def link_table(self):
|
||||
"""Maps verification state to a tuple of link text and location."""
|
||||
return {
|
||||
'verification-deadline-passed': (_('Learn More'), ''),
|
||||
'verification-deadline-retry': (_('Retry Verification'), reverse('verify_student_reverify')),
|
||||
'verification-deadline-upcoming': (
|
||||
_('Verify My Identity'),
|
||||
reverse('verify_student_verify_now', args=(self.course.id,))
|
||||
)
|
||||
}
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
if self.deadline_has_passed():
|
||||
return _('Missed Verification Deadline')
|
||||
return _('Verification Deadline')
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
if self.deadline_has_passed():
|
||||
return _(
|
||||
"Unfortunately you missed this course's deadline for"
|
||||
" a successful verification."
|
||||
)
|
||||
return _(
|
||||
"You must successfully complete verification before"
|
||||
" this date to qualify for a Verified Certificate."
|
||||
)
|
||||
|
||||
@lazy
|
||||
def date(self):
|
||||
return VerificationDeadline.deadline_for_course(self.course.id)
|
||||
|
||||
@lazy
|
||||
def is_enabled(self):
|
||||
if self.date is None:
|
||||
return False
|
||||
(mode, is_active) = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
||||
if is_active and mode == 'verified':
|
||||
return self.verification_status in ('expired', 'none', 'must_reverify')
|
||||
return False
|
||||
|
||||
@lazy
|
||||
def verification_status(self):
|
||||
"""Return the verification status for this user."""
|
||||
return SoftwareSecurePhotoVerification.user_status(self.user)[0]
|
||||
|
||||
def deadline_has_passed(self):
|
||||
"""
|
||||
Return True if a verification deadline exists, and has already passed.
|
||||
"""
|
||||
deadline = self.date
|
||||
return deadline is not None and deadline <= datetime.now(pytz.UTC)
|
||||
|
||||
def must_retry(self):
|
||||
"""Return True if the user must re-submit verification, False otherwise."""
|
||||
return self.verification_status == 'must_reverify'
|
||||
247
lms/djangoapps/courseware/tests/test_date_summary.py
Normal file
247
lms/djangoapps/courseware/tests/test_date_summary.py
Normal file
@@ -0,0 +1,247 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for course home page date summary blocks."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import ddt
|
||||
from django.core.urlresolvers import reverse
|
||||
import freezegun
|
||||
from nose.plugins.attrib import attr
|
||||
import pytz
|
||||
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.courses import _get_course_date_summary_blocks
|
||||
from courseware.date_summary import (
|
||||
CourseEndDate,
|
||||
CourseStartDate,
|
||||
DateSummary,
|
||||
TodaysDate,
|
||||
VerificationDeadlineDate,
|
||||
VerifiedUpgradeDeadlineDate,
|
||||
)
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from verify_student.models import VerificationDeadline
|
||||
from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
"""Tests for course date summary blocks."""
|
||||
|
||||
def setUp(self):
|
||||
SelfPacedConfiguration(enable_course_home_improvements=True).save()
|
||||
super(CourseDateSummaryTest, self).setUp()
|
||||
|
||||
def setup_course_and_user(
|
||||
self,
|
||||
days_till_start=1,
|
||||
days_till_end=14,
|
||||
days_till_upgrade_deadline=4,
|
||||
enrollment_mode=CourseMode.VERIFIED,
|
||||
days_till_verification_deadline=14,
|
||||
verification_status=None,
|
||||
):
|
||||
"""Set up the course and user for this test."""
|
||||
now = datetime.now(pytz.UTC)
|
||||
self.course = CourseFactory.create( # pylint: disable=attribute-defined-outside-init
|
||||
start=now + timedelta(days=days_till_start),
|
||||
end=now + timedelta(days=days_till_end),
|
||||
)
|
||||
self.user = UserFactory.create() # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
if enrollment_mode is not None and days_till_upgrade_deadline is not None:
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=enrollment_mode,
|
||||
expiration_datetime=now + timedelta(days=days_till_upgrade_deadline)
|
||||
)
|
||||
CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user, mode=enrollment_mode)
|
||||
else:
|
||||
CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user)
|
||||
|
||||
if days_till_verification_deadline is not None:
|
||||
VerificationDeadline.objects.create(
|
||||
course_key=self.course.id,
|
||||
deadline=now + timedelta(days=days_till_verification_deadline)
|
||||
)
|
||||
|
||||
if verification_status is not None:
|
||||
SoftwareSecurePhotoVerificationFactory.create(user=self.user, status=verification_status)
|
||||
|
||||
def test_course_info_feature_flag(self):
|
||||
SelfPacedConfiguration(enable_course_home_improvements=False).save()
|
||||
self.setup_course_and_user()
|
||||
url = reverse('info', args=(self.course.id,))
|
||||
response = self.client.get(url)
|
||||
self.assertNotIn('date-summary', response.content)
|
||||
|
||||
# Tests for which blocks are enabled
|
||||
|
||||
def assert_block_types(self, expected_blocks):
|
||||
"""Assert that the enabled block types for this course are as expected."""
|
||||
blocks = _get_course_date_summary_blocks(self.course, self.user)
|
||||
self.assertEqual(len(blocks), len(expected_blocks))
|
||||
self.assertEqual(set(type(b) for b in blocks), set(expected_blocks))
|
||||
|
||||
@ddt.data(
|
||||
# Before course starts
|
||||
({}, (CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate)),
|
||||
# After course end
|
||||
({'days_till_start': -10,
|
||||
'days_till_end': -5,
|
||||
'days_till_upgrade_deadline': -6,
|
||||
'days_till_verification_deadline': -5,
|
||||
'verification_status': 'approved'},
|
||||
(TodaysDate, CourseEndDate)),
|
||||
# During course run
|
||||
({'days_till_start': -1},
|
||||
(TodaysDate, CourseEndDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate)),
|
||||
# Verification approved
|
||||
({'days_till_start': -10,
|
||||
'days_till_upgrade_deadline': -1,
|
||||
'days_till_verification_deadline': 1,
|
||||
'verification_status': 'approved'},
|
||||
(TodaysDate, CourseEndDate)),
|
||||
# After upgrade deadline
|
||||
({'days_till_start': -10, 'days_till_upgrade_deadline': -1},
|
||||
(TodaysDate, CourseEndDate, VerificationDeadlineDate)),
|
||||
# After verification deadline
|
||||
({'days_till_start': -10,
|
||||
'days_till_upgrade_deadline': -2,
|
||||
'days_till_verification_deadline': -1},
|
||||
(TodaysDate, CourseEndDate, VerificationDeadlineDate))
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_enabled_block_types(self, course_options, expected_blocks):
|
||||
self.setup_course_and_user(**course_options)
|
||||
self.assert_block_types(expected_blocks)
|
||||
|
||||
# Specific block type tests
|
||||
|
||||
## Base DateSummary -- test empty defaults
|
||||
|
||||
def test_date_summary(self):
|
||||
self.setup_course_and_user()
|
||||
block = DateSummary(self.course, self.user)
|
||||
html = '<div class="date-summary-container"><div class="date-summary date-summary-"></div></div>'
|
||||
self.assertHTMLEqual(block.render(), html)
|
||||
self.assertFalse(block.is_enabled)
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
def test_date_render(self):
|
||||
self.setup_course_and_user()
|
||||
block = DateSummary(self.course, self.user)
|
||||
block.date = datetime.now(pytz.UTC)
|
||||
self.assertIn('Jan 02, 2015', block.render())
|
||||
|
||||
## TodaysDate
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
def test_todays_date(self):
|
||||
self.setup_course_and_user()
|
||||
block = TodaysDate(self.course, self.user)
|
||||
self.assertTrue(block.is_enabled)
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC))
|
||||
self.assertEqual(block.title, 'Today is Jan 02, 2015')
|
||||
self.assertNotIn('date-summary-date', block.render())
|
||||
|
||||
## CourseStartDate
|
||||
|
||||
def test_course_start_date(self):
|
||||
self.setup_course_and_user()
|
||||
block = CourseStartDate(self.course, self.user)
|
||||
self.assertEqual(block.date, self.course.start)
|
||||
|
||||
## CourseEndDate
|
||||
|
||||
def test_course_end_date_during_course(self):
|
||||
self.setup_course_and_user(days_till_start=-1)
|
||||
block = CourseEndDate(self.course, self.user)
|
||||
self.assertEqual(
|
||||
block.description,
|
||||
'To earn a certificate, you must complete all requirements before this date.'
|
||||
)
|
||||
|
||||
def test_course_end_date_after_course(self):
|
||||
self.setup_course_and_user(days_till_start=-2, days_till_end=-1)
|
||||
block = CourseEndDate(self.course, self.user)
|
||||
self.assertEqual(
|
||||
block.description,
|
||||
'This course is archived, which means you can review course content but it is no longer active.'
|
||||
)
|
||||
|
||||
## VerifiedUpgradeDeadlineDate
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
def test_verified_upgrade_deadline_date(self):
|
||||
self.setup_course_and_user(days_till_upgrade_deadline=1)
|
||||
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=1))
|
||||
self.assertEqual(block.link, reverse('verify_student_upgrade_and_verify', args=(self.course.id,)))
|
||||
|
||||
def test_without_upgrade_deadline(self):
|
||||
self.setup_course_and_user(enrollment_mode=None)
|
||||
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
|
||||
self.assertIsNone(block.date)
|
||||
|
||||
## VerificationDeadlineDate
|
||||
|
||||
def test_no_verification_deadline(self):
|
||||
self.setup_course_and_user(days_till_start=-1, days_till_verification_deadline=None)
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertFalse(block.is_enabled)
|
||||
|
||||
def test_no_verified_enrollment(self):
|
||||
self.setup_course_and_user(days_till_start=-1, enrollment_mode=CourseMode.AUDIT)
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertFalse(block.is_enabled)
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
def test_verification_deadline_date_upcoming(self):
|
||||
self.setup_course_and_user(days_till_start=-1)
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.css_class, 'verification-deadline-upcoming')
|
||||
self.assertEqual(block.title, 'Verification Deadline')
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=14))
|
||||
self.assertEqual(
|
||||
block.description,
|
||||
'You must successfully complete verification before this date to qualify for a Verified Certificate.'
|
||||
)
|
||||
self.assertEqual(block.link_text, 'Verify My Identity')
|
||||
self.assertEqual(block.link, reverse('verify_student_verify_now', args=(self.course.id,)))
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
def test_verification_deadline_date_retry(self):
|
||||
self.setup_course_and_user(days_till_start=-1, verification_status='denied')
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.css_class, 'verification-deadline-retry')
|
||||
self.assertEqual(block.title, 'Verification Deadline')
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=14))
|
||||
self.assertEqual(
|
||||
block.description,
|
||||
'You must successfully complete verification before this date to qualify for a Verified Certificate.'
|
||||
)
|
||||
self.assertEqual(block.link_text, 'Retry Verification')
|
||||
self.assertEqual(block.link, reverse('verify_student_reverify'))
|
||||
|
||||
@freezegun.freeze_time('2015-01-02')
|
||||
def test_verification_deadline_date_denied(self):
|
||||
self.setup_course_and_user(
|
||||
days_till_start=-10,
|
||||
verification_status='denied',
|
||||
days_till_verification_deadline=-1,
|
||||
)
|
||||
block = VerificationDeadlineDate(self.course, self.user)
|
||||
self.assertEqual(block.css_class, 'verification-deadline-passed')
|
||||
self.assertEqual(block.title, 'Missed Verification Deadline')
|
||||
self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=-1))
|
||||
self.assertEqual(
|
||||
block.description,
|
||||
"Unfortunately you missed this course's deadline for a successful verification."
|
||||
)
|
||||
self.assertEqual(block.link_text, 'Learn More')
|
||||
self.assertEqual(block.link, '')
|
||||
@@ -369,3 +369,62 @@
|
||||
}
|
||||
}
|
||||
|
||||
// pfogg - ECOM-2604
|
||||
// styling for date summary blocks on the course info page
|
||||
.date-summary-container {
|
||||
.date-summary {
|
||||
@include clearfix;
|
||||
margin-top: $baseline/2;
|
||||
margin-bottom: $baseline/2;
|
||||
padding: 10px;
|
||||
background-color: $gray-l4;
|
||||
@include border-left(3px solid $gray-l3);
|
||||
|
||||
.heading {
|
||||
@include float(left);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: $baseline/2;
|
||||
margin-bottom: $baseline/2;
|
||||
display: inline-block;
|
||||
color: $lighter-base-font-color;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.date-summary-link {
|
||||
@include float(right);
|
||||
font-size: 80%;
|
||||
font-weight: $font-semibold;
|
||||
a {
|
||||
color: $base-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
@include float(right);
|
||||
color: $lighter-base-font-color;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
&-todays-date {
|
||||
@include border-left(3px solid $blue);
|
||||
}
|
||||
|
||||
&-verified-upgrade-deadline {
|
||||
@include border-left(3px solid $green);
|
||||
}
|
||||
|
||||
&-verification-deadline-passed {
|
||||
@include border-left(3px solid $red);
|
||||
}
|
||||
|
||||
&-verification-deadline-retry {
|
||||
@include border-left(3px solid $red);
|
||||
}
|
||||
|
||||
&-verification-deadline-upcoming {
|
||||
@include border-left(3px solid $orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
lms/templates/courseware/date_summary.html
Normal file
18
lms/templates/courseware/date_summary.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="date-summary-container">
|
||||
<div class="date-summary date-summary-${css_class}">
|
||||
% if title:
|
||||
<h3 class="heading">${title}</h3>
|
||||
% endif
|
||||
% if date:
|
||||
<h4 class="date">${date}</h4>
|
||||
% endif
|
||||
% if description:
|
||||
<p class="description">${description}</p>
|
||||
% endif
|
||||
% if link and link_text:
|
||||
<span class="date-summary-link">
|
||||
<a href="${link}">${link_text} <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
|
||||
</span>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,7 +2,9 @@
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from courseware.courses import get_course_info_section
|
||||
from courseware.courses import get_course_info_section, get_course_date_summary
|
||||
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
%>
|
||||
|
||||
<%block name="pagetitle">${_("{course_number} Course Info").format(course_number=course.display_number_with_default)}</%block>
|
||||
@@ -59,6 +61,11 @@ $(document).ready(function(){
|
||||
${get_course_info_section(request, course, 'updates')}
|
||||
</section>
|
||||
<section aria-label="${_('Handout Navigation')}" class="handouts">
|
||||
% if SelfPacedConfiguration.current().enable_course_home_improvements:
|
||||
<h1>${_("Important Course Dates")}</h1>
|
||||
${get_course_date_summary(course, user)}
|
||||
% endif
|
||||
|
||||
<h1>${_(course.info_sidebar_name)}</h1>
|
||||
${get_course_info_section(request, course, 'handouts')}
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'SelfPacedConfiguration.enable_course_home_improvements'
|
||||
db.add_column('self_paced_selfpacedconfiguration', 'enable_course_home_improvements',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'SelfPacedConfiguration.enable_course_home_improvements'
|
||||
db.delete_column('self_paced_selfpacedconfiguration', 'enable_course_home_improvements')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'self_paced.selfpacedconfiguration': {
|
||||
'Meta': {'ordering': "('-change_date',)", 'object_name': 'SelfPacedConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enable_course_home_improvements': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['self_paced']
|
||||
@@ -2,6 +2,9 @@
|
||||
Configuration for self-paced courses.
|
||||
"""
|
||||
|
||||
from django.db.models import BooleanField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
|
||||
|
||||
@@ -9,4 +12,8 @@ class SelfPacedConfiguration(ConfigurationModel):
|
||||
"""
|
||||
Configuration for self-paced courses.
|
||||
"""
|
||||
pass
|
||||
|
||||
enable_course_home_improvements = BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Enable course home page improvements.")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user