diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py index ba8a753289..fb012c8a32 100644 --- a/cms/djangoapps/contentstore/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/views/tests/test_videos.py @@ -1541,9 +1541,10 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): self.assertEqual(response.status_code, 200) self.assertEqual( response["Content-Disposition"], - u"attachment; filename={course}_video_urls.csv".format(course=self.course.id.course) + u"attachment; filename=\"{course}_video_urls.csv\"".format(course=self.course.id.course) ) - response_reader = StringIO(response.content.decode('utf-8') if six.PY3 else response.content) + response_content = b"".join(response.streaming_content) + response_reader = StringIO(response_content.decode('utf-8') if six.PY3 else response_content) reader = csv.DictReader(response_reader, dialect=csv.excel) self.assertEqual( reader.fieldnames, @@ -1605,5 +1606,5 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): self.assertEqual(response.status_code, 200) self.assertEqual( response["Content-Disposition"], - u"attachment; filename=video_urls.csv; filename*=utf-8''n%C3%B3n-%C3%A4scii_video_urls.csv" + u"attachment; filename*=utf-8''n%C3%B3n-%C3%A4scii_video_urls.csv" ) diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py index 2c60838d23..505ba5e324 100644 --- a/cms/djangoapps/contentstore/views/videos.py +++ b/cms/djangoapps/contentstore/views/videos.py @@ -3,21 +3,22 @@ Views related to the video upload feature """ +import codecs import csv import json import logging from contextlib import closing from datetime import datetime, timedelta +import io from uuid import uuid4 -import rfc6266_parser import six from boto import s3 from boto.sts import STSConnection from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.staticfiles.storage import staticfiles_storage -from django.http import HttpResponse, HttpResponseNotFound +from django.http import FileResponse, HttpResponseNotFound from django.urls import reverse from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop @@ -461,17 +462,12 @@ def video_encodings_download(request, course_key_string): for key, value in ret.items() } - response = HttpResponse(content_type="text/csv") - # Translators: This is the suggested filename when downloading the URL - # listing for videos uploaded through Studio - filename = _("{course}_video_urls").format(course=course.id.course) - # See https://tools.ietf.org/html/rfc6266#appendix-D - response["Content-Disposition"] = rfc6266_parser.build_header( - filename + ".csv", - filename_compat="video_urls.csv" - ) + # Write csv to bytes-like object. We need a separate writer and buffer as the csv + # writer writes str and the FileResponse expects a bytes files. + buffer = io.BytesIO() + buffer_writer = codecs.getwriter("utf-8")(buffer) writer = csv.DictWriter( - response, + buffer_writer, [ col_name.encode("utf-8") if six.PY2 else col_name for col_name @@ -482,7 +478,12 @@ def video_encodings_download(request, course_key_string): writer.writeheader() for video in videos: writer.writerow(make_csv_dict(video)) - return response + buffer.seek(0) + + # Translators: This is the suggested filename when downloading the URL + # listing for videos uploaded through Studio + filename = _("{course}_video_urls").format(course=course.id.course) + ".csv" + return FileResponse(buffer, as_attachment=True, filename=filename, content_type="text/csv") def _get_and_validate_course(course_key_string, user): diff --git a/requirements/edx/base.in b/requirements/edx/base.in index 20047d3d3b..4e9cfa45ff 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -136,7 +136,6 @@ python3-saml pyuca # For more accurate sorting of translated country names in django-countries recommender-xblock # https://github.com/edx/RecommenderXBlock rest-condition # DRF's recommendation for supporting complex permissions -rfc6266-parser # Used to generate Content-Disposition headers. social-auth-core pysrt # Support for SubRip subtitle files, used in the video XModule pytz # Time zone information database diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 04609e303a..a4518aac3f 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -142,7 +142,6 @@ jsonfield2==3.0.3 # via -c requirements/edx/../constraints.txt, -r requi kombu==3.0.37 # via celery laboratory==1.0.2 # via -r requirements/edx/base.in lazy==1.4 # via -r requirements/edx/paver.txt, acid-xblock, lti-consumer-xblock, ora2 -lepl==5.1.3 # via rfc6266-parser libsass==0.10.0 # via -r requirements/edx/paver.txt, ora2 loremipsum==1.0.5 # via ora2 lti-consumer-xblock==2.3 # via -r requirements/edx/base.in @@ -206,7 +205,6 @@ regex==2020.7.14 # via -r requirements/edx/../edx-sandbox/shared.txt, n requests-oauthlib==1.3.0 # via -r requirements/edx/base.in, social-auth-core requests==2.24.0 # via -r requirements/edx/paver.txt, analytics-python, coreapi, django-oauth-toolkit, edx-analytics-data-api-client, edx-bulk-grades, edx-drf-extensions, edx-enterprise, edx-rest-api-client, geoip2, mailsnake, pyjwkest, python-swiftclient, requests-oauthlib, sailthru-client, slumber, social-auth-core rest-condition==1.0.3 # via -r requirements/edx/base.in, edx-drf-extensions -rfc6266-parser==0.0.6 # via -r requirements/edx/base.in ruamel.yaml.clib==0.2.0 # via ruamel.yaml ruamel.yaml==0.16.10 # via drf-yasg rules==2.2 # via -r requirements/edx/base.in, edx-enterprise, edx-proctoring diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 68fc9e3905..00e7e5bf38 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -171,7 +171,6 @@ kombu==3.0.37 # via -r requirements/edx/testing.txt, celery laboratory==1.0.2 # via -r requirements/edx/testing.txt lazy-object-proxy==1.4.3 # via -r requirements/edx/testing.txt, astroid lazy==1.4 # via -r requirements/edx/testing.txt, acid-xblock, bok-choy, lti-consumer-xblock, ora2 -lepl==5.1.3 # via -r requirements/edx/testing.txt, rfc6266-parser libsass==0.10.0 # via -r requirements/edx/testing.txt, ora2 loremipsum==1.0.5 # via -r requirements/edx/testing.txt, ora2 lti-consumer-xblock==2.3 # via -r requirements/edx/testing.txt @@ -259,7 +258,6 @@ regex==2020.7.14 # via -r requirements/edx/testing.txt, nltk requests-oauthlib==1.3.0 # via -r requirements/edx/testing.txt, social-auth-core requests==2.24.0 # via -r requirements/edx/testing.txt, analytics-python, coreapi, django-oauth-toolkit, edx-analytics-data-api-client, edx-bulk-grades, edx-drf-extensions, edx-enterprise, edx-rest-api-client, geoip2, mailsnake, pyjwkest, python-swiftclient, requests-oauthlib, sailthru-client, slumber, social-auth-core, sphinx, transifex-client rest-condition==1.0.3 # via -r requirements/edx/testing.txt, edx-drf-extensions -rfc6266-parser==0.0.6 # via -r requirements/edx/testing.txt ruamel.yaml.clib==0.2.0 # via -r requirements/edx/testing.txt, ruamel.yaml ruamel.yaml==0.16.10 # via -r requirements/edx/testing.txt, drf-yasg rules==2.2 # via -r requirements/edx/testing.txt, edx-enterprise, edx-proctoring diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 4849ffdd34..b280fccf17 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -165,7 +165,6 @@ kombu==3.0.37 # via -r requirements/edx/base.txt, celery laboratory==1.0.2 # via -r requirements/edx/base.txt lazy-object-proxy==1.4.3 # via astroid lazy==1.4 # via -r requirements/edx/base.txt, acid-xblock, bok-choy, lti-consumer-xblock, ora2 -lepl==5.1.3 # via -r requirements/edx/base.txt, rfc6266-parser libsass==0.10.0 # via -r requirements/edx/base.txt, ora2 loremipsum==1.0.5 # via -r requirements/edx/base.txt, ora2 lti-consumer-xblock==2.3 # via -r requirements/edx/base.txt @@ -248,7 +247,6 @@ regex==2020.7.14 # via -r requirements/edx/base.txt, nltk requests-oauthlib==1.3.0 # via -r requirements/edx/base.txt, social-auth-core requests==2.24.0 # via -r requirements/edx/base.txt, analytics-python, coreapi, django-oauth-toolkit, edx-analytics-data-api-client, edx-bulk-grades, edx-drf-extensions, edx-enterprise, edx-rest-api-client, geoip2, mailsnake, pyjwkest, python-swiftclient, requests-oauthlib, sailthru-client, slumber, social-auth-core, transifex-client rest-condition==1.0.3 # via -r requirements/edx/base.txt, edx-drf-extensions -rfc6266-parser==0.0.6 # via -r requirements/edx/base.txt ruamel.yaml.clib==0.2.0 # via -r requirements/edx/base.txt, ruamel.yaml ruamel.yaml==0.16.10 # via -r requirements/edx/base.txt, drf-yasg rules==2.2 # via -r requirements/edx/base.txt, edx-enterprise, edx-proctoring