diff --git a/cms/djangoapps/contentstore/api/views/course_quality.py b/cms/djangoapps/contentstore/api/views/course_quality.py
index d1b90a83e5..5553a96da6 100644
--- a/cms/djangoapps/contentstore/api/views/course_quality.py
+++ b/cms/djangoapps/contentstore/api/views/course_quality.py
@@ -191,7 +191,8 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
def _videos_quality(self, course):
video_blocks_in_course = modulestore().get_items(course.id, qualifiers={'category': 'video'})
- videos_in_val = list(get_videos_for_course(course.id))
+ videos, __ = get_videos_for_course(course.id)
+ videos_in_val = list(videos)
video_durations = [video['duration'] for video in videos_in_val]
return dict(
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 1ad872db72..cf71230201 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -1953,7 +1953,8 @@ class RerunCourseTest(ContentStoreTestCase):
source_course = CourseFactory.create()
destination_course_key = self.post_rerun_request(source_course.id)
self.verify_rerun_course(source_course.id, destination_course_key, self.destination_course_data['display_name'])
- videos = list(get_videos_for_course(text_type(destination_course_key)))
+ videos, __ = get_videos_for_course(text_type(destination_course_key))
+ videos = list(videos)
self.assertEqual(0, len(videos))
self.assertInCourseListing(destination_course_key)
@@ -1989,8 +1990,10 @@ class RerunCourseTest(ContentStoreTestCase):
self.verify_rerun_course(source_course.id, destination_course_key, self.destination_course_data['display_name'])
# Verify that the VAL copies videos to the rerun
- source_videos = list(get_videos_for_course(text_type(source_course.id)))
- target_videos = list(get_videos_for_course(text_type(destination_course_key)))
+ videos, __ = get_videos_for_course(text_type(source_course.id))
+ source_videos = list(videos)
+ videos, __ = get_videos_for_course(text_type(destination_course_key))
+ target_videos = list(videos)
self.assertEqual(1, len(source_videos))
self.assertEqual(source_videos, target_videos)
diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py
index f826cabee9..115668333e 100644
--- a/cms/djangoapps/contentstore/views/tests/test_videos.py
+++ b/cms/djangoapps/contentstore/views/tests/test_videos.py
@@ -30,6 +30,7 @@ from contentstore.utils import reverse_course_url
from contentstore.views.videos import (
_get_default_video_image_url,
VIDEO_IMAGE_UPLOAD_ENABLED,
+ ENABLE_VIDEO_UPLOAD_PAGINATION,
WAFFLE_SWITCHES,
TranscriptProvider
)
@@ -39,6 +40,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from openedx.core.djangoapps.video_pipeline.config.waffle import waffle_flags, DEPRECATE_YOUTUBE
from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
+from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from edxval.api import create_or_update_transcript_preferences, get_transcript_preferences
from waffle.testutils import override_flag
@@ -326,6 +328,16 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
# Crude check for presence of data in returned HTML
for video in self.previous_uploads:
self.assertIn(video["edx_video_id"], response.content)
+ self.assertNotIn('video_upload_pagination', response.content)
+
+ @override_waffle_flag(ENABLE_VIDEO_UPLOAD_PAGINATION, active=True)
+ def test_get_html_paginated(self):
+ """
+ Tests that response is paginated.
+ """
+ response = self.client.get(self.url)
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('video_upload_pagination', response.content)
def test_post_non_json(self):
response = self.client.post(self.url, {"files": []})
diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py
index 7cb981ded9..572fc3365d 100644
--- a/cms/djangoapps/contentstore/views/videos.py
+++ b/cms/djangoapps/contentstore/views/videos.py
@@ -43,7 +43,7 @@ from contentstore.video_utils import validate_video_image
from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag
from openedx.core.djangoapps.video_pipeline.config.waffle import waffle_flags, DEPRECATE_YOUTUBE
-from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
+from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleSwitchNamespace, WaffleFlagNamespace
from util.json_request import JsonResponse, expect_json
from .course import get_course_and_check_access
@@ -64,6 +64,14 @@ WAFFLE_SWITCHES = WaffleSwitchNamespace(name=WAFFLE_NAMESPACE)
# Waffle switch for enabling/disabling video image upload feature
VIDEO_IMAGE_UPLOAD_ENABLED = 'video_image_upload_enabled'
+# Waffle flag namespace for studio
+WAFFLE_STUDIO_FLAG_NAMESPACE = WaffleFlagNamespace(name=u'studio')
+
+ENABLE_VIDEO_UPLOAD_PAGINATION = CourseWaffleFlag(
+ waffle_namespace=WAFFLE_STUDIO_FLAG_NAMESPACE,
+ flag_name=u'enable_video_upload_pagination',
+ flag_undefined_default=False
+)
# Default expiration, in seconds, of one-time URLs used for uploading videos.
KEY_EXPIRATION_IN_SECONDS = 86400
@@ -77,6 +85,8 @@ VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5
# maximum time for video to remain in upload state
MAX_UPLOAD_HOURS = 24
+VIDEOS_PER_PAGE = 100
+
class TranscriptProvider(object):
"""
@@ -176,14 +186,16 @@ def videos_handler(request, course_key_string, edx_video_id=None):
if request.method == "GET":
if "application/json" in request.META.get("HTTP_ACCEPT", ""):
return videos_index_json(course)
- else:
- return videos_index_html(course)
+ pagination_conf = _generate_pagination_configuration(course_key_string, request)
+ return videos_index_html(course, pagination_conf)
elif request.method == "DELETE":
remove_video_for_course(course_key_string, edx_video_id)
return JsonResponse()
else:
if is_status_update_request(request.json):
return send_video_status_update(request.json)
+ elif _is_pagination_context_update_request(request):
+ return _update_pagination_context(request)
return videos_post(course, request)
@@ -361,8 +373,8 @@ def video_encodings_download(request, course_key_string):
return _("{profile_name} URL").format(profile_name=profile)
profile_whitelist = VideoUploadConfig.get_profile_whitelist()
-
- videos = list(_get_videos(course))
+ videos, __ = _get_videos(course)
+ videos = list(videos)
name_col = _("Name")
duration_col = _("Duration")
added_col = _("Date Added")
@@ -479,11 +491,17 @@ def convert_video_status(video, is_video_encodes_ready=False):
return status
-def _get_videos(course):
+def _get_videos(course, pagination_conf=None):
"""
Retrieves the list of videos from VAL corresponding to this course.
"""
- videos = list(get_videos_for_course(unicode(course.id), VideoSortField.created, SortDirection.desc))
+ videos, pagination_context = get_videos_for_course(
+ unicode(course.id),
+ VideoSortField.created,
+ SortDirection.desc,
+ pagination_conf
+ )
+ videos = list(videos)
# This is required to see if edx video pipeline is enabled while converting the video status.
course_video_upload_token = course.video_upload_pipeline.get('course_video_upload_token')
@@ -506,7 +524,7 @@ def _get_videos(course):
# Convert the video status.
video['status'] = convert_video_status(video, is_video_encodes_ready)
- return videos
+ return videos, pagination_context
def _get_default_video_image_url():
@@ -516,7 +534,7 @@ def _get_default_video_image_url():
return staticfiles_storage.url(settings.VIDEO_IMAGE_DEFAULT_FILENAME)
-def _get_index_videos(course):
+def _get_index_videos(course, pagination_conf=None):
"""
Returns the information about each video upload required for the video list
"""
@@ -539,10 +557,10 @@ def _get_index_videos(course):
values[attr] = video[attr]
return values
-
+ videos, pagination_context = _get_videos(course, pagination_conf)
return [
- _get_values(video) for video in _get_videos(course)
- ]
+ _get_values(video) for video in videos
+ ], pagination_context
def get_all_transcript_languages():
@@ -570,18 +588,19 @@ def get_all_transcript_languages():
return all_languages
-def videos_index_html(course):
+def videos_index_html(course, pagination_conf=None):
"""
Returns an HTML page to display previous video uploads and allow new ones
"""
is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course.id)
+ previous_uploads, pagination_context = _get_index_videos(course, pagination_conf)
context = {
'context_course': course,
'image_upload_url': reverse_course_url('video_images_handler', unicode(course.id)),
'video_handler_url': reverse_course_url('videos_handler', unicode(course.id)),
'encodings_download_url': reverse_course_url('video_encodings_download', unicode(course.id)),
'default_video_image_url': _get_default_video_image_url(),
- 'previous_uploads': _get_index_videos(course),
+ 'previous_uploads': previous_uploads,
'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0),
'video_supported_file_formats': VIDEO_SUPPORTED_FILE_FORMATS.keys(),
'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB,
@@ -602,7 +621,8 @@ def videos_index_html(course):
'transcript_upload_handler_url': reverse('transcript_upload_handler'),
'transcript_delete_handler_url': reverse_course_url('transcript_delete_handler', unicode(course.id)),
'trancript_download_file_format': Transcript.SRT
- }
+ },
+ 'pagination_context': pagination_context
}
if is_video_transcript_enabled:
@@ -638,7 +658,8 @@ def videos_index_json(course):
}]
}
"""
- return JsonResponse({"videos": _get_index_videos(course)}, status=200)
+ index_videos, __ = _get_index_videos(course)
+ return JsonResponse({"videos": index_videos}, status=200)
def videos_post(course, request):
@@ -784,3 +805,39 @@ def is_status_update_request(request_data):
Returns True if `request_data` contains status update else False.
"""
return any('status' in update for update in request_data)
+
+
+def _generate_pagination_configuration(course_key_string, request):
+ """
+ Returns pagination configuration
+ """
+ course_key = CourseKey.from_string(course_key_string)
+ if not ENABLE_VIDEO_UPLOAD_PAGINATION.is_enabled(course_key):
+ return None
+ return {
+ 'page_number': request.GET.get('page', 1),
+ 'videos_per_page': request.session.get("VIDEOS_PER_PAGE", VIDEOS_PER_PAGE)
+ }
+
+
+def _is_pagination_context_update_request(request):
+ """
+ Checks if request contains `videos_per_page`
+ """
+ return request.POST.get('id', '') == "videos_per_page"
+
+
+def _update_pagination_context(request):
+ """
+ Updates session with posted value
+ """
+ error_msg = _(u'A non zero postive integar is expected')
+ try:
+ videos_per_page = int(request.POST.get('value'))
+ if videos_per_page <= 0:
+ return JsonResponse({'error': error_msg}, status=500)
+ except ValueError:
+ return JsonResponse({'error': error_msg}, status=500)
+
+ request.session['VIDEOS_PER_PAGE'] = videos_per_page
+ return JsonResponse()
diff --git a/cms/templates/videos_index.html b/cms/templates/videos_index.html
index 55934add59..0ab0ef96c5 100644
--- a/cms/templates/videos_index.html
+++ b/cms/templates/videos_index.html
@@ -75,4 +75,8 @@
+% if pagination_context:
+ <%include file="videos_index_pagination.html"/>
+% endif
+
%block>
diff --git a/cms/templates/videos_index_pagination.html b/cms/templates/videos_index_pagination.html
new file mode 100644
index 0000000000..aece51624a
--- /dev/null
+++ b/cms/templates/videos_index_pagination.html
@@ -0,0 +1,40 @@
+<%page expression_filter="h"/>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.js_utils import dump_js_escaped_json
+%>
+
+
+
+
+
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index e670657dfe..357a2b700f 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -49,7 +49,8 @@ argparse==1.4.0
asn1crypto==0.24.0
attrs==17.4.0
babel==1.3
-beautifulsoup4==4.6.3 # via pynliner
+backports.functools-lru-cache==1.5 # via soupsieve
+beautifulsoup4==4.7.0 # via pynliner
billiard==3.3.0.23 # via celery
bleach==2.1.4
boto3==1.4.8
@@ -89,7 +90,7 @@ django-oauth-toolkit==1.1.3
django-object-actions==0.10.0 # via edx-enterprise
django-pyfs==2.0
django-ratelimit-backend==1.1.1
-django-ratelimit==1.1.0
+django-ratelimit==2.0.0
django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==0.10.0
@@ -109,7 +110,7 @@ docopt==0.6.2
docutils==0.14 # via botocore
dogapi==1.2.1
edx-ace==0.1.10
-edx-analytics-data-api-client==0.14.4
+edx-analytics-data-api-client==0.15.2
edx-ccx-keys==0.2.1
edx-celeryutils==0.2.7
edx-completion==1.0.1
@@ -129,7 +130,7 @@ edx-rest-api-client==1.9.2
edx-search==1.2.1
edx-submissions==2.0.12
edx-user-state-client==1.0.4
-edxval==0.1.23
+edxval==0.1.25
elasticsearch==1.9.0 # via edx-search
enum34==1.1.6
event-tracking==0.2.7
@@ -228,6 +229,7 @@ social-auth-app-django==2.1.0
social-auth-core==1.7.0
sorl-thumbnail==12.3
sortedcontainers==0.9.2
+soupsieve==1.6.2 # via beautifulsoup4
sqlparse==0.2.4
stevedore==1.10.0
sympy==0.7.1
@@ -240,7 +242,7 @@ voluptuous==0.11.5
watchdog==0.9.0
web-fragments==0.2.2
webencodings==0.5.1 # via html5lib
-webob==1.8.4 # via xblock
+webob==1.8.5 # via xblock
wrapt==1.10.5
xblock-review==1.1.5
xblock-utils==1.2.0
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index cfaf668b26..2ee0fc734d 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -57,7 +57,7 @@ atomicwrites==1.2.1
attrs==17.4.0
babel==1.3
backports.functools-lru-cache==1.5
-beautifulsoup4==4.6.3
+beautifulsoup4==4.7.0
before-after==1.0.1
billiard==3.3.0.23
bleach==2.1.4
@@ -108,7 +108,7 @@ django-oauth-toolkit==1.1.3
django-object-actions==0.10.0
django-pyfs==2.0
django-ratelimit-backend==1.1.1
-django-ratelimit==1.1.0
+django-ratelimit==2.0.0
django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==0.10.0
@@ -128,7 +128,7 @@ docopt==0.6.2
docutils==0.14
dogapi==1.2.1
edx-ace==0.1.10
-edx-analytics-data-api-client==0.14.4
+edx-analytics-data-api-client==0.15.2
edx-ccx-keys==0.2.1
edx-celeryutils==0.2.7
edx-completion==1.0.1
@@ -150,7 +150,7 @@ edx-search==1.2.1
edx-sphinx-theme==1.4.0
edx-submissions==2.0.12
edx-user-state-client==1.0.4
-edxval==0.1.23
+edxval==0.1.25
elasticsearch==1.9.0
enum34==1.1.6
event-tracking==0.2.7
@@ -213,7 +213,7 @@ mccabe==0.6.1
mock==1.0.1
modernize==0.6.1
mongoengine==0.10.0
-more-itertools==4.3.0
+more-itertools==5.0.0
moto==0.3.1
mysql-python==1.2.5
networkx==1.7
@@ -241,8 +241,8 @@ polib==1.1.0
psutil==1.2.1
py2neo==3.1.2
py==1.7.0
-pyasn1-modules==0.2.2
-pyasn1==0.4.4
+pyasn1-modules==0.2.3
+pyasn1==0.4.5
pycodestyle==2.4.0
pycontracts==1.7.1
pycountry==1.20
@@ -314,6 +314,7 @@ social-auth-app-django==2.1.0
social-auth-core==1.7.0
sorl-thumbnail==12.3
sortedcontainers==0.9.2
+soupsieve==1.6.2
sphinx==1.8.3
sphinxcontrib-websupport==1.1.0 # via sphinx
splinter==0.9.0
@@ -339,14 +340,14 @@ uritemplate==3.0.0
urllib3==1.23
urlobject==2.4.3
user-util==0.1.5
-virtualenv==16.1.0
+virtualenv==16.2.0
voluptuous==0.11.5
vulture==1.0
w3lib==1.19.0
watchdog==0.9.0
web-fragments==0.2.2
webencodings==0.5.1
-webob==1.8.4
+webob==1.8.5
werkzeug==0.14.1
wrapt==1.10.5
xblock-review==1.1.5
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index fb9c730de4..d0adb981e4 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -53,8 +53,8 @@ astroid==1.5.3 # via edx-lint, pylint, pylint-celery
atomicwrites==1.2.1 # via pytest
attrs==17.4.0
babel==1.3
-backports.functools-lru-cache==1.5 # via astroid, pylint
-beautifulsoup4==4.6.3
+backports.functools-lru-cache==1.5
+beautifulsoup4==4.7.0
before-after==1.0.1
billiard==3.3.0.23
bleach==2.1.4
@@ -104,7 +104,7 @@ django-oauth-toolkit==1.1.3
django-object-actions==0.10.0
django-pyfs==2.0
django-ratelimit-backend==1.1.1
-django-ratelimit==1.1.0
+django-ratelimit==2.0.0
django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==0.10.0
@@ -123,7 +123,7 @@ docopt==0.6.2
docutils==0.14
dogapi==1.2.1
edx-ace==0.1.10
-edx-analytics-data-api-client==0.14.4
+edx-analytics-data-api-client==0.15.2
edx-ccx-keys==0.2.1
edx-celeryutils==0.2.7
edx-completion==1.0.1
@@ -144,7 +144,7 @@ edx-rest-api-client==1.9.2
edx-search==1.2.1
edx-submissions==2.0.12
edx-user-state-client==1.0.4
-edxval==0.1.23
+edxval==0.1.25
elasticsearch==1.9.0
enum34==1.1.6
event-tracking==0.2.7
@@ -205,7 +205,7 @@ markupsafe==1.1.0
mccabe==0.6.1 # via flake8, pylint
mock==1.0.1
mongoengine==0.10.0
-more-itertools==4.3.0 # via pytest
+more-itertools==5.0.0 # via pytest
moto==0.3.1
mysql-python==1.2.5
networkx==1.7
@@ -231,8 +231,8 @@ polib==1.1.0
psutil==1.2.1
py2neo==3.1.2
py==1.7.0 # via pytest, tox
-pyasn1-modules==0.2.2 # via service-identity
-pyasn1==0.4.4 # via pyasn1-modules, service-identity
+pyasn1-modules==0.2.3 # via service-identity
+pyasn1==0.4.5 # via pyasn1-modules, service-identity
pycodestyle==2.4.0
pycontracts==1.7.1
pycountry==1.20
@@ -301,6 +301,7 @@ social-auth-app-django==2.1.0
social-auth-core==1.7.0
sorl-thumbnail==12.3
sortedcontainers==0.9.2
+soupsieve==1.6.2
splinter==0.9.0
sqlparse==0.2.4
stevedore==1.10.0
@@ -323,13 +324,13 @@ uritemplate==3.0.0
urllib3==1.23
urlobject==2.4.3 # via pa11ycrawler
user-util==0.1.5
-virtualenv==16.1.0 # via tox
+virtualenv==16.2.0 # via tox
voluptuous==0.11.5
w3lib==1.19.0 # via parsel, scrapy
watchdog==0.9.0
web-fragments==0.2.2
webencodings==0.5.1
-webob==1.8.4
+webob==1.8.5
werkzeug==0.14.1 # via flask
wrapt==1.10.5
xblock-review==1.1.5