Commit 7c59a3af by Adam

Merge pull request #7964 from edx/hotfix/2015-05-07

prevent overly nesting _field_data (TNL-2050)
parents 0866f056 2842029a
...@@ -214,13 +214,18 @@ def _load_preview_module(request, descriptor): ...@@ -214,13 +214,18 @@ def _load_preview_module(request, descriptor):
""" """
student_data = KvsFieldData(SessionKeyValueStore(request)) student_data = KvsFieldData(SessionKeyValueStore(request))
if _has_author_view(descriptor): if _has_author_view(descriptor):
field_data = CmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access wrapper = partial(CmsFieldData, student_data=student_data)
else: else:
field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access wrapper = partial(LmsFieldData, student_data=student_data)
# wrap the _field_data upfront to pass to _preview_module_system
wrapped_field_data = wrapper(descriptor._field_data) # pylint: disable=protected-access
preview_runtime = _preview_module_system(request, descriptor, wrapped_field_data)
descriptor.bind_for_student( descriptor.bind_for_student(
_preview_module_system(request, descriptor, field_data), preview_runtime,
field_data, request.user.id,
request.user.id [wrapper]
) )
return descriptor return descriptor
......
...@@ -427,3 +427,20 @@ class ExportTestCase(CourseTestCase): ...@@ -427,3 +427,20 @@ class ExportTestCase(CourseTestCase):
self.assertEqual(video_xml.get('youtube_id_1_0'), youtube_id) self.assertEqual(video_xml.get('youtube_id_1_0'), youtube_id)
finally: finally:
shutil.rmtree(root_dir / name) shutil.rmtree(root_dir / name)
def test_export_success_with_custom_tag(self):
"""
Verify that course export with customtag
"""
xml_string = '<impl>slides</impl>'
vertical = ItemFactory.create(
parent_location=self.course.location, category='vertical', display_name='foo'
)
ItemFactory.create(
parent_location=vertical.location,
category='customtag',
display_name='custom_tag_foo',
data=xml_string
)
self.test_export_targz_urlparam()
...@@ -19,6 +19,7 @@ class StubYouTubeServiceTest(unittest.TestCase): ...@@ -19,6 +19,7 @@ class StubYouTubeServiceTest(unittest.TestCase):
response = requests.get(self.url + 'unused_url') response = requests.get(self.url + 'unused_url')
self.assertEqual("Unused url", response.content) self.assertEqual("Unused url", response.content)
@unittest.skip('Failing intermittently due to inconsistent responses from YT. See TE-871')
def test_video_url(self): def test_video_url(self):
response = requests.get( response = requests.get(
self.url + 'test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func' self.url + 'test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func'
......
...@@ -15,6 +15,7 @@ from django.test import TestCase ...@@ -15,6 +15,7 @@ from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error
from xmodule.contentstore.django import _CONTENTSTORE from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore, clear_existing_modulestores from xmodule.modulestore.django import modulestore, clear_existing_modulestores
...@@ -261,6 +262,11 @@ class ModuleStoreTestCase(TestCase): ...@@ -261,6 +262,11 @@ class ModuleStoreTestCase(TestCase):
self.addCleanup(XMODULE_FACTORY_LOCK.disable) self.addCleanup(XMODULE_FACTORY_LOCK.disable)
XMODULE_FACTORY_LOCK.enable() XMODULE_FACTORY_LOCK.enable()
# When testing CCX, we should make sure that
# OverrideFieldData.provider_classes is always reset to `None` so
# that they're recalculated for every test
OverrideFieldData.provider_classes = None
super(ModuleStoreTestCase, self).setUp() super(ModuleStoreTestCase, self).setUp()
self.store = modulestore() self.store = modulestore()
......
...@@ -14,6 +14,7 @@ from importlib import import_module ...@@ -14,6 +14,7 @@ from importlib import import_module
from lxml import etree from lxml import etree
from path import path from path import path
from contextlib import contextmanager from contextlib import contextmanager
from lazy import lazy
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import make_error_tracker, exc_info_to_str from xmodule.errortracker import make_error_tracker, exc_info_to_str
...@@ -933,6 +934,9 @@ class LibraryXMLModuleStore(XMLModuleStore): ...@@ -933,6 +934,9 @@ class LibraryXMLModuleStore(XMLModuleStore):
here manually. here manually.
""" """
init_dict = {key: getattr(library_descriptor, key) for key in library_descriptor.fields.keys()} init_dict = {key: getattr(library_descriptor, key) for key in library_descriptor.fields.keys()}
# if set, invalidate '_unwrapped_field_data' so it will be reset
# the next time it will be called
lazy.invalidate(library_descriptor, '_unwrapped_field_data')
# pylint: disable=protected-access # pylint: disable=protected-access
library_descriptor._field_data = inheriting_field_data(InheritanceKeyValueStore(init_dict)) library_descriptor._field_data = inheriting_field_data(InheritanceKeyValueStore(init_dict))
......
...@@ -126,7 +126,7 @@ def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')): ...@@ -126,7 +126,7 @@ def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
# So, bind to the same one as the current descriptor. # So, bind to the same one as the current descriptor.
module_system.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access module_system.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access
descriptor.bind_for_student(module_system, descriptor._field_data, user.id) descriptor.bind_for_student(module_system, user.id)
return descriptor return descriptor
......
...@@ -281,7 +281,13 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -281,7 +281,13 @@ class ImportTestCase(BaseCourseTestCase):
due=from_date_string, org=ORG, course=COURSE, url_name=url_name, unicorn_color=unicorn_color due=from_date_string, org=ORG, course=COURSE, url_name=url_name, unicorn_color=unicorn_color
) )
descriptor = system.process_xml(start_xml) descriptor = system.process_xml(start_xml)
# pylint: disable=protected-access
original_unwrapped = descriptor._unwrapped_field_data
LibraryXMLModuleStore.patch_descriptor_kvs(descriptor) LibraryXMLModuleStore.patch_descriptor_kvs(descriptor)
# '_unwrapped_field_data' is reset in `patch_descriptor_kvs`
# pylint: disable=protected-access
self.assertIsNot(original_unwrapped, descriptor._unwrapped_field_data)
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
# Check the course module, since it has inheritance # Check the course module, since it has inheritance
descriptor = descriptor.get_children()[0] descriptor = descriptor.get_children()[0]
......
...@@ -60,7 +60,7 @@ class LibraryContentTest(MixedSplitTestCase): ...@@ -60,7 +60,7 @@ class LibraryContentTest(MixedSplitTestCase):
sub_module_system = get_test_system(course_id=module.location.course_key) sub_module_system = get_test_system(course_id=module.location.course_key)
sub_module_system.get_module = get_module sub_module_system.get_module = get_module
sub_module_system.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access sub_module_system.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access
descriptor.bind_for_student(sub_module_system, descriptor._field_data, self.user_id) # pylint: disable=protected-access descriptor.bind_for_student(sub_module_system, self.user_id)
return descriptor return descriptor
module_system.get_module = get_module module_system.get_module = get_module
......
...@@ -100,7 +100,6 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase): ...@@ -100,7 +100,6 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase):
self.split_test_module = self.course_sequence.get_children()[0] self.split_test_module = self.course_sequence.get_children()[0]
self.split_test_module.bind_for_student( self.split_test_module.bind_for_student(
self.module_system, self.module_system,
self.split_test_module._field_data, # pylint: disable=protected-access
user.id user.id
) )
......
...@@ -16,6 +16,7 @@ from pkg_resources import ( ...@@ -16,6 +16,7 @@ from pkg_resources import (
) )
from webob import Response from webob import Response
from webob.multidict import MultiDict from webob.multidict import MultiDict
from lazy import lazy
from xblock.core import XBlock, XBlockAside from xblock.core import XBlock, XBlockAside
from xblock.fields import ( from xblock.fields import (
...@@ -361,6 +362,14 @@ class XModuleMixin(XModuleFields, XBlockMixin): ...@@ -361,6 +362,14 @@ class XModuleMixin(XModuleFields, XBlockMixin):
self.save() self.save()
return self._field_data._kvs # pylint: disable=protected-access return self._field_data._kvs # pylint: disable=protected-access
@lazy
def _unwrapped_field_data(self):
"""
This property hold the value _field_data here before we wrap it in
the LmsFieldData or OverrideFieldData classes.
"""
return self._field_data
def get_explicitly_set_fields_by_scope(self, scope=Scope.content): def get_explicitly_set_fields_by_scope(self, scope=Scope.content):
""" """
Get a dictionary of the fields for the given scope which are set explicitly on this xblock. (Including Get a dictionary of the fields for the given scope which are set explicitly on this xblock. (Including
...@@ -542,14 +551,17 @@ class XModuleMixin(XModuleFields, XBlockMixin): ...@@ -542,14 +551,17 @@ class XModuleMixin(XModuleFields, XBlockMixin):
""" """
return None return None
def bind_for_student(self, xmodule_runtime, field_data, user_id): def bind_for_student(self, xmodule_runtime, user_id, wrappers=None):
""" """
Set up this XBlock to act as an XModule instead of an XModuleDescriptor. Set up this XBlock to act as an XModule instead of an XModuleDescriptor.
Arguments: Arguments:
xmodule_runtime (:class:`ModuleSystem'): the runtime to use when accessing student facing methods xmodule_runtime (:class:`ModuleSystem'): the runtime to use when accessing student facing methods
field_data (:class:`FieldData`): The :class:`FieldData` to use for all subsequent data access
user_id: The user_id to set in scope_ids user_id: The user_id to set in scope_ids
wrappers: These are a list functions that put a wrapper, such as
LmsFieldData or OverrideFieldData, around the field_data.
Note that the functions will be applied in the order in
which they're listed. So [f1, f2] -> f2(f1(field_data))
""" """
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
...@@ -578,7 +590,15 @@ class XModuleMixin(XModuleFields, XBlockMixin): ...@@ -578,7 +590,15 @@ class XModuleMixin(XModuleFields, XBlockMixin):
# Set the new xmodule_runtime and field_data (which are user-specific) # Set the new xmodule_runtime and field_data (which are user-specific)
self.xmodule_runtime = xmodule_runtime self.xmodule_runtime = xmodule_runtime
self._field_data = field_data
if wrappers is None:
wrappers = []
wrapped_field_data = self._unwrapped_field_data
for wrapper in wrappers:
wrapped_field_data = wrapper(wrapped_field_data)
self._field_data = wrapped_field_data
@property @property
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
...@@ -1237,7 +1257,7 @@ class MetricsMixin(object): ...@@ -1237,7 +1257,7 @@ class MetricsMixin(object):
) )
class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylint: disable=abstract-method class DescriptorSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abstract-method
""" """
Base class for :class:`Runtime`s to be used with :class:`XModuleDescriptor`s Base class for :class:`Runtime`s to be used with :class:`XModuleDescriptor`s
""" """
...@@ -1483,7 +1503,7 @@ class XMLParsingSystem(DescriptorSystem): ...@@ -1483,7 +1503,7 @@ class XMLParsingSystem(DescriptorSystem):
setattr(xblock, field.name, field_value) setattr(xblock, field.name, field_value)
class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylint: disable=abstract-method class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abstract-method
""" """
This is an abstraction such that x_modules can function independent This is an abstraction such that x_modules can function independent
of the courseware (e.g. import into other types of courseware, LMS, of the courseware (e.g. import into other types of courseware, LMS,
......
...@@ -446,7 +446,7 @@ class XmlParserMixin(object): ...@@ -446,7 +446,7 @@ class XmlParserMixin(object):
node.tag = xml_object.tag node.tag = xml_object.tag
node.text = xml_object.text node.text = xml_object.text
node.tail = xml_object.tail node.tail = xml_object.tail
node.attrib = xml_object.attrib node.attrib.update(xml_object.attrib)
node.extend(xml_object) node.extend(xml_object)
node.set('url_name', self.url_name) node.set('url_name', self.url_name)
......
...@@ -3,6 +3,7 @@ Acceptance tests for Video Times(Start, End and Finish) functionality. ...@@ -3,6 +3,7 @@ Acceptance tests for Video Times(Start, End and Finish) functionality.
""" """
from flaky import flaky from flaky import flaky
from .test_video_module import VideoBaseTest from .test_video_module import VideoBaseTest
import unittest
class VideoTimesTest(VideoBaseTest): class VideoTimesTest(VideoBaseTest):
...@@ -105,6 +106,7 @@ class VideoTimesTest(VideoBaseTest): ...@@ -105,6 +106,7 @@ class VideoTimesTest(VideoBaseTest):
self.assertIn(self.video.position, ('0:15', '0:16')) self.assertIn(self.video.position, ('0:15', '0:16'))
@unittest.skip('This is actually a bug! See TNL-1619')
def test_video_end_time_and_finish_time(self): def test_video_end_time_and_finish_time(self):
""" """
Scenario: Youtube video works after pausing at end time and then plays again from End Time to the end. Scenario: Youtube video works after pausing at end time and then plays again from End Time to the end.
......
...@@ -497,15 +497,16 @@ def get_module_system_for_user(user, field_data_cache, ...@@ -497,15 +497,16 @@ def get_module_system_for_user(user, field_data_cache,
user_location=user_location, user_location=user_location,
request_token=request_token request_token=request_token
) )
# rebinds module to a different student. We'll change system, student_data, and scope_ids
authored_data = OverrideFieldData.wrap(
real_user, module.descriptor._field_data # pylint: disable=protected-access
)
module.descriptor.bind_for_student( module.descriptor.bind_for_student(
inner_system, inner_system,
LmsFieldData(authored_data, inner_student_data),
real_user.id, real_user.id,
[
partial(OverrideFieldData.wrap, real_user),
partial(LmsFieldData, student_data=inner_student_data),
],
) )
module.descriptor.scope_ids = ( module.descriptor.scope_ids = (
module.descriptor.scope_ids._replace(user_id=real_user.id) # pylint: disable=protected-access module.descriptor.scope_ids._replace(user_id=real_user.id) # pylint: disable=protected-access
) )
...@@ -692,8 +693,15 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours ...@@ -692,8 +693,15 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
request_token=request_token request_token=request_token
) )
authored_data = OverrideFieldData.wrap(user, descriptor._field_data) # pylint: disable=protected-access descriptor.bind_for_student(
descriptor.bind_for_student(system, LmsFieldData(authored_data, student_data), user.id) system,
user.id,
[
partial(OverrideFieldData.wrap, user),
partial(LmsFieldData, student_data=student_data),
],
)
descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) # pylint: disable=protected-access descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) # pylint: disable=protected-access
# Do not check access when it's a noauth request. # Do not check access when it's a noauth request.
......
...@@ -12,6 +12,7 @@ from django.http import Http404, HttpResponse ...@@ -12,6 +12,7 @@ from django.http import Http404, HttpResponse
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from mock import MagicMock, patch, Mock from mock import MagicMock, patch, Mock
from opaque_keys.edx.keys import UsageKey, CourseKey from opaque_keys.edx.keys import UsageKey, CourseKey
...@@ -27,6 +28,7 @@ from xblock.fragment import Fragment ...@@ -27,6 +28,7 @@ from xblock.fragment import Fragment
from capa.tests.response_xml_factory import OptionResponseXMLFactory from capa.tests.response_xml_factory import OptionResponseXMLFactory
from courseware import module_render as render from courseware import module_render as render
from courseware.courses import get_course_with_access, course_image_url, get_course_info_section from courseware.courses import get_course_with_access, course_image_url, get_course_info_section
from courseware.field_overrides import OverrideFieldData
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.module_render import hash_resource, get_module_for_descriptor from courseware.module_render import hash_resource, get_module_for_descriptor
from courseware.models import StudentModule from courseware.models import StudentModule
...@@ -34,6 +36,7 @@ from courseware.tests.factories import StudentModuleFactory, UserFactory, Global ...@@ -34,6 +36,7 @@ from courseware.tests.factories import StudentModuleFactory, UserFactory, Global
from courseware.tests.tests import LoginEnrollmentTestCase from courseware.tests.tests import LoginEnrollmentTestCase
from courseware.tests.test_submitting_problems import TestSubmittingProblems from courseware.tests.test_submitting_problems import TestSubmittingProblems
from lms.djangoapps.lms_xblock.runtime import quote_slashes from lms.djangoapps.lms_xblock.runtime import quote_slashes
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MIXED_TOY_MODULESTORE, TEST_DATA_MIXED_TOY_MODULESTORE,
...@@ -99,10 +102,15 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -99,10 +102,15 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.dispatch = 'score_update' self.dispatch = 'score_update'
# Construct a 'standard' xqueue_callback url # Construct a 'standard' xqueue_callback url
self.callback_url = reverse('xqueue_callback', kwargs=dict(course_id=self.course_key.to_deprecated_string(), self.callback_url = reverse(
userid=str(self.mock_user.id), 'xqueue_callback',
mod_id=self.mock_module.id, kwargs=dict(
dispatch=self.dispatch)) course_id=self.course_key.to_deprecated_string(),
userid=str(self.mock_user.id),
mod_id=self.mock_module.id,
dispatch=self.dispatch
)
)
def test_get_module(self): def test_get_module(self):
self.assertEqual( self.assertEqual(
...@@ -249,6 +257,63 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -249,6 +257,63 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id) render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id)
render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id) render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id)
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'ccx.overrides.CustomCoursesForEdxOverrideProvider',))
def test_rebind_different_users_ccx(self):
"""
This tests the rebinding a descriptor to a student does not result
in overly nested _field_data when CCX is enabled.
"""
request = self.request_factory.get('')
request.user = self.mock_user
course = CourseFactory()
descriptor = ItemFactory(category='html', parent=course)
field_data_cache = FieldDataCache(
[self.toy_course, descriptor], self.toy_course.id, self.mock_user
)
# grab what _field_data was originally set to
original_field_data = descriptor._field_data # pylint: disable=protected-access, no-member
render.get_module_for_descriptor(
self.mock_user, request, descriptor, field_data_cache, self.toy_course.id
)
# check that _unwrapped_field_data is the same as the original
# _field_data, but now _field_data as been reset.
# pylint: disable=protected-access, no-member
self.assertIs(descriptor._unwrapped_field_data, original_field_data)
self.assertIsNot(descriptor._unwrapped_field_data, descriptor._field_data)
# now bind this module to a few other students
for user in [UserFactory(), UserFactory(), UserFactory()]:
render.get_module_for_descriptor(
user,
request,
descriptor,
field_data_cache,
self.toy_course.id
)
# _field_data should now be wrapped by LmsFieldData
# pylint: disable=protected-access, no-member
self.assertIsInstance(descriptor._field_data, LmsFieldData)
# the LmsFieldData should now wrap OverrideFieldData
self.assertIsInstance(
# pylint: disable=protected-access, no-member
descriptor._field_data._authored_data._source,
OverrideFieldData
)
# the OverrideFieldData should point to the original unwrapped field_data
self.assertIs(
# pylint: disable=protected-access, no-member
descriptor._field_data._authored_data._source.fallback,
descriptor._unwrapped_field_data
)
def test_hash_resource(self): def test_hash_resource(self):
""" """
Ensure that the resource hasher works and does not fail on unicode, Ensure that the resource hasher works and does not fail on unicode,
......
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