Merge pull request #10404 from edx/feature/self-paced

Enable self-paced courses.
This commit is contained in:
Peter Fogg
2015-10-28 20:46:35 -04:00
44 changed files with 907 additions and 64 deletions

View File

@@ -9,6 +9,7 @@ from nose.plugins.attrib import attr
from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error
from django.test.utils import override_settings
from lms.djangoapps.courseware.tests.test_field_overrides import inject_field_overrides
from request_cache.middleware import RequestCache
from student.tests.factories import AdminFactory # pylint: disable=import-error
from xmodule.modulestore.tests.django_utils import (
@@ -69,13 +70,7 @@ class TestFieldOverrides(ModuleStoreTestCase):
self.addCleanup(RequestCache.clear_request_cache)
# Apparently the test harness doesn't use LmsFieldStorage, and I'm not
# sure if there's a way to poke the test harness to do so. So, we'll
# just inject the override field storage in this brute force manner.
OverrideFieldData.provider_classes = None
for block in iter_blocks(ccx.course):
block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access
AdminFactory.create(), course, block._field_data) # pylint: disable=protected-access
inject_field_overrides(iter_blocks(ccx.course), course, AdminFactory.create())
def cleanup_provider_classes():
"""

View File

@@ -24,7 +24,7 @@ from xblock.field_data import FieldData
from xmodule.modulestore.inheritance import InheritanceMixin
NOTSET = object()
ENABLED_OVERRIDE_PROVIDERS_KEY = "courseware.field_overrides.enabled_providers"
ENABLED_OVERRIDE_PROVIDERS_KEY = "courseware.field_overrides.enabled_providers.{course_id}"
def resolve_dotted(name):
@@ -77,7 +77,6 @@ class OverrideFieldData(FieldData):
settings.FIELD_OVERRIDE_PROVIDERS))
enabled_providers = cls._providers_for_course(course)
if enabled_providers:
# TODO: we might not actually want to return here. Might be better
# to check for instance.providers after the instance is built. This
@@ -98,14 +97,16 @@ class OverrideFieldData(FieldData):
course: The course XBlock
"""
request_cache = RequestCache.get_request_cache()
enabled_providers = request_cache.data.get(
ENABLED_OVERRIDE_PROVIDERS_KEY, NOTSET
)
if course is None:
cache_key = ENABLED_OVERRIDE_PROVIDERS_KEY.format(course_id='None')
else:
cache_key = ENABLED_OVERRIDE_PROVIDERS_KEY.format(course_id=unicode(course.id))
enabled_providers = request_cache.data.get(cache_key, NOTSET)
if enabled_providers == NOTSET:
enabled_providers = tuple(
(provider_class for provider_class in cls.provider_classes if provider_class.enabled_for(course))
)
request_cache.data[ENABLED_OVERRIDE_PROVIDERS_KEY] = enabled_providers
request_cache.data[cache_key] = enabled_providers
return enabled_providers

View File

@@ -0,0 +1,28 @@
"""
Field overrides for self-paced courses. This allows overriding due
dates for each block in the course.
"""
from .field_overrides import FieldOverrideProvider
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
class SelfPacedDateOverrideProvider(FieldOverrideProvider):
"""
A concrete implementation of
:class:`~courseware.field_overrides.FieldOverrideProvider` which allows for
due dates to be overridden for self-paced courses.
"""
def get(self, block, name, default):
# Remove due dates
if name == 'due':
return None
# Remove release dates for course content
if name == 'start' and block.category != 'course':
return None
return default
@classmethod
def enabled_for(cls, course):
"""This provider is enabled for self-paced courses only."""
return SelfPacedConfiguration.current().enabled and course.self_paced

View File

@@ -7,12 +7,13 @@ from urllib import urlencode
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from util.date_utils import strftime_localized
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_CLOSED_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from student.models import CourseEnrollment
from .helpers import LoginEnrollmentTestCase
@@ -114,3 +115,34 @@ class CourseInfoTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertNotIn(self.xml_data, resp.content)
@attr('shard_1')
@override_settings(FEATURES=dict(settings.FEATURES, EMBARGO=False))
class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
"""
Tests for the info page of self-paced courses.
"""
def setUp(self):
super(SelfPacedCourseInfoTestCase, self).setUp()
self.instructor_led_course = CourseFactory.create(self_paced=False)
self.self_paced_course = CourseFactory.create(self_paced=True)
self.setup_user()
def fetch_course_info_with_queries(self, course, sql_queries, mongo_queries):
"""
Fetch the given course's info page, asserting the number of SQL
and Mongo queries.
"""
url = reverse('info', args=[unicode(course.id)])
with self.assertNumQueries(sql_queries):
with check_mongo_calls(mongo_queries):
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
def test_num_queries_instructor_led(self):
self.fetch_course_info_with_queries(self.instructor_led_course, 14, 4)
def test_num_queries_self_paced(self):
self.fetch_course_info_with_queries(self.self_paced_course, 14, 4)

