Commit 9e7b916f by Nimisha Asthagiri

Merge pull request #6797 from edx/mobile/perf_improvements

Mobile performance improvements
parents 296167e5 e9e59e85
...@@ -304,14 +304,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -304,14 +304,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
self._field_data.set_many(self, field_data) self._field_data.set_many(self, field_data)
del self.data del self.data
editable_fields = super(VideoDescriptor, self).editable_metadata_fields
self.source_visible = False 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 self.source:
# If `source` field value exist in the `html5_sources` field values, # If `source` field value exist in the `html5_sources` field values,
# then delete `source` field value and use value from `html5_sources` field. # then delete `source` field value and use value from `html5_sources` field.
...@@ -320,14 +313,17 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -320,14 +313,17 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
self.download_video = True self.download_video = True
else: # Otherwise, `source` field value will be used. else: # Otherwise, `source` field value will be used.
self.source_visible = True self.source_visible = True
if not download_video['explicitly_set']: if not self.fields['download_video'].is_set_on(self):
self.download_video = True 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. # for backward compatibility.
# If course was existed and was not re-imported by the moment of adding `download_track` field, # 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: # we should enable `download_track` if following is true:
download_track = editable_fields['download_track'] if not self.fields['download_track'].is_set_on(self) and self.track:
if not download_track['explicitly_set'] and self.track:
self.download_track = True self.download_track = True
def editor_saved(self, user, old_metadata, old_content): def editor_saved(self, user, old_metadata, old_content):
......
...@@ -403,7 +403,7 @@ class XModuleMixin(XBlockMixin): ...@@ -403,7 +403,7 @@ class XModuleMixin(XBlockMixin):
else: else:
return [self.display_name_with_default] 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 """Returns a list of XBlock instances for the children of
this module""" this module"""
...@@ -413,6 +413,9 @@ class XModuleMixin(XBlockMixin): ...@@ -413,6 +413,9 @@ class XModuleMixin(XBlockMixin):
if getattr(self, '_child_instances', None) is None: if getattr(self, '_child_instances', None) is None:
self._child_instances = [] # pylint: disable=attribute-defined-outside-init self._child_instances = [] # pylint: disable=attribute-defined-outside-init
for child_loc in self.children: for child_loc in self.children:
# Skip if it doesn't satisfy the filter function
if not usage_key_filter(child_loc):
continue
try: try:
child = self.runtime.get_block(child_loc) child = self.runtime.get_block(child_loc)
if child is None: if child is None:
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Video xmodule tests in mongo.""" """Video xmodule tests in mongo."""
import json import json
import unittest
from collections import OrderedDict from collections import OrderedDict
from mock import patch, PropertyMock, MagicMock from mock import patch, MagicMock
from django.conf import settings from django.conf import settings
from xblock.fields import ScopeIds from xmodule.video_module import create_youtube_string
from xblock.field_data import DictFieldData
from xmodule.video_module import create_youtube_string, VideoDescriptor
from xmodule.x_module import STUDENT_VIEW from xmodule.x_module import STUDENT_VIEW
from xmodule.tests import get_test_descriptor_system
from xmodule.tests.test_video import VideoDescriptorTestBase from xmodule.tests.test_video import VideoDescriptorTestBase
from edxval.api import ( from edxval.api import create_profile, create_video
ValVideoNotFoundError,
get_video_info,
create_profile,
create_video
)
import mock
from . import BaseTestXmodule from . import BaseTestXmodule
from .test_video_xml import SOURCE_XML from .test_video_xml import SOURCE_XML
...@@ -417,7 +406,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -417,7 +406,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
# it'll just fall back to the values in the VideoDescriptor. # it'll just fall back to the values in the VideoDescriptor.
self.assertIn("example_source.mp4", self.item_descriptor.render(STUDENT_VIEW).content) 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): def test_get_html_with_mocked_edx_video_id(self, mock_get_video_info):
mock_get_video_info.return_value = { mock_get_video_info.return_value = {
'url': '/edxval/video/example', 'url': '/edxval/video/example',
...@@ -798,81 +787,22 @@ class TestVideoDescriptorInitialization(BaseTestXmodule): ...@@ -798,81 +787,22 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
self.assertFalse(self.item_descriptor.source_visible) self.assertFalse(self.item_descriptor.source_visible)
def test_download_video_is_explicitly_set(self): def test_download_video_is_explicitly_set(self):
with patch( metadata = {
'xmodule.editing_module.TabsEditingDescriptor.editable_metadata_fields', 'track': u'http://some_track.srt',
new_callable=PropertyMock, 'source': 'http://example.org/video.mp4',
return_value={ 'html5_sources': ['http://youtu.be/3_yD_cEKoCk.mp4'],
'download_video': { 'download_video': False,
'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'],
}
self.initialize_module(metadata=metadata) self.initialize_module(metadata=metadata)
fields = self.item_descriptor.editable_metadata_fields fields = self.item_descriptor.editable_metadata_fields
self.assertIn('source', fields) self.assertIn('source', fields)
self.assertIn('download_video', fields)
self.assertFalse(self.item_descriptor.download_video) self.assertFalse(self.item_descriptor.download_video)
self.assertTrue(self.item_descriptor.source_visible) self.assertTrue(self.item_descriptor.source_visible)
self.assertTrue(self.item_descriptor.download_track) self.assertTrue(self.item_descriptor.download_track)
def test_source_is_empty(self): def test_source_is_empty(self):
metadata = { metadata = {
......
...@@ -90,23 +90,32 @@ class CourseStatusAPITestCase(MobileAPITestCase): ...@@ -90,23 +90,32 @@ class CourseStatusAPITestCase(MobileAPITestCase):
""" """
REVERSE_INFO = {'name': 'user-course-status', 'params': ['username', 'course_id']} 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 Creates a basic course structure for our course
""" """
section = ItemFactory.create( super(CourseStatusAPITestCase, self).setUp()
parent_location=self.course.location,
self.section = ItemFactory.create(
parent=self.course,
category='chapter',
)
self.sub_section = ItemFactory.create(
parent=self.section,
category='sequential',
) )
sub_section = ItemFactory.create( self.unit = ItemFactory.create(
parent_location=section.location, parent=self.sub_section,
category='vertical',
) )
unit = ItemFactory.create( self.other_sub_section = ItemFactory.create(
parent_location=sub_section.location, parent=self.section,
category='sequential',
) )
other_unit = ItemFactory.create( self.other_unit = ItemFactory.create(
parent_location=sub_section.location, parent=self.other_sub_section,
category='vertical',
) )
return section, sub_section, unit, other_unit
class TestCourseStatusGET(CourseStatusAPITestCase, MobileAuthUserTestMixin, MobileEnrolledCourseAccessTestMixin): class TestCourseStatusGET(CourseStatusAPITestCase, MobileAuthUserTestMixin, MobileEnrolledCourseAccessTestMixin):
...@@ -115,13 +124,15 @@ class TestCourseStatusGET(CourseStatusAPITestCase, MobileAuthUserTestMixin, Mobi ...@@ -115,13 +124,15 @@ class TestCourseStatusGET(CourseStatusAPITestCase, MobileAuthUserTestMixin, Mobi
""" """
def test_success(self): def test_success(self):
self.login_and_enroll() self.login_and_enroll()
(section, sub_section, unit, __) = self._setup_course_skeleton()
response = self.api_response() response = self.api_response()
self.assertEqual(response.data["last_visited_module_id"], unicode(unit.location))
self.assertEqual( self.assertEqual(
response.data["last_visited_module_path"], response.data["last_visited_module_id"], # pylint: disable=no-member
[unicode(module.location) for module in [unit, sub_section, section, self.course]] 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 ...@@ -135,37 +146,45 @@ class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin, Mo
def test_success(self): def test_success(self):
self.login_and_enroll() self.login_and_enroll()
(__, __, __, other_unit) = self._setup_course_skeleton() response = self.api_response(data={"last_visited_module_id": unicode(self.other_unit.location)})
self.assertEqual(
response = self.api_response(data={"last_visited_module_id": unicode(other_unit.location)}) response.data["last_visited_module_id"], # pylint: disable=no-member
self.assertEqual(response.data["last_visited_module_id"], unicode(other_unit.location)) unicode(self.other_sub_section.location)
)
def test_invalid_module(self): def test_invalid_module(self):
self.login_and_enroll() self.login_and_enroll()
response = self.api_response(data={"last_visited_module_id": "abc"}, expected_response_code=400) 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): def test_nonexistent_module(self):
self.login_and_enroll() self.login_and_enroll()
non_existent_key = self.course.id.make_usage_key('video', 'non-existent') 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) 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): def test_no_timezone(self):
self.login_and_enroll() self.login_and_enroll()
(__, __, __, other_unit) = self._setup_course_skeleton()
past_date = datetime.datetime.now() past_date = datetime.datetime.now()
response = self.api_response( response = self.api_response(
data={ 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 "modification_date": past_date.isoformat() # pylint: disable=maybe-no-member
}, },
expected_response_code=400 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 Helper for test cases that use a modification to decide whether
to update the course status to update the course status
...@@ -182,36 +201,41 @@ class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin, Mo ...@@ -182,36 +201,41 @@ class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin, Mo
"modification_date": date.isoformat() "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): def test_old_date(self):
self.login_and_enroll() self.login_and_enroll()
(__, __, unit, other_unit) = self._setup_course_skeleton()
date = timezone.now() + datetime.timedelta(days=-100) 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): def test_new_date(self):
self.login_and_enroll() self.login_and_enroll()
(__, __, unit, other_unit) = self._setup_course_skeleton()
date = timezone.now() + datetime.timedelta(days=100) 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): def test_no_initial_date(self):
self.login_and_enroll() self.login_and_enroll()
(__, __, _, other_unit) = self._setup_course_skeleton()
response = self.api_response( response = self.api_response(
data={ data={
"last_visited_module_id": unicode(other_unit.location), "last_visited_module_id": unicode(self.other_unit.location),
"modification_date": timezone.now().isoformat() "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): def test_invalid_date(self):
self.login_and_enroll() self.login_and_enroll()
response = self.api_response(data={"modification_date": "abc"}, expected_response_code=400) 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): class TestCourseEnrollmentSerializer(MobileAPITestCase):
......
...@@ -107,15 +107,14 @@ class UserCourseStatus(views.APIView): ...@@ -107,15 +107,14 @@ class UserCourseStatus(views.APIView):
course.id, request.user, course, depth=2) course.id, request.user, course, depth=2)
course_module = get_module_for_descriptor(request.user, request, course, field_data_cache, course.id) course_module = get_module_for_descriptor(request.user, request, course, field_data_cache, course.id)
current = course_module
path = [] path = [course_module]
child = current chapter = get_current_child(course_module, min_depth=2)
while child: if chapter is not None:
path.append(child) path.append(chapter)
child = get_current_child(current) section = get_current_child(chapter, min_depth=1)
if child: if section is not None:
current = child path.append(section)
path.reverse() path.reverse()
return path return path
...@@ -160,7 +159,7 @@ class UserCourseStatus(views.APIView): ...@@ -160,7 +159,7 @@ class UserCourseStatus(views.APIView):
save_positions_recursively_up(request.user, request, field_data_cache, module) save_positions_recursively_up(request.user, request, field_data_cache, module)
return self._get_course_info(request, course) 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 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. Get the ID of the module that the specified user last visited in the specified course.
...@@ -168,7 +167,7 @@ class UserCourseStatus(views.APIView): ...@@ -168,7 +167,7 @@ class UserCourseStatus(views.APIView):
return self._get_course_info(request, course) 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 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. Update the ID of the module that the specified user last visited in the specified course.
......
...@@ -5,11 +5,13 @@ Common utility methods and decorators for Mobile APIs. ...@@ -5,11 +5,13 @@ Common utility methods and decorators for Mobile APIs.
import functools 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 import permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication 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): def mobile_course_access(depth=0, verify_enrolled=True):
""" """
...@@ -25,13 +27,14 @@ 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. Raises 404 if access to course is disallowed.
""" """
course_id = CourseKey.from_string(kwargs.pop('course_id')) course_id = CourseKey.from_string(kwargs.pop('course_id'))
course = get_course_with_access( with modulestore().bulk_operations(course_id):
request.user, course = get_course_with_access(
'load_mobile' if verify_enrolled else 'load_mobile_no_enrollment_check', request.user,
course_id, 'load_mobile' if verify_enrolled else 'load_mobile_no_enrollment_check',
depth=depth course_id,
) depth=depth
return func(self, request, course=course, *args, **kwargs) )
return func(self, request, course=course, *args, **kwargs)
return _wrapper return _wrapper
return _decorator return _decorator
......
...@@ -3,6 +3,7 @@ Serializer for video outline ...@@ -3,6 +3,7 @@ Serializer for video outline
""" """
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from xmodule.modulestore.mongo.base import BLOCK_TYPES_WITH_CHILDREN
from courseware.access import has_access from courseware.access import has_access
from edxval.api import ( from edxval.api import (
...@@ -14,10 +15,10 @@ class BlockOutline(object): ...@@ -14,10 +15,10 @@ class BlockOutline(object):
""" """
Serializes course videos, pulling data from VAL and the video modules. 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.""" """Create a BlockOutline using `start_block` as a starting point."""
self.start_block = start_block self.start_block = start_block
self.categories_to_outliner = categories_to_outliner self.block_types = block_types
self.course_id = course_id self.course_id = course_id
self.request = request # needed for making full URLS self.request = request # needed for making full URLS
self.local_cache = {} self.local_cache = {}
...@@ -143,11 +144,11 @@ class BlockOutline(object): ...@@ -143,11 +144,11 @@ class BlockOutline(object):
# from the table-of-contents. # from the table-of-contents.
continue 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): if not has_access(user, 'load', curr_block, course_key=self.course_id):
continue continue
summary_fn = self.categories_to_outliner[curr_block.category] summary_fn = self.block_types[curr_block.category]
block_path = list(path(curr_block)) block_path = list(path(curr_block))
unit_url, section_url = find_urls(curr_block) unit_url, section_url = find_urls(curr_block)
...@@ -159,8 +160,17 @@ class BlockOutline(object): ...@@ -159,8 +160,17 @@ class BlockOutline(object):
"summary": summary_fn(self.course_id, curr_block, self.request, self.local_cache) "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: 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) stack.append(block)
child_to_parent[block] = curr_block child_to_parent[block] = curr_block
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment