diff --git a/cms/djangoapps/contentstore/admin.py b/cms/djangoapps/contentstore/admin.py index eb1b756ff5..6fdb475b37 100644 --- a/cms/djangoapps/contentstore/admin.py +++ b/cms/djangoapps/contentstore/admin.py @@ -5,6 +5,6 @@ Admin site bindings for contentstore from django.contrib import admin from config_models.admin import ConfigurationModelAdmin -from contentstore.models import VideoEncodingDownloadConfig +from contentstore.models import VideoUploadConfig -admin.site.register(VideoEncodingDownloadConfig, ConfigurationModelAdmin) +admin.site.register(VideoUploadConfig, ConfigurationModelAdmin) diff --git a/cms/djangoapps/contentstore/migrations/0001_initial.py b/cms/djangoapps/contentstore/migrations/0001_initial.py index 0ece625496..e824bac7d0 100644 --- a/cms/djangoapps/contentstore/migrations/0001_initial.py +++ b/cms/djangoapps/contentstore/migrations/0001_initial.py @@ -8,8 +8,8 @@ from django.db import models class Migration(SchemaMigration): def forwards(self, orm): - # Adding model 'VideoEncodingDownloadConfig' - db.create_table('contentstore_videoencodingdownloadconfig', ( + # Adding model 'VideoUploadConfig' + db.create_table('contentstore_videouploadconfig', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), @@ -17,13 +17,17 @@ class Migration(SchemaMigration): ('profile_whitelist', self.gf('django.db.models.fields.TextField')(blank=True)), ('status_whitelist', self.gf('django.db.models.fields.TextField')(blank=True)), )) - db.send_create_signal('contentstore', ['VideoEncodingDownloadConfig']) + db.send_create_signal('contentstore', ['VideoUploadConfig']) + if not db.dry_run: + orm.VideoUploadConfig.objects.create( + profile_whitelist="desktop_mp4,desktop_webm,mobile_low,youtube", + status_whitelist="Uploading,In Progress,Complete,Failed,Invalid Token,Unknown" + ) def backwards(self, orm): - # Deleting model 'VideoEncodingDownloadConfig' - db.delete_table('contentstore_videoencodingdownloadconfig') - + # Deleting model 'VideoUploadConfig' + db.delete_table('contentstore_videouploadconfig') models = { 'auth.group': { @@ -55,8 +59,8 @@ class Migration(SchemaMigration): 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) }, - 'contentstore.videoencodingdownloadconfig': { - 'Meta': {'object_name': 'VideoEncodingDownloadConfig'}, + 'contentstore.videouploadconfig': { + 'Meta': {'object_name': 'VideoUploadConfig'}, 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), @@ -73,4 +77,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['contentstore'] \ No newline at end of file + complete_apps = ['contentstore'] diff --git a/cms/djangoapps/contentstore/models.py b/cms/djangoapps/contentstore/models.py index 6cd9b68e37..690e6c2df2 100644 --- a/cms/djangoapps/contentstore/models.py +++ b/cms/djangoapps/contentstore/models.py @@ -1,21 +1,25 @@ """ Models for contentstore """ +# pylint: disable=no-member from django.db.models.fields import TextField from config_models.models import ConfigurationModel -class VideoEncodingDownloadConfig(ConfigurationModel): - """Configuration for what to include in video encoding downloads""" +class VideoUploadConfig(ConfigurationModel): + """Configuration for the video upload feature.""" profile_whitelist = TextField( blank=True, - help_text="A comma-separated list of names of profiles to include in video encoding downloads" + help_text="A comma-separated list of names of profiles to include in video encoding downloads." ) status_whitelist = TextField( blank=True, - help_text="A comma-separated list of status values; only videos with these status values will be included in video encoding downloads" + help_text=( + "A comma-separated list of Studio status values;" + + " only videos with these status values will be included in video encoding downloads." + ) ) @classmethod diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py index ee498a6442..5cfa8b2331 100644 --- a/cms/djangoapps/contentstore/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/views/tests/test_videos.py @@ -15,8 +15,8 @@ from mock import Mock, patch from edxval.api import create_profile, create_video, get_video_info -from contentstore.models import VideoEncodingDownloadConfig -from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_ASSET_TYPE +from contentstore.models import VideoUploadConfig +from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_ASSET_TYPE, status_display_string from contentstore.tests.utils import CourseTestCase from contentstore.utils import reverse_course_url from xmodule.assetstore import AssetMetadata @@ -59,7 +59,7 @@ class VideoUploadTestMixin(object): "edx_video_id": "test1", "client_video_id": "test1.mp4", "duration": 42.0, - "status": "transcode_active", + "status": "upload", "encoded_videos": [], }, { @@ -86,7 +86,7 @@ class VideoUploadTestMixin(object): "edx_video_id": "non-ascii", "client_video_id": u"nón-ascii-näme.mp4", "duration": 256.0, - "status": "file_delivered", + "status": "transcode_active", "encoded_videos": [ { "profile": "profile1", @@ -169,8 +169,9 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): set(["edx_video_id", "client_video_id", "created", "duration", "status"]) ) dateutil.parser.parse(response_video["created"]) - for field in ["edx_video_id", "client_video_id", "duration", "status"]: + for field in ["edx_video_id", "client_video_id", "duration"]: self.assertEqual(response_video[field], original_video[field]) + self.assertEqual(response_video["status"], status_display_string(original_video["status"])) def test_get_html(self): response = self.client.get(self.url) @@ -312,9 +313,12 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): def setUp(self): super(VideoUrlsCsvTestCase, self).setUp() - VideoEncodingDownloadConfig( + VideoUploadConfig( profile_whitelist="profile1", - status_whitelist="file_delivered,file_complete" + status_whitelist=( + status_display_string("file_complete") + "," + + status_display_string("transcode_active") + ) ).save() def _check_csv_response(self, expected_video_ids, expected_profiles): @@ -333,7 +337,7 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): self.assertEqual( reader.fieldnames, ( - ["Name", "Duration", "Date Added", "Video ID"] + + ["Name", "Duration", "Date Added", "Video ID", "Status"] + ["{} URL".format(profile) for profile in expected_profiles] ) ) @@ -366,9 +370,13 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): self._check_csv_response(["test2", "non-ascii"], ["profile1"]) def test_config(self): - VideoEncodingDownloadConfig( + VideoUploadConfig( profile_whitelist="profile1,profile2", - status_whitelist="file_delivered,file_complete,transcode_active" + status_whitelist=( + status_display_string("file_complete") + "," + + status_display_string("transcode_active") + "," + + status_display_string("upload") + ) ).save() self._check_csv_response(["test1", "test2", "non-ascii"], ["profile1", "profile2"]) diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py index f166bd2228..fb26305e38 100644 --- a/cms/djangoapps/contentstore/views/videos.py +++ b/cms/djangoapps/contentstore/views/videos.py @@ -15,7 +15,7 @@ import rfc6266 from edxval.api import create_video, get_videos_for_ids from opaque_keys.edx.keys import CourseKey -from contentstore.models import VideoEncodingDownloadConfig +from contentstore.models import VideoUploadConfig from contentstore.utils import reverse_course_url from edxmako.shortcuts import render_to_response from util.json_request import expect_json, JsonResponse @@ -35,6 +35,43 @@ VIDEO_ASSET_TYPE = "video" KEY_EXPIRATION_IN_SECONDS = 86400 +class StatusDisplayStrings(object): + """ + Enum of display strings for Video Status presented in Studio (e.g., in UI and in CSV download). + """ + # Translators: This is the status of an active video upload + UPLOADING = _("Uploading") + # Translators: This is the status for a video that the servers are currently processing + IN_PROGRESS = _("In Progress") + # Translators: This is the status for a video that the servers have successfully processed + COMPLETE = _("Complete") + # Translators: This is the status for a video that the servers have failed to process + FAILED = _("Failed"), + # Translators: This is the status for a video for which an invalid + # processing token was provided in the course settings + INVALID_TOKEN = _("Invalid Token"), + # Translators: This is the status for a video that is in an unknown state + UNKNOWN = _("Unknown") + + +def status_display_string(val_status): + """ + Converts VAL status string to Studio status string. + """ + status_map = { + "upload": StatusDisplayStrings.UPLOADING, + "ingest": StatusDisplayStrings.IN_PROGRESS, + "transcode_queue": StatusDisplayStrings.IN_PROGRESS, + "transcode_active": StatusDisplayStrings.IN_PROGRESS, + "file_delivered": StatusDisplayStrings.COMPLETE, + "file_complete": StatusDisplayStrings.COMPLETE, + "file_corrupt": StatusDisplayStrings.FAILED, + "pipeline_error": StatusDisplayStrings.FAILED, + "invalid_token": StatusDisplayStrings.INVALID_TOKEN + } + return status_map.get(val_status, StatusDisplayStrings.UNKNOWN) + + @expect_json @login_required @require_http_methods(("GET", "POST")) @@ -73,8 +110,8 @@ def video_encodings_download(request, course_key_string): Returns a CSV report containing the encoded video URLs for video uploads in the following format: - Video ID,Name,Profile1 URL,Profile2 URL - aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa,video.mp4,http://example.com/profile1.mp4,http://example.com/profile2.mp4 + Video ID,Name,Status,Profile1 URL,Profile2 URL + aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa,video.mp4,Complete,http://example.com/prof1.mp4,http://example.com/prof2.mp4 """ course = _get_and_validate_course(course_key_string, request.user) @@ -88,14 +125,15 @@ def video_encodings_download(request, course_key_string): # (e.g. desktop, mobile high quality, mobile low quality) return _("{profile_name} URL").format(profile_name=profile) - profile_whitelist = VideoEncodingDownloadConfig.get_profile_whitelist() - status_whitelist = VideoEncodingDownloadConfig.get_status_whitelist() + profile_whitelist = VideoUploadConfig.get_profile_whitelist() + status_whitelist = VideoUploadConfig.get_status_whitelist() videos = list(_get_videos(course)) name_col = _("Name") duration_col = _("Duration") added_col = _("Date Added") video_id_col = _("Video ID") + status_col = _("Status") profile_cols = [get_profile_header(profile) for profile in profile_whitelist] def make_csv_dict(video): @@ -115,6 +153,7 @@ def video_encodings_download(request, course_key_string): (duration_col, duration_val), (added_col, video["created"].isoformat()), (video_id_col, video["edx_video_id"]), + (status_col, video["status"]), ] + [ (get_profile_header(encoded_video["profile"]), encoded_video["url"]) @@ -138,7 +177,7 @@ def video_encodings_download(request, course_key_string): ) writer = csv.DictWriter( response, - [name_col, duration_col, added_col, video_id_col] + profile_cols, + [name_col, duration_col, added_col, video_id_col, status_col] + profile_cols, dialect=csv.excel ) writer.writeheader() @@ -173,13 +212,20 @@ def _get_and_validate_course(course_key_string, user): def _get_videos(course): """ Retrieves the list of videos from VAL corresponding to the videos listed in - the asset metadata store + the asset metadata store. """ edx_videos_ids = [ v.asset_id.path for v in modulestore().get_all_asset_metadata(course.id, VIDEO_ASSET_TYPE) ] - return get_videos_for_ids(edx_videos_ids) + + videos = list(get_videos_for_ids(edx_videos_ids)) + + # convert VAL's status to studio's Video Upload feature status. + for video in videos: + video["status"] = status_display_string(video["status"]) + + return videos def _get_index_videos(course): diff --git a/cms/static/js/spec/views/previous_video_upload_spec.js b/cms/static/js/spec/views/previous_video_upload_spec.js index 18a64969bf..8d6127465d 100644 --- a/cms/static/js/spec/views/previous_video_upload_spec.js +++ b/cms/static/js/spec/views/previous_video_upload_spec.js @@ -67,16 +67,12 @@ define( _.each( [ - {status: "upload", expected: "Uploading"}, - {status: "ingest", expected: "In Progress"}, - {status: "transcode_queue", expected: "In Progress"}, - {status: "transcode_active", expected: "In Progress"}, - {status: "file_delivered", expected: "Complete"}, - {status: "file_complete", expected: "Complete"}, - {status: "file_corrupt", expected: "Failed"}, - {status: "pipeline_error", expected: "Failed"}, - {status: "invalid_token", expected: "Invalid Token"}, - {status: "unexpected_status_string", expected: "Unknown"} + {status: "Uploading", expected: "Uploading"}, + {status: "In Progress", expected: "In Progress"}, + {status: "Complete", expected: "Complete"}, + {status: "Failed", expected: "Failed"}, + {status: "Invalid Token", expected: "Invalid Token"}, + {status: "Unknown", expected: "Unknown"} ], function(caseInfo) { it("should render " + caseInfo.status + " status correctly", function() { diff --git a/cms/static/js/views/previous_video_upload.js b/cms/static/js/views/previous_video_upload.js index 9f6af23e58..61f0f2dc1a 100644 --- a/cms/static/js/views/previous_video_upload.js +++ b/cms/static/js/views/previous_video_upload.js @@ -3,38 +3,6 @@ define( function(gettext, DateUtils, BaseView) { "use strict"; - var statusDisplayStrings = { - // Translators: This is the status of an active video upload - UPLOADING: gettext("Uploading"), - // Translators: This is the status for a video that the servers - // are currently processing - IN_PROGRESS: gettext("In Progress"), - // Translators: This is the status for a video that the servers - // have successfully processed - COMPLETE: gettext("Complete"), - // Translators: This is the status for a video that the servers - // have failed to process - FAILED: gettext("Failed"), - // Translators: This is the status for a video for which an invalid - // processing token was provided in the course settings - INVALID_TOKEN: gettext("Invalid Token"), - // Translators: This is the status for a video that is in an unknown - // state - UNKNOWN: gettext("Unknown") - }; - - var statusMap = { - "upload": statusDisplayStrings.UPLOADING, - "ingest": statusDisplayStrings.IN_PROGRESS, - "transcode_queue": statusDisplayStrings.IN_PROGRESS, - "transcode_active": statusDisplayStrings.IN_PROGRESS, - "file_delivered": statusDisplayStrings.COMPLETE, - "file_complete": statusDisplayStrings.COMPLETE, - "file_corrupt": statusDisplayStrings.FAILED, - "pipeline_error": statusDisplayStrings.FAILED, - "invalid_token": statusDisplayStrings.INVALID_TOKEN - }; - var PreviousVideoUploadView = BaseView.extend({ tagName: "tr", @@ -57,7 +25,7 @@ define( // the servers where its duration is determined. duration: duration > 0 ? this.renderDuration(duration) : gettext("Pending"), created: DateUtils.renderDate(this.model.get("created")), - status: statusMap[this.model.get("status")] || statusDisplayStrings.UNKNOWN + status: this.model.get("status") }; this.$el.html( this.template(_.extend({}, this.model.attributes, renderedAttributes))