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