Merge pull request #10404 from edx/feature/self-paced
Enable self-paced courses.
This commit is contained in:
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
28
lms/djangoapps/courseware/self_paced_overrides.py
Normal file
28
lms/djangoapps/courseware/self_paced_overrides.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
56
lms/djangoapps/courseware/tests/test_self_paced_overrides.py
Normal file
56
lms/djangoapps/courseware/tests/test_self_paced_overrides.py
Normal 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)
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1966,6 +1966,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# programs support
|
||||
'openedx.core.djangoapps.programs',
|
||||
|
||||
# Self-paced course configuration
|
||||
'openedx.core.djangoapps.self_paced',
|
||||
)
|
||||
|
||||
######################### CSRF #########################################
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user