Commit c2a0b0f0 by Greg Price

Merge pull request #6631 from edx/gprice/video-upload-improvements

Improve code for video upload feature
parents e592eef0 874cd5c7
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'VideoUploadConfig.status_whitelist'
db.delete_column('contentstore_videouploadconfig', 'status_whitelist')
def backwards(self, orm):
# Adding field 'VideoUploadConfig.status_whitelist'
db.add_column('contentstore_videouploadconfig', 'status_whitelist',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'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.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'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'profile_whitelist': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['contentstore']
\ No newline at end of file
...@@ -14,23 +14,8 @@ class VideoUploadConfig(ConfigurationModel): ...@@ -14,23 +14,8 @@ class VideoUploadConfig(ConfigurationModel):
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(
blank=True,
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
def get_profile_whitelist(cls): def get_profile_whitelist(cls):
"""Get the list of profiles to include in the encoding download""" """Get the list of profiles to include in the encoding download"""
return [profile for profile in cls.current().profile_whitelist.split(",") if profile] return [profile for profile in cls.current().profile_whitelist.split(",") if profile]
@classmethod
def get_status_whitelist(cls):
"""
Get the list of status values to include files for in the encoding
download
"""
return [status for status in cls.current().status_whitelist.split(",") if status]
...@@ -16,7 +16,7 @@ from mock import Mock, patch ...@@ -16,7 +16,7 @@ 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 VideoUploadConfig from contentstore.models import VideoUploadConfig
from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_ASSET_TYPE, status_display_string from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, VIDEO_ASSET_TYPE, StatusDisplayStrings
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
...@@ -171,7 +171,10 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): ...@@ -171,7 +171,10 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
dateutil.parser.parse(response_video["created"]) dateutil.parser.parse(response_video["created"])
for field in ["edx_video_id", "client_video_id", "duration"]: 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"])) self.assertEqual(
response_video["status"],
StatusDisplayStrings.get(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)
...@@ -313,18 +316,12 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): ...@@ -313,18 +316,12 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
def setUp(self): def setUp(self):
super(VideoUrlsCsvTestCase, self).setUp() super(VideoUrlsCsvTestCase, self).setUp()
VideoUploadConfig( VideoUploadConfig(profile_whitelist="profile1").save()
profile_whitelist="profile1",
status_whitelist=(
status_display_string("file_complete") + "," +
status_display_string("transcode_active")
)
).save()
def _check_csv_response(self, expected_video_ids, expected_profiles): def _check_csv_response(self, expected_profiles):
""" """
Check that the response is a valid CSV response containing rows Check that the response is a valid CSV response containing rows
corresponding to expected_video_ids. corresponding to previous_uploads and including the expected profiles.
""" """
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -346,6 +343,7 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): ...@@ -346,6 +343,7 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
response_video = { response_video = {
key.decode("utf-8"): value.decode("utf-8") for key, value in row.items() key.decode("utf-8"): value.decode("utf-8") for key, value in row.items()
} }
self.assertNotIn(response_video["Video ID"], actual_video_ids)
actual_video_ids.append(response_video["Video ID"]) actual_video_ids.append(response_video["Video ID"])
original_video = self._get_previous_upload(response_video["Video ID"]) original_video = self._get_previous_upload(response_video["Video ID"])
self.assertEqual(response_video["Name"], original_video["client_video_id"]) self.assertEqual(response_video["Name"], original_video["client_video_id"])
...@@ -364,21 +362,14 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): ...@@ -364,21 +362,14 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
self.assertEqual(response_profile_url, original_encoded_for_profile["url"]) self.assertEqual(response_profile_url, original_encoded_for_profile["url"])
else: else:
self.assertEqual(response_profile_url, "") self.assertEqual(response_profile_url, "")
self.assertEqual(set(actual_video_ids), set(expected_video_ids)) self.assertEqual(len(actual_video_ids), len(self.previous_uploads))
def test_basic(self): def test_basic(self):
self._check_csv_response(["test2", "non-ascii"], ["profile1"]) self._check_csv_response(["profile1"])
def test_config(self): def test_profile_whitelist(self):
VideoUploadConfig( VideoUploadConfig(profile_whitelist="profile1,profile2").save()
profile_whitelist="profile1,profile2", self._check_csv_response(["profile1", "profile2"])
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"])
def test_non_ascii_course(self): def test_non_ascii_course(self):
course = CourseFactory.create( course = CourseFactory.create(
......
...@@ -8,7 +8,7 @@ from uuid import uuid4 ...@@ -8,7 +8,7 @@ from uuid import uuid4
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseNotFound from django.http import HttpResponse, HttpResponseNotFound
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _, ugettext_noop
from django.views.decorators.http import require_GET, require_http_methods from django.views.decorators.http import require_GET, require_http_methods
import rfc6266 import rfc6266
...@@ -37,39 +37,40 @@ KEY_EXPIRATION_IN_SECONDS = 86400 ...@@ -37,39 +37,40 @@ KEY_EXPIRATION_IN_SECONDS = 86400
class StatusDisplayStrings(object): class StatusDisplayStrings(object):
""" """
Enum of display strings for Video Status presented in Studio (e.g., in UI and in CSV download). A class to map status strings as stored in VAL to display strings for the
video upload page
""" """
# Translators: This is the status of an active video upload # Translators: This is the status of an active video upload
UPLOADING = _("Uploading") _UPLOADING = ugettext_noop("Uploading")
# Translators: This is the status for a video that the servers are currently processing # Translators: This is the status for a video that the servers are currently processing
IN_PROGRESS = _("In Progress") _IN_PROGRESS = ugettext_noop("In Progress")
# Translators: This is the status for a video that the servers have successfully processed # Translators: This is the status for a video that the servers have successfully processed
COMPLETE = _("Complete") _COMPLETE = ugettext_noop("Complete")
# Translators: This is the status for a video that the servers have failed to process # Translators: This is the status for a video that the servers have failed to process
FAILED = _("Failed"), _FAILED = ugettext_noop("Failed"),
# Translators: This is the status for a video for which an invalid # Translators: This is the status for a video for which an invalid
# processing token was provided in the course settings # processing token was provided in the course settings
INVALID_TOKEN = _("Invalid Token"), _INVALID_TOKEN = ugettext_noop("Invalid Token"),
# Translators: This is the status for a video that is in an unknown state # Translators: This is the status for a video that is in an unknown state
UNKNOWN = _("Unknown") _UNKNOWN = ugettext_noop("Unknown")
_STATUS_MAP = {
def status_display_string(val_status): "upload": _UPLOADING,
""" "ingest": _IN_PROGRESS,
Converts VAL status string to Studio status string. "transcode_queue": _IN_PROGRESS,
""" "transcode_active": _IN_PROGRESS,
status_map = { "file_delivered": _COMPLETE,
"upload": StatusDisplayStrings.UPLOADING, "file_complete": _COMPLETE,
"ingest": StatusDisplayStrings.IN_PROGRESS, "file_corrupt": _FAILED,
"transcode_queue": StatusDisplayStrings.IN_PROGRESS, "pipeline_error": _FAILED,
"transcode_active": StatusDisplayStrings.IN_PROGRESS, "invalid_token": _INVALID_TOKEN
"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)
@staticmethod
def get(val_status):
"""Map a VAL status string to a localized display string"""
return _(StatusDisplayStrings._STATUS_MAP.get(val_status, StatusDisplayStrings._UNKNOWN))
@expect_json @expect_json
...@@ -126,7 +127,6 @@ def video_encodings_download(request, course_key_string): ...@@ -126,7 +127,6 @@ def video_encodings_download(request, course_key_string):
return _("{profile_name} URL").format(profile_name=profile) return _("{profile_name} URL").format(profile_name=profile)
profile_whitelist = VideoUploadConfig.get_profile_whitelist() profile_whitelist = VideoUploadConfig.get_profile_whitelist()
status_whitelist = VideoUploadConfig.get_status_whitelist()
videos = list(_get_videos(course)) videos = list(_get_videos(course))
name_col = _("Name") name_col = _("Name")
...@@ -153,7 +153,7 @@ def video_encodings_download(request, course_key_string): ...@@ -153,7 +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"]), (status_col, StatusDisplayStrings.get(video["status"])),
] + ] +
[ [
(get_profile_header(encoded_video["profile"]), encoded_video["url"]) (get_profile_header(encoded_video["profile"]), encoded_video["url"])
...@@ -182,8 +182,7 @@ def video_encodings_download(request, course_key_string): ...@@ -182,8 +182,7 @@ def video_encodings_download(request, course_key_string):
) )
writer.writeheader() writer.writeheader()
for video in videos: for video in videos:
if video["status"] in status_whitelist: writer.writerow(make_csv_dict(video))
writer.writerow(make_csv_dict(video))
return response return response
...@@ -223,7 +222,7 @@ def _get_videos(course): ...@@ -223,7 +222,7 @@ def _get_videos(course):
# convert VAL's status to studio's Video Upload feature status. # convert VAL's status to studio's Video Upload feature status.
for video in videos: for video in videos:
video["status"] = status_display_string(video["status"]) video["status"] = StatusDisplayStrings.get(video["status"])
return videos return videos
......
...@@ -65,22 +65,11 @@ define( ...@@ -65,22 +65,11 @@ define(
expect($el.find(".video-id-col").text()).toEqual(testId); expect($el.find(".video-id-col").text()).toEqual(testId);
}); });
_.each( it("should render status correctly", function() {
[ var testStatus = "Test Status";
{status: "Uploading", expected: "Uploading"}, var $el = render({status: testStatus});
{status: "In Progress", expected: "In Progress"}, expect($el.find(".status-col").text()).toEqual(testStatus);
{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() {
var $el = render({status: caseInfo.status});
expect($el.find(".status-col").text()).toEqual(caseInfo.expected);
});
}
);
}); });
} }
); );
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