feat: add drf for studio video page (#33528)
This commit is contained in:
@@ -14,6 +14,11 @@ from .proctoring import (
|
||||
)
|
||||
from .settings import CourseSettingsSerializer
|
||||
from .xblock import XblockSerializer
|
||||
from .videos import VideoUploadSerializer, VideoImageSerializer
|
||||
from .videos import (
|
||||
CourseVideosSerializer,
|
||||
VideoUploadSerializer,
|
||||
VideoImageSerializer,
|
||||
VideoUsageSerializer
|
||||
)
|
||||
from .transcripts import TranscriptSerializer
|
||||
from .assets import AssetSerializer
|
||||
|
||||
@@ -11,6 +11,91 @@ class FileSpecSerializer(StrictSerializer):
|
||||
content_type = serializers.ChoiceField(choices=['video/mp4', 'video/webm', 'video/ogg'])
|
||||
|
||||
|
||||
class VideoImageSettingsSerializer(serializers.Serializer):
|
||||
"""Serializer for image settings"""
|
||||
video_image_upload_enabled = serializers.BooleanField()
|
||||
max_size = serializers.IntegerField()
|
||||
min_size = serializers.IntegerField()
|
||||
max_width = serializers.IntegerField()
|
||||
max_height = serializers.IntegerField()
|
||||
supported_file_formats = serializers.DictField(
|
||||
child=serializers.CharField()
|
||||
)
|
||||
|
||||
|
||||
class VideoTranscriptSettingsSerializer(serializers.Serializer):
|
||||
"""Serializer for transcript settings"""
|
||||
transcript_download_handler_url = serializers.CharField()
|
||||
transcript_upload_handler_url = serializers.CharField()
|
||||
transcript_delete_handler_url = serializers.CharField()
|
||||
trancript_download_file_format = serializers.CharField()
|
||||
transcript_preferences_handler_url = serializers.CharField(required=False, allow_null=True)
|
||||
transcript_credentials_handler_url = serializers.CharField(required=False, allow_null=True)
|
||||
transcription_plans = serializers.DictField(
|
||||
child=serializers.DictField(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class VideoModelSerializer(serializers.Serializer):
|
||||
"""Serializer for a video"""
|
||||
client_video_id = serializers.CharField()
|
||||
course_video_image_url = serializers.CharField()
|
||||
created = serializers.CharField()
|
||||
duration = serializers.FloatField()
|
||||
edx_video_id = serializers.CharField()
|
||||
error_description = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
file_size = serializers.IntegerField()
|
||||
download_link = serializers.CharField()
|
||||
transcript_urls = serializers.DictField(
|
||||
child=serializers.CharField()
|
||||
)
|
||||
transcription_status = serializers.CharField()
|
||||
transcripts = serializers.ListField(
|
||||
child=serializers.CharField()
|
||||
)
|
||||
|
||||
|
||||
class CourseVideosSerializer(serializers.Serializer):
|
||||
"""Serializer for course videos"""
|
||||
image_upload_url = serializers.CharField()
|
||||
video_handler_url = serializers.CharField()
|
||||
encodings_download_url = serializers.CharField()
|
||||
default_video_image_url = serializers.CharField()
|
||||
previous_uploads = VideoModelSerializer(many=True, required=False)
|
||||
concurrent_upload_limit = serializers.IntegerField()
|
||||
video_supported_file_formats = serializers.ListField(
|
||||
child=serializers.CharField()
|
||||
)
|
||||
video_upload_max_file_size = serializers.CharField()
|
||||
video_image_settings = VideoImageSettingsSerializer(required=True, allow_null=False)
|
||||
is_video_transcript_enabled = serializers.BooleanField()
|
||||
active_transcript_preferences = serializers.BooleanField(required=False, allow_null=True)
|
||||
transcript_credentials = serializers.DictField(
|
||||
child=serializers.CharField()
|
||||
)
|
||||
transcript_available_languages = serializers.ListField(
|
||||
child=serializers.DictField(
|
||||
child=serializers.CharField()
|
||||
)
|
||||
)
|
||||
video_transcript_settings = VideoTranscriptSettingsSerializer()
|
||||
pagination_context = serializers.DictField(
|
||||
child=serializers.CharField(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class VideoUsageSerializer(serializers.Serializer):
|
||||
"""Serializer for video usage"""
|
||||
usage_locations = serializers.ListField(
|
||||
child=serializers.CharField()
|
||||
)
|
||||
|
||||
|
||||
class VideoUploadSerializer(StrictSerializer):
|
||||
"""
|
||||
Strict Serializer for video upload urls.
|
||||
|
||||
@@ -11,6 +11,7 @@ from .views import (
|
||||
CourseGradingView,
|
||||
CourseRerunView,
|
||||
CourseSettingsView,
|
||||
CourseVideosView,
|
||||
HomePageView,
|
||||
ProctoredExamSettingsView,
|
||||
ProctoringErrorsView,
|
||||
@@ -19,6 +20,7 @@ from .views import (
|
||||
videos,
|
||||
transcripts,
|
||||
HelpUrlsView,
|
||||
VideoUsageView
|
||||
)
|
||||
|
||||
app_name = 'v1'
|
||||
@@ -31,6 +33,16 @@ urlpatterns = [
|
||||
HomePageView.as_view(),
|
||||
name="home"
|
||||
),
|
||||
re_path(
|
||||
fr'^videos/{COURSE_ID_PATTERN}$',
|
||||
CourseVideosView.as_view(),
|
||||
name="course_videos"
|
||||
),
|
||||
re_path(
|
||||
fr'^videos/{COURSE_ID_PATTERN}/{VIDEO_ID_PATTERN}/usage$',
|
||||
VideoUsageView.as_view(),
|
||||
name="video_usage"
|
||||
),
|
||||
re_path(
|
||||
fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$',
|
||||
ProctoredExamSettingsView.as_view(),
|
||||
|
||||
@@ -11,10 +11,12 @@ from .settings import CourseSettingsView
|
||||
from .xblock import XblockView, XblockCreateView
|
||||
from .assets import AssetsCreateRetrieveView, AssetsUpdateDestroyView
|
||||
from .videos import (
|
||||
CourseVideosView,
|
||||
VideosUploadsView,
|
||||
VideosCreateUploadView,
|
||||
VideoImagesView,
|
||||
VideoEncodingsDownloadView,
|
||||
VideoFeaturesView
|
||||
VideoFeaturesView,
|
||||
VideoUsageView,
|
||||
)
|
||||
from .help_urls import HelpUrlsView
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Unit tests for course settings views.
|
||||
"""
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles import WaffleSwitch
|
||||
from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
from edxval.api import (
|
||||
get_3rd_party_transcription_plans,
|
||||
get_transcript_credentials_state_for_org,
|
||||
get_transcript_preferences,
|
||||
)
|
||||
from mock import patch
|
||||
from rest_framework import status
|
||||
|
||||
from cms.djangoapps.contentstore.video_storage_handlers import get_all_transcript_languages
|
||||
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
|
||||
from cms.djangoapps.contentstore.utils import reverse_course_url
|
||||
|
||||
from ...mixins import PermissionAccessMixin
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseVideosViewTest(CourseTestCase, PermissionAccessMixin):
|
||||
"""
|
||||
Tests for CourseVideosView.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url = reverse(
|
||||
"cms.djangoapps.contentstore:v1:course_videos",
|
||||
kwargs={"course_id": self.course.id},
|
||||
)
|
||||
|
||||
def test_course_videos_response(self):
|
||||
"""Check successful response content"""
|
||||
response = self.client.get(self.url)
|
||||
expected_response = {
|
||||
"image_upload_url": reverse_course_url("video_images_handler", str(self.course.id)),
|
||||
"video_handler_url": reverse_course_url("videos_handler", str(self.course.id)),
|
||||
"encodings_download_url": reverse_course_url("video_encodings_download", str(self.course.id)),
|
||||
"default_video_image_url": staticfiles_storage.url(settings.VIDEO_IMAGE_DEFAULT_FILENAME),
|
||||
"previous_uploads": [],
|
||||
"concurrent_upload_limit": settings.VIDEO_UPLOAD_PIPELINE.get("CONCURRENT_UPLOAD_LIMIT", 0),
|
||||
"video_supported_file_formats": [".mp4", ".mov"],
|
||||
"video_upload_max_file_size": "5",
|
||||
"video_image_settings": {
|
||||
"video_image_upload_enabled": False,
|
||||
"max_size": settings.VIDEO_IMAGE_SETTINGS["VIDEO_IMAGE_MAX_BYTES"],
|
||||
"min_size": settings.VIDEO_IMAGE_SETTINGS["VIDEO_IMAGE_MIN_BYTES"],
|
||||
"max_width": settings.VIDEO_IMAGE_MAX_WIDTH,
|
||||
"max_height": settings.VIDEO_IMAGE_MAX_HEIGHT,
|
||||
"supported_file_formats": settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS
|
||||
},
|
||||
"is_video_transcript_enabled": False,
|
||||
"active_transcript_preferences": None,
|
||||
"transcript_credentials": None,
|
||||
"transcript_available_languages": get_all_transcript_languages(),
|
||||
"video_transcript_settings": {
|
||||
"transcript_download_handler_url": reverse('transcript_download_handler'),
|
||||
"transcript_upload_handler_url": reverse('transcript_upload_handler'),
|
||||
"transcript_delete_handler_url": reverse_course_url("transcript_delete_handler", str(self.course.id)),
|
||||
"trancript_download_file_format": "srt",
|
||||
"transcript_preferences_handler_url": None,
|
||||
"transcript_credentials_handler_url": None,
|
||||
"transcription_plans": None
|
||||
},
|
||||
"pagination_context": {}
|
||||
}
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(expected_response, response.data)
|
||||
|
||||
@override_waffle_switch(WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation
|
||||
'videos.video_image_upload_enabled', __name__
|
||||
), True)
|
||||
def test_video_image_upload_enabled(self):
|
||||
"""
|
||||
Make sure if the feature flag is enabled we have updated the dict keys in response.
|
||||
"""
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn("video_image_settings", response.data)
|
||||
|
||||
imageSettings = response.data["video_image_settings"]
|
||||
self.assertIn("video_image_upload_enabled", imageSettings)
|
||||
self.assertTrue(imageSettings["video_image_upload_enabled"])
|
||||
|
||||
def test_VideoTranscriptEnabledFlag_enabled(self):
|
||||
"""
|
||||
Make sure if the feature flags are enabled we have updated the dict keys in response.
|
||||
"""
|
||||
with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature:
|
||||
feature.return_value = True
|
||||
response = self.client.get(self.url)
|
||||
self.assertIn("is_video_transcript_enabled", response.data)
|
||||
self.assertTrue(response.data["is_video_transcript_enabled"])
|
||||
|
||||
expect_active_preferences = get_transcript_preferences(str(self.course.id))
|
||||
self.assertIn("active_transcript_preferences", response.data)
|
||||
self.assertEqual(expect_active_preferences, response.data["active_transcript_preferences"])
|
||||
|
||||
expected_credentials = get_transcript_credentials_state_for_org(self.course.id.org)
|
||||
self.assertIn("transcript_credentials", response.data)
|
||||
self.assertDictEqual(expected_credentials, response.data["transcript_credentials"])
|
||||
|
||||
transcript_settings = response.data["video_transcript_settings"]
|
||||
|
||||
expected_plans = get_3rd_party_transcription_plans()
|
||||
self.assertIn("transcription_plans", transcript_settings)
|
||||
self.assertDictEqual(expected_plans, transcript_settings["transcription_plans"])
|
||||
|
||||
expected_preference_handler = reverse_course_url(
|
||||
'transcript_preferences_handler',
|
||||
str(self.course.id)
|
||||
)
|
||||
self.assertIn("transcript_preferences_handler_url", transcript_settings)
|
||||
self.assertEqual(expected_preference_handler, transcript_settings["transcript_preferences_handler_url"])
|
||||
|
||||
expected_credentials_handler = reverse_course_url(
|
||||
'transcript_credentials_handler',
|
||||
str(self.course.id)
|
||||
)
|
||||
self.assertIn("transcript_credentials_handler_url", transcript_settings)
|
||||
self.assertEqual(expected_credentials_handler, transcript_settings["transcript_credentials_handler_url"])
|
||||
@@ -1,29 +1,42 @@
|
||||
"""
|
||||
Public rest API endpoints for the CMS API video assets.
|
||||
"""
|
||||
import edx_api_doc_tools as apidocs
|
||||
import logging
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework.generics import (
|
||||
CreateAPIView,
|
||||
RetrieveAPIView,
|
||||
DestroyAPIView
|
||||
)
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.parsers import (MultiPartParser, FormParser)
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import Http404
|
||||
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes, verify_course_exists
|
||||
from openedx.core.lib.api.parsers import TypedFileUploadParser
|
||||
from common.djangoapps.student.auth import has_studio_read_access
|
||||
from common.djangoapps.util.json_request import expect_json_in_class_view
|
||||
|
||||
from ....api import course_author_access_required
|
||||
from ....utils import get_course_videos_context
|
||||
|
||||
from cms.djangoapps.contentstore.video_storage_handlers import (
|
||||
handle_videos,
|
||||
get_video_encodings_download,
|
||||
handle_video_images,
|
||||
enabled_video_features
|
||||
enabled_video_features,
|
||||
get_video_usage_path
|
||||
)
|
||||
from cms.djangoapps.contentstore.rest_api.v1.serializers import (
|
||||
CourseVideosSerializer,
|
||||
VideoUploadSerializer,
|
||||
VideoImageSerializer,
|
||||
VideoUsageSerializer,
|
||||
)
|
||||
from cms.djangoapps.contentstore.rest_api.v1.serializers import VideoUploadSerializer, VideoImageSerializer
|
||||
import cms.djangoapps.contentstore.toggles as contentstore_toggles
|
||||
from .utils import validate_request_with_serializer
|
||||
|
||||
@@ -32,6 +45,161 @@ log = logging.getLogger(__name__)
|
||||
toggles = contentstore_toggles
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
class CourseVideosView(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
View for course videos.
|
||||
"""
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
|
||||
],
|
||||
responses={
|
||||
200: CourseVideosSerializer,
|
||||
401: "The requester is not authenticated",
|
||||
403: "The requester cannot access the specified course",
|
||||
404: "The requested course does not exist",
|
||||
},
|
||||
)
|
||||
@verify_course_exists()
|
||||
def get(self, request: Request, course_id: str):
|
||||
"""
|
||||
Get an object containing course videos.
|
||||
**Example Request**
|
||||
GET /api/contentstore/v1/videos/{course_id}/{edx_video_id}
|
||||
**Response Values**
|
||||
If the request is successful, an HTTP 200 "OK" response is returned.
|
||||
The HTTP 200 response contains a single dict that contains keys that
|
||||
are the course's videos.
|
||||
**Example Response**
|
||||
```json
|
||||
{
|
||||
image_upload_url: '/video_images/course_id',
|
||||
video_handler_url: '/videos/course_id',
|
||||
encodings_download_url: '/video_encodings_download/course_id',
|
||||
default_video_image_url: '/static/studio/images/video-images/default_video_image.png',
|
||||
previous_uploads: [
|
||||
{
|
||||
edx_video_id: 'mOckID1',
|
||||
clientVideoId: 'mOckID1.mp4',
|
||||
created: '',
|
||||
courseVideoImageUrl: '/video',
|
||||
transcripts: [],
|
||||
status: 'Imported',
|
||||
file_size: 123,
|
||||
download_link: 'http:/download_video.com'
|
||||
},
|
||||
{
|
||||
edx_video_id: 'mOckID5',
|
||||
clientVideoId: 'mOckID5.mp4',
|
||||
created: '',
|
||||
courseVideoImageUrl: 'http:/video',
|
||||
transcripts: ['en'],
|
||||
status: 'Failed',
|
||||
file_size: 0,
|
||||
download_link: ''
|
||||
},
|
||||
{
|
||||
edx_video_id: 'mOckID3',
|
||||
clientVideoId: 'mOckID3.mp4',
|
||||
created: '',
|
||||
courseVideoImageUrl: null,
|
||||
transcripts: ['en'],
|
||||
status: 'Ready',
|
||||
file_size: 123,
|
||||
download_link: 'http:/download_video.com'
|
||||
},
|
||||
],
|
||||
concurrent_upload_limit: 4,
|
||||
video_supported_file_formats: ['.mp4', '.mov'],
|
||||
video_upload_max_file_size: '5',
|
||||
video_image_settings: {
|
||||
video_image_upload_enabled: false,
|
||||
max_size: 2097152,
|
||||
min_size: 2048,
|
||||
max_width: 1280,
|
||||
max_height: 720,
|
||||
supported_file_formats: {
|
||||
'.bmp': 'image/bmp',
|
||||
'.bmp2': 'image/x-ms-bmp',
|
||||
'.gif': 'image/gif',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
},
|
||||
},
|
||||
is_video_transcript_enabled: false,
|
||||
active_transcript_preferences: null,
|
||||
transcript_credentials: {},
|
||||
transcript_available_languages: [{ language_code: 'ab', language_text: 'Abkhazian' }],
|
||||
video_transcript_settings: {
|
||||
transcript_download_handler_url: '/transcript_download/',
|
||||
transcript_upload_handler_url: '/transcript_upload/',
|
||||
transcript_delete_handler_url: '/transcript_delete/course_id',
|
||||
trancript_download_file_format: 'srt',
|
||||
},
|
||||
pagination_context: {},
|
||||
}
|
||||
```
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
if not has_studio_read_access(request.user, course_key):
|
||||
self.permission_denied(request)
|
||||
|
||||
course_videos_context = get_course_videos_context(
|
||||
None,
|
||||
None,
|
||||
course_key,
|
||||
)
|
||||
serializer = CourseVideosSerializer(course_videos_context)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
class VideoUsageView(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
View for course video usage locations.
|
||||
"""
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
|
||||
apidocs.string_parameter("edx_video_id", apidocs.ParameterLocation.PATH, description="edX Video ID"),
|
||||
],
|
||||
responses={
|
||||
200: VideoUsageSerializer,
|
||||
401: "The requester is not authenticated",
|
||||
403: "The requester cannot access the specified course",
|
||||
404: "The requested course does not exist",
|
||||
},
|
||||
)
|
||||
@verify_course_exists()
|
||||
def get(self, request: Request, course_id: str, edx_video_id: str):
|
||||
"""
|
||||
Get an object containing course videos.
|
||||
**Example Request**
|
||||
GET /api/contentstore/v1/videos/{course_id}/{edx_video_id}
|
||||
**Response Values**
|
||||
If the request is successful, an HTTP 200 "OK" response is returned.
|
||||
The HTTP 200 response contains a single dict that contains keys that
|
||||
are the course's videos.
|
||||
**Example Response**
|
||||
```json
|
||||
{
|
||||
"usage_locations": ["subsection - unit/xblock"],
|
||||
}
|
||||
```
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
if not has_studio_read_access(request.user, course_key):
|
||||
self.permission_denied(request)
|
||||
|
||||
usage_locations = get_video_usage_path(request, course_key, edx_video_id)
|
||||
serializer = VideoUsageSerializer(usage_locations)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@view_auth_classes()
|
||||
class VideosUploadsView(DeveloperErrorViewMixin, RetrieveAPIView, DestroyAPIView):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user