Commit 4b53f4df by Nimisha Asthagiri

Studio video upload CSV download changes.

parent 8d8ca83d
...@@ -5,6 +5,6 @@ Admin site bindings for contentstore ...@@ -5,6 +5,6 @@ Admin site bindings for contentstore
from django.contrib import admin from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin 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)
...@@ -8,8 +8,8 @@ from django.db import models ...@@ -8,8 +8,8 @@ from django.db import models
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding model 'VideoEncodingDownloadConfig' # Adding model 'VideoUploadConfig'
db.create_table('contentstore_videoencodingdownloadconfig', ( db.create_table('contentstore_videouploadconfig', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('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)), ('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)), ('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): ...@@ -17,13 +17,17 @@ class Migration(SchemaMigration):
('profile_whitelist', self.gf('django.db.models.fields.TextField')(blank=True)), ('profile_whitelist', self.gf('django.db.models.fields.TextField')(blank=True)),
('status_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): def backwards(self, orm):
# Deleting model 'VideoEncodingDownloadConfig' # Deleting model 'VideoUploadConfig'
db.delete_table('contentstore_videoencodingdownloadconfig') db.delete_table('contentstore_videouploadconfig')
models = { models = {
'auth.group': { 'auth.group': {
...@@ -55,8 +59,8 @@ class Migration(SchemaMigration): ...@@ -55,8 +59,8 @@ class Migration(SchemaMigration):
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), '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'}) 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
}, },
'contentstore.videoencodingdownloadconfig': { 'contentstore.videouploadconfig': {
'Meta': {'object_name': 'VideoEncodingDownloadConfig'}, 'Meta': {'object_name': 'VideoUploadConfig'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), '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'}), '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'}), 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
...@@ -73,4 +77,4 @@ class Migration(SchemaMigration): ...@@ -73,4 +77,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['contentstore'] complete_apps = ['contentstore']
\ No newline at end of file
""" """
Models for contentstore Models for contentstore
""" """
# pylint: disable=no-member
from django.db.models.fields import TextField from django.db.models.fields import TextField
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
class VideoEncodingDownloadConfig(ConfigurationModel): class VideoUploadConfig(ConfigurationModel):
"""Configuration for what to include in video encoding downloads""" """Configuration for the video upload feature."""
profile_whitelist = TextField( profile_whitelist = TextField(
blank=True, 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( status_whitelist = TextField(
blank=True, 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 @classmethod
......
...@@ -15,8 +15,8 @@ from mock import Mock, patch ...@@ -15,8 +15,8 @@ from mock import Mock, patch
from edxval.api import create_profile, create_video, get_video_info from edxval.api import create_profile, create_video, get_video_info
from contentstore.models import VideoEncodingDownloadConfig from contentstore.models import VideoUploadConfig
from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_ASSET_TYPE from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_ASSET_TYPE, status_display_string
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url from contentstore.utils import reverse_course_url
from xmodule.assetstore import AssetMetadata from xmodule.assetstore import AssetMetadata
...@@ -59,7 +59,7 @@ class VideoUploadTestMixin(object): ...@@ -59,7 +59,7 @@ class VideoUploadTestMixin(object):
"edx_video_id": "test1", "edx_video_id": "test1",
"client_video_id": "test1.mp4", "client_video_id": "test1.mp4",
"duration": 42.0, "duration": 42.0,
"status": "transcode_active", "status": "upload",
"encoded_videos": [], "encoded_videos": [],
}, },
{ {
...@@ -86,7 +86,7 @@ class VideoUploadTestMixin(object): ...@@ -86,7 +86,7 @@ class VideoUploadTestMixin(object):
"edx_video_id": "non-ascii", "edx_video_id": "non-ascii",
"client_video_id": u"nón-ascii-näme.mp4", "client_video_id": u"nón-ascii-näme.mp4",
"duration": 256.0, "duration": 256.0,
"status": "file_delivered", "status": "transcode_active",
"encoded_videos": [ "encoded_videos": [
{ {
"profile": "profile1", "profile": "profile1",
...@@ -169,8 +169,9 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): ...@@ -169,8 +169,9 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
set(["edx_video_id", "client_video_id", "created", "duration", "status"]) set(["edx_video_id", "client_video_id", "created", "duration", "status"])
) )
dateutil.parser.parse(response_video["created"]) 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[field], original_video[field])
self.assertEqual(response_video["status"], status_display_string(original_video["status"]))
def test_get_html(self): def test_get_html(self):
response = self.client.get(self.url) response = self.client.get(self.url)
...@@ -312,9 +313,12 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): ...@@ -312,9 +313,12 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
def setUp(self): def setUp(self):
super(VideoUrlsCsvTestCase, self).setUp() super(VideoUrlsCsvTestCase, self).setUp()
VideoEncodingDownloadConfig( VideoUploadConfig(
profile_whitelist="profile1", profile_whitelist="profile1",
status_whitelist="file_delivered,file_complete" status_whitelist=(
status_display_string("file_complete") + "," +
status_display_string("transcode_active")
)
).save() ).save()
def _check_csv_response(self, expected_video_ids, expected_profiles): def _check_csv_response(self, expected_video_ids, expected_profiles):
...@@ -333,7 +337,7 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): ...@@ -333,7 +337,7 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
self.assertEqual( self.assertEqual(
reader.fieldnames, reader.fieldnames,
( (
["Name", "Duration", "Date Added", "Video ID"] + ["Name", "Duration", "Date Added", "Video ID", "Status"] +
["{} URL".format(profile) for profile in expected_profiles] ["{} URL".format(profile) for profile in expected_profiles]
) )
) )
...@@ -366,9 +370,13 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): ...@@ -366,9 +370,13 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
self._check_csv_response(["test2", "non-ascii"], ["profile1"]) self._check_csv_response(["test2", "non-ascii"], ["profile1"])
def test_config(self): def test_config(self):
VideoEncodingDownloadConfig( VideoUploadConfig(
profile_whitelist="profile1,profile2", 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() ).save()
self._check_csv_response(["test1", "test2", "non-ascii"], ["profile1", "profile2"]) self._check_csv_response(["test1", "test2", "non-ascii"], ["profile1", "profile2"])
......
...@@ -15,7 +15,7 @@ import rfc6266 ...@@ -15,7 +15,7 @@ import rfc6266
from edxval.api import create_video, get_videos_for_ids from edxval.api import create_video, get_videos_for_ids
from opaque_keys.edx.keys import CourseKey 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 contentstore.utils import reverse_course_url
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
...@@ -35,6 +35,43 @@ VIDEO_ASSET_TYPE = "video" ...@@ -35,6 +35,43 @@ VIDEO_ASSET_TYPE = "video"
KEY_EXPIRATION_IN_SECONDS = 86400 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 @expect_json
@login_required @login_required
@require_http_methods(("GET", "POST")) @require_http_methods(("GET", "POST"))
...@@ -73,8 +110,8 @@ def video_encodings_download(request, course_key_string): ...@@ -73,8 +110,8 @@ def video_encodings_download(request, course_key_string):
Returns a CSV report containing the encoded video URLs for video uploads Returns a CSV report containing the encoded video URLs for video uploads
in the following format: in the following format:
Video ID,Name,Profile1 URL,Profile2 URL Video ID,Name,Status,Profile1 URL,Profile2 URL
aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa,video.mp4,http://example.com/profile1.mp4,http://example.com/profile2.mp4 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) course = _get_and_validate_course(course_key_string, request.user)
...@@ -88,14 +125,15 @@ def video_encodings_download(request, course_key_string): ...@@ -88,14 +125,15 @@ def video_encodings_download(request, course_key_string):
# (e.g. desktop, mobile high quality, mobile low quality) # (e.g. desktop, mobile high quality, mobile low quality)
return _("{profile_name} URL").format(profile_name=profile) return _("{profile_name} URL").format(profile_name=profile)
profile_whitelist = VideoEncodingDownloadConfig.get_profile_whitelist() profile_whitelist = VideoUploadConfig.get_profile_whitelist()
status_whitelist = VideoEncodingDownloadConfig.get_status_whitelist() status_whitelist = VideoUploadConfig.get_status_whitelist()
videos = list(_get_videos(course)) videos = list(_get_videos(course))
name_col = _("Name") name_col = _("Name")
duration_col = _("Duration") duration_col = _("Duration")
added_col = _("Date Added") added_col = _("Date Added")
video_id_col = _("Video ID") video_id_col = _("Video ID")
status_col = _("Status")
profile_cols = [get_profile_header(profile) for profile in profile_whitelist] profile_cols = [get_profile_header(profile) for profile in profile_whitelist]
def make_csv_dict(video): def make_csv_dict(video):
...@@ -115,6 +153,7 @@ def video_encodings_download(request, course_key_string): ...@@ -115,6 +153,7 @@ def video_encodings_download(request, course_key_string):
(duration_col, duration_val), (duration_col, duration_val),
(added_col, video["created"].isoformat()), (added_col, video["created"].isoformat()),
(video_id_col, video["edx_video_id"]), (video_id_col, video["edx_video_id"]),
(status_col, video["status"]),
] + ] +
[ [
(get_profile_header(encoded_video["profile"]), encoded_video["url"]) (get_profile_header(encoded_video["profile"]), encoded_video["url"])
...@@ -138,7 +177,7 @@ def video_encodings_download(request, course_key_string): ...@@ -138,7 +177,7 @@ def video_encodings_download(request, course_key_string):
) )
writer = csv.DictWriter( writer = csv.DictWriter(
response, 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 dialect=csv.excel
) )
writer.writeheader() writer.writeheader()
...@@ -173,13 +212,20 @@ def _get_and_validate_course(course_key_string, user): ...@@ -173,13 +212,20 @@ def _get_and_validate_course(course_key_string, user):
def _get_videos(course): def _get_videos(course):
""" """
Retrieves the list of videos from VAL corresponding to the videos listed in Retrieves the list of videos from VAL corresponding to the videos listed in
the asset metadata store the asset metadata store.
""" """
edx_videos_ids = [ edx_videos_ids = [
v.asset_id.path v.asset_id.path
for v in modulestore().get_all_asset_metadata(course.id, VIDEO_ASSET_TYPE) 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): def _get_index_videos(course):
......
...@@ -67,16 +67,12 @@ define( ...@@ -67,16 +67,12 @@ define(
_.each( _.each(
[ [
{status: "upload", expected: "Uploading"}, {status: "Uploading", expected: "Uploading"},
{status: "ingest", expected: "In Progress"}, {status: "In Progress", expected: "In Progress"},
{status: "transcode_queue", expected: "In Progress"}, {status: "Complete", expected: "Complete"},
{status: "transcode_active", expected: "In Progress"}, {status: "Failed", expected: "Failed"},
{status: "file_delivered", expected: "Complete"}, {status: "Invalid Token", expected: "Invalid Token"},
{status: "file_complete", expected: "Complete"}, {status: "Unknown", expected: "Unknown"}
{status: "file_corrupt", expected: "Failed"},
{status: "pipeline_error", expected: "Failed"},
{status: "invalid_token", expected: "Invalid Token"},
{status: "unexpected_status_string", expected: "Unknown"}
], ],
function(caseInfo) { function(caseInfo) {
it("should render " + caseInfo.status + " status correctly", function() { it("should render " + caseInfo.status + " status correctly", function() {
......
...@@ -3,38 +3,6 @@ define( ...@@ -3,38 +3,6 @@ define(
function(gettext, DateUtils, BaseView) { function(gettext, DateUtils, BaseView) {
"use strict"; "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({ var PreviousVideoUploadView = BaseView.extend({
tagName: "tr", tagName: "tr",
...@@ -57,7 +25,7 @@ define( ...@@ -57,7 +25,7 @@ define(
// the servers where its duration is determined. // the servers where its duration is determined.
duration: duration > 0 ? this.renderDuration(duration) : gettext("Pending"), duration: duration > 0 ? this.renderDuration(duration) : gettext("Pending"),
created: DateUtils.renderDate(this.model.get("created")), created: DateUtils.renderDate(this.model.get("created")),
status: statusMap[this.model.get("status")] || statusDisplayStrings.UNKNOWN status: this.model.get("status")
}; };
this.$el.html( this.$el.html(
this.template(_.extend({}, this.model.attributes, renderedAttributes)) this.template(_.extend({}, this.model.attributes, renderedAttributes))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment