Add basic pagination in video upload page

This commit is contained in:
noraiz-anwar
2018-12-27 19:24:31 +05:00
committed by Noraiz Anwar
parent 5350064731
commit 3b7918b07a
9 changed files with 165 additions and 44 deletions

View File

@@ -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(

View File

@@ -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)

View File

@@ -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": []})

View File

@@ -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()

View File

@@ -75,4 +75,8 @@
</section>
</div>
% if pagination_context:
<%include file="videos_index_pagination.html"/>
% endif
</%block>

View File

@@ -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
%>
<link href="//cdnjs.cloudflare.com/ajax/libs/simplePagination.js/1.6/simplePagination.min.css" rel="stylesheet">
<script src="//cdnjs.cloudflare.com/ajax/libs/simplePagination.js/1.6/jquery.simplePagination.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jinplace/1.2.1/jinplace.min.js"></script>
<nav style="text-align: center">
<br>
<div id="video_upload_pagination" style="display: inline-block"></div>
<br>
<span id="videos_per_page"
data-ok-button="${_('Submit')}"
data-cancel-button="${_('Cancel')}"
data-data="${pagination_context['items_on_one_page']}"
data-placeholder="${_('Changing..')}"
data-activator="#edit-activator">
${_('Videos per page:')} ${pagination_context['items_on_one_page']}
</span>
<button id="edit-activator" class="btn-default edit-button action-button">
<span class="icon fa fa-pencil" aria-hidden="true"></span>
<span class="action-button-text">${_("Change")}</span>
</button>
</nav>
<script>
$(function() {
$('#video_upload_pagination').pagination({
pages: "${pagination_context['total_pages']| n, dump_js_escaped_json}",
currentPage:"${pagination_context['current_page']| n, dump_js_escaped_json}",
cssStyle: 'light-theme',
hrefTextPrefix:"?page=",
});
$('#videos_per_page').jinplace({
}).on('jinplace:done',
function() {
window.location = window.location.pathname;
});
});
</script>

View File

@@ -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

View File

@@ -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

View File

@@ -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