From 3b7918b07aabfc8a48677ac41a868af7fb0ea90b Mon Sep 17 00:00:00 2001 From: noraiz-anwar Date: Thu, 27 Dec 2018 19:24:31 +0500 Subject: [PATCH] Add basic pagination in video upload page --- .../contentstore/api/views/course_quality.py | 3 +- .../contentstore/tests/test_contentstore.py | 9 +- .../contentstore/views/tests/test_videos.py | 12 +++ cms/djangoapps/contentstore/views/videos.py | 89 +++++++++++++++---- cms/templates/videos_index.html | 4 + cms/templates/videos_index_pagination.html | 40 +++++++++ requirements/edx/base.txt | 12 +-- requirements/edx/development.txt | 19 ++-- requirements/edx/testing.txt | 21 ++--- 9 files changed, 165 insertions(+), 44 deletions(-) create mode 100644 cms/templates/videos_index_pagination.html 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 + 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