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
9e7b916f
Commit
9e7b916f
authored
Feb 10, 2015
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #6797 from edx/mobile/perf_improvements
Mobile performance improvements
parents
296167e5
e9e59e85
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
155 additions
and
190 deletions
+155
-190
common/lib/xmodule/xmodule/video_module/video_module.py
+6
-10
common/lib/xmodule/xmodule/x_module.py
+4
-1
lms/djangoapps/courseware/tests/test_video_mongo.py
+17
-87
lms/djangoapps/mobile_api/users/tests.py
+59
-35
lms/djangoapps/mobile_api/users/views.py
+9
-10
lms/djangoapps/mobile_api/utils.py
+12
-9
lms/djangoapps/mobile_api/video_outlines/serializers.py
+15
-5
lms/djangoapps/mobile_api/video_outlines/tests.py
+33
-33
No files found.
common/lib/xmodule/xmodule/video_module/video_module.py
View file @
9e7b916f
...
...
@@ -304,14 +304,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
self
.
_field_data
.
set_many
(
self
,
field_data
)
del
self
.
data
editable_fields
=
super
(
VideoDescriptor
,
self
)
.
editable_metadata_fields
self
.
source_visible
=
False
# Set download_video field to default value if its not explicitly set for backward compatibility.
download_video
=
editable_fields
[
'download_video'
]
if
not
download_video
[
'explicitly_set'
]:
self
.
download_video
=
self
.
download_video
if
self
.
source
:
# If `source` field value exist in the `html5_sources` field values,
# then delete `source` field value and use value from `html5_sources` field.
...
...
@@ -320,14 +313,17 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
self
.
download_video
=
True
else
:
# Otherwise, `source` field value will be used.
self
.
source_visible
=
True
if
not
download_video
[
'explicitly_set'
]
:
if
not
self
.
fields
[
'download_video'
]
.
is_set_on
(
self
)
:
self
.
download_video
=
True
# Set download_video field to default value if its not explicitly set for backward compatibility.
if
not
self
.
fields
[
'download_video'
]
.
is_set_on
(
self
):
self
.
download_video
=
self
.
download_video
# for backward compatibility.
# If course was existed and was not re-imported by the moment of adding `download_track` field,
# we should enable `download_track` if following is true:
download_track
=
editable_fields
[
'download_track'
]
if
not
download_track
[
'explicitly_set'
]
and
self
.
track
:
if
not
self
.
fields
[
'download_track'
]
.
is_set_on
(
self
)
and
self
.
track
:
self
.
download_track
=
True
def
editor_saved
(
self
,
user
,
old_metadata
,
old_content
):
...
...
common/lib/xmodule/xmodule/x_module.py
View file @
9e7b916f
...
...
@@ -403,7 +403,7 @@ class XModuleMixin(XBlockMixin):
else
:
return
[
self
.
display_name_with_default
]
def
get_children
(
self
):
def
get_children
(
self
,
usage_key_filter
=
lambda
location
:
True
):
"""Returns a list of XBlock instances for the children of
this module"""
...
...
@@ -413,6 +413,9 @@ class XModuleMixin(XBlockMixin):
if
getattr
(
self
,
'_child_instances'
,
None
)
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
if
not
usage_key_filter
(
child_loc
):
continue
try
:
child
=
self
.
runtime
.
get_block
(
child_loc
)
if
child
is
None
:
...
...
lms/djangoapps/courseware/tests/test_video_mongo.py
View file @
9e7b916f
# -*- coding: utf-8 -*-
"""Video xmodule tests in mongo."""
import
json
import
unittest
from
collections
import
OrderedDict
from
mock
import
patch
,
PropertyMock
,
MagicMock
from
mock
import
patch
,
MagicMock
from
django.conf
import
settings
from
xblock.fields
import
ScopeIds
from
xblock.field_data
import
DictFieldData
from
xmodule.video_module
import
create_youtube_string
,
VideoDescriptor
from
xmodule.video_module
import
create_youtube_string
from
xmodule.x_module
import
STUDENT_VIEW
from
xmodule.tests
import
get_test_descriptor_system
from
xmodule.tests.test_video
import
VideoDescriptorTestBase
from
edxval.api
import
(
ValVideoNotFoundError
,
get_video_info
,
create_profile
,
create_video
)
import
mock
from
edxval.api
import
create_profile
,
create_video
from
.
import
BaseTestXmodule
from
.test_video_xml
import
SOURCE_XML
...
...
@@ -417,7 +406,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
# it'll just fall back to the values in the VideoDescriptor.
self
.
assertIn
(
"example_source.mp4"
,
self
.
item_descriptor
.
render
(
STUDENT_VIEW
)
.
content
)
@
mock.
patch
(
'edxval.api.get_video_info'
)
@patch
(
'edxval.api.get_video_info'
)
def
test_get_html_with_mocked_edx_video_id
(
self
,
mock_get_video_info
):
mock_get_video_info
.
return_value
=
{
'url'
:
'/edxval/video/example'
,
...
...
@@ -798,81 +787,22 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
self
.
assertFalse
(
self
.
item_descriptor
.
source_visible
)
def
test_download_video_is_explicitly_set
(
self
):
with
patch
(
'xmodule.editing_module.TabsEditingDescriptor.editable_metadata_fields'
,
new_callable
=
PropertyMock
,
return_value
=
{
'download_video'
:
{
'default_value'
:
False
,
'explicitly_set'
:
True
,
'display_name'
:
'Video Download Allowed'
,
'help'
:
'Show a link beneath the video to allow students to download the video.'
,
'type'
:
'Boolean'
,
'value'
:
False
,
'field_name'
:
'download_video'
,
'options'
:
[
{
'display_name'
:
"True"
,
"value"
:
True
},
{
'display_name'
:
"False"
,
"value"
:
False
}
],
},
'html5_sources'
:
{
'default_value'
:
[],
'explicitly_set'
:
False
,
'display_name'
:
'Video Sources'
,
'help'
:
'A list of filenames to be used with HTML5 video.'
,
'type'
:
'List'
,
'value'
:
[
u'http://youtu.be/3_yD_cEKoCk.mp4'
],
'field_name'
:
'html5_sources'
,
'options'
:
[],
},
'source'
:
{
'default_value'
:
''
,
'explicitly_set'
:
False
,
'display_name'
:
'Download Video'
,
'help'
:
'The external URL to download the video.'
,
'type'
:
'Generic'
,
'value'
:
u'http://example.org/video.mp4'
,
'field_name'
:
'source'
,
'options'
:
[],
},
'track'
:
{
'default_value'
:
''
,
'explicitly_set'
:
False
,
'display_name'
:
'Download Transcript'
,
'help'
:
'The external URL to download the timed transcript track.'
,
'type'
:
'Generic'
,
'value'
:
u'http://some_track.srt'
,
'field_name'
:
'track'
,
'options'
:
[],
},
'download_track'
:
{
'default_value'
:
False
,
'explicitly_set'
:
False
,
'display_name'
:
'Transcript Download Allowed'
,
'help'
:
'Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.'
,
'type'
:
'Generic'
,
'value'
:
False
,
'field_name'
:
'download_track'
,
'options'
:
[],
},
'transcripts'
:
{},
'handout'
:
{},
}
):
metadata
=
{
'track'
:
u'http://some_track.srt'
,
'source'
:
'http://example.org/video.mp4'
,
'html5_sources'
:
[
'http://youtu.be/3_yD_cEKoCk.mp4'
],
}
metadata
=
{
'track'
:
u'http://some_track.srt'
,
'source'
:
'http://example.org/video.mp4'
,
'html5_sources'
:
[
'http://youtu.be/3_yD_cEKoCk.mp4'
],
'download_video'
:
False
,
}
self
.
initialize_module
(
metadata
=
metadata
)
self
.
initialize_module
(
metadata
=
metadata
)
fields
=
self
.
item_descriptor
.
editable_metadata_fields
self
.
assertIn
(
'source'
,
fields
)
fields
=
self
.
item_descriptor
.
editable_metadata_fields
self
.
assertIn
(
'source'
,
fields
)
self
.
assertIn
(
'download_video'
,
fields
)
self
.
assertFalse
(
self
.
item_descriptor
.
download_video
)
self
.
assertTrue
(
self
.
item_descriptor
.
source_visible
)
self
.
assertTrue
(
self
.
item_descriptor
.
download_track
)
self
.
assertFalse
(
self
.
item_descriptor
.
download_video
)
self
.
assertTrue
(
self
.
item_descriptor
.
source_visible
)
self
.
assertTrue
(
self
.
item_descriptor
.
download_track
)
def
test_source_is_empty
(
self
):
metadata
=
{
...
...
lms/djangoapps/mobile_api/users/tests.py
View file @
9e7b916f
...
...
@@ -90,23 +90,32 @@ class CourseStatusAPITestCase(MobileAPITestCase):
"""
REVERSE_INFO
=
{
'name'
:
'user-course-status'
,
'params'
:
[
'username'
,
'course_id'
]}
def
_setup_course_skeleton
(
self
):
def
setUp
(
self
):
"""
Creates a basic course structure for our course
"""
section
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
super
(
CourseStatusAPITestCase
,
self
)
.
setUp
()
self
.
section
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
'chapter'
,
)
self
.
sub_section
=
ItemFactory
.
create
(
parent
=
self
.
section
,
category
=
'sequential'
,
)
sub_section
=
ItemFactory
.
create
(
parent_location
=
section
.
location
,
self
.
unit
=
ItemFactory
.
create
(
parent
=
self
.
sub_section
,
category
=
'vertical'
,
)
unit
=
ItemFactory
.
create
(
parent_location
=
sub_section
.
location
,
self
.
other_sub_section
=
ItemFactory
.
create
(
parent
=
self
.
section
,
category
=
'sequential'
,
)
other_unit
=
ItemFactory
.
create
(
parent_location
=
sub_section
.
location
,
self
.
other_unit
=
ItemFactory
.
create
(
parent
=
self
.
other_sub_section
,
category
=
'vertical'
,
)
return
section
,
sub_section
,
unit
,
other_unit
class
TestCourseStatusGET
(
CourseStatusAPITestCase
,
MobileAuthUserTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
...
...
@@ -115,13 +124,15 @@ class TestCourseStatusGET(CourseStatusAPITestCase, MobileAuthUserTestMixin, Mobi
"""
def
test_success
(
self
):
self
.
login_and_enroll
()
(
section
,
sub_section
,
unit
,
__
)
=
self
.
_setup_course_skeleton
()
response
=
self
.
api_response
()
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
unit
.
location
))
self
.
assertEqual
(
response
.
data
[
"last_visited_module_path"
],
[
unicode
(
module
.
location
)
for
module
in
[
unit
,
sub_section
,
section
,
self
.
course
]]
response
.
data
[
"last_visited_module_id"
],
# pylint: disable=no-member
unicode
(
self
.
sub_section
.
location
)
)
self
.
assertEqual
(
response
.
data
[
"last_visited_module_path"
],
# pylint: disable=no-member
[
unicode
(
module
.
location
)
for
module
in
[
self
.
sub_section
,
self
.
section
,
self
.
course
]]
)
...
...
@@ -135,37 +146,45 @@ class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin, Mo
def
test_success
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
__
,
other_unit
)
=
self
.
_setup_course_skeleton
()
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
)})
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
self
.
other_unit
.
location
)})
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
# pylint: disable=no-member
unicode
(
self
.
other_sub_section
.
location
)
)
def
test_invalid_module
(
self
):
self
.
login_and_enroll
()
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
"abc"
},
expected_response_code
=
400
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODULE_ID
)
self
.
assertEqual
(
response
.
data
,
# pylint: disable=no-member
errors
.
ERROR_INVALID_MODULE_ID
)
def
test_nonexistent_module
(
self
):
self
.
login_and_enroll
()
non_existent_key
=
self
.
course
.
id
.
make_usage_key
(
'video'
,
'non-existent'
)
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
non_existent_key
},
expected_response_code
=
400
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODULE_ID
)
self
.
assertEqual
(
response
.
data
,
# pylint: disable=no-member
errors
.
ERROR_INVALID_MODULE_ID
)
def
test_no_timezone
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
__
,
other_unit
)
=
self
.
_setup_course_skeleton
()
past_date
=
datetime
.
datetime
.
now
()
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
),
"last_visited_module_id"
:
unicode
(
self
.
other_unit
.
location
),
"modification_date"
:
past_date
.
isoformat
()
# pylint: disable=maybe-no-member
},
expected_response_code
=
400
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
self
.
assertEqual
(
response
.
data
,
# pylint: disable=no-member
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
def
_date_sync
(
self
,
date
,
initial_unit
,
update_unit
,
expected_
unit
):
def
_date_sync
(
self
,
date
,
initial_unit
,
update_unit
,
expected_
subsection
):
"""
Helper for test cases that use a modification to decide whether
to update the course status
...
...
@@ -182,36 +201,41 @@ class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin, Mo
"modification_date"
:
date
.
isoformat
()
}
)
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
expected_unit
.
location
))
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
# pylint: disable=no-member
unicode
(
expected_subsection
.
location
)
)
def
test_old_date
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
unit
,
other_unit
)
=
self
.
_setup_course_skeleton
()
date
=
timezone
.
now
()
+
datetime
.
timedelta
(
days
=-
100
)
self
.
_date_sync
(
date
,
unit
,
other_unit
,
unit
)
self
.
_date_sync
(
date
,
self
.
unit
,
self
.
other_unit
,
self
.
sub_section
)
def
test_new_date
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
unit
,
other_unit
)
=
self
.
_setup_course_skeleton
()
date
=
timezone
.
now
()
+
datetime
.
timedelta
(
days
=
100
)
self
.
_date_sync
(
date
,
unit
,
other_unit
,
other_unit
)
self
.
_date_sync
(
date
,
self
.
unit
,
self
.
other_unit
,
self
.
other_sub_section
)
def
test_no_initial_date
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
_
,
other_unit
)
=
self
.
_setup_course_skeleton
()
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
),
"last_visited_module_id"
:
unicode
(
self
.
other_unit
.
location
),
"modification_date"
:
timezone
.
now
()
.
isoformat
()
}
)
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
# pylint: disable=no-member
unicode
(
self
.
other_sub_section
.
location
)
)
def
test_invalid_date
(
self
):
self
.
login_and_enroll
()
response
=
self
.
api_response
(
data
=
{
"modification_date"
:
"abc"
},
expected_response_code
=
400
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
self
.
assertEqual
(
response
.
data
,
# pylint: disable=no-member
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
class
TestCourseEnrollmentSerializer
(
MobileAPITestCase
):
...
...
lms/djangoapps/mobile_api/users/views.py
View file @
9e7b916f
...
...
@@ -107,15 +107,14 @@ class UserCourseStatus(views.APIView):
course
.
id
,
request
.
user
,
course
,
depth
=
2
)
course_module
=
get_module_for_descriptor
(
request
.
user
,
request
,
course
,
field_data_cache
,
course
.
id
)
current
=
course_module
path
=
[]
ch
ild
=
current
while
child
:
path
.
append
(
ch
ild
)
child
=
get_current_child
(
current
)
if
child
:
current
=
child
path
=
[
course_module
]
ch
apter
=
get_current_child
(
course_module
,
min_depth
=
2
)
if
chapter
is
not
None
:
path
.
append
(
ch
apter
)
section
=
get_current_child
(
chapter
,
min_depth
=
1
)
if
section
is
not
None
:
path
.
append
(
section
)
path
.
reverse
()
return
path
...
...
@@ -160,7 +159,7 @@ class UserCourseStatus(views.APIView):
save_positions_recursively_up
(
request
.
user
,
request
,
field_data_cache
,
module
)
return
self
.
_get_course_info
(
request
,
course
)
@mobile_course_access
()
@mobile_course_access
(
depth
=
2
)
def
get
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Get the ID of the module that the specified user last visited in the specified course.
...
...
@@ -168,7 +167,7 @@ class UserCourseStatus(views.APIView):
return
self
.
_get_course_info
(
request
,
course
)
@mobile_course_access
()
@mobile_course_access
(
depth
=
2
)
def
patch
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Update the ID of the module that the specified user last visited in the specified course.
...
...
lms/djangoapps/mobile_api/utils.py
View file @
9e7b916f
...
...
@@ -5,11 +5,13 @@ Common utility methods and decorators for Mobile APIs.
import
functools
from
opaque_keys.edx.keys
import
CourseKey
from
courseware.courses
import
get_course_with_access
from
rest_framework
import
permissions
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
opaque_keys.edx.keys
import
CourseKey
from
xmodule.modulestore.django
import
modulestore
from
courseware.courses
import
get_course_with_access
def
mobile_course_access
(
depth
=
0
,
verify_enrolled
=
True
):
"""
...
...
@@ -25,13 +27,14 @@ def mobile_course_access(depth=0, verify_enrolled=True):
Raises 404 if access to course is disallowed.
"""
course_id
=
CourseKey
.
from_string
(
kwargs
.
pop
(
'course_id'
))
course
=
get_course_with_access
(
request
.
user
,
'load_mobile'
if
verify_enrolled
else
'load_mobile_no_enrollment_check'
,
course_id
,
depth
=
depth
)
return
func
(
self
,
request
,
course
=
course
,
*
args
,
**
kwargs
)
with
modulestore
()
.
bulk_operations
(
course_id
):
course
=
get_course_with_access
(
request
.
user
,
'load_mobile'
if
verify_enrolled
else
'load_mobile_no_enrollment_check'
,
course_id
,
depth
=
depth
)
return
func
(
self
,
request
,
course
=
course
,
*
args
,
**
kwargs
)
return
_wrapper
return
_decorator
...
...
lms/djangoapps/mobile_api/video_outlines/serializers.py
View file @
9e7b916f
...
...
@@ -3,6 +3,7 @@ Serializer for video outline
"""
from
rest_framework.reverse
import
reverse
from
xmodule.modulestore.mongo.base
import
BLOCK_TYPES_WITH_CHILDREN
from
courseware.access
import
has_access
from
edxval.api
import
(
...
...
@@ -14,10 +15,10 @@ class BlockOutline(object):
"""
Serializes course videos, pulling data from VAL and the video modules.
"""
def
__init__
(
self
,
course_id
,
start_block
,
categories_to_outliner
,
request
):
def
__init__
(
self
,
course_id
,
start_block
,
block_types
,
request
):
"""Create a BlockOutline using `start_block` as a starting point."""
self
.
start_block
=
start_block
self
.
categories_to_outliner
=
categories_to_outliner
self
.
block_types
=
block_types
self
.
course_id
=
course_id
self
.
request
=
request
# needed for making full URLS
self
.
local_cache
=
{}
...
...
@@ -143,11 +144,11 @@ class BlockOutline(object):
# from the table-of-contents.
continue
if
curr_block
.
category
in
self
.
categories_to_outliner
:
if
curr_block
.
location
.
block_type
in
self
.
block_types
:
if
not
has_access
(
user
,
'load'
,
curr_block
,
course_key
=
self
.
course_id
):
continue
summary_fn
=
self
.
categories_to_outliner
[
curr_block
.
category
]
summary_fn
=
self
.
block_types
[
curr_block
.
category
]
block_path
=
list
(
path
(
curr_block
))
unit_url
,
section_url
=
find_urls
(
curr_block
)
...
...
@@ -159,8 +160,17 @@ class BlockOutline(object):
"summary"
:
summary_fn
(
self
.
course_id
,
curr_block
,
self
.
request
,
self
.
local_cache
)
}
def
parent_or_requested_block_type
(
usage_key
):
"""
Returns whether the usage_key's block_type is one of self.block_types or a parent type.
"""
return
(
usage_key
.
block_type
in
self
.
block_types
or
usage_key
.
block_type
in
BLOCK_TYPES_WITH_CHILDREN
)
if
curr_block
.
has_children
:
for
block
in
reversed
(
curr_block
.
get_children
()):
for
block
in
reversed
(
curr_block
.
get_children
(
usage_key_filter
=
parent_or_requested_block_type
)):
stack
.
append
(
block
)
child_to_parent
[
block
]
=
curr_block
...
...
lms/djangoapps/mobile_api/video_outlines/tests.py
View file @
9e7b916f
This diff is collapsed.
Click to expand it.
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