From d7417d62cc718d98654ee1fffc750e8186cc12d8 Mon Sep 17 00:00:00 2001
From: Peter Fogg <pfogg@edx.org>
Date: Fri, 23 Oct 2015 10:52:42 -0400
Subject: [PATCH] Date summary blocks on the course home page.

Enabled behind the
`SelfPacedConfiguration.enable_course_home_improvements` flag.

ECOM-2604
---
 lms/djangoapps/courseware/courses.py                                                                                         |  36 ++++++++++++++++++++++++++++++++++++
 lms/djangoapps/courseware/date_summary.py                                                                                    | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 lms/djangoapps/courseware/tests/test_date_summary.py                                                                         | 247 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 lms/static/sass/_developer.scss                                                                                              |  59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 lms/templates/courseware/date_summary.html                                                                                   |  18 ++++++++++++++++++
 lms/templates/courseware/info.html                                                                                           |   9 ++++++++-
 openedx/core/djangoapps/self_paced/migrations/0002_auto__add_field_selfpacedconfiguration_enable_course_home_improvements.py |  69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 openedx/core/djangoapps/self_paced/models.py                                                                                 |   9 ++++++++-
 8 files changed, 693 insertions(+), 2 deletions(-)
 create mode 100644 lms/djangoapps/courseware/date_summary.py
 create mode 100644 lms/djangoapps/courseware/tests/test_date_summary.py
 create mode 100644 lms/templates/courseware/date_summary.html
 create mode 100644 openedx/core/djangoapps/self_paced/migrations/0002_auto__add_field_selfpacedconfiguration_enable_course_home_improvements.py

diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 059fb4a..dce1a75 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -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.
diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py
new file mode 100644
index 0000000..68cc759
--- /dev/null
+++ b/lms/djangoapps/courseware/date_summary.py
@@ -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'
diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py
new file mode 100644
index 0000000..d96c8b5
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/test_date_summary.py
@@ -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, '')
diff --git a/lms/static/sass/_developer.scss b/lms/static/sass/_developer.scss
index 4fdeaaf..6f43b80 100644
--- a/lms/static/sass/_developer.scss
+++ b/lms/static/sass/_developer.scss
@@ -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);
+    }
+  }
+}
diff --git a/lms/templates/courseware/date_summary.html b/lms/templates/courseware/date_summary.html
new file mode 100644
index 0000000..5fec70a
--- /dev/null
+++ b/lms/templates/courseware/date_summary.html
@@ -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>
diff --git a/lms/templates/courseware/info.html b/lms/templates/courseware/info.html
index fb19cc1..e277016 100644
--- a/lms/templates/courseware/info.html
+++ b/lms/templates/courseware/info.html
@@ -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>
diff --git a/openedx/core/djangoapps/self_paced/migrations/0002_auto__add_field_selfpacedconfiguration_enable_course_home_improvements.py b/openedx/core/djangoapps/self_paced/migrations/0002_auto__add_field_selfpacedconfiguration_enable_course_home_improvements.py
new file mode 100644
index 0000000..6de0a78
--- /dev/null
+++ b/openedx/core/djangoapps/self_paced/migrations/0002_auto__add_field_selfpacedconfiguration_enable_course_home_improvements.py
@@ -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']
\ No newline at end of file
diff --git a/openedx/core/djangoapps/self_paced/models.py b/openedx/core/djangoapps/self_paced/models.py
index f76974d..35d41f9 100644
--- a/openedx/core/djangoapps/self_paced/models.py
+++ b/openedx/core/djangoapps/self_paced/models.py
@@ -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.")
+    )
--
libgit2 0.26.0