View File

@@ -132,3 +132,16 @@ class TestOverrideProvider(FieldOverrideProvider):
@classmethod
def enabled_for(cls, course):
return True
def inject_field_overrides(blocks, course, user):
"""
Apparently the test harness doesn't use LmsFieldStorage, and I'm
not sure if there's a way to poke the test harness to do so. So,
we'll just inject the override field storage in this brute force
manner.
"""
OverrideFieldData.provider_classes = None
for block in blocks:
block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access
user, course, block._field_data) # pylint: disable=protected-access

View File

@@ -0,0 +1,56 @@
"""
Tests for self-paced course due date overrides.
"""
from datetime import datetime
from dateutil.tz import tzutc
from django.test.utils import override_settings
from student.tests.factories import UserFactory
from lms.djangoapps.ccx.tests.test_overrides import inject_field_overrides
from lms.djangoapps.courseware.field_overrides import OverrideFieldData
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@override_settings(
FIELD_OVERRIDE_PROVIDERS=('courseware.self_paced_overrides.SelfPacedDateOverrideProvider',)
)
class SelfPacedDateOverrideTest(ModuleStoreTestCase):
"""
Tests for self-paced due date overrides.
"""
def setUp(self):
SelfPacedConfiguration(enabled=True).save()
super(SelfPacedDateOverrideTest, self).setUp()
self.due_date = datetime(2015, 5, 26, 8, 30, 00).replace(tzinfo=tzutc())
def tearDown(self):
super(SelfPacedDateOverrideTest, self).tearDown()
OverrideFieldData.provider_classes = None
def setup_course(self, display_name, self_paced):
"""Set up a course with `display_name` and `self_paced` attributes.
Creates a child block with a due date, and ensures that field
overrides are correctly applied for both blocks.
"""
course = CourseFactory.create(display_name=display_name, self_paced=self_paced)
section = ItemFactory.create(parent=course, due=self.due_date)
inject_field_overrides((course, section), course, UserFactory.create())
return (course, section)
def test_instructor_led(self):
__, il_section = self.setup_course("Instructor Led Course", False)
self.assertEqual(self.due_date, il_section.due)
def test_self_paced(self):
__, sp_section = self.setup_course("Self-Paced Course", True)
self.assertIsNone(sp_section.due)
def test_self_paced_disabled(self):
SelfPacedConfiguration(enabled=False).save()
__, sp_section = self.setup_course("Self-Paced Course", True)
self.assertEqual(self.due_date, sp_section.due)

View File

