Commit 13c71864 by Muzaffar yousaf Committed by GitHub

Merge pull request #99 from edx/ammar/org-specific-credentials-state

Organization specific transcript credentials state
parents 92c935e1 b27cdc0d
"""
Admin file for django app edxval.
"""
from django import forms
from django.contrib import admin
from .models import (CourseVideo, EncodedVideo, Profile, TranscriptPreference,
Video, VideoImage, VideoTranscript)
Video, VideoImage, VideoTranscript, ThirdPartyTranscriptCredentialsState)
class ProfileAdmin(admin.ModelAdmin): # pylint: disable=C0111
......@@ -81,9 +80,18 @@ class TranscriptPreferenceAdmin(admin.ModelAdmin):
model = TranscriptPreference
class ThirdPartyTranscriptCredentialsStateAdmin(admin.ModelAdmin):
list_display = ('org', 'provider', 'exists', 'created', 'modified')
model = ThirdPartyTranscriptCredentialsState
verbose_name = 'Organization Transcript Credential State'
verbose_name_plural = 'Organization Transcript Credentials State'
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Video, VideoAdmin)
admin.site.register(VideoTranscript, VideoTranscriptAdmin)
admin.site.register(TranscriptPreference, TranscriptPreferenceAdmin)
admin.site.register(VideoImage, VideoImageAdmin)
admin.site.register(CourseVideo, CourseVideoAdmin)
admin.site.register(ThirdPartyTranscriptCredentialsState, ThirdPartyTranscriptCredentialsStateAdmin)
......@@ -17,7 +17,7 @@ from edxval.exceptions import (InvalidTranscriptFormat,
from edxval.models import (CourseVideo, EncodedVideo, Profile,
TranscriptFormat, TranscriptPreference,
TranscriptProviderType, Video, VideoImage,
VideoTranscript)
VideoTranscript, ThirdPartyTranscriptCredentialsState)
from edxval.serializers import TranscriptPreferenceSerializer, TranscriptSerializer, VideoSerializer
from edxval.utils import THIRD_PARTY_TRANSCRIPTION_PLANS
......@@ -143,6 +143,47 @@ def update_video_status(edx_video_id, status):
video.save()
def get_transcript_credentials_state_for_org(org, provider=None):
"""
Returns transcript credentials state for an org
Arguments:
org (unicode): course organization
provider (unicode): transcript provider
Returns:
dict: provider name and their credential existance map
{
u'Cielo24': True
}
{
u'3PlayMedia': False,
u'Cielo24': True
}
"""
query_filter = {'org': org}
if provider:
query_filter['provider'] = provider
return {
credential.provider: credential.exists
for credential in ThirdPartyTranscriptCredentialsState.objects.filter(**query_filter)
}
def update_transcript_credentials_state_for_org(org, provider, exists):
"""
Updates transcript credentials state for a course organization.
Arguments:
org (unicode): course organization
provider (unicode): transcript provider
exists (bool): state of credentials
"""
ThirdPartyTranscriptCredentialsState.update_or_create(org, provider, exists)
def is_transcript_available(video_id, language_code=None):
"""
Returns whether the transcripts are available for a video.
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-10-10 08:15
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
import model_utils.fields
class Migration(migrations.Migration):
dependencies = [
('edxval', '0006_auto_20171009_0725'),
]
operations = [
migrations.CreateModel(
name='ThirdPartyTranscriptCredentialsState',
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')),
('org', models.CharField(max_length=32, verbose_name=b'Course Organization')),
('provider', models.CharField(choices=[(b'Custom', b'Custom'), (b'3PlayMedia', b'3PlayMedia'), (b'Cielo24', b'Cielo24')], max_length=20, verbose_name=b'Transcript Provider')),
('exists', models.BooleanField(default=False, help_text=b'Transcript credentials state')),
],
),
migrations.AlterUniqueTogether(
name='thirdpartytranscriptcredentialsstate',
unique_together=set([('org', 'provider')]),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('edxval', '0007_transcript_credentials_state'),
]
operations = [
migrations.RemoveField(
model_name='subtitle',
name='video',
),
migrations.DeleteModel(
name='Subtitle',
),
]
......@@ -482,47 +482,6 @@ class VideoTranscript(TimeStampedModel):
return u'{lang} Transcript for {video}'.format(lang=self.language_code, video=self.video_id)
SUBTITLE_FORMATS = (
('srt', 'SubRip'),
('sjson', 'SRT JSON')
)
class Subtitle(models.Model):
"""
Subtitle for video
Attributes:
video: the video that the subtitles are for
fmt: the format of the subttitles file
"""
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
video = models.ForeignKey(Video, related_name="subtitles")
fmt = models.CharField(max_length=20, db_index=True, choices=SUBTITLE_FORMATS)
language = models.CharField(max_length=8, db_index=True)
content = models.TextField(default='')
def __str__(self):
return '%s Subtitle for %s' % (self.language, self.video)
def get_absolute_url(self):
"""
Returns the full url link to the edx_video_id
"""
return reverse('subtitle-content', args=[self.video.edx_video_id, self.language])
@property
def content_type(self):
"""
Sjson is returned as application/json, otherwise text/plain
"""
if self.fmt == 'sjson':
return 'application/json'
else:
return 'text/plain'
class Cielo24Turnaround(object):
"""
Cielo24 turnarounds.
......@@ -612,6 +571,47 @@ class TranscriptPreference(TimeStampedModel):
return u'{course_id} - {provider}'.format(course_id=self.course_id, provider=self.provider)
class ThirdPartyTranscriptCredentialsState(TimeStampedModel):
"""
State of transcript credentials for a course organization
"""
class Meta:
unique_together = ('org', 'provider')
org = models.CharField(verbose_name='Course Organization', max_length=32)
provider = models.CharField(
verbose_name='Transcript Provider',
max_length=20,
choices=TranscriptProviderType.CHOICES,
)
exists = models.BooleanField(default=False, help_text='Transcript credentials state')
@classmethod
def update_or_create(cls, org, provider, exists):
"""
Update or create credentials state.
"""
instance, created = cls.objects.update_or_create(
org=org,
provider=provider,
defaults={'exists': exists},
)
return instance, created
def __unicode__(self):
"""
Returns unicode representation of provider credentials state for an organization.
NOTE: Message will look like below:
edX has Cielo24 credentials
edX doesn't have 3PlayMedia credentials
"""
return u'{org} {state} {provider} credentials'.format(
org=self.org, provider=self.provider, state='has' if self.exists else "doesn't have"
)
@receiver(models.signals.post_save, sender=Video)
def video_status_update_callback(sender, **kwargs): # pylint: disable=unused-argument
"""
......
......@@ -66,7 +66,6 @@ VIDEO_DICT_NEGATIVE_DURATION = dict(
edx_video_id="thisis12char-thisis7",
status="test",
encoded_videos=[],
subtitles=[]
)
VIDEO_DICT_BEE_INVALID = dict(
client_video_id="Barking Bee",
......@@ -80,7 +79,6 @@ VIDEO_DICT_INVALID_ID = dict(
edx_video_id="sloppy/sloth!!",
status="test",
encoded_videos=[],
subtitles=[]
)
ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict(
url="http://www.meowmix.com",
......@@ -101,7 +99,6 @@ VIDEO_DICT_NON_LATIN_TITLE = dict(
edx_video_id="ID",
status="test",
encoded_videos=[],
subtitles=[]
)
VIDEO_DICT_NON_LATIN_ID = dict(
client_video_id="Hungry Hamster",
......@@ -109,22 +106,9 @@ VIDEO_DICT_NON_LATIN_ID = dict(
edx_video_id="밥줘",
status="test",
encoded_videos=[],
subtitles=[]
)
PROFILE_INVALID_NAME = "lo/lol"
"""
Subtitles
"""
SUBTITLE_DICT_SRT = dict(
fmt="srt",
language="en",
content="0:0:0\nhello"
)
SUBTITLE_DICT_SJSON = dict(
fmt="sjson",
language="fr",
content='{"start": "00:00:00"}'
)
"""
Fish
"""
......@@ -199,7 +183,6 @@ COMPLETE_SET_FISH = dict(
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP
],
subtitles=[SUBTITLE_DICT_SRT, SUBTITLE_DICT_SJSON],
**VIDEO_DICT_FISH
)
COMPLETE_SET_FISH_WITH_HLS = dict(
......@@ -208,7 +191,6 @@ COMPLETE_SET_FISH_WITH_HLS = dict(
ENCODED_VIDEO_DICT_FISH_DESKTOP,
ENCODED_VIDEO_DICT_FISH_HLS,
],
subtitles=[SUBTITLE_DICT_SRT, SUBTITLE_DICT_SJSON],
**VIDEO_DICT_FISH
)
COMPLETE_SET_TWO_MOBILE_FISH = dict(
......@@ -216,7 +198,6 @@ COMPLETE_SET_TWO_MOBILE_FISH = dict(
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_MOBILE
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_FISH
)
COMPLETE_SET_UPDATE_FISH = dict(
......@@ -225,7 +206,6 @@ COMPLETE_SET_UPDATE_FISH = dict(
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP,
ENCODED_VIDEO_DICT_UPDATE_FISH_HLS,
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_FISH
)
COMPLETE_SET_DIFFERENT_ID_UPDATE_FISH = dict(
......@@ -233,7 +213,6 @@ COMPLETE_SET_DIFFERENT_ID_UPDATE_FISH = dict(
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE,
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP
],
subtitles=[SUBTITLE_DICT_SRT, SUBTITLE_DICT_SJSON],
**VIDEO_DICT_DIFFERENT_ID_FISH
)
COMPLETE_SET_FIRST_HALF_UPDATE_FISH = dict(
......@@ -241,14 +220,12 @@ COMPLETE_SET_FIRST_HALF_UPDATE_FISH = dict(
ENCODED_VIDEO_DICT_UPDATE_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP
],
subtitles=[SUBTITLE_DICT_SRT, SUBTITLE_DICT_SJSON],
**VIDEO_DICT_FISH
)
COMPLETE_SET_UPDATE_ONLY_DESKTOP_FISH = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_UPDATE_FISH_DESKTOP
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_FISH
)
COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict(
......@@ -256,7 +233,6 @@ COMPLETE_SET_INVALID_ENCODED_VIDEO_FISH = dict(
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_INVALID_PROFILE
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_FISH
)
COMPLETE_SET_INVALID_VIDEO_FISH = dict(
......@@ -268,7 +244,6 @@ COMPLETE_SET_INVALID_VIDEO_FISH = dict(
ENCODED_VIDEO_DICT_FISH_MOBILE,
ENCODED_VIDEO_DICT_FISH_DESKTOP
],
subtitles=[SUBTITLE_DICT_SRT]
)
COMPLETE_SETS_ALL_INVALID = [
......@@ -300,14 +275,12 @@ COMPLETE_SET_STAR = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_STAR
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR
)
COMPLETE_SET_UPDATE_STAR = dict(
encoded_videos=[
ENCODED_VIDEO_UPDATE_DICT_STAR
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR
)
COMPLETE_SET_WITH_COURSE_KEY = dict(
......@@ -315,7 +288,6 @@ COMPLETE_SET_WITH_COURSE_KEY = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_STAR
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR
)
COMPLETE_SET_WITH_SOME_INVALID_COURSE_KEY = dict(
......@@ -323,7 +295,6 @@ COMPLETE_SET_WITH_SOME_INVALID_COURSE_KEY = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_STAR
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR
)
COMPLETE_SET_WITH_OTHER_COURSE_KEYS = dict(
......@@ -331,7 +302,6 @@ COMPLETE_SET_WITH_OTHER_COURSE_KEYS = dict(
encoded_videos=[
ENCODED_VIDEO_DICT_STAR
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR
)
COMPLETE_SET_NOT_A_LIST = dict(
......@@ -341,7 +311,6 @@ COMPLETE_SET_NOT_A_LIST = dict(
bitrate=42,
profile=1
),
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR
)
COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
......@@ -354,7 +323,6 @@ COMPLETE_SET_EXTRA_VIDEO_FIELD = dict(
video="This should be overridden by parent video field"
)
],
subtitles=[SUBTITLE_DICT_SRT],
**VIDEO_DICT_STAR
)
"""
......@@ -378,7 +346,6 @@ VIDEO_DICT_ZEBRA = dict(
edx_video_id="zestttt",
status="test",
encoded_videos=[],
subtitles=[]
)
VIDEO_DICT_ANIMAL = dict(
client_video_id="Average Animal",
......@@ -386,7 +353,6 @@ VIDEO_DICT_ANIMAL = dict(
edx_video_id="mediocrity",
status="test",
encoded_videos=[],
subtitles=[]
)
VIDEO_DICT_UPDATE_ANIMAL = dict(
client_video_id="Above Average Animal",
......@@ -394,7 +360,6 @@ VIDEO_DICT_UPDATE_ANIMAL = dict(
edx_video_id="mediocrity",
status="test",
encoded_videos=[],
subtitles=[]
)
VIDEO_TRANSCRIPT_CIELO24 = dict(
......
......@@ -25,7 +25,7 @@ from edxval.api import (InvalidTranscriptFormat, InvalidTranscriptProvider,
VideoSortField)
from edxval.models import (LIST_MAX_ITEMS, CourseVideo, EncodedVideo, Profile,
TranscriptFormat, TranscriptProviderType, Video,
VideoImage, VideoTranscript, TranscriptPreference)
VideoImage, VideoTranscript, TranscriptPreference, ThirdPartyTranscriptCredentialsState)
from edxval.tests import APIAuthTestCase, constants
from edxval import utils
......@@ -2045,3 +2045,66 @@ class TranscriptPreferencesTest(TestCase):
# Verify that there should be 2 preferences exists
self.assertEqual(TranscriptPreference.objects.count(), 2)
@ddt
class TranscripCredentialsStateTest(TestCase):
"""
ThirdPartyTranscriptCredentialsState Tests
"""
def setUp(self):
"""
Tests setup
"""
ThirdPartyTranscriptCredentialsState.objects.create(
org='edX', provider='Cielo24', exists=True
)
ThirdPartyTranscriptCredentialsState.objects.create(
org='edX', provider='3PlayMedia', exists=False
)
@data(
{'org': 'MAX', 'provider': 'Cielo24', 'exists': True},
{'org': 'MAX', 'provider': '3PlayMedia', 'exists': True},
{'org': 'edx', 'provider': '3PlayMedia', 'exists': True},
)
@unpack
def test_credentials_state_update(self, **kwargs):
"""
Verify that `update_transcript_credentials_state_for_org` method works as expected
"""
api.update_transcript_credentials_state_for_org(**kwargs)
credentials_state = ThirdPartyTranscriptCredentialsState.objects.get(org=kwargs['org'])
for key in kwargs:
self.assertEqual(getattr(credentials_state, key), kwargs[key])
@data(
{
'org': 'edX',
'provider': 'Cielo24',
'result': {u'Cielo24': True}
},
{
'org': 'edX',
'provider': '3PlayMedia',
'result': {u'3PlayMedia': False}
},
{
'org': 'edX',
'provider': None,
'result': {u'3PlayMedia': False, u'Cielo24': True}
},
{
'org': 'does_not_exist',
'provider': 'does_not_exist',
'result': {}
},
)
@unpack
def test_get_credentials_state(self, org, provider, result):
"""
Verify that `get_transcript_credentials_state_for_org` method works as expected
"""
credentials_state = api.get_transcript_credentials_state_for_org(org=org, provider=provider)
self.assertEqual(credentials_state, result)
......@@ -209,33 +209,6 @@ 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)
# Sanity check that the subtitles have been created
videos = Video.objects.all()
self.assertEqual(len(videos), 1)
self.assertEqual(len(videos[0].subtitles.all()), 1)
# Update with an empty list of subtitles
url = reverse(
'video-detail',
kwargs={"edx_video_id": constants.COMPLETE_SET_STAR.get("edx_video_id")}
)
response = self.client.put(
url,
dict(subtitles=[], encoded_videos=[], **constants.VIDEO_DICT_STAR),
format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Expect that subtitles have been removed
videos = Video.objects.all()
self.assertEqual(len(videos), 1)
self.assertEqual(len(videos[0].subtitles.all()), 0)
def test_update_remove_encoded_videos(self):
# Create some encoded videos
self._create_videos(constants.COMPLETE_SET_STAR)
......@@ -626,7 +599,6 @@ class VideoListTest(APIAuthTestCase):
'bitrate': 6767,
}
],
'subtitles': [],
'courses': ['youtube'],
'client_video_id': "Funny Cats",
'duration': 122
......@@ -734,89 +706,6 @@ class VideoDetailTest(APIAuthTestCase):
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
"""
def setUp(self):
Profile.objects.create(profile_name=constants.PROFILE_MOBILE)
Profile.objects.create(profile_name=constants.PROFILE_DESKTOP)
super(SubtitleDetailTest, self).setUp()
def test_get_subtitle_content(self):
"""
Get subtitle content
"""
url = reverse('video-list')
response = self.client.post(
url, constants.COMPLETE_SET_FISH, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
video = self.client.get("/edxval/videos/").data
self.assertEqual(len(video), 1)
self.assertEqual(len(video[0].get("subtitles")), 2)
video_subtitles = video[0]['subtitles'][0]
response = self.client.get(video_subtitles['content_url'])
self.assertEqual(response.content, constants.SUBTITLE_DICT_SRT['content'])
self.assertEqual(response['Content-Type'], 'text/plain')
video_subtitles = video[0]['subtitles'][1]
response = self.client.get(video_subtitles['content_url'])
self.assertEqual(response.content, constants.SUBTITLE_DICT_SJSON['content'])
self.assertEqual(response['Content-Type'], 'application/json')
def test_update_subtitle(self):
"""
Update an SRT subtitle
"""
url = reverse('video-list')
response = self.client.post(
url, constants.COMPLETE_SET_FISH, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
video = response.data
video_subtitles = video['subtitles'][0]
url = reverse('subtitle-detail', kwargs={'video__edx_video_id': video['edx_video_id'], 'language': video_subtitles['language']})
video_subtitles['content'] = 'testing 123'
response = self.client.put(
url, video_subtitles, format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.client.get(video_subtitles['content_url']).content, 'testing 123')
def test_update_json_subtitle(self):
"""
Update a JSON subtitle
"""
url = reverse('video-list')
response = self.client.post(
url, constants.COMPLETE_SET_FISH, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
video = response.data
video_subtitles = video['subtitles'][1]
url = reverse('subtitle-detail', kwargs={'video__edx_video_id': video['edx_video_id'], 'language': video_subtitles['language']})
video_subtitles['content'] = 'testing 123'
response = self.client.put(
url, video_subtitles, format='json'
)
# not in json format
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
video_subtitles['content'] = """{"start": "00:00:00"
}"""
response = self.client.put(
url, video_subtitles, format='json'
)
self.assertEqual(self.client.get(video_subtitles['content_url']).content, '{"start": "00:00:00"}')
@ddt
class VideoImagesViewTest(APIAuthTestCase):
"""
......
......@@ -7,6 +7,7 @@ PACKAGES = [
'edxval.tests',
]
def is_requirement(line):
"""
Return True if the requirement line is a package requirement;
......@@ -24,6 +25,7 @@ def is_requirement(line):
line.startswith('git+')
)
def load_requirements(*requirements_paths):
"""
Load all requirements from the specified requirements files.
......@@ -39,7 +41,7 @@ def load_requirements(*requirements_paths):
setup(
name='edxval',
version='0.1.2',
version='0.1.3',
author='edX',
url='http://github.com/edx/edx-val',
description='edx-val',
......
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