Commit 01e67fb1 by Muzaffar yousaf Committed by GitHub

Merge pull request #87 from edx/mrehan/val-transcripts-backend-api

VAL changes for Video Transcripts.
parents cb392214 c2b29bee
......@@ -8,3 +8,7 @@ omit =
**/tests/*
**/settings.py
**/migrations*
[html]
title = edx-val Python Test Coverage Report
directory = html_coverage
......@@ -68,4 +68,6 @@ logs/*/*.log*
venv/
venvs/
src/
video-images/
video-transcripts/
Christopher Lee <clee@edx.org>
Mushtaq Ali <mushtaak@gmail.com>
Muhammad Ammar <mammar@gmail.com>
Muhammad Rehan <mrehan@edx.org>
"""
Admin file for django app edxval.
"""
from django import forms
from django.contrib import admin
from .models import Video, Profile, EncodedVideo, Subtitle, CourseVideo, VideoImage
from .models import (CourseVideo, EncodedVideo, Profile, TranscriptPreference,
Video, VideoImage, VideoTranscript)
class ProfileAdmin(admin.ModelAdmin): # pylint: disable=C0111
......@@ -35,19 +37,53 @@ class VideoAdmin(admin.ModelAdmin): # pylint: disable=C0111
class VideoImageAdmin(admin.ModelAdmin):
raw_id_fields = ('course_video', )
list_display = ('get_course_video', 'image', 'generated_images')
def get_course_video(self, obj):
return u'"{course_id}" -- "{edx_video_id}" '.format(
course_id=obj.course_video.course_id,
edx_video_id=obj.course_video.video.edx_video_id
)
get_course_video.admin_order_field = 'course_video'
get_course_video.short_description = 'Course Video'
model = VideoImage
verbose_name = 'Video Image'
verbose_name_plural = 'Video Images'
class CourseVideoAdmin(admin.ModelAdmin):
list_display = ('course_id', 'get_video_id', 'is_hidden')
def get_video_id(self, obj):
return obj.video.edx_video_id
get_video_id.admin_order_field = 'video'
get_video_id.short_description = 'edX Video Id'
model = CourseVideo
verbose_name = 'Course Video'
verbose_name_plural = 'Course Videos'
class VideoTranscriptAdmin(admin.ModelAdmin):
list_display = ('video_id', 'language_code', 'provider', 'file_format')
model = VideoTranscript
class TranscriptPreferenceAdmin(admin.ModelAdmin):
list_display = ('course_id', 'provider', 'video_source_language', 'preferred_languages')
model = TranscriptPreference
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Video, VideoAdmin)
admin.site.register(Subtitle)
admin.site.register(VideoTranscript, VideoTranscriptAdmin)
admin.site.register(TranscriptPreference, TranscriptPreferenceAdmin)
admin.site.register(VideoImage, VideoImageAdmin)
admin.site.register(CourseVideo, CourseVideoAdmin)
......@@ -48,3 +48,17 @@ class ValCannotUpdateError(ValError):
This error is raised when an object cannot be updated
"""
pass
class InvalidTranscriptFormat(ValError):
"""
This error is raised when an transcript format is not supported
"""
pass
class InvalidTranscriptProvider(ValError):
"""
This error is raised when an transcript provider is not supported
"""
pass
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import model_utils.fields
import django.utils.timezone
import edxval.models
class Migration(migrations.Migration):
dependencies = [
('edxval', '0005_videoimage'),
]
operations = [
migrations.CreateModel(
name='TranscriptPreference',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
('course_id', models.CharField(unique=True, max_length=255, verbose_name=b'Course ID')),
('provider', models.CharField(max_length=20, verbose_name=b'Provider', choices=[(b'Custom', b'Custom'), (b'3PlayMedia', b'3PlayMedia'), (b'Cielo24', b'Cielo24')])),
('cielo24_fidelity', models.CharField(blank=True, max_length=20, null=True, verbose_name=b'Cielo24 Fidelity', choices=[(b'MECHANICAL', b'Mechanical, 75% Accuracy'), (b'PREMIUM', b'Premium, 95% Accuracy'), (b'PROFESSIONAL', b'Professional, 99% Accuracy')])),
('cielo24_turnaround', models.CharField(blank=True, max_length=20, null=True, verbose_name=b'Cielo24 Turnaround', choices=[(b'STANDARD', b'Standard, 48h'), (b'PRIORITY', b'Priority, 24h')])),
('three_play_turnaround', models.CharField(blank=True, max_length=20, null=True, verbose_name=b'3PlayMedia Turnaround', choices=[(b'extended_service', b'10-Day/Extended'), (b'default', b'4-Day/Default'), (b'expedited_service', b'2-Day/Expedited'), (b'rush_service', b'24 hour/Rush'), (b'same_day_service', b'Same Day')])),
('preferred_languages', edxval.models.ListField(default=[], verbose_name=b'Preferred Languages', max_items=50, blank=True)),
('video_source_language', models.CharField(help_text=b'This specifies the speech language of a Video.', max_length=50, null=True, verbose_name=b'Video Source Language', blank=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='VideoTranscript',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
('video_id', models.CharField(help_text=b'It can be an edx_video_id or an external video id', max_length=255)),
('transcript', edxval.models.CustomizableFileField(null=True, blank=True)),
('language_code', models.CharField(max_length=50, db_index=True)),
('provider', models.CharField(default=b'Custom', max_length=30, choices=[(b'Custom', b'Custom'), (b'3PlayMedia', b'3PlayMedia'), (b'Cielo24', b'Cielo24')])),
('file_format', models.CharField(db_index=True, max_length=20, choices=[(b'srt', b'SubRip'), (b'sjson', b'SRT JSON')])),
],
),
migrations.AlterUniqueTogether(
name='videotranscript',
unique_together=set([('video_id', 'language_code')]),
),
]
......@@ -5,9 +5,10 @@ Serialization is usually sent through the VideoSerializer which uses the
EncodedVideoSerializer which uses the profile_name as it's profile field.
"""
from rest_framework import serializers
from rest_framework.fields import IntegerField, DateTimeField
from rest_framework.fields import DateTimeField, IntegerField
from edxval.models import Profile, Video, EncodedVideo, Subtitle, CourseVideo, VideoImage
from edxval.models import (CourseVideo, EncodedVideo, Profile, TranscriptPreference, Video,
VideoImage, VideoTranscript)
class EncodedVideoSerializer(serializers.ModelSerializer):
......@@ -50,37 +51,22 @@ class EncodedVideoSerializer(serializers.ModelSerializer):
return data.get('profile', None)
class SubtitleSerializer(serializers.ModelSerializer):
class TranscriptSerializer(serializers.ModelSerializer):
"""
Serializer for Subtitle objects
Serializer for VideoTranscript objects
"""
content_url = serializers.CharField(source='get_absolute_url', read_only=True)
content = serializers.CharField(write_only=True)
class Meta: # pylint: disable=C1001, C0111
model = VideoTranscript
lookup_field = 'video_id'
fields = ('video_id', 'url', 'language_code', 'provider', 'file_format')
def validate(self, data):
"""
Validate that the subtitle is in the correct format
"""
value = data.get("content")
if data.get("fmt") == "sjson":
import json
try:
loaded = json.loads(value)
except ValueError:
raise serializers.ValidationError("Not in JSON format")
else:
data["content"] = json.dumps(loaded)
return data
url = serializers.SerializerMethodField()
class Meta: # pylint: disable=C1001, C0111
model = Subtitle
lookup_field = "id"
fields = (
"fmt",
"language",
"content_url",
"content",
)
def get_url(self, transcript):
"""
Retrieves the transcript url.
"""
return transcript.url()
class CourseSerializer(serializers.RelatedField):
......@@ -118,7 +104,6 @@ class VideoSerializer(serializers.ModelSerializer):
encoded_videos takes a list of dicts EncodedVideo data.
"""
encoded_videos = EncodedVideoSerializer(many=True)
subtitles = SubtitleSerializer(many=True, required=False)
courses = CourseSerializer(
many=True,
read_only=False,
......@@ -170,7 +155,6 @@ class VideoSerializer(serializers.ModelSerializer):
"""
courses = validated_data.pop("courses", [])
encoded_videos = validated_data.pop("encoded_videos", [])
subtitles = validated_data.pop("subtitles", [])
video = Video.objects.create(**validated_data)
......@@ -179,11 +163,6 @@ class VideoSerializer(serializers.ModelSerializer):
for video_data in encoded_videos
)
Subtitle.objects.bulk_create(
Subtitle(video=video, **subtitle_data)
for subtitle_data in subtitles
)
# The CourseSerializer will already have converted the course data
# to CourseVideo models, so we can just set the video and save.
# Also create VideoImage objects if an image filename is present
......@@ -211,13 +190,6 @@ class VideoSerializer(serializers.ModelSerializer):
for video_data in validated_data.get("encoded_videos", [])
)
# Set subtitles
instance.subtitles.all().delete()
Subtitle.objects.bulk_create(
Subtitle(video=instance, **subtitle_data)
for subtitle_data in validated_data.get("subtitles", [])
)
# Set courses
# NOTE: for backwards compatibility with the DRF v2 behavior,
# we do NOT delete existing course videos during the update.
......@@ -229,3 +201,30 @@ class VideoSerializer(serializers.ModelSerializer):
VideoImage.create_or_update(course_video, image_name)
return instance
class TranscriptPreferenceSerializer(serializers.ModelSerializer):
"""
Serializer for TranscriptPreference
"""
class Meta: # pylint: disable=C1001, C0111
model = TranscriptPreference
fields = (
'course_id',
'provider',
'cielo24_fidelity',
'cielo24_turnaround',
'three_play_turnaround',
'preferred_languages',
'video_source_language',
'modified',
)
preferred_languages = serializers.SerializerMethodField()
def get_preferred_languages(self, transcript_preference):
"""
Returns python list for preferred_languages model field.
"""
return transcript_preference.preferred_languages
......@@ -190,3 +190,13 @@ VIDEO_IMAGE_SETTINGS = dict(
VIDEO_IMAGE_MIN_BYTES=100,
DIRECTORY_PREFIX='video-images/',
)
VIDEO_TRANSCRIPTS_SETTINGS = dict(
# Backend storage
# STORAGE_CLASS='storages.backends.s3boto.S3BotoStorage',
# STORAGE_KWARGS=dict(bucket='video-transcripts-bucket'),
# If you are changing prefix value then update the .gitignore accordingly
# so that transcripts created during tests due to upload should be ignored
VIDEO_TRANSCRIPTS_MAX_BYTES=3145728, # 3 MB
DIRECTORY_PREFIX='video-transcripts/',
)
......@@ -3,6 +3,14 @@
"""
Constants used for tests.
"""
from edxval.models import (
TranscriptFormat,
TranscriptProviderType,
Cielo24Fidelity,
Cielo24Turnaround,
ThreePlayTurnaround
)
EDX_VIDEO_ID = "itchyjacket"
"""
Generic Profiles for manually creating profile objects
......@@ -388,3 +396,44 @@ VIDEO_DICT_UPDATE_ANIMAL = dict(
encoded_videos=[],
subtitles=[]
)
VIDEO_TRANSCRIPT_CIELO24 = dict(
video_id='super-soaker',
language_code='en',
transcript='wow.srt',
provider=TranscriptProviderType.CIELO24,
file_format=TranscriptFormat.SRT,
)
VIDEO_TRANSCRIPT_3PLAY = dict(
video_id='super-soaker',
language_code='de',
transcript='wow.sjson',
provider=TranscriptProviderType.THREE_PLAY_MEDIA,
file_format=TranscriptFormat.SJSON,
)
VIDEO_TRANSCRIPT_CUSTOM = dict(
video_id='external_video_id',
language_code='de',
transcript='wow.srt',
provider=TranscriptProviderType.CUSTOM,
file_format=TranscriptFormat.SRT,
)
TRANSCRIPT_PREFERENCES_CIELO24 = dict(
course_id='edX/DemoX/Demo_Course',
provider=TranscriptProviderType.CIELO24,
cielo24_fidelity=Cielo24Fidelity.PROFESSIONAL,
cielo24_turnaround=Cielo24Turnaround.PRIORITY,
preferred_languages=['ar'],
video_source_language='en',
)
TRANSCRIPT_PREFERENCES_3PLAY = dict(
course_id='edX/DemoX/Demo_Course',
provider=TranscriptProviderType.THREE_PLAY_MEDIA,
three_play_turnaround=ThreePlayTurnaround.SAME_DAY_SERVICE,
preferred_languages=['ar', 'en'],
video_source_language='en',
)
1
00:00:07,180 --> 00:00:08,460
This is Arrow line 1.
2
00:00:08,460 --> 00:00:10,510
This is Arrow line 2.
3
00:00:10,510 --> 00:00:13,560
This is Arrow line 3.
4
00:00:13,560 --> 00:00:14,360
This is Arrow line 4.
5
00:00:14,370 --> 00:00:16,530
This is Arrow line 5.
6
00:00:16,500 --> 00:00:18,600
可以用“我不太懂艺术 但我知道我喜欢什么”做比喻.
\ No newline at end of file
1
00:00:07,180 --> 00:00:08,460
This is Flash line 1.
2
00:00:08,460 --> 00:00:10,510
This is Flash line 2.
3
00:00:10,510 --> 00:00:13,560
This is Flash line 3.
4
00:00:13,560 --> 00:00:14,360
This is Flash line 4.
5
00:00:14,370 --> 00:00:16,530
This is Flash line 5.
6
00:00:16,500 --> 00:00:18,600
可以用“我不太懂艺术 但我知道我喜欢什么”做比喻.
\ No newline at end of file
......@@ -3,13 +3,16 @@
Tests for Video Abstraction Layer views
"""
import json
from ddt import ddt, data, unpack
import unittest
from ddt import data, ddt, unpack
from django.core.urlresolvers import reverse
from rest_framework import status
from edxval.tests import constants, APIAuthTestCase
from edxval.models import Profile, Video, CourseVideo
from edxval.models import (CourseVideo, Profile, TranscriptFormat,
TranscriptProviderType, Video, VideoTranscript)
from edxval.serializers import TranscriptSerializer
from edxval.tests import APIAuthTestCase, constants
class VideoDetail(APIAuthTestCase):
......@@ -206,6 +209,7 @@ class VideoDetail(APIAuthTestCase):
)
self.assertEqual(len(videos[0].encoded_videos.all()), 1)
@unittest.skip("Skipping for now. We may need this later when we create transcripts alongwith video")
def test_update_remove_subtitles(self):
# Create some subtitles
self._create_videos(constants.COMPLETE_SET_STAR)
......@@ -665,7 +669,7 @@ class VideoListTest(APIAuthTestCase):
Tests number of queries for a Video with no Encoded Videos
"""
url = reverse('video-list')
with self.assertNumQueries(9):
with self.assertNumQueries(8):
self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json')
def test_queries_for_two_encoded_video(self):
......@@ -673,7 +677,7 @@ class VideoListTest(APIAuthTestCase):
Tests number of queries for a Video/EncodedVideo(2) pair
"""
url = reverse('video-list')
with self.assertNumQueries(15):
with self.assertNumQueries(13):
self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
def test_queries_for_single_encoded_videos(self):
......@@ -681,7 +685,7 @@ class VideoListTest(APIAuthTestCase):
Tests number of queries for a Video/EncodedVideo(1) pair
"""
url = reverse('video-list')
with self.assertNumQueries(13):
with self.assertNumQueries(11):
self.client.post(url, constants.COMPLETE_SET_STAR, format='json')
......@@ -718,18 +722,19 @@ class VideoDetailTest(APIAuthTestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(9):
with self.assertNumQueries(7):
self.client.get("/edxval/videos/").data
response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(12):
with self.assertNumQueries(9):
self.client.get("/edxval/videos/").data
response = self.client.post(url, constants.COMPLETE_SET_STAR, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(14):
with self.assertNumQueries(10):
self.client.get("/edxval/videos/").data
@unittest.skip("Skipping for now. We may need these later when we create transcripts alongwith video")
class SubtitleDetailTest(APIAuthTestCase):
"""
Tests for subtitle API
......@@ -811,6 +816,7 @@ class SubtitleDetailTest(APIAuthTestCase):
)
self.assertEqual(self.client.get(video_subtitles['content_url']).content, '{"start": "00:00:00"}')
@ddt
class VideoImagesViewTest(APIAuthTestCase):
"""
......@@ -897,3 +903,135 @@ class VideoImagesViewTest(APIAuthTestCase):
response.data['message'],
message
)
@ddt
class VideoTranscriptViewTest(APIAuthTestCase):
"""
Tests VideoTranscriptView.
"""
def setUp(self):
"""
Tests setup.
"""
self.url = reverse('create-video-transcript')
self.video = Video.objects.create(**constants.VIDEO_DICT_FISH)
self.transcript_data = constants.VIDEO_TRANSCRIPT_CIELO24
super(VideoTranscriptViewTest, self).setUp()
def test_create_transcript(self):
"""
Tests POSTing transcript successfully.
"""
post_transcript_data = dict(self.transcript_data)
post_transcript_data['name'] = post_transcript_data.pop('transcript')
response = self.client.post(self.url, post_transcript_data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
serialized_data = TranscriptSerializer(VideoTranscript.objects.first()).data
post_transcript_data['url'] = post_transcript_data.pop('name')
self.assertEqual(serialized_data, post_transcript_data)
def test_update_existing_transcript(self):
"""
Tests updating existing transcript works as expected.
"""
VideoTranscript.objects.create(**self.transcript_data)
post_transcript_data = dict(self.transcript_data)
post_transcript_data['name'] = post_transcript_data.pop('transcript')
response = self.client.post(self.url, post_transcript_data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data['message'],
u'Can not override existing transcript for video "{video_id}" and language code "{language}".'.format(
video_id=self.video.edx_video_id, language=post_transcript_data['language_code'])
)
@data(
{
'post_data': {},
'message': u'video_id and name and language_code and provider and file_format must be specified.'
},
{
'post_data': {
'video_id': 'super-soaker',
'name': 'abc.xyz',
'language_code': 'en',
'provider': TranscriptProviderType.CIELO24,
'file_format': 'xyz'
},
'message': u'"xyz" transcript file type is not supported. Supported formats are "{}"'.format(
sorted(dict(TranscriptFormat.CHOICES).keys())
)
},
{
'post_data': {
'video_id': 'super-soaker',
'name': 'abc.srt',
'language_code': 'en',
'provider': 'xyz',
'file_format': TranscriptFormat.SRT
},
'message': u'"xyz" provider is not supported. Supported transcription providers are "{}"'.format(
sorted(dict(TranscriptProviderType.CHOICES).keys())
)
},
)
@unpack
def test_error_responses(self, post_data, message):
"""
Tests error responses occurred during POSTing.
"""
response = self.client.post(self.url, post_data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data['message'], message)
@ddt
class VideoStatusViewTest(APIAuthTestCase):
"""
VideoStatusView Tests.
"""
def setUp(self):
"""
Tests setup.
"""
self.url = reverse('video-status-update')
self.video = Video.objects.create(**constants.VIDEO_DICT_FISH)
super(VideoStatusViewTest, self).setUp()
@data(
{
'patch_data': {},
'message': u'"edx_video_id and status" params must be specified.',
'status_code': status.HTTP_400_BAD_REQUEST,
},
{
'patch_data': {'edx_video_id': 'super-soaker', 'status': 'fake'},
'message': u'"fake" is not a valid Video status.',
'status_code': status.HTTP_400_BAD_REQUEST,
},
{
'patch_data': {'edx_video_id': 'fake', 'status': 'transcript_ready'},
'message': u'Video is not found for specified edx_video_id: fake',
'status_code': status.HTTP_400_BAD_REQUEST,
},
{
'patch_data': {'edx_video_id': 'super-soaker', 'status': 'transcript_ready'},
'message': None,
'status_code': status.HTTP_200_OK,
},
)
@unpack
def test_transcript_status(self, patch_data, message, status_code):
"""
Tests PATCHing video transcript status.
"""
response = self.client.patch(self.url, patch_data, format='json')
self.assertEqual(response.status_code, status_code)
self.assertEqual(response.data.get('message'), message)
......@@ -9,22 +9,22 @@ from edxval import views
urlpatterns = [
url(r'^videos/$',
views.VideoList.as_view(),
name="video-list"
name='video-list'
),
url(
r'^videos/(?P<edx_video_id>[-\w]+)$',
views.VideoDetail.as_view(),
name="video-detail"
name='video-detail'
),
url(
r'^videos/(?P<video__edx_video_id>[-\w]+)/(?P<language>[-_\w]+)$',
views.SubtitleDetail.as_view(),
name="subtitle-detail"
r'^videos/status/$',
views.VideoStatusView.as_view(),
name='video-status-update'
),
url(
r'^videos/(?P<edx_video_id>[-\w]+)/(?P<language>[-_\w]+)/subtitle$',
views.get_subtitle,
name="subtitle-content"
r'^videos/video-transcripts/create/$',
views.VideoTranscriptView.as_view(),
name='create-video-transcript'
),
url(
r'^videos/video-images/update/$',
......
......@@ -5,6 +5,120 @@ Util methods to be used in api and models.
from django.conf import settings
from django.core.files.storage import get_storage_class
# 3rd Party Transcription Plans
THIRD_PARTY_TRANSCRIPTION_PLANS = {
'Cielo24': {
'display_name': 'Cielo24',
'turnaround': {
'PRIORITY': 'Priority (24 hours)',
'STANDARD': 'Standard (48 hours)'
},
'fidelity': {
'MECHANICAL': {
'display_name': 'Mechanical (75% accuracy)',
'languages': {
'nl': 'Dutch',
'en': 'English',
'fr': 'French',
'de': 'German',
'it': 'Italian',
'es': 'Spanish',
}
},
'PREMIUM': {
'display_name': 'Premium (95% accuracy)',
'languages': {
'en': 'English',
}
},
'PROFESSIONAL': {
'display_name': 'Professional (99% accuracy)',
'languages': {
'ar': 'Arabic',
'zh-tw': 'Chinese - Mandarin (Traditional)',
'zh-cmn': 'Chinese - Mandarin (Simplified)',
'zh-yue': 'Chinese - Cantonese (Traditional)',
'nl': 'Dutch',
'en': 'English',
'fr': 'French',
'de': 'German',
'he': 'Hebrew',
'hi': 'Hindi',
'it': 'Italian',
'ja': 'Japanese',
'ko': 'Korean',
'pt': 'Portuguese',
'ru': 'Russian',
'es': 'Spanish',
'tr': 'Turkish',
}
},
}
},
'3PlayMedia': {
'display_name': '3Play Media',
'turnaround': {
'same_day_service': 'Same day',
'rush_service': '24 hours (rush)',
'expedited_service': '2 days (expedited)',
'default': '4 days (default)',
'extended_service':'10 days (extended)'
},
'languages': {
'en': 'English',
'fr': 'French',
'de': 'German',
'it': 'Italian',
'nl': 'Dutch',
'es': 'Spanish',
'el': 'Greek',
'pt': 'Portuguese',
'zh': 'Chinese',
'ar': 'Arabic',
'he': 'Hebrew',
'ru': 'Russian',
'ja': 'Japanese',
'sv': 'Swedish',
'cs': 'Czech',
'da': 'Danish',
'fi': 'Finnish',
'id': 'Indonesian',
'ko': 'Korean',
'no': 'Norwegian',
'pl': 'Polish',
'th': 'Thai',
'tr': 'Turkish',
'vi': 'Vietnamese',
'ro': 'Romanian',
'hu': 'Hungarian',
'ms': 'Malay',
'bg': 'Bulgarian',
'tl': 'Tagalog',
'sr': 'Serbian',
'sk': 'Slovak',
'uk': 'Ukrainian',
},
# Valid translations -- a mapping of source languages to the
# translatable target languages.
'translations': {
'es': [
'en'
],
'en': [
'el', 'en', 'zh', 'vi',
'it', 'ar', 'cs', 'id',
'es', 'ru', 'nl', 'pt',
'no', 'tr', 'tl', 'th',
'ro', 'pl', 'fr', 'bg',
'uk', 'de', 'da', 'fi',
'hu', 'ja', 'he', 'sr',
'ko', 'sv', 'sk', 'ms'
],
}
}
}
def video_image_path(video_image_instance, filename): # pylint:disable=unused-argument
"""
......@@ -29,3 +143,28 @@ def get_video_image_storage():
# during edx-platform loading this method gets called but settings are not ready yet
# so in that case we will return default(FileSystemStorage) storage class instance
return get_storage_class()()
def video_transcript_path(video_transcript_instance, filename): # pylint:disable=unused-argument
"""
Returns video transcript path.
Arguments:
video_transcript_instance (VideoTranscript): This is passed automatically by models.CustomizableFileField
filename (str): name of image file
"""
return u'{}{}'.format(settings.VIDEO_TRANSCRIPTS_SETTINGS.get('DIRECTORY_PREFIX', ''), filename)
def get_video_transcript_storage():
"""
Return the configured django storage backend for video transcripts.
"""
if hasattr(settings, 'VIDEO_TRANSCRIPTS_SETTINGS'):
return get_storage_class(
settings.VIDEO_TRANSCRIPTS_SETTINGS.get('STORAGE_CLASS'),
)(**settings.VIDEO_TRANSCRIPTS_SETTINGS.get('STORAGE_KWARGS', {}))
else:
# during edx-platform loading this method gets called but settings are not ready yet
# so in that case we will return default(FileSystemStorage) storage class instance
return get_storage_class()()
"""
Views file for django app edxval.
"""
from rest_framework.views import APIView
from rest_framework import generics
from rest_framework.authentication import SessionAuthentication
from rest_framework_oauth.authentication import OAuth2Authentication
from rest_framework.permissions import DjangoModelPermissions
from rest_framework.response import Response
from rest_framework import status
import logging
from django.core.exceptions import ValidationError
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError
from django.views.decorators.http import last_modified
from rest_framework import generics, status
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import DjangoModelPermissions
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_oauth.authentication import OAuth2Authentication
from edxval.api import (create_or_update_video_transcript,
get_video_transcript, update_video_status)
from edxval.models import (CourseVideo, Profile, TranscriptFormat,
TranscriptProviderType, Video, VideoImage,
VideoTranscript)
from edxval.serializers import TranscriptSerializer, VideoSerializer
from edxval.models import Video, Profile, Subtitle, CourseVideo, VideoImage
from edxval.serializers import (
VideoSerializer,
SubtitleSerializer
)
LOGGER = logging.getLogger(__name__) # pylint: disable=C0103
VALID_VIDEO_STATUSES = [
'transcription_in_progress',
'transcript_ready',
]
class ReadRestrictedDjangoModelPermissions(DjangoModelPermissions):
......@@ -92,15 +101,116 @@ class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = VideoSerializer
class SubtitleDetail(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
class VideoTranscriptView(APIView):
"""
Gets a subtitle instance given its id
A Transcription View, used by edx-video-pipeline to create video transcripts.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
# noinspection PyMethodMayBeStatic
def post(self, request):
"""
Creates a video transcript instance with the given information.
Arguments:
request: A WSGI request.
"""
attrs = ('video_id', 'name', 'language_code', 'provider', 'file_format')
missing = [attr for attr in attrs if attr not in request.data]
if missing:
LOGGER.warn(
'[VAL] Required transcript params are missing. %s', ' and '.join(missing)
)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data=dict(message=u'{missing} must be specified.'.format(missing=' and '.join(missing)))
)
video_id = request.data['video_id']
language_code = request.data['language_code']
transcript_name = request.data['name']
provider = request.data['provider']
file_format = request.data['file_format']
supported_formats = sorted(dict(TranscriptFormat.CHOICES).keys())
if file_format not in supported_formats:
message = (
u'"{format}" transcript file type is not supported. Supported formats are "{supported_formats}"'
).format(format=file_format, supported_formats=supported_formats)
return Response(status=status.HTTP_400_BAD_REQUEST, data={'message': message})
supported_providers = sorted(dict(TranscriptProviderType.CHOICES).keys())
if provider not in supported_providers:
message = (
u'"{provider}" provider is not supported. Supported transcription providers are "{supported_providers}"'
).format(provider=provider, supported_providers=supported_providers)
return Response(status=status.HTTP_400_BAD_REQUEST, data={'message': message})
transcript = VideoTranscript.get_or_none(video_id, language_code)
if transcript is None:
create_or_update_video_transcript(
video_id,
language_code,
transcript_name,
file_format,
provider,
)
response = Response(status=status.HTTP_200_OK)
else:
message = (
u'Can not override existing transcript for video "{video_id}" and language code "{language}".'
).format(video_id=video_id, language=language_code)
response = Response(status=status.HTTP_400_BAD_REQUEST, data={'message': message})
return response
class VideoStatusView(APIView):
"""
A Video View to update the status of a video.
Note:
Currently, the valid statuses are `transcription_in_progress` and `transcript_ready` because it
was intended to only be used for video transcriptions but if you found it helpful to your needs, you
can add more statuses so that you can use it for updating other video statuses too.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
lookup_fields = ("video__edx_video_id", "language")
queryset = Subtitle.objects.all()
serializer_class = SubtitleSerializer
def patch(self, request):
"""
Update the status of a video.
"""
attrs = ('edx_video_id', 'status')
missing = [attr for attr in attrs if attr not in request.data]
if missing:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={'message': u'"{missing}" params must be specified.'.format(missing=' and '.join(missing))}
)
edx_video_id = request.data['edx_video_id']
video_status = request.data['status']
if video_status not in VALID_VIDEO_STATUSES:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={'message': u'"{status}" is not a valid Video status.'.format(status=video_status)}
)
try:
video = Video.objects.get(edx_video_id=edx_video_id)
video.status = video_status
video.save()
response_status = status.HTTP_200_OK
response_payload = {}
except Video.DoesNotExist:
response_status = status.HTTP_400_BAD_REQUEST
response_payload = {
'message': u'Video is not found for specified edx_video_id: {edx_video_id}'.format(
edx_video_id=edx_video_id
)
}
return Response(status=response_status, data=response_payload)
class VideoImagesView(APIView):
......@@ -148,19 +258,3 @@ class VideoImagesView(APIView):
)
return Response()
def _last_modified_subtitle(request, edx_video_id, language): # pylint: disable=W0613
"""
Returns the last modified subtitle
"""
return Subtitle.objects.get(video__edx_video_id=edx_video_id, language=language).modified
@last_modified(last_modified_func=_last_modified_subtitle)
def get_subtitle(request, edx_video_id, language): # pylint: disable=W0613
"""
Return content of subtitle by id
"""
sub = Subtitle.objects.get(video__edx_video_id=edx_video_id, language=language)
response = HttpResponse(sub.content, content_type=sub.content_type)
return response
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