Commit d7417d62 by Peter Fogg

Date summary blocks on the course home page.

Enabled behind the
`SelfPacedConfiguration.enable_course_home_improvements` flag.

ECOM-2604
parent cdd5a6f8
...@@ -19,6 +19,13 @@ from xmodule.x_module import STUDENT_VIEW ...@@ -19,6 +19,13 @@ from xmodule.x_module import STUDENT_VIEW
from microsite_configuration import microsite from microsite_configuration import microsite
from courseware.access import has_access from courseware.access import has_access
from courseware.date_summary import (
CourseEndDate,
CourseStartDate,
TodaysDate,
VerificationDeadlineDate,
VerifiedUpgradeDeadlineDate,
)
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.module_render import get_module from courseware.module_render import get_module
from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException
...@@ -27,6 +34,7 @@ import branding ...@@ -27,6 +34,7 @@ import branding
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -301,6 +309,34 @@ def get_course_info_section(request, course, section_key): ...@@ -301,6 +309,34 @@ def get_course_info_section(request, course, section_key):
return html 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. # 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 # arjun will address this by the end of October if no one does so prior to
# then. # then.
......
# 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'
...@@ -369,3 +369,62 @@ ...@@ -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);
}
}
}
<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 @@ ...@@ -2,7 +2,9 @@
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%! <%!
from django.utils.translation import ugettext as _ 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> <%block name="pagetitle">${_("{course_number} Course Info").format(course_number=course.display_number_with_default)}</%block>
...@@ -59,6 +61,11 @@ $(document).ready(function(){ ...@@ -59,6 +61,11 @@ $(document).ready(function(){
${get_course_info_section(request, course, 'updates')} ${get_course_info_section(request, course, 'updates')}
</section> </section>
<section aria-label="${_('Handout Navigation')}" class="handouts"> <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> <h1>${_(course.info_sidebar_name)}</h1>
${get_course_info_section(request, course, 'handouts')} ${get_course_info_section(request, course, 'handouts')}
</section> </section>
......
# -*- 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
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
Configuration for self-paced courses. 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 from config_models.models import ConfigurationModel
...@@ -9,4 +12,8 @@ class SelfPacedConfiguration(ConfigurationModel): ...@@ -9,4 +12,8 @@ class SelfPacedConfiguration(ConfigurationModel):
""" """
Configuration for self-paced courses. Configuration for self-paced courses.
""" """
pass
enable_course_home_improvements = BooleanField(
default=False,
verbose_name=_("Enable course home page improvements.")
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment