Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
8e4ee78f
Commit
8e4ee78f
authored
Mar 17, 2015
by
Calen Pennington
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7262 from edx/cale/xblock-filtered-children-tests
Clear children cache when binding an XBlock to a user.
parents
9b49059e
60bbfc5a
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
572 additions
and
160 deletions
+572
-160
cms/djangoapps/contentstore/views/preview.py
+2
-1
common/lib/xmodule/xmodule/course_module.py
+9
-9
common/lib/xmodule/xmodule/modulestore/__init__.py
+1
-1
common/lib/xmodule/xmodule/modulestore/mongo/base.py
+97
-39
common/lib/xmodule/xmodule/tests/__init__.py
+1
-1
common/lib/xmodule/xmodule/tests/test_conditional.py
+1
-0
common/lib/xmodule/xmodule/tests/test_library_content.py
+1
-1
common/lib/xmodule/xmodule/tests/test_split_test_module.py
+7
-2
common/lib/xmodule/xmodule/x_module.py
+52
-29
lms/djangoapps/courseware/model_data.py
+47
-29
lms/djangoapps/courseware/module_render.py
+3
-6
lms/djangoapps/courseware/tests/test_courses.py
+56
-6
lms/djangoapps/courseware/tests/test_module_render.py
+194
-16
lms/djangoapps/courseware/tests/test_views.py
+93
-15
lms/djangoapps/courseware/views.py
+4
-4
lms/djangoapps/lms_xblock/field_data.py
+3
-0
requirements/edx/github.txt
+1
-1
No files found.
cms/djangoapps/contentstore/views/preview.py
View file @
8e4ee78f
...
...
@@ -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
...
...
common/lib/xmodule/xmodule/course_module.py
View file @
8e4ee78f
...
...
@@ -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
c
hapter
in
self
.
get_children
():
for
s
ection
in
chapter
.
get_children
():
if
s
ection
.
graded
:
xmoduledescriptors
=
list
(
yield_descriptor_descendents
(
s
ection
))
xmoduledescriptors
.
append
(
s
ection
)
# 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'
:
s
ection
,
'xmoduledescriptors'
:
[
child
for
child
in
xmoduledescriptors
if
child
.
has_score
]
}
section_format
=
s
.
format
if
s
.
format
is
not
None
else
''
section_format
=
s
ection
.
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
(
s
ection
)
return
{
'graded_sections'
:
graded_sections
,
'all_descriptors'
:
all_descriptors
,
}
...
...
common/lib/xmodule/xmodule/modulestore/__init__.py
View file @
8e4ee78f
...
...
@@ -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.
...
...
common/lib/xmodule/xmodule/modulestore/mongo/base.py
View file @
8e4ee78f
...
...
@@ -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
):
...
...
common/lib/xmodule/xmodule/tests/__init__.py
View file @
8e4ee78f
...
...
@@ -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
...
...
common/lib/xmodule/xmodule/tests/test_conditional.py
View file @
8e4ee78f
...
...
@@ -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
...
...
common/lib/xmodule/xmodule/tests/test_library_content.py
View file @
8e4ee78f
...
...
@@ -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
...
...
common/lib/xmodule/xmodule/tests/test_split_test_module.py
View file @
8e4ee78f
...
...
@@ -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
...
...
common/lib/xmodule/xmodule/x_module.py
View file @
8e4ee78f
...
...
@@ -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
):
...
...
lms/djangoapps/courseware/model_data.py
View file @
8e4ee78f
...
...
@@ -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
...
...
lms/djangoapps/courseware/module_render.py
View file @
8e4ee78f
...
...
@@ -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
...
...
lms/djangoapps/courseware/tests/test_courses.py
View file @
8e4ee78f
...
...
@@ -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
)
lms/djangoapps/courseware/tests/test_module_render.py
View file @
8e4ee78f
...
...
@@ -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
()))
lms/djangoapps/courseware/tests/test_views.py
View file @
8e4ee78f
...
...
@@ -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
)
lms/djangoapps/courseware/views.py
View file @
8e4ee78f
...
...
@@ -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
)
...
...
lms/djangoapps/lms_xblock/field_data.py
View file @
8e4ee78f
...
...
@@ -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
))
requirements/edx/github.txt
View file @
8e4ee78f
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment