Commit 5b1339b3 by Qubad786 Committed by muhammad-ammar

Add TranscriptPreference, prepare 3rd party transcription plans and their respective api methods

parent ac5c5c23
......@@ -3,7 +3,9 @@ Admin file for django app edxval.
"""
from django.contrib import admin
from .models import Video, Profile, EncodedVideo, VideoTranscript, CourseVideo, VideoImage
from .models import (CourseVideo, EncodedVideo, Profile, TranscriptPreference,
Video, VideoImage, VideoTranscript)
class ProfileAdmin(admin.ModelAdmin): # pylint: disable=C0111
......@@ -49,5 +51,6 @@ class CourseVideoAdmin(admin.ModelAdmin):
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Video, VideoAdmin)
admin.site.register(VideoTranscript)
admin.site.register(TranscriptPreference)
admin.site.register(VideoImage, VideoImageAdmin)
admin.site.register(CourseVideo, CourseVideoAdmin)
......@@ -4,9 +4,9 @@
The internal API for VAL.
"""
import logging
from enum import Enum
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from enum import Enum
from lxml.etree import Element, SubElement
from edxval.exceptions import (InvalidTranscriptFormat,
......@@ -14,9 +14,11 @@ from edxval.exceptions import (InvalidTranscriptFormat,
ValCannotUpdateError, ValInternalError,
ValVideoNotFoundError)
from edxval.models import (CourseVideo, EncodedVideo, Profile,
TranscriptFormat, TranscriptProviderType, Video,
VideoImage, VideoTranscript)
from edxval.serializers import TranscriptSerializer, VideoSerializer
TranscriptFormat, TranscriptPreference,
TranscriptProviderType, Video, VideoImage,
VideoTranscript)
from edxval.serializers import TranscriptPreferenceSerializer, TranscriptSerializer, VideoSerializer
from edxval.utils import THIRD_PARTY_TRANSCRIPTION_PLANS
logger = logging.getLogger(__name__) # pylint: disable=C0103
......@@ -226,6 +228,44 @@ def create_or_update_video_transcript(
return video_transcript.url()
def get_3rd_party_transcription_plans():
"""
Retrieves 3rd party transcription plans.
"""
return THIRD_PARTY_TRANSCRIPTION_PLANS
def get_transcript_preferences(course_id):
"""
Retrieves course wide transcript preferences
Arguments:
course_id (str): course id
"""
try:
transcript_preference = TranscriptPreference.objects.get(course_id=course_id)
except TranscriptPreference.DoesNotExist:
return
return TranscriptPreferenceSerializer(transcript_preference).data
def create_or_update_transcript_preferences(course_id, **preferences):
"""
Creates or updates course-wide transcript preferences
Arguments:
course_id(str): course id
Keyword Arguments:
preferences(dict): keyword arguments
"""
transcript_preference, __ = TranscriptPreference.objects.update_or_create(
course_id=course_id, defaults=preferences
)
return TranscriptPreferenceSerializer(transcript_preference).data
def get_course_video_image_url(course_id, edx_video_id):
"""
Returns course video image url or None if no image found
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-24 07:21
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
import edxval.models
import model_utils.fields
class Migration(migrations.Migration):
dependencies = [
('edxval', '0006_auto_20170823_0015'),
]
operations = [
migrations.CreateModel(
name='TranscriptPreference',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('course_id', models.CharField(max_length=255, unique=True, verbose_name=b'Course ID')),
('provider', models.CharField(choices=[(b'Custom', b'Custom'), (b'3PlayMedia', b'3PlayMedia'), (b'Cielo24', b'Cielo24')], max_length=20, verbose_name=b'Provider')),
('cielo24_fidelity', models.CharField(blank=True, choices=[(b'MECHANICAL', b'Mechanical, 75% Accuracy'), (b'PREMIUM', b'Premium, 95% Accuracy'), (b'PROFESSIONAL', b'Professional, 99% Accuracy')], max_length=20, null=True, verbose_name=b'Cielo24 Fidelity')),
('cielo24_turnaround', models.CharField(blank=True, choices=[(b'STANDARD', b'Standard, 48h'), (b'PRIORITY', b'Priority, 24h')], max_length=20, null=True, verbose_name=b'Cielo24 Turnaround')),
('three_play_turnaround', models.CharField(blank=True, 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')], max_length=20, null=True, verbose_name=b'3PlayMedia Turnaround')),
('preferred_languages', edxval.models.ListField(blank=True, default=[], max_items=50, verbose_name=b'Preferred Languages')),
],
options={
'abstract': False,
},
),
]
......@@ -210,13 +210,17 @@ class ListField(models.TextField):
"""
ListField use to store and retrieve list data.
"""
def __init__(self, max_items=LIST_MAX_ITEMS, *args, **kwargs):
self.max_items = max_items
super(ListField, self).__init__(*args, **kwargs)
def get_prep_value(self, value):
"""
Converts a list to its json represetation to store in database as text.
Converts a list to its json representation to store in database as text.
"""
if value and not isinstance(value, list):
raise ValidationError(u'ListField value {} is not a list.'.format(value))
return json.dumps(self.validate(value) or [])
return json.dumps(self.validate_list(value) or [])
def from_db_value(self, value, expression, connection, context):
"""
......@@ -233,7 +237,7 @@ class ListField(models.TextField):
# If a list is set then validated its items
if isinstance(value, list):
return self.validate(value)
py_list = self.validate_list(value)
else: # try to de-serialize value and expect list and then validate
try:
py_list = json.loads(value)
......@@ -241,13 +245,13 @@ class ListField(models.TextField):
if not isinstance(py_list, list):
raise TypeError
self.validate(py_list)
self.validate_list(py_list)
except (ValueError, TypeError):
raise ValidationError(u'Must be a valid list of strings.')
return py_list
def validate(self, value):
def validate_list(self, value):
"""
Validate data before saving to database.
......@@ -260,14 +264,23 @@ class ListField(models.TextField):
Raises:
ValidationError
"""
if len(value) > LIST_MAX_ITEMS:
raise ValidationError(u'list must not contain more than {} items.'.format(LIST_MAX_ITEMS))
if len(value) > self.max_items:
raise ValidationError(
u'list must not contain more than {max_items} items.'.format(max_items=self.max_items)
)
if all(isinstance(item, basestring) for item in value) is False:
raise ValidationError(u'list must only contain strings.')
return value
def deconstruct(self):
name, path, args, kwargs = super(ListField, self).deconstruct()
# Only include kwarg if it's not the default
if self.max_items != LIST_MAX_ITEMS:
kwargs['max_items'] = self.max_items
return name, path, args, kwargs
class VideoImage(TimeStampedModel):
"""
......@@ -494,6 +507,88 @@ class Subtitle(models.Model):
return 'text/plain'
class Cielo24Turnaround(object):
"""
Cielo24 turnarounds.
"""
STANDARD = 'STANDARD'
PRIORITY = 'PRIORITY'
CHOICES = (
(STANDARD, 'Standard, 48h'),
(PRIORITY, 'Priority, 24h'),
)
class Cielo24Fidelity(object):
"""
Cielo24 fidelity.
"""
MECHANICAL = 'MECHANICAL'
PREMIUM = 'PREMIUM'
PROFESSIONAL = 'PROFESSIONAL'
CHOICES = (
(MECHANICAL, 'Mechanical, 75% Accuracy'),
(PREMIUM, 'Premium, 95% Accuracy'),
(PROFESSIONAL, 'Professional, 99% Accuracy'),
)
class ThreePlayTurnaround(object):
"""
3PlayMedia turnarounds.
"""
EXTENDED_SERVICE = 'extended_service'
DEFAULT = 'default'
EXPEDITED_SERVICE = 'expedited_service'
RUSH_SERVICE = 'rush_service'
SAME_DAY_SERVICE = 'same_day_service'
CHOICES = (
(EXTENDED_SERVICE, '10-Day/Extended'),
(DEFAULT, '4-Day/Default'),
(EXPEDITED_SERVICE, '2-Day/Expedited'),
(RUSH_SERVICE, '24 hour/Rush'),
(SAME_DAY_SERVICE, 'Same Day'),
)
class TranscriptPreference(TimeStampedModel):
"""
Third Party Transcript Preferences for a Course
"""
course_id = models.CharField(verbose_name='Course ID', max_length=255, unique=True)
provider = models.CharField(
verbose_name='Provider',
max_length=20,
choices=TranscriptProviderType.CHOICES,
)
cielo24_fidelity = models.CharField(
verbose_name='Cielo24 Fidelity',
max_length=20,
choices=Cielo24Fidelity.CHOICES,
null=True,
blank=True,
)
cielo24_turnaround = models.CharField(
verbose_name='Cielo24 Turnaround',
max_length=20,
choices=Cielo24Turnaround.CHOICES,
null=True,
blank=True,
)
three_play_turnaround = models.CharField(
verbose_name='3PlayMedia Turnaround',
max_length=20,
choices=ThreePlayTurnaround.CHOICES,
null=True,
blank=True,
)
preferred_languages = ListField(verbose_name='Preferred Languages', max_items=50, default=[], blank=True)
def __unicode__(self):
return u'{course_id} - {provider}'.format(course_id=self.course_id, provider=self.provider)
@receiver(models.signals.post_save, sender=Video)
def video_status_update_callback(sender, **kwargs): # pylint: disable=unused-argument
"""
......
......@@ -7,7 +7,7 @@ EncodedVideoSerializer which uses the profile_name as it's profile field.
from rest_framework import serializers
from rest_framework.fields import DateTimeField, IntegerField
from edxval.models import (CourseVideo, EncodedVideo, Profile, Video,
from edxval.models import (CourseVideo, EncodedVideo, Profile, TranscriptPreference, Video,
VideoImage, VideoTranscript)
......@@ -68,6 +68,7 @@ class TranscriptSerializer(serializers.ModelSerializer):
"""
return transcript.url()
class CourseSerializer(serializers.RelatedField):
"""
Field for CourseVideo
......@@ -200,3 +201,29 @@ 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',
'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
......@@ -3,7 +3,13 @@
"""
Constants used for tests.
"""
from edxval.models import TranscriptFormat, TranscriptProviderType
from edxval.models import (
TranscriptFormat,
TranscriptProviderType,
Cielo24Fidelity,
Cielo24Turnaround,
ThreePlayTurnaround
)
EDX_VIDEO_ID = "itchyjacket"
"""
......@@ -406,3 +412,18 @@ VIDEO_TRANSCRIPT_3PLAY = dict(
provider=TranscriptProviderType.THREE_PLAY_MEDIA,
file_format=TranscriptFormat.SJSON,
)
TRANSCRIPT_PREFERENCES_CIELO24 = dict(
course_id='edX/DemoX/Demo_Course',
provider=TranscriptProviderType.CIELO24,
cielo24_fidelity=Cielo24Fidelity.PROFESSIONAL,
cielo24_turnaround=Cielo24Turnaround.PRIORITY,
preferred_languages=['ar']
)
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']
)
......@@ -25,8 +25,9 @@ from edxval.api import (InvalidTranscriptFormat, InvalidTranscriptProvider,
VideoSortField)
from edxval.models import (LIST_MAX_ITEMS, CourseVideo, EncodedVideo, Profile,
TranscriptFormat, TranscriptProviderType, Video,
VideoImage, VideoTranscript)
VideoImage, VideoTranscript, TranscriptPreference)
from edxval.tests import APIAuthTestCase, constants
from edxval import utils
FILE_DATA = """
......@@ -1684,3 +1685,71 @@ class TranscriptTest(TestCase):
File(open(existing_transcript_url))
self.assertEqual(file_open_exception.exception.strerror, u'No such file or directory')
@ddt
class TranscriptPreferencesTest(TestCase):
"""
TranscriptPreferences API Tests
"""
def setUp(self):
"""
Tests setup
"""
self.course_id = 'edX/DemoX/Demo_Course'
self.transcript_preferences = TranscriptPreference.objects.create(
**constants.TRANSCRIPT_PREFERENCES_CIELO24
)
self.prefs = dict(constants.TRANSCRIPT_PREFERENCES_CIELO24)
self.prefs.update(constants.TRANSCRIPT_PREFERENCES_3PLAY)
def assert_prefs(self, received, expected):
"""
Compare `received` with `expected` and assert if not equal
"""
# no need to compare modified datetime
del received['modified']
self.assertEqual(received, expected)
def test_get_3rd_party_transcription_plans(self):
"""
Verify that `get_3rd_party_transcription_plans` api function works as expected
"""
self.assertEqual(
api.get_3rd_party_transcription_plans(),
utils.THIRD_PARTY_TRANSCRIPTION_PLANS
)
def test_get_transcript_preferences(self):
"""
Verify that `get_transcript_preferences` api function works as expected
"""
cielo24_prefs = dict(constants.TRANSCRIPT_PREFERENCES_CIELO24)
cielo24_prefs['three_play_turnaround'] = None
transcript_preferences = api.get_transcript_preferences(self.course_id)
self.assert_prefs(transcript_preferences, cielo24_prefs)
def test_update_transcript_preferences(self):
"""
Verify that `create_or_update_transcript_preferences` api function updates as expected
"""
transcript_preferences = api.create_or_update_transcript_preferences(**constants.TRANSCRIPT_PREFERENCES_3PLAY)
self.assert_prefs(transcript_preferences, self.prefs)
def test_create_transcript_preferences(self):
"""
Verify that `create_or_update_transcript_preferences` api function creates as expected
"""
self.prefs['course_id'] = 'edX/DemoX/Astonomy'
# Verify that no preference is present for course id `edX/DemoX/Astonomy`
self.assertIsNone(api.get_transcript_preferences(self.prefs['course_id']))
# create new preference
transcript_preferences = api.create_or_update_transcript_preferences(**self.prefs)
self.assert_prefs(transcript_preferences, self.prefs)
# Verify that there should be 2 preferences exists
self.assertEqual(TranscriptPreference.objects.count(), 2)
......@@ -990,3 +990,48 @@ class VideoTranscriptViewTest(APIAuthTestCase):
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': 'transcription_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': 'transcription_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,12 +9,17 @@ 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/status/$',
views.VideoStatusView.as_view(),
name='video-status-update'
),
url(
r'^videos/video-transcripts/create/$',
......
......@@ -5,6 +5,104 @@ 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': {
'STANDARD': 'Standard, 48h',
'PRIORITY': 'Priority, 24h'
},
'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': '3PlayMedia',
'turnaround': {
'extended_service':'10-Day/Extended',
'default': '4-Day/Default',
'expedited_service': '2-Day/Expedited',
'rush_service': '24-hour/Rush',
'same_day_service': 'Same Day'
},
'languages': {
'en': 'English',
'fr': 'French',
'de': 'German',
'it': 'Italian',
'nl': 'Dutch',
'es-419': 'Spanish (Latin America)',
'pt': 'Portuguese',
'zh-hans': 'Chinese (Simplified)',
'zh-cmn-Hant': 'Chinese (Traditional)',
'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',
}
}
}
def video_image_path(video_image_instance, filename): # pylint:disable=unused-argument
"""
......
......@@ -14,7 +14,8 @@ 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, create_or_update_video_transcript
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)
......@@ -22,6 +23,11 @@ from edxval.serializers import TranscriptSerializer, VideoSerializer
LOGGER = logging.getLogger(__name__) # pylint: disable=C0103
VALID_VIDEO_STATUSES = [
'transcription_in_progress',
'transcription_ready',
]
class ReadRestrictedDjangoModelPermissions(DjangoModelPermissions):
"""Extending DjangoModelPermissions to allow us to restrict read access.
......@@ -159,6 +165,54 @@ class VideoTranscriptView(APIView):
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 `transcription_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)
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):
"""
View to update course video images.
......
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