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
a78b94d8
Commit
a78b94d8
authored
Oct 28, 2015
by
Nimisha Asthagiri
Committed by
J. Cliff Dyer
Nov 05, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Transformer: ContentLibraryTransformer
parent
d1674ca8
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
464 additions
and
48 deletions
+464
-48
common/lib/xmodule/xmodule/library_content_module.py
+122
-48
lms/djangoapps/course_blocks/transformers/library_content.py
+177
-0
lms/djangoapps/course_blocks/transformers/tests/test_library_content.py
+165
-0
No files found.
common/lib/xmodule/xmodule/library_content_module.py
View file @
a78b94d8
...
...
@@ -134,8 +134,65 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
any particular student.
"""
@classmethod
def
make_selection
(
cls
,
selected
,
children
,
max_count
,
mode
):
"""
Dynamically selects block_ids indicating which of the possible children are displayed to the current user.
Arguments:
selected - list of (block_type, block_id) tuples assigned to this student
children - children of this block
max_count - number of components to display to each student
mode - how content is drawn from the library
Returns:
A dict containing the following keys:
'selected' (set) of (block_type, block_id) tuples assigned to this student
'invalid' (set) of dropped (block_type, block_id) tuples that are no longer valid
'overlimit' (set) of dropped (block_type, block_id) tuples that were previously selected
'added' (set) of newly added (block_type, block_id) tuples
"""
selected
=
set
(
tuple
(
k
)
for
k
in
selected
)
# set of (block_type, block_id) tuples assigned to this student
# Determine which of our children we will show:
valid_block_keys
=
set
([(
c
.
block_type
,
c
.
block_id
)
for
c
in
children
])
# Remove any selected blocks that are no longer valid:
invalid_block_keys
=
(
selected
-
valid_block_keys
)
if
invalid_block_keys
:
selected
-=
invalid_block_keys
# If max_count has been decreased, we may have to drop some previously selected blocks:
overlimit_block_keys
=
set
()
while
len
(
selected
)
>
max_count
:
overlimit_block_keys
.
add
(
selected
.
pop
())
# Do we have enough blocks now?
num_to_add
=
max_count
-
len
(
selected
)
added_block_keys
=
None
if
num_to_add
>
0
:
# We need to select [more] blocks to display to this user:
pool
=
valid_block_keys
-
selected
if
mode
==
"random"
:
num_to_add
=
min
(
len
(
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.
else
:
raise
NotImplementedError
(
"Unsupported mode."
)
selected
|=
added_block_keys
return
{
'selected'
:
selected
,
'invalid'
:
invalid_block_keys
,
'overlimit'
:
overlimit_block_keys
,
'added'
:
added_block_keys
,
}
def
_publish_event
(
self
,
event_name
,
result
,
**
kwargs
):
""" Helper method to publish an event for analytics purposes """
"""
Helper method to publish an event for analytics purposes
"""
event_data
=
{
"location"
:
unicode
(
self
.
location
),
"result"
:
result
,
...
...
@@ -146,6 +203,61 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
self
.
runtime
.
publish
(
self
,
"edx.librarycontentblock.content.{}"
.
format
(
event_name
),
event_data
)
self
.
_last_event_result_count
=
len
(
result
)
# pylint: disable=attribute-defined-outside-init
@classmethod
def
publish_selected_children_events
(
cls
,
block_keys
,
format_block_keys
,
publish_event
):
"""
Helper method for publishing events when children blocks are
selected/updated for a user. This helper is also used by
the ContentLibraryTransformer.
Arguments:
block_keys -
A dict describing which events to publish (add or
remove), see `make_selection` above for format details.
format_block_keys -
A function to convert block keys to the format expected
by publish_event. Must have the signature:
[(block_type, block_id)] -> T
Where T is a collection of block keys as accepted by
`publish_event`.
publish_event -
Function that handles the actual publishing. Must have
the signature:
<'removed'|'assigned'> -> result:T -> removed:T -> reason:basestring -> None
Where T is a collection of block_keys as returned by
`format_block_keys`.
"""
if
block_keys
[
'invalid'
]:
# reason "invalid" means deleted from library or a different library is now being used.
publish_event
(
"removed"
,
result
=
format_block_keys
(
block_keys
[
'selected'
]),
removed
=
format_block_keys
(
block_keys
[
'invalid'
]),
reason
=
"invalid"
)
if
block_keys
[
'overlimit'
]:
publish_event
(
"removed"
,
result
=
format_block_keys
(
block_keys
[
'selected'
]),
removed
=
format_block_keys
(
block_keys
[
'overlimit'
]),
reason
=
"overlimit"
)
if
block_keys
[
'added'
]:
publish_event
(
"assigned"
,
result
=
format_block_keys
(
block_keys
[
'selected'
]),
added
=
format_block_keys
(
block_keys
[
'added'
])
)
def
selected_children
(
self
):
"""
Returns a set() of block_ids indicating which of the possible children
...
...
@@ -161,61 +273,23 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
# Already done:
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
block_keys
=
self
.
make_selection
(
self
.
selected
,
self
.
children
,
self
.
max_count
,
"random"
)
# pylint: disable=no-member
# Publish events for analytics purposes:
lib_tools
=
self
.
runtime
.
service
(
self
,
'library_tools'
)
format_block_keys
=
lambda
keys
:
lib_tools
.
create_block_analytics_summary
(
self
.
location
.
course_key
,
keys
)
# Determine which of our children we will show:
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:
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.
self
.
_publish_event
(
"removed"
,
result
=
format_block_keys
(
selected
),
removed
=
format_block_keys
(
invalid_block_keys
),
reason
=
"invalid"
)
# 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
:
overlimit_block_keys
.
add
(
selected
.
pop
())
if
overlimit_block_keys
:
# Publish an event for analytics purposes:
self
.
_publish_event
(
"removed"
,
result
=
format_block_keys
(
selected
),
removed
=
format_block_keys
(
overlimit_block_keys
),
reason
=
"overlimit"
)
# Do we have enough blocks now?
num_to_add
=
self
.
max_count
-
len
(
selected
)
if
num_to_add
>
0
:
added_block_keys
=
None
# We need to select [more] blocks to display to this user:
pool
=
valid_block_keys
-
selected
if
self
.
mode
==
"random"
:
num_to_add
=
min
(
len
(
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.
else
:
raise
NotImplementedError
(
"Unsupported mode."
)
selected
|=
added_block_keys
if
added_block_keys
:
# Publish an event for analytics purposes:
self
.
_publish_event
(
"assigned"
,
result
=
format_block_keys
(
selected
),
added
=
format_block_keys
(
added_block_keys
)
self
.
publish_selected_children_events
(
block_keys
,
format_block_keys
,
self
.
_publish_event
,
)
# Save our selections to the user state, to ensure consistency:
selected
=
block_keys
[
'selected'
]
self
.
selected
=
list
(
selected
)
# TODO: this doesn't save from the LMS "Progress" page.
# Cache the results
self
.
_selected_set
=
selected
# pylint: disable=attribute-defined-outside-init
return
selected
def
_get_selected_child_blocks
(
self
):
...
...
lms/djangoapps/course_blocks/transformers/library_content.py
0 → 100644
View file @
a78b94d8
"""
Content Library Transformer.
"""
import
json
from
courseware.models
import
StudentModule
from
openedx.core.lib.block_cache.transformer
import
BlockStructureTransformer
from
xmodule.library_content_module
import
LibraryContentModule
from
xmodule.modulestore.django
import
modulestore
from
eventtracking
import
tracker
class
ContentLibraryTransformer
(
BlockStructureTransformer
):
"""
A transformer that manipulates the block structure by removing all
blocks within a library_content module to which a user should not
have access.
Staff users are *not* exempted from library content pathways.
"""
VERSION
=
1
@classmethod
def
name
(
cls
):
"""
Unique identifier for the transformer's class;
same identifier used in setup.py.
"""
return
"library_content"
@classmethod
def
collect
(
cls
,
block_structure
):
"""
Collects any information that's necessary to execute this
transformer's transform method.
"""
block_structure
.
request_xblock_fields
(
'mode'
)
block_structure
.
request_xblock_fields
(
'max_count'
)
block_structure
.
request_xblock_fields
(
'category'
)
store
=
modulestore
()
# needed for analytics purposes
def
summarize_block
(
usage_key
):
""" Basic information about the given block """
orig_key
,
orig_version
=
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
,
}
# For each block check if block is library_content.
# If library_content add children array to content_library_children field
for
block_key
in
block_structure
.
topological_traversal
(
filter_func
=
lambda
block_key
:
block_key
.
block_type
==
'library_content'
,
yield_descendants_of_unyielded
=
True
,
):
xblock
=
block_structure
.
get_xblock
(
block_key
)
for
child_key
in
xblock
.
children
:
summary
=
summarize_block
(
child_key
)
block_structure
.
set_transformer_block_field
(
child_key
,
cls
,
'block_analytics_summary'
,
summary
)
def
transform
(
self
,
usage_info
,
block_structure
):
"""
Mutates block_structure based on the given usage_info.
"""
all_library_children
=
set
()
all_selected_children
=
set
()
for
block_key
in
block_structure
.
topological_traversal
(
filter_func
=
lambda
block_key
:
block_key
.
block_type
==
'library_content'
,
yield_descendants_of_unyielded
=
True
,
):
library_children
=
block_structure
.
get_children
(
block_key
)
if
library_children
:
all_library_children
.
update
(
library_children
)
selected
=
[]
mode
=
block_structure
.
get_xblock_field
(
block_key
,
'mode'
)
max_count
=
block_structure
.
get_xblock_field
(
block_key
,
'max_count'
)
# Retrieve "selected" json from LMS MySQL database.
module
=
self
.
_get_student_module
(
usage_info
.
user
,
usage_info
.
course_key
,
block_key
)
if
module
:
state_dict
=
json
.
loads
(
module
.
state
)
# Add all selected entries for this user for this
# library module to the selected list.
for
state
in
state_dict
[
'selected'
]:
usage_key
=
usage_info
.
course_key
.
make_usage_key
(
state
[
0
],
state
[
1
])
if
usage_key
in
library_children
:
selected
.
append
((
state
[
0
],
state
[
1
]))
# update selected
previous_count
=
len
(
selected
)
block_keys
=
LibraryContentModule
.
make_selection
(
selected
,
library_children
,
max_count
,
mode
)
selected
=
block_keys
[
'selected'
]
# publish events for analytics
self
.
_publish_events
(
block_structure
,
block_key
,
previous_count
,
max_count
,
block_keys
)
all_selected_children
.
update
(
usage_info
.
course_key
.
make_usage_key
(
s
[
0
],
s
[
1
])
for
s
in
selected
)
def
check_child_removal
(
block_key
):
"""
Return True if selected block should be removed.
Block is removed if it is part of library_content, but has
not been selected for current user.
"""
if
block_key
not
in
all_library_children
:
return
False
if
block_key
in
all_selected_children
:
return
False
return
True
# Check and remove all non-selected children from course
# structure.
block_structure
.
remove_block_if
(
check_child_removal
)
@classmethod
def
_get_student_module
(
cls
,
user
,
course_key
,
block_key
):
"""
Get the student module for the given user for the given block.
Arguments:
user (User)
course_key (CourseLocator)
block_key (BlockUsageLocator)
Returns:
StudentModule if exists, or None.
"""
try
:
return
StudentModule
.
objects
.
get
(
student
=
user
,
course_id
=
course_key
,
module_state_key
=
block_key
,
state__contains
=
'"selected": [['
)
except
StudentModule
.
DoesNotExist
:
return
None
@classmethod
def
_publish_events
(
cls
,
block_structure
,
location
,
previous_count
,
max_count
,
block_keys
):
"""
Helper method to publish events for analytics purposes
"""
def
format_block_keys
(
keys
):
"""
Helper function to format block keys
"""
json_result
=
[]
for
key
in
keys
:
info
=
block_structure
.
get_transformer_block_field
(
key
,
ContentLibraryTransformer
,
'block_analytics_summary'
)
json_result
.
append
(
info
)
return
json_result
def
publish_event
(
event_name
,
result
,
**
kwargs
):
"""
Helper function to publish an event for analytics purposes
"""
event_data
=
{
"location"
:
unicode
(
location
),
"previous_count"
:
previous_count
,
"result"
:
result
,
"max_count"
:
max_count
}
event_data
.
update
(
kwargs
)
tracker
.
emit
(
"edx.librarycontentblock.content.{}"
.
format
(
event_name
),
event_data
)
LibraryContentModule
.
publish_selected_children_events
(
block_keys
,
format_block_keys
,
publish_event
,
)
lms/djangoapps/course_blocks/transformers/tests/test_library_content.py
0 → 100644
View file @
a78b94d8
"""
Tests for ContentLibraryTransformer.
"""
import
mock
from
student.tests.factories
import
CourseEnrollmentFactory
from
course_blocks.transformers.library_content
import
ContentLibraryTransformer
from
course_blocks.api
import
get_course_blocks
,
clear_course_from_cache
from
lms.djangoapps.course_blocks.transformers.tests.test_helpers
import
CourseStructureTestCase
class
MockedModule
(
object
):
"""
Object with mocked selected modules for user.
"""
def
__init__
(
self
,
state
):
"""
Set state attribute on initialize.
"""
self
.
state
=
state
class
ContentLibraryTransformerTestCase
(
CourseStructureTestCase
):
"""
ContentLibraryTransformer Test
"""
def
setUp
(
self
):
"""
Setup course structure and create user for content library transformer test.
"""
super
(
ContentLibraryTransformerTestCase
,
self
)
.
setUp
()
# Build course.
self
.
course_hierarchy
=
self
.
get_course_hierarchy
()
self
.
blocks
=
self
.
build_course
(
self
.
course_hierarchy
)
self
.
course
=
self
.
blocks
[
'course'
]
clear_course_from_cache
(
self
.
course
.
id
)
# Enroll user in course.
CourseEnrollmentFactory
.
create
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
,
is_active
=
True
)
self
.
selected_module
=
MockedModule
(
'{"selected": [["vertical", "vertical_vertical2"]]}'
)
self
.
transformer
=
ContentLibraryTransformer
()
def
get_course_hierarchy
(
self
):
"""
Get a course hierarchy to test with.
"""
return
[{
'org'
:
'ContentLibraryTransformer'
,
'course'
:
'CL101F'
,
'run'
:
'test_run'
,
'#type'
:
'course'
,
'#ref'
:
'course'
,
'#children'
:
[
{
'#type'
:
'chapter'
,
'#ref'
:
'chapter1'
,
'#children'
:
[
{
'#type'
:
'sequential'
,
'#ref'
:
'lesson1'
,
'#children'
:
[
{
'#type'
:
'vertical'
,
'#ref'
:
'vertical1'
,
'#children'
:
[
{
'metadata'
:
{
'category'
:
'library_content'
},
'#type'
:
'library_content'
,
'#ref'
:
'library_content1'
,
'#children'
:
[
{
'metadata'
:
{
'display_name'
:
"CL Vertical 2"
},
'#type'
:
'vertical'
,
'#ref'
:
'vertical2'
,
'#children'
:
[
{
'metadata'
:
{
'display_name'
:
"HTML1"
},
'#type'
:
'html'
,
'#ref'
:
'html1'
,
}
]
},
{
'metadata'
:
{
'display_name'
:
"CL Vertical 3"
},
'#type'
:
'vertical'
,
'#ref'
:
'vertical3'
,
'#children'
:
[
{
'metadata'
:
{
'display_name'
:
"HTML2"
},
'#type'
:
'html'
,
'#ref'
:
'html2'
,
}
]
}
]
}
],
}
],
}
],
}
]
}]
def
test_content_library
(
self
):
"""
Test when course has content library section.
First test user can't see any content library section,
and after that mock response from MySQL db.
Check user can see mocked sections in content library.
"""
raw_block_structure
=
get_course_blocks
(
self
.
user
,
self
.
course
.
location
,
transformers
=
{}
)
self
.
assertEqual
(
len
(
list
(
raw_block_structure
.
get_block_keys
())),
len
(
self
.
blocks
))
clear_course_from_cache
(
self
.
course
.
id
)
trans_block_structure
=
get_course_blocks
(
self
.
user
,
self
.
course
.
location
,
transformers
=
{
self
.
transformer
}
)
# Should dynamically assign a block to student
trans_keys
=
set
(
trans_block_structure
.
get_block_keys
())
block_key_set
=
self
.
get_block_key_set
(
self
.
blocks
,
'course'
,
'chapter1'
,
'lesson1'
,
'vertical1'
,
'library_content1'
)
for
key
in
block_key_set
:
self
.
assertIn
(
key
,
trans_keys
)
vertical2_selected
=
self
.
get_block_key_set
(
self
.
blocks
,
'vertical2'
)
.
pop
()
in
trans_keys
vertical3_selected
=
self
.
get_block_key_set
(
self
.
blocks
,
'vertical3'
)
.
pop
()
in
trans_keys
self
.
assertTrue
(
vertical2_selected
or
vertical3_selected
)
# Check course structure again, with mocked selected modules for a user.
with
mock
.
patch
(
'course_blocks.transformers.library_content.ContentLibraryTransformer._get_student_module'
,
return_value
=
self
.
selected_module
):
clear_course_from_cache
(
self
.
course
.
id
)
trans_block_structure
=
get_course_blocks
(
self
.
user
,
self
.
course
.
location
,
transformers
=
{
self
.
transformer
}
)
self
.
assertEqual
(
set
(
trans_block_structure
.
get_block_keys
()),
self
.
get_block_key_set
(
self
.
blocks
,
'course'
,
'chapter1'
,
'lesson1'
,
'vertical1'
,
'library_content1'
,
'vertical2'
,
'html1'
)
)
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