Commit 8e4ee78f by Calen Pennington

Merge pull request #7262 from edx/cale/xblock-filtered-children-tests

Clear children cache when binding an XBlock to a user.
parents 9b49059e 60bbfc5a
......@@ -213,7 +213,8 @@ def _load_preview_module(request, descriptor):
field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access
descriptor.bind_for_student(
_preview_module_system(request, descriptor, field_data),
field_data
field_data,
request.user.id
)
return descriptor
......
......@@ -1211,23 +1211,23 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
for module_descriptor in yield_descriptor_descendents(child):
yield module_descriptor
for c in self.get_children():
for s in c.get_children():
if s.graded:
xmoduledescriptors = list(yield_descriptor_descendents(s))
xmoduledescriptors.append(s)
for chapter in self.get_children():
for section in chapter.get_children():
if section.graded:
xmoduledescriptors = list(yield_descriptor_descendents(section))
xmoduledescriptors.append(section)
# The xmoduledescriptors included here are only the ones that have scores.
section_description = {
'section_descriptor': s,
'xmoduledescriptors': filter(lambda child: child.has_score, xmoduledescriptors)
'section_descriptor': section,
'xmoduledescriptors': [child for child in xmoduledescriptors if child.has_score]
}
section_format = s.format if s.format is not None else ''
section_format = section.format if section.format is not None else ''
graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description]
all_descriptors.extend(xmoduledescriptors)
all_descriptors.append(s)
all_descriptors.append(section)
return {'graded_sections': graded_sections,
'all_descriptors': all_descriptors, }
......
......@@ -693,7 +693,7 @@ class ModuleStoreRead(ModuleStoreAssetBase):
pass
@abstractmethod
def get_item(self, usage_key, depth=0, **kwargs):
def get_item(self, usage_key, depth=0, using_descriptor_system=None, **kwargs):
"""
Returns an XModuleDescriptor instance for the item at location.
......
......@@ -151,12 +151,28 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
else:
return False
def __repr__(self):
return "MongoKeyValueStore{!r}<{!r}, {!r}>".format(
(self._data, self._parent, self._children, self._metadata),
self._fields,
self.inherited_settings
)
class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
"""
A system that has a cache of module json that it will use to load modules
from, with a backup of calling to the underlying modulestore for more data
"""
def __repr__(self):
return "CachingDescriptorSystem{!r}".format((
self.modulestore,
unicode(self.course_id),
[unicode(key) for key in self.module_data.keys()],
self.default_class,
[unicode(key) for key in self.cached_metadata.keys()],
))
def __init__(self, modulestore, course_key, module_data, default_class, cached_metadata, **kwargs):
"""
modulestore: the module store that can be used to retrieve additional modules
......@@ -202,10 +218,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
assert isinstance(location, UsageKey)
json_data = self.module_data.get(location)
if json_data is None:
module = self.modulestore.get_item(location)
if module is not None:
# update our own cache after going to the DB to get cache miss
self.module_data.update(module.runtime.module_data)
module = self.modulestore.get_item(location, using_descriptor_system=self)
return module
else:
# load the module and apply the inherited metadata
......@@ -387,6 +400,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
return []
new_contract('CachingDescriptorSystem', CachingDescriptorSystem)
# The only thing using this w/ wildcards is contentstore.mongo for asset retrieval
def location_to_query(location, wildcard=True, tag='i4x'):
"""
......@@ -839,9 +855,27 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
return data
def _load_item(self, course_key, item, data_cache, apply_cached_metadata=True):
@contract(
course_key=CourseKey,
item=dict,
apply_cached_metadata=bool,
using_descriptor_system="None|CachingDescriptorSystem"
)
def _load_item(self, course_key, item, data_cache, apply_cached_metadata=True, using_descriptor_system=None):
"""
Load an XModuleDescriptor from item, using the children stored in data_cache
Arguments:
course_key (CourseKey): which course to load from
item (dict): A dictionary with the following keys:
location: The serialized UsageKey for the item to load
data_dir (optional): The directory name to use as the root data directory for this XModule
data_cache (dict): A dictionary mapping from UsageKeys to xblock field data
(this is the xblock data loaded from the database)
apply_cached_metadata (bool): Whether to use the cached metadata for inheritance
purposes.
using_descriptor_system (CachingDescriptorSystem): The existing CachingDescriptorSystem
to add data to, and to load the XBlocks from.
"""
course_key = self.fill_in_run(course_key)
location = Location._from_deprecated_son(item['location'], course_key.run)
......@@ -853,32 +887,38 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
if apply_cached_metadata:
cached_metadata = self._get_cached_metadata_inheritance_tree(course_key)
services = {}
if self.i18n_service:
services["i18n"] = self.i18n_service
if self.fs_service:
services["fs"] = self.fs_service
if self.user_service:
services["user"] = self.user_service
system = CachingDescriptorSystem(
modulestore=self,
course_key=course_key,
module_data=data_cache,
default_class=self.default_class,
resources_fs=resource_fs,
error_tracker=self.error_tracker,
render_template=self.render_template,
cached_metadata=cached_metadata,
mixins=self.xblock_mixins,
select=self.xblock_select,
services=services,
)
if using_descriptor_system is None:
services = {}
if self.i18n_service:
services["i18n"] = self.i18n_service
if self.fs_service:
services["fs"] = self.fs_service
if self.user_service:
services["user"] = self.user_service
system = CachingDescriptorSystem(
modulestore=self,
course_key=course_key,
module_data=data_cache,
default_class=self.default_class,
resources_fs=resource_fs,
error_tracker=self.error_tracker,
render_template=self.render_template,
cached_metadata=cached_metadata,
mixins=self.xblock_mixins,
select=self.xblock_select,
services=services,
)
else:
system = using_descriptor_system
system.module_data.update(data_cache)
system.cached_metadata.update(cached_metadata)
return system.load_item(location)
def _load_items(self, course_key, items, depth=0):
def _load_items(self, course_key, items, depth=0, using_descriptor_system=None):
"""
Load a list of xmodules from the data in items, with children cached up
to specified depth
......@@ -890,8 +930,11 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
# bother with the metadata inheritance
return [
self._load_item(
course_key, item, data_cache,
apply_cached_metadata=(item['location']['category'] != 'course' or depth != 0)
course_key,
item,
data_cache,
apply_cached_metadata=(item['location']['category'] != 'course' or depth != 0),
using_descriptor_system=using_descriptor_system
)
for item in items
]
......@@ -990,7 +1033,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
except ItemNotFoundError:
return False
def get_item(self, usage_key, depth=0):
def get_item(self, usage_key, depth=0, using_descriptor_system=None):
"""
Returns an XModuleDescriptor instance for the item at location.
......@@ -999,14 +1042,22 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
If no object is found at that location, raises
xmodule.modulestore.exceptions.ItemNotFoundError
usage_key: a :class:`.UsageKey` instance
depth (int): An argument that some module stores may use to prefetch
descendents of the queried modules for more efficient results later
in the request. The depth is counted in the number of
calls to get_children() to cache. None indicates to cache all descendents.
Arguments:
usage_key: a :class:`.UsageKey` instance
depth (int): An argument that some module stores may use to prefetch
descendents of the queried modules for more efficient results later
in the request. The depth is counted in the number of
calls to get_children() to cache. None indicates to cache all descendents.
using_descriptor_system (CachingDescriptorSystem): The existing CachingDescriptorSystem
to add data to, and to load the XBlocks from.
"""
item = self._find_one(usage_key)
module = self._load_items(usage_key.course_key, [item], depth)[0]
module = self._load_items(
usage_key.course_key,
[item],
depth,
using_descriptor_system=using_descriptor_system
)[0]
return module
@staticmethod
......@@ -1038,6 +1089,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
content=None,
key_revision=MongoRevisionKey.published,
qualifiers=None,
using_descriptor_system=None,
**kwargs
):
"""
......@@ -1069,6 +1121,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
For this modulestore, ``name`` is a commonly provided key (Location based stores)
This modulestore does not allow searching dates by comparison or edited_by, previous_version,
update_version info.
using_descriptor_system (CachingDescriptorSystem): The existing CachingDescriptorSystem
to add data to, and to load the XBlocks from.
"""
qualifiers = qualifiers.copy() if qualifiers else {} # copy the qualifiers (destructively manipulated here)
query = self._course_key_to_son(course_id)
......@@ -1090,7 +1144,11 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
sort=[SORT_REVISION_FAVOR_DRAFT],
)
modules = self._load_items(course_id, list(items))
modules = self._load_items(
course_id,
list(items),
using_descriptor_system=using_descriptor_system
)
return modules
def create_course(self, org, course, run, user_id, fields=None, **kwargs):
......
......@@ -110,7 +110,7 @@ def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
# So, bind to the same one as the current descriptor.
module_system.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access
descriptor.bind_for_student(module_system, descriptor._field_data)
descriptor.bind_for_student(module_system, descriptor._field_data, user.id)
return descriptor
......
......@@ -206,6 +206,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
location = descriptor
descriptor = self.modulestore.get_item(location, depth=None)
descriptor.xmodule_runtime = get_test_system()
descriptor.xmodule_runtime.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access
descriptor.xmodule_runtime.get_module = inner_get_module
return descriptor
......
......@@ -58,7 +58,7 @@ class LibraryContentTest(MixedSplitTestCase):
sub_module_system = get_test_system(course_id=self.course.location.course_key)
sub_module_system.get_module = get_module
sub_module_system.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access
descriptor.bind_for_student(sub_module_system, descriptor._field_data) # pylint: disable=protected-access
descriptor.bind_for_student(sub_module_system, descriptor._field_data, self.user_id) # pylint: disable=protected-access
return descriptor
module_system.get_module = get_module
......
......@@ -81,6 +81,7 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase):
self.module_system.descriptor_runtime = self.course._runtime # pylint: disable=protected-access
self.course.runtime.export_fs = MemoryFS()
user = Mock(username='ma', email='ma@edx.org', is_staff=False, is_active=True)
self.partitions_service = StaticPartitionService(
[
self.user_partition,
......@@ -90,14 +91,18 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase):
MockUserPartitionScheme()
)
],
user=Mock(username='ma', email='ma@edx.org', is_staff=False, is_active=True),
user=user,
course_id=self.course.id,
track_function=Mock(name='track_function'),
)
self.module_system._services['partitions'] = self.partitions_service # pylint: disable=protected-access
self.split_test_module = self.course_sequence.get_children()[0]
self.split_test_module.bind_for_student(self.module_system, self.split_test_module._field_data) # pylint: disable=protected-access
self.split_test_module.bind_for_student(
self.module_system,
self.split_test_module._field_data, # pylint: disable=protected-access
user.id
)
@ddt.ddt
......
......@@ -17,8 +17,11 @@ from webob import Response
from webob.multidict import MultiDict
from xblock.core import XBlock, XBlockAside
from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String, Dict, ScopeIds, Reference, \
ReferenceList, ReferenceValueDict
from xblock.fields import (
Scope, Integer, Float, List, XBlockMixin,
String, Dict, ScopeIds, Reference, ReferenceList,
ReferenceValueDict, UserScope
)
from xblock.fragment import Fragment
from xblock.runtime import Runtime, IdReader, IdGenerator
from xmodule.fields import RelativeTime
......@@ -281,6 +284,8 @@ class XModuleMixin(XBlockMixin):
def __init__(self, *args, **kwargs):
self.xmodule_runtime = None
self._child_instances = None
super(XModuleMixin, self).__init__(*args, **kwargs)
@property
......@@ -356,7 +361,7 @@ class XModuleMixin(XBlockMixin):
return result
def has_children_at_depth(self, depth):
"""
r"""
Returns true if self has children at the given depth. depth==0 returns
false if self is a leaf, true otherwise.
......@@ -379,7 +384,7 @@ class XModuleMixin(XBlockMixin):
return any(child.has_children_at_depth(depth - 1) for child in self.get_children())
def get_content_titles(self):
"""
r"""
Returns list of content titles for all of self's children.
SEQUENCE
......@@ -410,7 +415,7 @@ class XModuleMixin(XBlockMixin):
if not self.has_children:
return []
if getattr(self, '_child_instances', None) is None:
if self._child_instances is None:
self._child_instances = [] # pylint: disable=attribute-defined-outside-init
for child_loc in self.children:
# Skip if it doesn't satisfy the filter function
......@@ -525,15 +530,39 @@ class XModuleMixin(XBlockMixin):
"""
return None
def bind_for_student(self, xmodule_runtime, field_data):
def bind_for_student(self, xmodule_runtime, field_data, user_id):
"""
Set up this XBlock to act as an XModule instead of an XModuleDescriptor.
Arguments:
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
"""
# pylint: disable=attribute-defined-outside-init
# Skip rebinding if we're already bound a user, and it's this user.
if self.scope_ids.user_id is not None and user_id == self.scope_ids.user_id:
return
# If we are switching users mid-request, save the data from the old user.
self.save()
# Update scope_ids to point to the new user.
self.scope_ids = self.scope_ids._replace(user_id=user_id)
# Clear out any cached instantiated children.
self._child_instances = None
# Clear out any cached field data scoped to the old user.
for field in self.fields.values():
if field.scope in (Scope.parent, Scope.children):
continue
if field.scope.user == UserScope.ONE:
field._del_cached_value(self) # pylint: disable=protected-access
# Set the new xmodule_runtime and field_data (which are user-specific)
self.xmodule_runtime = xmodule_runtime
self._field_data = field_data
......@@ -615,10 +644,19 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me
# Set the descriptor first so that we can proxy to it
self.descriptor = descriptor
super(XModule, self).__init__(*args, **kwargs)
self._loaded_children = None
self._runtime = None
super(XModule, self).__init__(*args, **kwargs)
self.runtime.xmodule_instance = self
@property
def runtime(self):
return CombinedSystem(self._runtime, self.descriptor._runtime) # pylint: disable=protected-access
@runtime.setter
def runtime(self, value): # pylint: disable=arguments-differ
self._runtime = value
def __unicode__(self):
return u'<x_module(id={0})>'.format(self.id)
......@@ -659,26 +697,6 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me
response_data = self.handle_ajax(suffix, request_post)
return Response(response_data, content_type='application/json')
def get_children(self):
"""
Return module instances for all the children of this module.
"""
if self._loaded_children is None:
child_descriptors = self.get_child_descriptors()
# This deliberately uses system.get_module, rather than runtime.get_block,
# because we're looking at XModule children, rather than XModuleDescriptor children.
# That means it can use the deprecated XModule apis, rather than future XBlock apis
# TODO: Once we're in a system where this returns a mix of XModuleDescriptors
# and XBlocks, we're likely to have to change this more
children = [self.system.get_module(descriptor) for descriptor in child_descriptors]
# get_module returns None if the current user doesn't have access
# to the location.
self._loaded_children = [c for c in children if c is not None]
return self._loaded_children
def get_child_descriptors(self):
"""
Returns the descriptors of the child modules
......@@ -1567,8 +1585,13 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
"""provide uniform access to attributes (like etree)"""
self.__dict__[attr] = val
def __str__(self):
return str(self.__dict__)
def __repr__(self):
kwargs = self.__dict__.copy()
# Remove value set transiently by XBlock
kwargs.pop('_view_name')
return "{}{}".format(self.__class__.__name__, kwargs)
@property
def ajax_url(self):
......
......@@ -62,7 +62,6 @@ class FieldDataCache(object):
asides: The list of aside types to load, or None to prefetch no asides.
'''
self.cache = {}
self.descriptors = descriptors
self.select_for_update = select_for_update
if asides is None:
......@@ -74,24 +73,27 @@ class FieldDataCache(object):
self.course_id = course_id
self.user = user
if user.is_authenticated():
for scope, fields in self._fields_to_cache().items():
for field_object in self._retrieve_fields(scope, fields):
self.add_descriptors_to_cache(descriptors)
def add_descriptors_to_cache(self, descriptors):
"""
Add all `descriptors` to this FieldDataCache.
"""
if self.user.is_authenticated():
for scope, fields in self._fields_to_cache(descriptors).items():
for field_object in self._retrieve_fields(scope, fields, descriptors):
self.cache[self._cache_key_from_field_object(scope, field_object)] = field_object
@classmethod
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
descriptor_filter=lambda descriptor: True,
select_for_update=False, asides=None):
def add_descriptor_descendents(self, descriptor, depth=None, descriptor_filter=lambda descriptor: True):
"""
course_id: the course in the context of which we want StudentModules.
user: the django user for whom to load modules.
descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached
select_for_update: Flag indicating whether the rows should be locked until end of transaction
Add all descendents of `descriptor` to this FieldDataCache.
Arguments:
descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached
"""
def get_child_descriptors(descriptor, depth, descriptor_filter):
......@@ -120,7 +122,25 @@ class FieldDataCache(object):
with modulestore().bulk_operations(descriptor.location.course_key):
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
return FieldDataCache(descriptors, course_id, user, select_for_update, asides=asides)
self.add_descriptors_to_cache(descriptors)
@classmethod
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
descriptor_filter=lambda descriptor: True,
select_for_update=False, asides=None):
"""
course_id: the course in the context of which we want StudentModules.
user: the django user for whom to load modules.
descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached
select_for_update: Flag indicating whether the rows should be locked until end of transaction
"""
cache = FieldDataCache([], course_id, user, select_for_update, asides=asides)
cache.add_descriptor_descendents(descriptor, depth, descriptor_filter)
return cache
def _query(self, model_class, **kwargs):
"""
......@@ -147,14 +167,13 @@ class FieldDataCache(object):
)
return res
@property
def _all_usage_ids(self):
def _all_usage_ids(self, descriptors):
"""
Return a set of all usage_ids for the descriptors that this FieldDataCache is caching
against, and well as all asides for those descriptors.
"""
usage_ids = set()
for descriptor in self.descriptors:
for descriptor in descriptors:
usage_ids.add(descriptor.scope_ids.usage_id)
for aside_type in self.asides:
......@@ -162,13 +181,12 @@ class FieldDataCache(object):
return usage_ids
@property
def _all_block_types(self):
def _all_block_types(self, descriptors):
"""
Return a set of all block_types that are cached by this FieldDataCache.
"""
block_types = set()
for descriptor in self.descriptors:
for descriptor in descriptors:
block_types.add(BlockTypeKeyV1(descriptor.entry_point, descriptor.scope_ids.block_type))
for aside_type in self.asides:
......@@ -176,7 +194,7 @@ class FieldDataCache(object):
return block_types
def _retrieve_fields(self, scope, fields):
def _retrieve_fields(self, scope, fields, descriptors):
"""
Queries the database for all of the fields in the specified scope
"""
......@@ -184,7 +202,7 @@ class FieldDataCache(object):
return self._chunked_query(
StudentModule,
'module_state_key__in',
self._all_usage_ids,
self._all_usage_ids(descriptors),
course_id=self.course_id,
student=self.user.pk,
)
......@@ -192,14 +210,14 @@ class FieldDataCache(object):
return self._chunked_query(
XModuleUserStateSummaryField,
'usage_id__in',
self._all_usage_ids,
self._all_usage_ids(descriptors),
field_name__in=set(field.name for field in fields),
)
elif scope == Scope.preferences:
return self._chunked_query(
XModuleStudentPrefsField,
'module_type__in',
self._all_block_types,
self._all_block_types(descriptors),
student=self.user.pk,
field_name__in=set(field.name for field in fields),
)
......@@ -212,12 +230,12 @@ class FieldDataCache(object):
else:
return []
def _fields_to_cache(self):
def _fields_to_cache(self, descriptors):
"""
Returns a map of scopes to fields in that scope that should be cached
"""
scope_map = defaultdict(set)
for descriptor in self.descriptors:
for descriptor in descriptors:
for field in descriptor.fields.values():
scope_map[field.scope].add(field)
return scope_map
......
......@@ -497,10 +497,8 @@ def get_module_system_for_user(user, field_data_cache,
# rebinds module to a different student. We'll change system, student_data, and scope_ids
module.descriptor.bind_for_student(
inner_system,
LmsFieldData(module.descriptor._field_data, inner_student_data) # pylint: disable=protected-access
)
module.descriptor.scope_ids = (
module.descriptor.scope_ids._replace(user_id=real_user.id) # pylint: disable=protected-access
LmsFieldData(module.descriptor._field_data, inner_student_data), # pylint: disable=protected-access
real_user.id,
)
module.scope_ids = module.descriptor.scope_ids # this is needed b/c NamedTuples are immutable
# now bind the module to the new ModuleSystem instance and vice-versa
......@@ -688,8 +686,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
request_token=request_token
)
descriptor.bind_for_student(system, field_data) # pylint: disable=protected-access
descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) # pylint: disable=protected-access
descriptor.bind_for_student(system, field_data, user.id) # pylint: disable=protected-access
return descriptor
......
......@@ -2,25 +2,32 @@
"""
Tests for course access
"""
import ddt
import itertools
import mock
from django.conf import settings
from django.test.utils import override_settings
import mock
from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import (
get_course_by_id, get_cms_course_link, course_image_url,
get_course_info_section, get_course_about_section, get_cms_block_link
)
from courseware.module_render import get_module_for_descriptor
from courseware.tests.helpers import get_request_for_user
from courseware.model_data import FieldDataCache
from student.tests.factories import UserFactory
import xmodule.modulestore.django as store_django
from xmodule.modulestore.django import _get_modulestore_branch_setting, modulestore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MOCK_MODULESTORE, TEST_DATA_MIXED_TOY_MODULESTORE
)
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.tests.xml import factories as xml
from xmodule.tests.xml import XModuleXmlImportTest
......@@ -60,7 +67,7 @@ class ModuleStoreBranchSettingTest(ModuleStoreTestCase):
MODULESTORE_BRANCH='fake_default_branch',
)
def test_default_modulestore_preview_mapping(self):
self.assertEqual(store_django._get_modulestore_branch_setting(), ModuleStoreEnum.Branch.draft_preferred)
self.assertEqual(_get_modulestore_branch_setting(), ModuleStoreEnum.Branch.draft_preferred)
@mock.patch(
'xmodule.modulestore.django.get_current_request_hostname',
......@@ -71,7 +78,7 @@ class ModuleStoreBranchSettingTest(ModuleStoreTestCase):
MODULESTORE_BRANCH='fake_default_branch',
)
def test_default_modulestore_branch_mapping(self):
self.assertEqual(store_django._get_modulestore_branch_setting(), 'fake_default_branch')
self.assertEqual(_get_modulestore_branch_setting(), 'fake_default_branch')
@override_settings(
......@@ -159,7 +166,7 @@ class CoursesRenderTest(ModuleStoreTestCase):
"""
super(CoursesRenderTest, self).setUp()
store = store_django.modulestore()
store = modulestore()
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['toy'])
course_key = course_items[0].id
self.course = get_course_by_id(course_key)
......@@ -216,3 +223,46 @@ class XmlCoursesRenderTest(ModuleStoreTestCase):
)
course_info = get_course_info_section(request, course, 'handouts')
self.assertIn("this module is temporarily unavailable", course_info)
@ddt.ddt
class CourseInstantiationTests(ModuleStoreTestCase):
"""
Tests around instantiating a course multiple times in the same request.
"""
def setUp(self):
super(CourseInstantiationTests, self).setUp()
self.factory = RequestFactory()
@ddt.data(*itertools.product(xrange(5), [ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split], [None, 0, 5]))
@ddt.unpack
def test_repeated_course_module_instantiation(self, loops, default_store, course_depth):
with modulestore().default_store(default_store):
course = CourseFactory.create()
chapter = ItemFactory(parent=course, category='chapter', graded=True)
section = ItemFactory(parent=chapter, category='sequential')
__ = ItemFactory(parent=section, category='problem')
fake_request = self.factory.get(
reverse('progress', kwargs={'course_id': unicode(course.id)})
)
course = modulestore().get_course(course.id, depth=course_depth)
for _ in xrange(loops):
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, self.user, course, depth=course_depth
)
course_module = get_module_for_descriptor(
self.user,
fake_request,
course,
field_data_cache,
course.id
)
for chapter in course_module.get_children():
for section in chapter.get_children():
for item in section.get_children():
self.assertTrue(item.graded)
......@@ -2,11 +2,12 @@
"""
Test for lms courseware app, module render unit
"""
from functools import partial
import ddt
import itertools
import json
from functools import partial
from bson import ObjectId
import ddt
from django.http import Http404, HttpResponse
from django.core.urlresolvers import reverse
from django.conf import settings
......@@ -15,7 +16,6 @@ from django.contrib.auth.models import AnonymousUser
from mock import MagicMock, patch, Mock
from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.module_render import hash_resource
from xblock.field_data import FieldData
from xblock.runtime import Runtime
from xblock.fields import ScopeIds
......@@ -25,18 +25,18 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory
from courseware import module_render as render
from courseware.courses import get_course_with_access, course_image_url, get_course_info_section
from courseware.model_data import FieldDataCache
from courseware.module_render import hash_resource, get_module_for_descriptor
from courseware.models import StudentModule
from courseware.tests.factories import StudentModuleFactory, UserFactory, GlobalStaffFactory
from courseware.tests.tests import LoginEnrollmentTestCase
from courseware.tests.test_submitting_problems import TestSubmittingProblems
from lms.djangoapps.lms_xblock.runtime import quote_slashes
from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MIXED_TOY_MODULESTORE,
TEST_DATA_XML_MODULESTORE,
)
from courseware.tests.test_submitting_problems import TestSubmittingProblems
from lms.djangoapps.lms_xblock.runtime import quote_slashes
from student.models import anonymous_id_for_user
from xmodule.lti_module import LTIDescriptor
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -456,11 +456,11 @@ class TestTOC(ModuleStoreTestCase):
# Split makes 6 queries to load the course to depth 2:
# - load the structure
# - load 5 definitions
# Split makes 2 queries to render the toc:
# Split makes 6 queries to render the toc:
# - it loads the active version at the start of the bulk operation
# - it loads the course definition for inheritance, because it's outside
# the bulk-operation marker that loaded the course descriptor
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 2))
# - it loads 5 definitions, because it instantiates the a CourseModule and 4 VideoModules
# each of which access a Scope.content field in __init__
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 6))
@ddt.unpack
def test_toc_toy_from_chapter(self, default_ms, setup_finds, setup_sends, toc_finds):
with self.store.default_store(default_ms):
......@@ -496,9 +496,9 @@ class TestTOC(ModuleStoreTestCase):
# - load 5 definitions
# Split makes 2 queries to render the toc:
# - it loads the active version at the start of the bulk operation
# - it loads the course definition for inheritance, because it's outside
# the bulk-operation marker that loaded the course descriptor
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 2))
# - it loads 5 definitions, because it instantiates the a CourseModule and 4 VideoModules
# each of which access a Scope.content field in __init__
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 6))
@ddt.unpack
def test_toc_toy_from_section(self, default_ms, setup_finds, setup_sends, toc_finds):
with self.store.default_store(default_ms):
......@@ -920,7 +920,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
location = course_id.make_usage_key('dummy_category', 'dummy_name')
descriptor = Mock(
spec=xblock_class,
_field_data=Mock(spec=FieldData),
_field_data=Mock(spec=FieldData, name='field_data'),
location=location,
static_asset_path=None,
_runtime=Mock(
......@@ -930,7 +930,10 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
name='runtime',
),
scope_ids=Mock(spec=ScopeIds),
name='descriptor'
name='descriptor',
_field_data_cache={},
_dirty_fields={},
fields={},
)
descriptor.runtime = CombinedSystem(descriptor._runtime, None) # pylint: disable=protected-access
# Use the xblock_class's bind_for_student method
......@@ -1241,3 +1244,178 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
)
service = runtime.service(descriptor, expected_service)
self.assertIsNotNone(service)
class PureXBlockWithChildren(PureXBlock):
"""
Pure XBlock with children to use in tests.
"""
has_children = True
class EmptyXModuleWithChildren(EmptyXModule): # pylint: disable=abstract-method
"""
Empty XModule for testing with no dependencies.
"""
has_children = True
class EmptyXModuleDescriptorWithChildren(EmptyXModuleDescriptor): # pylint: disable=abstract-method
"""
Empty XModule for testing with no dependencies.
"""
module_class = EmptyXModuleWithChildren
has_children = True
BLOCK_TYPES = ['xblock', 'xmodule']
USER_NUMBERS = range(2)
@ddt.ddt
class TestFilteredChildren(ModuleStoreTestCase):
"""
Tests that verify access to XBlock/XModule children work correctly
even when those children are filtered by the runtime when loaded.
"""
# pylint: disable=attribute-defined-outside-init, no-member
def setUp(self):
super(TestFilteredChildren, self).setUp()
self.users = {number: UserFactory() for number in USER_NUMBERS}
self.course = CourseFactory()
self._old_has_access = render.has_access
patcher = patch('courseware.module_render.has_access', self._has_access)
patcher.start()
self.addCleanup(patcher.stop)
@ddt.data(*BLOCK_TYPES)
@XBlock.register_temp_plugin(PureXBlockWithChildren, identifier='xblock')
@XBlock.register_temp_plugin(EmptyXModuleDescriptorWithChildren, identifier='xmodule')
def test_unbound(self, block_type):
block = self._load_block(block_type)
self.assertUnboundChildren(block)
@ddt.data(*itertools.product(BLOCK_TYPES, USER_NUMBERS))
@ddt.unpack
@XBlock.register_temp_plugin(PureXBlockWithChildren, identifier='xblock')
@XBlock.register_temp_plugin(EmptyXModuleDescriptorWithChildren, identifier='xmodule')
def test_unbound_then_bound_as_descriptor(self, block_type, user_number):
user = self.users[user_number]
block = self._load_block(block_type)
self.assertUnboundChildren(block)
self._bind_block(block, user)
self.assertBoundChildren(block, user)
@ddt.data(*itertools.product(BLOCK_TYPES, USER_NUMBERS))
@ddt.unpack
@XBlock.register_temp_plugin(PureXBlockWithChildren, identifier='xblock')
@XBlock.register_temp_plugin(EmptyXModuleDescriptorWithChildren, identifier='xmodule')
def test_unbound_then_bound_as_xmodule(self, block_type, user_number):
user = self.users[user_number]
block = self._load_block(block_type)
self.assertUnboundChildren(block)
self._bind_block(block, user)
# Validate direct XModule access as well
if isinstance(block, XModuleDescriptor):
self.assertBoundChildren(block._xmodule, user) # pylint: disable=protected-access
else:
self.assertBoundChildren(block, user)
@ddt.data(*itertools.product(BLOCK_TYPES, USER_NUMBERS))
@ddt.unpack
@XBlock.register_temp_plugin(PureXBlockWithChildren, identifier='xblock')
@XBlock.register_temp_plugin(EmptyXModuleDescriptorWithChildren, identifier='xmodule')
def test_bound_only_as_descriptor(self, block_type, user_number):
user = self.users[user_number]
block = self._load_block(block_type)
self._bind_block(block, user)
self.assertBoundChildren(block, user)
@ddt.data(*itertools.product(BLOCK_TYPES, USER_NUMBERS))
@ddt.unpack
@XBlock.register_temp_plugin(PureXBlockWithChildren, identifier='xblock')
@XBlock.register_temp_plugin(EmptyXModuleDescriptorWithChildren, identifier='xmodule')
def test_bound_only_as_xmodule(self, block_type, user_number):
user = self.users[user_number]
block = self._load_block(block_type)
self._bind_block(block, user)
# Validate direct XModule access as well
if isinstance(block, XModuleDescriptor):
self.assertBoundChildren(block._xmodule, user) # pylint: disable=protected-access
else:
self.assertBoundChildren(block, user)
def _load_block(self, block_type):
"""
Instantiate an XBlock of `block_type` with the appropriate set of children.
"""
self.parent = ItemFactory(category=block_type, parent=self.course)
# Create a child of each block type for each user
self.children_for_user = {
user: [
ItemFactory(category=child_type, parent=self.parent).scope_ids.usage_id
for child_type in BLOCK_TYPES
]
for user in self.users.itervalues()
}
self.all_children = sum(self.children_for_user.values(), [])
return modulestore().get_item(self.parent.scope_ids.usage_id)
def _bind_block(self, block, user):
"""
Bind `block` to the supplied `user`.
"""
course_id = self.course.id
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_id,
user,
block,
)
return get_module_for_descriptor(
user,
Mock(name='request', user=user),
block,
field_data_cache,
course_id,
)
def _has_access(self, user, action, obj, course_key=None):
"""
Mock implementation of `has_access` used to control which blocks
have access to which children during tests.
"""
if action != 'load':
return self._old_has_access(user, action, obj, course_key)
if isinstance(obj, XBlock):
key = obj.scope_ids.usage_id
elif isinstance(obj, UsageKey):
key = obj
if key == self.parent.scope_ids.usage_id:
return True
return key in self.children_for_user[user]
def assertBoundChildren(self, block, user):
"""
Ensure the bound children are indeed children.
"""
self.assertChildren(block, self.children_for_user[user])
def assertUnboundChildren(self, block):
"""
Ensure unbound children are indeed children.
"""
self.assertChildren(block, self.all_children)
def assertChildren(self, block, child_usage_ids):
"""
Used to assert that sets of children are equivalent.
"""
self.assertEquals(set(child_usage_ids), set(child.scope_ids.usage_id for child in block.get_children()))
......@@ -3,10 +3,10 @@
Tests courseware views.py
"""
import cgi
from datetime import datetime
from pytz import UTC
import unittest
import ddt
import json
import unittest
from datetime import datetime
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
......@@ -15,26 +15,31 @@ from django.http import Http404, HttpResponseBadRequest
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from certificates import api as certs_api
from certificates.models import CertificateStatuses, CertificateGenerationConfiguration
from certificates.tests.factories import GeneratedCertificateFactory
from edxmako.middleware import MakoMiddleware
from edxmako.tests import mako_middleware_process_request
from mock import MagicMock, patch, create_autospec, Mock
from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey
from pytz import UTC
from xblock.core import XBlock
from xblock.fields import String, Scope
from xblock.fragment import Fragment
import courseware.views as views
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE
from course_modes.models import CourseMode
import shoppingcart
from certificates import api as certs_api
from certificates.models import CertificateStatuses, CertificateGenerationConfiguration
from certificates.tests.factories import GeneratedCertificateFactory
from course_modes.models import CourseMode
from courseware.tests.factories import StudentModuleFactory
from edxmako.middleware import MakoMiddleware
from edxmako.tests import mako_middleware_process_request
from student.models import CourseEnrollment
from student.tests.factories import AdminFactory, UserFactory
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentFactory
from util.tests.test_date_utils import fake_ugettext, fake_pgettext
from util.views import ensure_valid_course_key
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
class TestJumpTo(ModuleStoreTestCase):
......@@ -858,3 +863,76 @@ class GenerateUserCertTests(ModuleStoreTestCase):
self.assertIn("You must be signed in to {platform_name} to create a certificate.".format(
platform_name=settings.PLATFORM_NAME
), resp.content)
class ViewCheckerBlock(XBlock):
"""
XBlock for testing user state in views.
"""
has_children = True
state = String(scope=Scope.user_state)
def student_view(self, context): # pylint: disable=unused-argument
"""
A student_view that asserts that the ``state`` field for this block
matches the block's usage_id.
"""
msg = "{} != {}".format(self.state, self.scope_ids.usage_id)
assert self.state == unicode(self.scope_ids.usage_id), msg
fragments = self.runtime.render_children(self)
result = Fragment(
content=u"<p>ViewCheckerPassed: {}</p>\n{}".format(
unicode(self.scope_ids.usage_id),
"\n".join(fragment.content for fragment in fragments),
)
)
return result
@ddt.ddt
class TestIndexView(ModuleStoreTestCase):
"""
Tests of the courseware.index view.
"""
@XBlock.register_temp_plugin(ViewCheckerBlock, 'view_checker')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_student_state(self, default_store):
"""
Verify that saved student state is loaded for xblocks rendered in the index view.
"""
user = UserFactory()
with modulestore().default_store(default_store):
course = CourseFactory.create()
chapter = ItemFactory.create(parent=course, category='chapter')
section = ItemFactory.create(parent=chapter, category='view_checker', display_name="Sequence Checker")
vertical = ItemFactory.create(parent=section, category='view_checker', display_name="Vertical Checker")
block = ItemFactory.create(parent=vertical, category='view_checker', display_name="Block Checker")
for item in (section, vertical, block):
StudentModuleFactory.create(
student=user,
course_id=course.id,
module_state_key=item.scope_ids.usage_id,
state=json.dumps({'state': unicode(item.scope_ids.usage_id)})
)
CourseEnrollmentFactory(user=user, course_id=course.id)
request = RequestFactory().get(
reverse(
'courseware_section',
kwargs={
'course_id': unicode(course.id),
'chapter': chapter.url_name,
'section': section.url_name,
}
)
)
request.user = user
mako_middleware_process_request(request)
# Trigger the assertions embedded in the ViewCheckerBlocks
response = views.index(request, unicode(course.id), chapter=chapter.url_name, section=section.url_name)
self.assertEquals(response.content.count("ViewCheckerPassed"), 3)
......@@ -314,7 +314,7 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseKey.from_string(course_id)
user = User.objects.prefetch_related("groups").get(id=request.user.id)
......@@ -489,8 +489,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children
section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_key, user, section_descriptor, depth=None, asides=XBlockAsidesConfig.possible_asides()
field_data_cache.add_descriptor_descendents(
section_descriptor, depth=None
)
# Verify that position a string is in fact an int
......@@ -504,7 +504,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
request.user,
request,
section_descriptor,
section_field_data_cache,
field_data_cache,
course_key,
position
)
......
......@@ -33,3 +33,6 @@ class LmsFieldData(SplitFieldData):
Scope.user_info: student_data,
Scope.preferences: student_data,
})
def __repr__(self):
return "LmsFieldData{!r}".format((self._authored_data, self._student_data))
......@@ -22,7 +22,7 @@
git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a0c695#egg=django-cas
# Our libraries:
-e git+https://github.com/edx/XBlock.git@0b865f62f2deaa81b77f819b9b7df0303cb0f70c#egg=XBlock
-e git+https://github.com/edx/XBlock.git@b5e83915d9d205076eac357b71a91f7cd6d8010d#egg=XBlock
-e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail
-e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool
-e git+https://github.com/edx/event-tracking.git@0.1.0#egg=event-tracking
......
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