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
16dc83df
Commit
16dc83df
authored
Jan 12, 2015
by
Braden MacDonald
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #6492 from open-craft/content_libraries/13-analytics-enhancements
Content libraries analytics enhancements (SOL-121)
parents
195d5b57
05fc6738
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
354 additions
and
72 deletions
+354
-72
cms/djangoapps/contentstore/features/courses.py
+1
-2
common/djangoapps/terrain/ui_helpers.py
+6
-7
common/lib/xmodule/xmodule/library_content_module.py
+36
-5
common/lib/xmodule/xmodule/library_tools.py
+38
-0
common/lib/xmodule/xmodule/modulestore/mixed.py
+12
-0
common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+22
-0
common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py
+9
-0
common/lib/xmodule/xmodule/modulestore/tests/utils.py
+1
-0
common/lib/xmodule/xmodule/tests/test_library_content.py
+196
-52
lms/djangoapps/courseware/module_render.py
+6
-0
lms/djangoapps/courseware/tests/test_module_render.py
+25
-6
lms/djangoapps/lms_xblock/runtime.py
+2
-0
No files found.
cms/djangoapps/contentstore/features/courses.py
View file @
16dc83df
...
@@ -34,9 +34,8 @@ def i_create_a_course(step):
...
@@ -34,9 +34,8 @@ def i_create_a_course(step):
create_a_course
()
create_a_course
()
# pylint: disable=invalid-name
@step
(
'I click the course link in Studio Home$'
)
@step
(
'I click the course link in Studio Home$'
)
def
i_click_the_course_link_in_studio_home
(
step
):
def
i_click_the_course_link_in_studio_home
(
step
):
# pylint: disable=invalid-name
course_css
=
'a.course-link'
course_css
=
'a.course-link'
world
.
css_click
(
course_css
)
world
.
css_click
(
course_css
)
...
...
common/djangoapps/terrain/ui_helpers.py
View file @
16dc83df
...
@@ -28,28 +28,27 @@ GLOBAL_WAIT_FOR_TIMEOUT = 60
...
@@ -28,28 +28,27 @@ GLOBAL_WAIT_FOR_TIMEOUT = 60
REQUIREJS_WAIT
=
{
REQUIREJS_WAIT
=
{
# Settings - Schedule & Details
# Settings - Schedule & Details
re
.
compile
(
'^Schedule & Details Settings
\
|'
):
[
re
.
compile
(
r
'^Schedule & Details Settings \|'
):
[
"jquery"
,
"js/base"
,
"js/models/course"
,
"jquery"
,
"js/base"
,
"js/models/course"
,
"js/models/settings/course_details"
,
"js/views/settings/main"
],
"js/models/settings/course_details"
,
"js/views/settings/main"
],
# Settings - Advanced Settings
# Settings - Advanced Settings
re
.
compile
(
'^Advanced Settings
\
|'
):
[
re
.
compile
(
r
'^Advanced Settings \|'
):
[
"jquery"
,
"js/base"
,
"js/models/course"
,
"js/models/settings/advanced"
,
"jquery"
,
"js/base"
,
"js/models/course"
,
"js/models/settings/advanced"
,
"js/views/settings/advanced"
,
"codemirror"
],
"js/views/settings/advanced"
,
"codemirror"
],
# Unit page
# Unit page
re
.
compile
(
'^Unit
\
|'
):
[
re
.
compile
(
r
'^Unit \|'
):
[
"jquery"
,
"js/base"
,
"js/models/xblock_info"
,
"js/views/pages/container"
,
"jquery"
,
"js/base"
,
"js/models/xblock_info"
,
"js/views/pages/container"
,
"js/collections/component_template"
,
"xmodule"
,
"coffee/src/main"
,
"xblock/cms.runtime.v1"
],
"js/collections/component_template"
,
"xmodule"
,
"coffee/src/main"
,
"xblock/cms.runtime.v1"
],
# Content - Outline
# Content - Outline
# Note that calling your org, course number, or display name, 'course' will mess this up
# Note that calling your org, course number, or display name, 'course' will mess this up
re
.
compile
(
'^Course Outline
\
|'
):
[
re
.
compile
(
r
'^Course Outline \|'
):
[
"js/base"
,
"js/models/course"
,
"js/models/location"
,
"js/models/section"
],
"js/base"
,
"js/models/course"
,
"js/models/location"
,
"js/models/section"
],
# Dashboard
# Dashboard
# pylint: disable=anomalous-backslash-in-string
re
.
compile
(
r'^Studio Home \|'
):
[
re
.
compile
(
'^Studio Home
\
|'
):
[
"js/sock"
,
"gettext"
,
"js/base"
,
"js/sock"
,
"gettext"
,
"js/base"
,
"jquery.ui"
,
"coffee/src/main"
,
"underscore"
],
"jquery.ui"
,
"coffee/src/main"
,
"underscore"
],
...
@@ -60,7 +59,7 @@ REQUIREJS_WAIT = {
...
@@ -60,7 +59,7 @@ REQUIREJS_WAIT = {
],
],
# Pages
# Pages
re
.
compile
(
'^Pages
\
|'
):
[
re
.
compile
(
r
'^Pages \|'
):
[
'js/models/explicit_url'
,
'coffee/src/views/tabs'
,
'js/models/explicit_url'
,
'coffee/src/views/tabs'
,
'xmodule'
,
'coffee/src/main'
,
'xblock/cms.runtime.v1'
'xmodule'
,
'coffee/src/main'
,
'xblock/cms.runtime.v1'
],
],
...
...
common/lib/xmodule/xmodule/library_content_module.py
View file @
16dc83df
...
@@ -220,25 +220,56 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
...
@@ -220,25 +220,56 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
if
hasattr
(
self
,
"_selected_set"
):
if
hasattr
(
self
,
"_selected_set"
):
# Already done:
# Already done:
return
self
.
_selected_set
# pylint: disable=access-member-before-definition
return
self
.
_selected_set
# pylint: disable=access-member-before-definition
selected
=
set
(
tuple
(
k
)
for
k
in
self
.
selected
)
# set of (block_type, block_id) tuples assigned to this student
previous_count
=
len
(
selected
)
lib_tools
=
self
.
runtime
.
service
(
self
,
'library_tools'
)
format_block_keys
=
lambda
keys
:
lib_tools
.
create_block_analytics_summary
(
self
.
location
.
course_key
,
keys
)
def
publish_event
(
event_name
,
**
kwargs
):
""" Publish an event for analytics purposes """
event_data
=
{
"location"
:
unicode
(
self
.
location
),
"result"
:
format_block_keys
(
selected
),
"previous_count"
:
previous_count
,
"max_count"
:
self
.
max_count
,
}
event_data
.
update
(
kwargs
)
self
.
runtime
.
publish
(
self
,
"edx.librarycontentblock.content.{}"
.
format
(
event_name
),
event_data
)
# Determine which of our children we will show:
# Determine which of our children we will show:
selected
=
set
(
tuple
(
k
)
for
k
in
self
.
selected
)
# set of (block_type, block_id) tuples
valid_block_keys
=
set
([(
c
.
block_type
,
c
.
block_id
)
for
c
in
self
.
children
])
# pylint: disable=no-member
valid_block_keys
=
set
([(
c
.
block_type
,
c
.
block_id
)
for
c
in
self
.
children
])
# pylint: disable=no-member
# Remove any selected blocks that are no longer valid:
# Remove any selected blocks that are no longer valid:
selected
-=
(
selected
-
valid_block_keys
)
invalid_block_keys
=
(
selected
-
valid_block_keys
)
if
invalid_block_keys
:
selected
-=
invalid_block_keys
# Publish an event for analytics purposes:
# reason "invalid" means deleted from library or a different library is now being used.
publish_event
(
"removed"
,
removed
=
format_block_keys
(
invalid_block_keys
),
reason
=
"invalid"
)
# If max_count has been decreased, we may have to drop some previously selected blocks:
# If max_count has been decreased, we may have to drop some previously selected blocks:
overlimit_block_keys
=
set
()
while
len
(
selected
)
>
self
.
max_count
:
while
len
(
selected
)
>
self
.
max_count
:
selected
.
pop
()
overlimit_block_keys
.
add
(
selected
.
pop
())
if
overlimit_block_keys
:
# Publish an event for analytics purposes:
publish_event
(
"removed"
,
removed
=
format_block_keys
(
overlimit_block_keys
),
reason
=
"overlimit"
)
# Do we have enough blocks now?
# Do we have enough blocks now?
num_to_add
=
self
.
max_count
-
len
(
selected
)
num_to_add
=
self
.
max_count
-
len
(
selected
)
if
num_to_add
>
0
:
if
num_to_add
>
0
:
added_block_keys
=
None
# We need to select [more] blocks to display to this user:
# We need to select [more] blocks to display to this user:
pool
=
valid_block_keys
-
selected
if
self
.
mode
==
"random"
:
if
self
.
mode
==
"random"
:
pool
=
valid_block_keys
-
selected
num_to_add
=
min
(
len
(
pool
),
num_to_add
)
num_to_add
=
min
(
len
(
pool
),
num_to_add
)
selected
|
=
set
(
random
.
sample
(
pool
,
num_to_add
))
added_block_keys
=
set
(
random
.
sample
(
pool
,
num_to_add
))
# We now have the correct n random children to show for this user.
# We now have the correct n random children to show for this user.
else
:
else
:
raise
NotImplementedError
(
"Unsupported mode."
)
raise
NotImplementedError
(
"Unsupported mode."
)
selected
|=
added_block_keys
if
added_block_keys
:
# Publish an event for analytics purposes:
publish_event
(
"assigned"
,
added
=
format_block_keys
(
added_block_keys
))
# Save our selections to the user state, to ensure consistency:
# Save our selections to the user state, to ensure consistency:
self
.
selected
=
list
(
selected
)
# TODO: this doesn't save from the LMS "Progress" page.
self
.
selected
=
list
(
selected
)
# TODO: this doesn't save from the LMS "Progress" page.
# Cache the results
# Cache the results
...
...
common/lib/xmodule/xmodule/library_tools.py
View file @
16dc83df
...
@@ -44,6 +44,44 @@ class LibraryToolsService(object):
...
@@ -44,6 +44,44 @@ class LibraryToolsService(object):
return
library
.
location
.
library_key
.
version_guid
return
library
.
location
.
library_key
.
version_guid
return
None
return
None
def
create_block_analytics_summary
(
self
,
course_key
,
block_keys
):
"""
Given a CourseKey and a list of (block_type, block_id) pairs,
prepare the JSON-ready metadata needed for analytics logging.
This is [
{"usage_key": x, "original_usage_key": y, "original_usage_version": z, "descendants": [...]}
]
where the main list contains all top-level blocks, and descendants contains a *flat* list of all
descendants of the top level blocks, if any.
"""
def
summarize_block
(
usage_key
):
""" Basic information about the given block """
orig_key
,
orig_version
=
self
.
store
.
get_block_original_usage
(
usage_key
)
return
{
"usage_key"
:
unicode
(
usage_key
),
"original_usage_key"
:
unicode
(
orig_key
)
if
orig_key
else
None
,
"original_usage_version"
:
unicode
(
orig_version
)
if
orig_version
else
None
,
}
result_json
=
[]
for
block_key
in
block_keys
:
key
=
course_key
.
make_usage_key
(
*
block_key
)
info
=
summarize_block
(
key
)
info
[
'descendants'
]
=
[]
try
:
block
=
self
.
store
.
get_item
(
key
,
depth
=
None
)
# Load the item and all descendants
children
=
list
(
getattr
(
block
,
"children"
,
[]))
while
children
:
child_key
=
children
.
pop
()
child
=
self
.
store
.
get_item
(
child_key
)
info
[
'descendants'
]
.
append
(
summarize_block
(
child_key
))
children
.
extend
(
getattr
(
child
,
"children"
,
[]))
except
ItemNotFoundError
:
pass
# The block has been deleted
result_json
.
append
(
info
)
return
result_json
def
_filter_child
(
self
,
usage_key
,
capa_type
):
def
_filter_child
(
self
,
usage_key
,
capa_type
):
"""
"""
Filters children by CAPA problem type, if configured
Filters children by CAPA problem type, if configured
...
...
common/lib/xmodule/xmodule/modulestore/mixed.py
View file @
16dc83df
...
@@ -509,6 +509,18 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
...
@@ -509,6 +509,18 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
store
=
self
.
_get_modulestore_for_courseid
(
location
.
course_key
)
store
=
self
.
_get_modulestore_for_courseid
(
location
.
course_key
)
return
store
.
get_parent_location
(
location
,
**
kwargs
)
return
store
.
get_parent_location
(
location
,
**
kwargs
)
def
get_block_original_usage
(
self
,
usage_key
):
"""
If a block was inherited into another structure using copy_from_template,
this will return the original block usage locator from which the
copy was inherited.
"""
try
:
store
=
self
.
_verify_modulestore_support
(
usage_key
.
course_key
,
'get_block_original_usage'
)
return
store
.
get_block_original_usage
(
usage_key
)
except
NotImplementedError
:
return
None
,
None
def
get_modulestore_type
(
self
,
course_id
):
def
get_modulestore_type
(
self
,
course_id
):
"""
"""
Returns a type which identifies which modulestore is servicing the given course_id.
Returns a type which identifies which modulestore is servicing the given course_id.
...
...
common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
View file @
16dc83df
...
@@ -454,12 +454,17 @@ class SplitBulkWriteMixin(BulkOperationsMixin):
...
@@ -454,12 +454,17 @@ class SplitBulkWriteMixin(BulkOperationsMixin):
if
block_info
[
'edit_info'
]
.
get
(
'update_version'
)
==
update_version
:
if
block_info
[
'edit_info'
]
.
get
(
'update_version'
)
==
update_version
:
return
return
original_usage
=
block_info
[
'edit_info'
]
.
get
(
'original_usage'
)
original_usage_version
=
block_info
[
'edit_info'
]
.
get
(
'original_usage_version'
)
block_info
[
'edit_info'
]
=
{
block_info
[
'edit_info'
]
=
{
'edited_on'
:
datetime
.
datetime
.
now
(
UTC
),
'edited_on'
:
datetime
.
datetime
.
now
(
UTC
),
'edited_by'
:
user_id
,
'edited_by'
:
user_id
,
'previous_version'
:
block_info
[
'edit_info'
][
'update_version'
],
'previous_version'
:
block_info
[
'edit_info'
][
'update_version'
],
'update_version'
:
update_version
,
'update_version'
:
update_version
,
}
}
if
original_usage
:
block_info
[
'edit_info'
][
'original_usage'
]
=
original_usage
block_info
[
'edit_info'
][
'original_usage_version'
]
=
original_usage_version
def
find_matching_course_indexes
(
self
,
branch
=
None
,
search_targets
=
None
):
def
find_matching_course_indexes
(
self
,
branch
=
None
,
search_targets
=
None
):
"""
"""
...
@@ -1254,6 +1259,21 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -1254,6 +1259,21 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
# TODO implement
# TODO implement
pass
pass
def
get_block_original_usage
(
self
,
usage_key
):
"""
If a block was inherited into another structure using copy_from_template,
this will return the original block usage locator and version from
which the copy was inherited.
Returns usage_key, version if the data is available, otherwise returns (None, None)
"""
blocks
=
self
.
_lookup_course
(
usage_key
.
course_key
)
.
structure
[
'blocks'
]
block
=
blocks
.
get
(
BlockKey
.
from_usage_key
(
usage_key
))
if
block
and
'original_usage'
in
block
[
'edit_info'
]:
usage_key
=
BlockUsageLocator
.
from_string
(
block
[
'edit_info'
][
'original_usage'
])
return
usage_key
,
block
[
'edit_info'
]
.
get
(
'original_usage_version'
)
return
None
,
None
def
create_definition_from_data
(
self
,
course_key
,
new_def_data
,
category
,
user_id
):
def
create_definition_from_data
(
self
,
course_key
,
new_def_data
,
category
,
user_id
):
"""
"""
Pull the definition fields out of descriptor and save to the db as a new definition
Pull the definition fields out of descriptor and save to the db as a new definition
...
@@ -2214,6 +2234,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -2214,6 +2234,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
# Setting it to the source_block_info structure version here breaks split_draft's has_changes() method.
# Setting it to the source_block_info structure version here breaks split_draft's has_changes() method.
new_block_info
[
'edit_info'
][
'edited_by'
]
=
user_id
new_block_info
[
'edit_info'
][
'edited_by'
]
=
user_id
new_block_info
[
'edit_info'
][
'edited_on'
]
=
datetime
.
datetime
.
now
(
UTC
)
new_block_info
[
'edit_info'
][
'edited_on'
]
=
datetime
.
datetime
.
now
(
UTC
)
new_block_info
[
'edit_info'
][
'original_usage'
]
=
unicode
(
usage_key
.
replace
(
branch
=
None
,
version_guid
=
None
))
new_block_info
[
'edit_info'
][
'original_usage_version'
]
=
source_block_info
[
'edit_info'
]
.
get
(
'update_version'
)
dest_structure
[
'blocks'
][
new_block_key
]
=
new_block_info
dest_structure
[
'blocks'
][
new_block_key
]
=
new_block_info
children
=
source_block_info
[
'fields'
]
.
get
(
'children'
)
children
=
source_block_info
[
'fields'
]
.
get
(
'children'
)
...
...
common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py
View file @
16dc83df
...
@@ -268,6 +268,15 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
...
@@ -268,6 +268,15 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
location
=
self
.
_map_revision_to_branch
(
location
,
revision
=
revision
)
location
=
self
.
_map_revision_to_branch
(
location
,
revision
=
revision
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_parent_location
(
location
,
**
kwargs
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_parent_location
(
location
,
**
kwargs
)
def
get_block_original_usage
(
self
,
usage_key
):
"""
If a block was inherited into another structure using copy_from_template,
this will return the original block usage locator from which the
copy was inherited.
"""
usage_key
=
self
.
_map_revision_to_branch
(
usage_key
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_block_original_usage
(
usage_key
)
def
get_orphans
(
self
,
course_key
,
**
kwargs
):
def
get_orphans
(
self
,
course_key
,
**
kwargs
):
course_key
=
self
.
_map_revision_to_branch
(
course_key
)
course_key
=
self
.
_map_revision_to_branch
(
course_key
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_orphans
(
course_key
,
**
kwargs
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_orphans
(
course_key
,
**
kwargs
)
...
...
common/lib/xmodule/xmodule/modulestore/tests/utils.py
View file @
16dc83df
...
@@ -119,6 +119,7 @@ class MixedSplitTestCase(TestCase):
...
@@ -119,6 +119,7 @@ class MixedSplitTestCase(TestCase):
extra
.
update
(
kwargs
)
extra
.
update
(
kwargs
)
return
ItemFactory
.
create
(
return
ItemFactory
.
create
(
category
=
category
,
category
=
category
,
parent
=
parent_block
,
parent_location
=
parent_block
.
location
,
parent_location
=
parent_block
.
location
,
modulestore
=
self
.
store
,
modulestore
=
self
.
store
,
**
extra
**
extra
...
...
common/lib/xmodule/xmodule/tests/test_library_content.py
View file @
16dc83df
...
@@ -5,22 +5,22 @@ Basic unit tests for LibraryContentModule
...
@@ -5,22 +5,22 @@ Basic unit tests for LibraryContentModule
Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`.
Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`.
"""
"""
from
bson.objectid
import
ObjectId
from
bson.objectid
import
ObjectId
from
mock
import
patch
from
mock
import
Mock
,
patch
from
opaque_keys.edx.locator
import
LibraryLocator
from
opaque_keys.edx.locator
import
LibraryLocator
from
unittest
import
TestCase
from
unittest
import
TestCase
from
xblock.fragment
import
Fragment
from
xblock.fragment
import
Fragment
from
xblock.runtime
import
Runtime
as
VanillaRuntime
from
xblock.runtime
import
Runtime
as
VanillaRuntime
from
xmodule.x_module
import
AUTHOR_VIEW
from
xmodule.library_content_module
import
(
from
xmodule.library_content_module
import
(
LibraryVersionReference
,
LibraryList
,
ANY_CAPA_TYPE_VALUE
,
LibraryContentDescriptor
LibraryVersionReference
,
LibraryList
,
ANY_CAPA_TYPE_VALUE
,
LibraryContentDescriptor
)
)
from
xmodule.modulestore.tests.factories
import
LibraryFactory
,
CourseFactory
,
ItemFactory
from
xmodule.library_tools
import
LibraryToolsService
from
xmodule.modulestore.tests.factories
import
LibraryFactory
,
CourseFactory
from
xmodule.modulestore.tests.utils
import
MixedSplitTestCase
from
xmodule.modulestore.tests.utils
import
MixedSplitTestCase
from
xmodule.tests
import
get_test_system
from
xmodule.tests
import
get_test_system
from
xmodule.validation
import
StudioValidationMessage
from
xmodule.validation
import
StudioValidationMessage
from
xmodule.x_module
import
AUTHOR_VIEW
dummy_render
=
lambda
block
,
_
:
Fragment
(
block
.
data
)
# pylint: disable=invalid-name
dummy_render
=
lambda
block
,
_
:
Fragment
(
block
.
data
)
# pylint: disable=invalid-name
...
@@ -32,46 +32,21 @@ class LibraryContentTest(MixedSplitTestCase):
...
@@ -32,46 +32,21 @@ class LibraryContentTest(MixedSplitTestCase):
def
setUp
(
self
):
def
setUp
(
self
):
super
(
LibraryContentTest
,
self
)
.
setUp
()
super
(
LibraryContentTest
,
self
)
.
setUp
()
self
.
tools
=
LibraryToolsService
(
self
.
store
)
self
.
library
=
LibraryFactory
.
create
(
modulestore
=
self
.
store
)
self
.
library
=
LibraryFactory
.
create
(
modulestore
=
self
.
store
)
self
.
lib_blocks
=
[
self
.
lib_blocks
=
[
ItemFactory
.
create
(
self
.
make_block
(
"html"
,
self
.
library
,
data
=
"Hello world from block {}"
.
format
(
i
))
category
=
"html"
,
parent_location
=
self
.
library
.
location
,
user_id
=
self
.
user_id
,
publish_item
=
False
,
metadata
=
{
"data"
:
"Hello world from block {}"
.
format
(
i
),
},
modulestore
=
self
.
store
,
)
for
i
in
range
(
1
,
5
)
for
i
in
range
(
1
,
5
)
]
]
self
.
course
=
CourseFactory
.
create
(
modulestore
=
self
.
store
)
self
.
course
=
CourseFactory
.
create
(
modulestore
=
self
.
store
)
self
.
chapter
=
ItemFactory
.
create
(
self
.
chapter
=
self
.
make_block
(
"chapter"
,
self
.
course
)
category
=
"chapter"
,
self
.
sequential
=
self
.
make_block
(
"sequential"
,
self
.
chapter
)
parent_location
=
self
.
course
.
location
,
self
.
vertical
=
self
.
make_block
(
"vertical"
,
self
.
sequential
)
user_id
=
self
.
user_id
,
self
.
lc_block
=
self
.
make_block
(
modulestore
=
self
.
store
,
"library_content"
,
)
self
.
vertical
,
self
.
sequential
=
ItemFactory
.
create
(
max_count
=
1
,
category
=
"sequential"
,
source_libraries
=
[
LibraryVersionReference
(
self
.
library
.
location
.
library_key
)]
parent_location
=
self
.
chapter
.
location
,
user_id
=
self
.
user_id
,
modulestore
=
self
.
store
,
)
self
.
vertical
=
ItemFactory
.
create
(
category
=
"vertical"
,
parent_location
=
self
.
sequential
.
location
,
user_id
=
self
.
user_id
,
modulestore
=
self
.
store
,
)
self
.
lc_block
=
ItemFactory
.
create
(
category
=
"library_content"
,
parent_location
=
self
.
vertical
.
location
,
user_id
=
self
.
user_id
,
modulestore
=
self
.
store
,
metadata
=
{
'max_count'
:
1
,
'source_libraries'
:
[
LibraryVersionReference
(
self
.
library
.
location
.
library_key
)]
}
)
)
def
_bind_course_module
(
self
,
module
):
def
_bind_course_module
(
self
,
module
):
...
@@ -80,6 +55,7 @@ class LibraryContentTest(MixedSplitTestCase):
...
@@ -80,6 +55,7 @@ class LibraryContentTest(MixedSplitTestCase):
"""
"""
module_system
=
get_test_system
(
course_id
=
self
.
course
.
location
.
course_key
)
module_system
=
get_test_system
(
course_id
=
self
.
course
.
location
.
course_key
)
module_system
.
descriptor_runtime
=
module
.
runtime
module_system
.
descriptor_runtime
=
module
.
runtime
module_system
.
_services
[
'library_tools'
]
=
self
.
tools
# pylint: disable=protected-access
def
get_module
(
descriptor
):
def
get_module
(
descriptor
):
"""Mocks module_system get_module function"""
"""Mocks module_system get_module function"""
...
@@ -92,6 +68,11 @@ class LibraryContentTest(MixedSplitTestCase):
...
@@ -92,6 +68,11 @@ class LibraryContentTest(MixedSplitTestCase):
module_system
.
get_module
=
get_module
module_system
.
get_module
=
get_module
module
.
xmodule_runtime
=
module_system
module
.
xmodule_runtime
=
module_system
class
TestLibraryContentModule
(
LibraryContentTest
):
"""
Basic unit tests for LibraryContentModule
"""
def
_get_capa_problem_type_xml
(
self
,
*
args
):
def
_get_capa_problem_type_xml
(
self
,
*
args
):
""" Helper function to create empty CAPA problem definition """
""" Helper function to create empty CAPA problem definition """
problem
=
"<problem>"
problem
=
"<problem>"
...
@@ -111,20 +92,8 @@ class LibraryContentTest(MixedSplitTestCase):
...
@@ -111,20 +92,8 @@ class LibraryContentTest(MixedSplitTestCase):
[
"coderesponse"
,
"optionresponse"
]
[
"coderesponse"
,
"optionresponse"
]
]
]
for
problem_type
in
problem_types
:
for
problem_type
in
problem_types
:
ItemFactory
.
create
(
self
.
make_block
(
"problem"
,
self
.
library
,
data
=
self
.
_get_capa_problem_type_xml
(
*
problem_type
))
category
=
"problem"
,
parent_location
=
self
.
library
.
location
,
user_id
=
self
.
user_id
,
publish_item
=
False
,
data
=
self
.
_get_capa_problem_type_xml
(
*
problem_type
),
modulestore
=
self
.
store
,
)
class
TestLibraryContentModule
(
LibraryContentTest
):
"""
Basic unit tests for LibraryContentModule
"""
def
test_lib_content_block
(
self
):
def
test_lib_content_block
(
self
):
"""
"""
Test that blocks from a library are copied and added as children
Test that blocks from a library are copied and added as children
...
@@ -338,3 +307,178 @@ class TestLibraryList(TestCase):
...
@@ -338,3 +307,178 @@ class TestLibraryList(TestCase):
lib_list
=
LibraryList
()
lib_list
=
LibraryList
()
with
self
.
assertRaises
(
ValueError
):
with
self
.
assertRaises
(
ValueError
):
lib_list
.
from_json
([
"Not-a-library-key,whatever"
])
lib_list
.
from_json
([
"Not-a-library-key,whatever"
])
class
TestLibraryContentAnalytics
(
LibraryContentTest
):
"""
Test analytics features of LibraryContentModule
"""
def
setUp
(
self
):
super
(
TestLibraryContentAnalytics
,
self
)
.
setUp
()
self
.
publisher
=
Mock
()
self
.
lc_block
.
refresh_children
()
self
.
lc_block
=
self
.
store
.
get_item
(
self
.
lc_block
.
location
)
self
.
_bind_course_module
(
self
.
lc_block
)
self
.
lc_block
.
xmodule_runtime
.
publish
=
self
.
publisher
def
_assert_event_was_published
(
self
,
event_type
):
"""
Check that a LibraryContentModule analytics event was published by self.lc_block.
"""
self
.
assertTrue
(
self
.
publisher
.
called
)
self
.
assertTrue
(
len
(
self
.
publisher
.
call_args
[
0
]),
3
)
_
,
event_name
,
event_data
=
self
.
publisher
.
call_args
[
0
]
self
.
assertEqual
(
event_name
,
"edx.librarycontentblock.content.{}"
.
format
(
event_type
))
self
.
assertEqual
(
event_data
[
"location"
],
unicode
(
self
.
lc_block
.
location
))
return
event_data
def
test_assigned_event
(
self
):
"""
Test the "assigned" event emitted when a student is assigned specific blocks.
"""
# In the beginning was the lc_block and it assigned one child to the student:
child
=
self
.
lc_block
.
get_child_descriptors
()[
0
]
child_lib_location
,
child_lib_version
=
self
.
store
.
get_block_original_usage
(
child
.
location
)
self
.
assertIsInstance
(
child_lib_version
,
ObjectId
)
event_data
=
self
.
_assert_event_was_published
(
"assigned"
)
block_info
=
{
"usage_key"
:
unicode
(
child
.
location
),
"original_usage_key"
:
unicode
(
child_lib_location
),
"original_usage_version"
:
unicode
(
child_lib_version
),
"descendants"
:
[],
}
self
.
assertEqual
(
event_data
,
{
"location"
:
unicode
(
self
.
lc_block
.
location
),
"added"
:
[
block_info
],
"result"
:
[
block_info
],
"previous_count"
:
0
,
"max_count"
:
1
,
})
self
.
publisher
.
reset_mock
()
# Now increase max_count so that one more child will be added:
self
.
lc_block
.
max_count
=
2
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
del
self
.
lc_block
.
_xmodule
.
_selected_set
children
=
self
.
lc_block
.
get_child_descriptors
()
self
.
assertEqual
(
len
(
children
),
2
)
child
,
new_child
=
children
if
children
[
0
]
.
location
==
child
.
location
else
reversed
(
children
)
event_data
=
self
.
_assert_event_was_published
(
"assigned"
)
self
.
assertEqual
(
event_data
[
"added"
][
0
][
"usage_key"
],
unicode
(
new_child
.
location
))
self
.
assertEqual
(
len
(
event_data
[
"result"
]),
2
)
self
.
assertEqual
(
event_data
[
"previous_count"
],
1
)
self
.
assertEqual
(
event_data
[
"max_count"
],
2
)
def
test_assigned_descendants
(
self
):
"""
Test the "assigned" event emitted includes descendant block information.
"""
# Replace the blocks in the library with a block that has descendants:
with
self
.
store
.
bulk_operations
(
self
.
library
.
location
.
library_key
):
self
.
library
.
children
=
[]
main_vertical
=
self
.
make_block
(
"vertical"
,
self
.
library
)
inner_vertical
=
self
.
make_block
(
"vertical"
,
main_vertical
)
html_block
=
self
.
make_block
(
"html"
,
inner_vertical
)
problem_block
=
self
.
make_block
(
"problem"
,
inner_vertical
)
self
.
lc_block
.
refresh_children
()
# Reload lc_block and set it up for a student:
self
.
lc_block
=
self
.
store
.
get_item
(
self
.
lc_block
.
location
)
self
.
_bind_course_module
(
self
.
lc_block
)
self
.
lc_block
.
xmodule_runtime
.
publish
=
self
.
publisher
# Get the keys of each of our blocks, as they appear in the course:
course_usage_main_vertical
=
self
.
lc_block
.
children
[
0
]
course_usage_inner_vertical
=
self
.
store
.
get_item
(
course_usage_main_vertical
)
.
children
[
0
]
inner_vertical_in_course
=
self
.
store
.
get_item
(
course_usage_inner_vertical
)
course_usage_html
=
inner_vertical_in_course
.
children
[
0
]
course_usage_problem
=
inner_vertical_in_course
.
children
[
1
]
# Trigger a publish event:
self
.
lc_block
.
get_child_descriptors
()
event_data
=
self
.
_assert_event_was_published
(
"assigned"
)
for
block_list
in
(
event_data
[
"added"
],
event_data
[
"result"
]):
self
.
assertEqual
(
len
(
block_list
),
1
)
# main_vertical is the only root block added, and is the only result.
self
.
assertEqual
(
block_list
[
0
][
"usage_key"
],
unicode
(
course_usage_main_vertical
))
# Check that "descendants" is a flat, unordered list of all of main_vertical's descendants:
descendants_expected
=
(
(
inner_vertical
.
location
,
course_usage_inner_vertical
),
(
html_block
.
location
,
course_usage_html
),
(
problem_block
.
location
,
course_usage_problem
),
)
descendant_data_expected
=
{}
for
lib_key
,
course_usage_key
in
descendants_expected
:
descendant_data_expected
[
unicode
(
course_usage_key
)]
=
{
"usage_key"
:
unicode
(
course_usage_key
),
"original_usage_key"
:
unicode
(
lib_key
),
"original_usage_version"
:
unicode
(
self
.
store
.
get_block_original_usage
(
course_usage_key
)[
1
]),
}
self
.
assertEqual
(
len
(
block_list
[
0
][
"descendants"
]),
len
(
descendant_data_expected
))
for
descendant
in
block_list
[
0
][
"descendants"
]:
self
.
assertEqual
(
descendant
,
descendant_data_expected
.
get
(
descendant
[
"usage_key"
]))
def
test_removed_overlimit
(
self
):
"""
Test the "removed" event emitted when we un-assign blocks previously assigned to a student.
We go from one blocks assigned to none because max_count has been decreased.
"""
# Decrease max_count to 1, causing the block to be overlimit:
self
.
lc_block
.
get_child_descriptors
()
# This line is needed in the test environment or the change has no effect
self
.
publisher
.
reset_mock
()
# Clear the "assigned" event that was just published.
self
.
lc_block
.
max_count
=
0
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
del
self
.
lc_block
.
_xmodule
.
_selected_set
# Check that the event says that one block was removed, leaving no blocks left:
children
=
self
.
lc_block
.
get_child_descriptors
()
self
.
assertEqual
(
len
(
children
),
0
)
event_data
=
self
.
_assert_event_was_published
(
"removed"
)
self
.
assertEqual
(
len
(
event_data
[
"removed"
]),
1
)
self
.
assertEqual
(
event_data
[
"result"
],
[])
self
.
assertEqual
(
event_data
[
"reason"
],
"overlimit"
)
def
test_removed_invalid
(
self
):
"""
Test the "removed" event emitted when we un-assign blocks previously assigned to a student.
We go from two blocks assigned, to one because the others have been deleted from the library.
"""
# Start by assigning two blocks to the student:
self
.
lc_block
.
get_child_descriptors
()
# This line is needed in the test environment or the change has no effect
self
.
lc_block
.
max_count
=
2
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
del
self
.
lc_block
.
_xmodule
.
_selected_set
initial_blocks_assigned
=
self
.
lc_block
.
get_child_descriptors
()
self
.
assertEqual
(
len
(
initial_blocks_assigned
),
2
)
self
.
publisher
.
reset_mock
()
# Clear the "assigned" event that was just published.
# Now make sure that one of the assigned blocks will have to be un-assigned.
# To cause an "invalid" event, we delete all blocks from the content library
# except for one of the two already assigned to the student:
keep_block_key
=
initial_blocks_assigned
[
0
]
.
location
keep_block_lib_usage_key
,
keep_block_lib_version
=
self
.
store
.
get_block_original_usage
(
keep_block_key
)
deleted_block_key
=
initial_blocks_assigned
[
1
]
.
location
self
.
library
.
children
=
[
keep_block_lib_usage_key
]
self
.
store
.
update_item
(
self
.
library
,
self
.
user_id
)
self
.
lc_block
.
refresh_children
()
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
del
self
.
lc_block
.
_xmodule
.
_selected_set
# Check that the event says that one block was removed, leaving one block left:
children
=
self
.
lc_block
.
get_child_descriptors
()
self
.
assertEqual
(
len
(
children
),
1
)
event_data
=
self
.
_assert_event_was_published
(
"removed"
)
self
.
assertEqual
(
event_data
[
"removed"
],
[{
"usage_key"
:
unicode
(
deleted_block_key
),
"original_usage_key"
:
None
,
# Note: original_usage_key info is sadly unavailable because the block has been
# deleted so that info can no longer be retrieved
"original_usage_version"
:
None
,
"descendants"
:
[],
}])
self
.
assertEqual
(
event_data
[
"result"
],
[{
"usage_key"
:
unicode
(
keep_block_key
),
"original_usage_key"
:
unicode
(
keep_block_lib_usage_key
),
"original_usage_version"
:
unicode
(
keep_block_lib_version
),
"descendants"
:
[],
}])
self
.
assertEqual
(
event_data
[
"reason"
],
"invalid"
)
lms/djangoapps/courseware/module_render.py
View file @
16dc83df
...
@@ -755,6 +755,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
...
@@ -755,6 +755,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
try
:
try
:
descriptor
=
modulestore
()
.
get_item
(
usage_key
)
descriptor
=
modulestore
()
.
get_item
(
usage_key
)
descriptor_orig_usage_key
,
descriptor_orig_version
=
modulestore
()
.
get_block_original_usage
(
usage_key
)
except
ItemNotFoundError
:
except
ItemNotFoundError
:
log
.
warn
(
log
.
warn
(
"Invalid location for course id {course_id}: {usage_key}"
.
format
(
"Invalid location for course id {course_id}: {usage_key}"
.
format
(
...
@@ -768,8 +769,13 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
...
@@ -768,8 +769,13 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user):
tracking_context
=
{
tracking_context
=
{
'module'
:
{
'module'
:
{
'display_name'
:
descriptor
.
display_name_with_default
,
'display_name'
:
descriptor
.
display_name_with_default
,
'usage_key'
:
unicode
(
descriptor
.
location
),
}
}
}
}
# For blocks that are inherited from a content library, we add some additional metadata:
if
descriptor_orig_usage_key
is
not
None
:
tracking_context
[
'module'
][
'original_usage_key'
]
=
unicode
(
descriptor_orig_usage_key
)
tracking_context
[
'module'
][
'original_usage_version'
]
=
unicode
(
descriptor_orig_version
)
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course_id
,
course_id
,
...
...
lms/djangoapps/courseware/tests/test_module_render.py
View file @
16dc83df
...
@@ -5,6 +5,7 @@ Test for lms courseware app, module render unit
...
@@ -5,6 +5,7 @@ Test for lms courseware app, module render unit
from
functools
import
partial
from
functools
import
partial
import
json
import
json
from
bson
import
ObjectId
import
ddt
import
ddt
from
django.http
import
Http404
,
HttpResponse
from
django.http
import
Http404
,
HttpResponse
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
...
@@ -13,6 +14,7 @@ from django.test.client import RequestFactory
...
@@ -13,6 +14,7 @@ from django.test.client import RequestFactory
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
django.contrib.auth.models
import
AnonymousUser
from
django.contrib.auth.models
import
AnonymousUser
from
mock
import
MagicMock
,
patch
,
Mock
from
mock
import
MagicMock
,
patch
,
Mock
from
opaque_keys.edx.keys
import
UsageKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
xblock.field_data
import
FieldData
from
xblock.field_data
import
FieldData
from
xblock.runtime
import
Runtime
from
xblock.runtime
import
Runtime
...
@@ -971,12 +973,13 @@ class TestModuleTrackingContext(ModuleStoreTestCase):
...
@@ -971,12 +973,13 @@ class TestModuleTrackingContext(ModuleStoreTestCase):
def
test_context_contains_display_name
(
self
,
mock_tracker
):
def
test_context_contains_display_name
(
self
,
mock_tracker
):
problem_display_name
=
u'Option Response Problem'
problem_display_name
=
u'Option Response Problem'
actual_display_name
=
self
.
handle_callback_and_get_display_name_from_event
(
mock_tracker
,
problem_display_name
)
module_info
=
self
.
handle_callback_and_get_module_info
(
mock_tracker
,
problem_display_name
)
self
.
assertEquals
(
problem_display_name
,
actual_display_name
)
self
.
assertEquals
(
problem_display_name
,
module_info
[
'display_name'
]
)
def
handle_callback_and_get_
display_name_from_event
(
self
,
mock_tracker
,
problem_display_name
=
None
):
def
handle_callback_and_get_
module_info
(
self
,
mock_tracker
,
problem_display_name
=
None
):
"""
"""
Creates a fake module, invokes the callback and extracts the display name from the emitted problem_check event.
Creates a fake module, invokes the callback and extracts the 'module'
metadata from the emitted problem_check event.
"""
"""
descriptor_kwargs
=
{
descriptor_kwargs
=
{
'category'
:
'problem'
,
'category'
:
'problem'
,
...
@@ -1000,12 +1003,28 @@ class TestModuleTrackingContext(ModuleStoreTestCase):
...
@@ -1000,12 +1003,28 @@ class TestModuleTrackingContext(ModuleStoreTestCase):
event
=
mock_call
[
1
][
0
]
event
=
mock_call
[
1
][
0
]
self
.
assertEquals
(
event
[
'event_type'
],
'problem_check'
)
self
.
assertEquals
(
event
[
'event_type'
],
'problem_check'
)
return
event
[
'context'
][
'module'
]
[
'display_name'
]
return
event
[
'context'
][
'module'
]
def
test_missing_display_name
(
self
,
mock_tracker
):
def
test_missing_display_name
(
self
,
mock_tracker
):
actual_display_name
=
self
.
handle_callback_and_get_
display_name_from_event
(
mock_tracker
)
actual_display_name
=
self
.
handle_callback_and_get_
module_info
(
mock_tracker
)[
'display_name'
]
self
.
assertTrue
(
actual_display_name
.
startswith
(
'problem'
))
self
.
assertTrue
(
actual_display_name
.
startswith
(
'problem'
))
def
test_library_source_information
(
self
,
mock_tracker
):
"""
Check that XBlocks that are inherited from a library include the
information about their library block source in events.
We patch the modulestore to avoid having to create a library.
"""
original_usage_key
=
UsageKey
.
from_string
(
u'block-v1:A+B+C+type@problem+block@abcd1234'
)
original_usage_version
=
ObjectId
()
mock_get_original_usage
=
lambda
_
,
key
:
(
original_usage_key
,
original_usage_version
)
with
patch
(
'xmodule.modulestore.mixed.MixedModuleStore.get_block_original_usage'
,
mock_get_original_usage
):
module_info
=
self
.
handle_callback_and_get_module_info
(
mock_tracker
)
self
.
assertIn
(
'original_usage_key'
,
module_info
)
self
.
assertEqual
(
module_info
[
'original_usage_key'
],
unicode
(
original_usage_key
))
self
.
assertIn
(
'original_usage_version'
,
module_info
)
self
.
assertEqual
(
module_info
[
'original_usage_version'
],
unicode
(
original_usage_version
))
class
TestXmoduleRuntimeEvent
(
TestSubmittingProblems
):
class
TestXmoduleRuntimeEvent
(
TestSubmittingProblems
):
"""
"""
...
...
lms/djangoapps/lms_xblock/runtime.py
View file @
16dc83df
...
@@ -10,6 +10,7 @@ from django.conf import settings
...
@@ -10,6 +10,7 @@ from django.conf import settings
from
lms.djangoapps.lms_xblock.models
import
XBlockAsidesConfig
from
lms.djangoapps.lms_xblock.models
import
XBlockAsidesConfig
from
openedx.core.djangoapps.user_api.api
import
course_tag
as
user_course_tag_api
from
openedx.core.djangoapps.user_api.api
import
course_tag
as
user_course_tag_api
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.library_tools
import
LibraryToolsService
from
xmodule.x_module
import
ModuleSystem
from
xmodule.x_module
import
ModuleSystem
from
xmodule.partitions.partitions_service
import
PartitionService
from
xmodule.partitions.partitions_service
import
PartitionService
...
@@ -199,6 +200,7 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract
...
@@ -199,6 +200,7 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract
course_id
=
kwargs
.
get
(
'course_id'
),
course_id
=
kwargs
.
get
(
'course_id'
),
track_function
=
kwargs
.
get
(
'track_function'
,
None
),
track_function
=
kwargs
.
get
(
'track_function'
,
None
),
)
)
services
[
'library_tools'
]
=
LibraryToolsService
(
modulestore
())
services
[
'fs'
]
=
xblock
.
reference
.
plugins
.
FSService
()
services
[
'fs'
]
=
xblock
.
reference
.
plugins
.
FSService
()
self
.
request_token
=
kwargs
.
pop
(
'request_token'
,
None
)
self
.
request_token
=
kwargs
.
pop
(
'request_token'
,
None
)
super
(
LmsModuleSystem
,
self
)
.
__init__
(
**
kwargs
)
super
(
LmsModuleSystem
,
self
)
.
__init__
(
**
kwargs
)
...
...
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