@@ -6,6 +6,7 @@ import cgi
from urllib import urlencode
import ddt
import json
import itertools
import unittest
from datetime import datetime
from HTMLParser import HTMLParser
@@ -36,6 +37,7 @@ from courseware.testutils import RenderXBlockTestMixin
from courseware.tests.factories import StudentModuleFactory
from courseware.user_state_client import DjangoXBlockUserStateClient
from edxmako.tests import mako_middleware_process_request
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from student.models import CourseEnrollment
from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentFactory
from util.tests.test_date_utils import fake_ugettext, fake_pgettext
@@ -45,7 +47,7 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
@attr('shard_1')
@@ -669,10 +671,18 @@ class ProgressPageTests(ModuleStoreTestCase):
self.request.user = self.user
mako_middleware_process_request(self.request)
self.setup_course()
def setup_course(self, **options):
"""Create the test course."""
course = CourseFactory.create(
start=datetime(2013, 9, 16, 7, 17, 28),
grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5},
**options
)
# pylint: disable=attribute-defined-outside-init
self.course = modulestore().get_course(course.id)
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
@@ -829,6 +839,18 @@ class ProgressPageTests(ModuleStoreTestCase):
resp = views.progress(self.request, course_id=unicode(self.course.id))
self.assertContains(resp, u"Download Your Certificate")
@ddt.data(
*itertools.product(((18, 4, True), (18, 4, False)), (True, False))
)
@ddt.unpack
def test_query_counts(self, (sql_calls, mongo_calls, self_paced), self_paced_enabled):
"""Test that query counts remain the same for self-paced and instructor-led courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced)
with self.assertNumQueries(sql_calls), check_mongo_calls(mongo_calls):
resp = views.progress(self.request, course_id=unicode(self.course.id))
self.assertEqual(resp.status_code, 200)
@attr('shard_1')
class VerifyCourseKeyDecoratorTests(TestCase):
@@ -1151,3 +1173,17 @@ class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase):
if url_encoded_params:
url += '?' + url_encoded_params
return self.client.get(url)
class TestRenderXBlockSelfPaced(TestRenderXBlock):
"""
Test rendering XBlocks for a self-paced course. Relies on the query
count assertions in the tests defined by RenderXBlockMixin.
"""
def setUp(self):
super(TestRenderXBlockSelfPaced, self).setUp()
SelfPacedConfiguration(enabled=True).save()
def course_options(self):
return {'self_paced': True}

View File

@@ -55,6 +55,13 @@ class RenderXBlockTestMixin(object):
"""
self.client.login(username=self.user.username, password='test')
def course_options(self):
"""
Options to configure the test course. Intended to be overridden by
subclasses.
"""
return {}
def setup_course(self, default_store=None):
"""
Helper method to create the course.
@@ -62,7 +69,7 @@ class RenderXBlockTestMixin(object):
if not default_store:
default_store = self.store.default_modulestore.get_modulestore_type()
with self.store.default_store(default_store):
self.course = CourseFactory.create() # pylint: disable=attribute-defined-outside-init
self.course = CourseFactory.create(**self.course_options()) # pylint: disable=attribute-defined-outside-init
chapter = ItemFactory.create(parent=self.course, category='chapter')
self.html_block = ItemFactory.create( # pylint: disable=attribute-defined-outside-init
parent=chapter,

View File

@@ -12,6 +12,7 @@ from django.test.utils import override_settings
from nose.plugins.attrib import attr
from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error
from lms.djangoapps.ccx.tests.test_overrides import inject_field_overrides
from student.tests.factories import UserFactory # pylint: disable=import-error
from xmodule.fields import Date
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
@@ -196,7 +197,6 @@ class TestSetDueDateExtension(ModuleStoreTestCase):
Fixtures.
"""
super(TestSetDueDateExtension, self).setUp()
OverrideFieldData.provider_classes = None
self.due = due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc)
course = CourseFactory.create()
@@ -216,12 +216,7 @@ class TestSetDueDateExtension(ModuleStoreTestCase):
self.week3 = week3
self.user = user
# Apparently the test harness doesn't use LmsFieldStorage, and I'm not
# sure if there's a way to poke the test harness to do so. So, we'll
# just inject the override field storage in this brute force manner.
for block in (course, week1, week2, week3, homework, assignment):
block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access
user, course, block._field_data) # pylint: disable=protected-access
inject_field_overrides((course, week1, week2, week3, homework, assignment), course, user)
def tearDown(self):
super(TestSetDueDateExtension, self).tearDown()

View File

@@ -676,6 +676,11 @@ if FEATURES.get('INDIVIDUAL_DUE_DATES'):
'courseware.student_field_overrides.IndividualStudentOverrideProvider',
)
##### Self-Paced Course Due Dates #####
FIELD_OVERRIDE_PROVIDERS += (
'courseware.self_paced_overrides.SelfPacedDateOverrideProvider',
)
# PROFILE IMAGE CONFIG
PROFILE_IMAGE_BACKEND = ENV_TOKENS.get('PROFILE_IMAGE_BACKEND', PROFILE_IMAGE_BACKEND)
PROFILE_IMAGE_SECRET_KEY = AUTH_TOKENS.get('PROFILE_IMAGE_SECRET_KEY', PROFILE_IMAGE_SECRET_KEY)

View File

@@ -128,6 +128,9 @@ FEATURES['ENABLE_TEAMS'] = True
# Enable custom content licensing
FEATURES['LICENSING'] = True
# Use the auto_auth workflow for creating users and logging them in
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
########################### Entrance Exams #################################
FEATURES['MILESTONES_APP'] = True
FEATURES['ENTRANCE_EXAMS'] = True

View File

@@ -1966,6 +1966,9 @@ INSTALLED_APPS = (
# programs support
'openedx.core.djangoapps.programs',
# Self-paced course configuration
'openedx.core.djangoapps.self_paced',
)
######################### CSRF #########################################

View File

@@ -11,6 +11,9 @@ import django.contrib.auth.views
from microsite_configuration import microsite
import auth_exchange.views
from config_models.views import ConfigurationModelCurrentAPIView
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
# Uncomment the next two lines to enable the admin:
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
admin.autodiscover()
@@ -739,6 +742,10 @@ if settings.FEATURES.get("ENABLE_LTI_PROVIDER"):
url(r'^lti_provider/', include('lti_provider.urls')),
)
urlpatterns += (
url(r'config/self_paced', ConfigurationModelCurrentAPIView.as_view(model=SelfPacedConfiguration)),
)
urlpatterns = patterns(*urlpatterns)
if settings.DEBUG: