diff --git a/edxval/admin.py b/edxval/admin.py index fba0a8c0..bca28634 100644 --- a/edxval/admin.py +++ b/edxval/admin.py @@ -1,11 +1,10 @@ """ 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) diff --git a/edxval/api.py b/edxval/api.py index 8481afc6..72fdf354 100644 --- a/edxval/api.py +++ b/edxval/api.py @@ -17,7 +17,7 @@ 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. diff --git a/edxval/migrations/0007_transcript_credentials_state.py b/edxval/migrations/0007_transcript_credentials_state.py new file mode 100644 index 00000000..b6e414cb --- /dev/null +++ b/edxval/migrations/0007_transcript_credentials_state.py @@ -0,0 +1,32 @@ +# -*- 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')]), + ), + ] diff --git a/edxval/migrations/0008_auto_20171026_0359.py b/edxval/migrations/0008_auto_20171026_0359.py new file mode 100644 index 00000000..40a37644 --- /dev/null +++ b/edxval/migrations/0008_auto_20171026_0359.py @@ -0,0 +1,21 @@ +# -*- 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', + ), + ] diff --git a/edxval/models.py b/edxval/models.py index 7633f79f..72dfef5d 100644 --- a/edxval/models.py +++ b/edxval/models.py @@ -482,47 +482,6 @@ def __unicode__(self): 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 @@ def __unicode__(self): 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 """ diff --git a/edxval/tests/constants.py b/edxval/tests/constants.py index 3f14ed60..e4568c94 100644 --- a/edxval/tests/constants.py +++ b/edxval/tests/constants.py @@ -66,7 +66,6 @@ edx_video_id="thisis12char-thisis7", status="test", encoded_videos=[], - subtitles=[] ) VIDEO_DICT_BEE_INVALID = dict( client_video_id="Barking Bee", @@ -80,7 +79,6 @@ 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 @@ edx_video_id="ID", status="test", encoded_videos=[], - subtitles=[] ) VIDEO_DICT_NON_LATIN_ID = dict( client_video_id="Hungry Hamster", @@ -109,22 +106,9 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ ENCODED_VIDEO_DICT_FISH_MOBILE, ENCODED_VIDEO_DICT_FISH_DESKTOP ], - subtitles=[SUBTITLE_DICT_SRT] ) COMPLETE_SETS_ALL_INVALID = [ @@ -300,14 +275,12 @@ 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 @@ 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 @@ encoded_videos=[ ENCODED_VIDEO_DICT_STAR ], - subtitles=[SUBTITLE_DICT_SRT], **VIDEO_DICT_STAR ) COMPLETE_SET_WITH_OTHER_COURSE_KEYS = dict( @@ -331,7 +302,6 @@ encoded_videos=[ ENCODED_VIDEO_DICT_STAR ], - subtitles=[SUBTITLE_DICT_SRT], **VIDEO_DICT_STAR ) COMPLETE_SET_NOT_A_LIST = dict( @@ -341,7 +311,6 @@ bitrate=42, profile=1 ), - subtitles=[SUBTITLE_DICT_SRT], **VIDEO_DICT_STAR ) COMPLETE_SET_EXTRA_VIDEO_FIELD = dict( @@ -354,7 +323,6 @@ video="This should be overridden by parent video field" ) ], - subtitles=[SUBTITLE_DICT_SRT], **VIDEO_DICT_STAR ) """ @@ -378,7 +346,6 @@ edx_video_id="zestttt", status="test", encoded_videos=[], - subtitles=[] ) VIDEO_DICT_ANIMAL = dict( client_video_id="Average Animal", @@ -386,7 +353,6 @@ edx_video_id="mediocrity", status="test", encoded_videos=[], - subtitles=[] ) VIDEO_DICT_UPDATE_ANIMAL = dict( client_video_id="Above Average Animal", @@ -394,7 +360,6 @@ edx_video_id="mediocrity", status="test", encoded_videos=[], - subtitles=[] ) VIDEO_TRANSCRIPT_CIELO24 = dict( diff --git a/edxval/tests/test_api.py b/edxval/tests/test_api.py index 368d4a32..2aefb3f5 100644 --- a/edxval/tests/test_api.py +++ b/edxval/tests/test_api.py @@ -25,7 +25,7 @@ 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 @@ def test_create_transcript_preferences(self): # 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) diff --git a/edxval/tests/test_views.py b/edxval/tests/test_views.py index b11a6cdd..e777e004 100644 --- a/edxval/tests/test_views.py +++ b/edxval/tests/test_views.py @@ -209,33 +209,6 @@ def test_update_one_of_two_encoded_videos(self): ) 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 @@ def test_lookup_youtube(self): 'bitrate': 6767, } ], - 'subtitles': [], 'courses': ['youtube'], 'client_video_id': "Funny Cats", 'duration': 122 @@ -734,89 +706,6 @@ def test_queries_for_get(self): 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): """ diff --git a/setup.py b/setup.py index 4af2f554..c1284ea7 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ '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',