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):
|
||||
"""
|
||||
|
||||
@@ -1568,6 +1568,92 @@ def get_course_rerun_context(course_key, course_block, user):
|
||||
return course_rerun_context
|
||||
|
||||
|
||||
def get_course_videos_context(course_block, pagination_conf, course_key=None):
|
||||
"""
|
||||
Utils is used to get contest of course videos.
|
||||
It is used for both DRF and django views.
|
||||
"""
|
||||
|
||||
from edx_toggles.toggles import WaffleSwitch
|
||||
from edxval.api import (
|
||||
get_3rd_party_transcription_plans,
|
||||
get_transcript_credentials_state_for_org,
|
||||
get_transcript_preferences,
|
||||
)
|
||||
from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag
|
||||
from xmodule.video_block.transcripts_utils import Transcript # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from .video_storage_handlers import (
|
||||
get_all_transcript_languages,
|
||||
_get_index_videos,
|
||||
_get_default_video_image_url
|
||||
)
|
||||
|
||||
VIDEO_SUPPORTED_FILE_FORMATS = {
|
||||
'.mp4': 'video/mp4',
|
||||
'.mov': 'video/quicktime',
|
||||
}
|
||||
VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5
|
||||
# Waffle switch for enabling/disabling video image upload feature
|
||||
VIDEO_IMAGE_UPLOAD_ENABLED = WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation
|
||||
'videos.video_image_upload_enabled', __name__
|
||||
)
|
||||
|
||||
course = course_block
|
||||
if not course:
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course = modulestore().get_course(course_key)
|
||||
|
||||
is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course.id)
|
||||
previous_uploads, pagination_context = _get_index_videos(course, pagination_conf)
|
||||
course_video_context = {
|
||||
'context_course': course,
|
||||
'image_upload_url': reverse_course_url('video_images_handler', str(course.id)),
|
||||
'video_handler_url': reverse_course_url('videos_handler', str(course.id)),
|
||||
'encodings_download_url': reverse_course_url('video_encodings_download', str(course.id)),
|
||||
'default_video_image_url': _get_default_video_image_url(),
|
||||
'previous_uploads': previous_uploads,
|
||||
'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0),
|
||||
'video_supported_file_formats': list(VIDEO_SUPPORTED_FILE_FORMATS.keys()),
|
||||
'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB,
|
||||
'video_image_settings': {
|
||||
'video_image_upload_enabled': VIDEO_IMAGE_UPLOAD_ENABLED.is_enabled(),
|
||||
'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': is_video_transcript_enabled,
|
||||
'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(course.id)),
|
||||
'trancript_download_file_format': Transcript.SRT
|
||||
},
|
||||
'pagination_context': pagination_context
|
||||
}
|
||||
if is_video_transcript_enabled:
|
||||
course_video_context['video_transcript_settings'].update({
|
||||
'transcript_preferences_handler_url': reverse_course_url(
|
||||
'transcript_preferences_handler',
|
||||
str(course.id)
|
||||
),
|
||||
'transcript_credentials_handler_url': reverse_course_url(
|
||||
'transcript_credentials_handler',
|
||||
str(course.id)
|
||||
),
|
||||
'transcription_plans': get_3rd_party_transcription_plans(),
|
||||
})
|
||||
course_video_context['active_transcript_preferences'] = get_transcript_preferences(str(course.id))
|
||||
# Cached state for transcript providers' credentials (org-specific)
|
||||
course_video_context['transcript_credentials'] = get_transcript_credentials_state_for_org(course.id.org)
|
||||
return course_video_context
|
||||
|
||||
|
||||
class StudioPermissionsService:
|
||||
"""
|
||||
Service that can provide information about a user's permissions.
|
||||
|
||||
@@ -15,9 +15,9 @@ from boto.s3.connection import S3Connection
|
||||
from boto import s3
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import FileResponse, HttpResponseNotFound
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_noop
|
||||
from edx_toggles.toggles import WaffleSwitch
|
||||
@@ -29,7 +29,6 @@ from edxval.api import (
|
||||
get_3rd_party_transcription_plans,
|
||||
get_available_transcript_languages,
|
||||
get_video_transcript_url,
|
||||
get_transcript_credentials_state_for_org,
|
||||
get_transcript_preferences,
|
||||
get_videos_for_course,
|
||||
remove_transcript_preferences,
|
||||
@@ -43,6 +42,7 @@ from rest_framework import status as rest_status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.djangoapps.edxmako.shortcuts import render_to_response
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from common.djangoapps.util.json_request import JsonResponse
|
||||
from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag
|
||||
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
|
||||
@@ -51,11 +51,11 @@ from openedx.core.djangoapps.video_pipeline.config.waffle import (
|
||||
ENABLE_DEVSTACK_VIDEO_UPLOADS,
|
||||
)
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
from xmodule.video_block.transcripts_utils import Transcript # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from .models import VideoUploadConfig
|
||||
from .toggles import use_new_video_uploads_page, use_mock_video_uploads
|
||||
from .utils import reverse_course_url, get_video_uploads_url
|
||||
from .utils import get_video_uploads_url, get_course_videos_context
|
||||
from .video_utils import validate_video_image
|
||||
from .views.course import get_course_and_check_access
|
||||
|
||||
@@ -223,6 +223,33 @@ def handle_videos(request, course_key_string, edx_video_id=None):
|
||||
return JsonResponse(data, status=status)
|
||||
|
||||
|
||||
def get_video_usage_path(request, course_key, edx_video_id):
|
||||
"""
|
||||
API for fetching the locations a specific video is used in a course.
|
||||
Returns a list of paths to a video.
|
||||
"""
|
||||
if not has_course_author_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
store = modulestore()
|
||||
usage_locations = []
|
||||
videos = store.get_items(
|
||||
course_key,
|
||||
qualifiers={
|
||||
'category': 'video'
|
||||
},
|
||||
)
|
||||
for video in videos:
|
||||
video_id = getattr(video, 'edx_video_id', '')
|
||||
if video_id == edx_video_id:
|
||||
unit = video.get_parent()
|
||||
subsection = unit.get_parent()
|
||||
subsection_display_name = getattr(subsection, 'display_name', '')
|
||||
unit_display_name = getattr(unit, 'display_name', '')
|
||||
xblock_display_name = getattr(video, 'display_name', '')
|
||||
usage_locations.append(f'{subsection_display_name} - {unit_display_name} / {xblock_display_name}')
|
||||
return {'usage_locations': usage_locations}
|
||||
|
||||
|
||||
def handle_generate_video_upload_link(request, course_key_string):
|
||||
"""
|
||||
API for creating a video upload. Returns an edx_video_id and a presigned URL that can be used
|
||||
@@ -585,7 +612,7 @@ def _get_index_videos(course, pagination_conf=None):
|
||||
course_id = str(course.id)
|
||||
attrs = [
|
||||
'edx_video_id', 'client_video_id', 'created', 'duration',
|
||||
'status', 'courses', 'transcripts', 'transcription_status',
|
||||
'status', 'courses', 'encoded_videos', 'transcripts', 'transcription_status',
|
||||
'transcript_urls', 'error_description'
|
||||
]
|
||||
|
||||
@@ -598,9 +625,15 @@ def _get_index_videos(course, pagination_conf=None):
|
||||
if attr == 'courses':
|
||||
course = [c for c in video['courses'] if course_id in c]
|
||||
(__, values['course_video_image_url']), = list(course[0].items())
|
||||
elif attr == 'encoded_videos':
|
||||
values['download_link'] = ''
|
||||
values['file_size'] = 0
|
||||
for encoding in video['encoded_videos']:
|
||||
if encoding['profile'] == 'desktop_mp4':
|
||||
values['download_link'] = encoding['url']
|
||||
values['file_size'] = encoding['file_size']
|
||||
else:
|
||||
values[attr] = video[attr]
|
||||
|
||||
return values
|
||||
|
||||
videos, pagination_context = _get_videos(course, pagination_conf)
|
||||
@@ -636,54 +669,10 @@ def videos_index_html(course, pagination_conf=None):
|
||||
"""
|
||||
Returns an HTML page to display previous video uploads and allow new ones
|
||||
"""
|
||||
is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course.id)
|
||||
previous_uploads, pagination_context = _get_index_videos(course, pagination_conf)
|
||||
context = {
|
||||
'context_course': course,
|
||||
'image_upload_url': reverse_course_url('video_images_handler', str(course.id)),
|
||||
'video_handler_url': reverse_course_url('videos_handler', str(course.id)),
|
||||
'encodings_download_url': reverse_course_url('video_encodings_download', str(course.id)),
|
||||
'default_video_image_url': _get_default_video_image_url(),
|
||||
'previous_uploads': previous_uploads,
|
||||
'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0),
|
||||
'video_supported_file_formats': list(VIDEO_SUPPORTED_FILE_FORMATS.keys()),
|
||||
'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB,
|
||||
'video_image_settings': {
|
||||
'video_image_upload_enabled': VIDEO_IMAGE_UPLOAD_ENABLED.is_enabled(),
|
||||
'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': is_video_transcript_enabled,
|
||||
'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(course.id)),
|
||||
'trancript_download_file_format': Transcript.SRT
|
||||
},
|
||||
'pagination_context': pagination_context
|
||||
}
|
||||
|
||||
if is_video_transcript_enabled:
|
||||
context['video_transcript_settings'].update({
|
||||
'transcript_preferences_handler_url': reverse_course_url(
|
||||
'transcript_preferences_handler',
|
||||
str(course.id)
|
||||
),
|
||||
'transcript_credentials_handler_url': reverse_course_url(
|
||||
'transcript_credentials_handler',
|
||||
str(course.id)
|
||||
),
|
||||
'transcription_plans': get_3rd_party_transcription_plans(),
|
||||
})
|
||||
context['active_transcript_preferences'] = get_transcript_preferences(str(course.id))
|
||||
# Cached state for transcript providers' credentials (org-specific)
|
||||
context['transcript_credentials'] = get_transcript_credentials_state_for_org(course.id.org)
|
||||
context = get_course_videos_context(
|
||||
course,
|
||||
pagination_conf,
|
||||
)
|
||||
if use_new_video_uploads_page(course.id):
|
||||
return redirect(get_video_uploads_url(course.id))
|
||||
return render_to_response('videos_index.html', context)
|
||||
|
||||
@@ -358,6 +358,7 @@ class VideosHandlerTestCase(
|
||||
for i, response_video in enumerate(response_videos):
|
||||
# Videos should be returned by creation date descending
|
||||
original_video = self.previous_uploads[-(i + 1)]
|
||||
print(response_video.keys())
|
||||
self.assertEqual(
|
||||
set(response_video.keys()),
|
||||
{
|
||||
@@ -367,6 +368,8 @@ class VideosHandlerTestCase(
|
||||
'duration',
|
||||
'status',
|
||||
'course_video_image_url',
|
||||
'file_size',
|
||||
'download_link',
|
||||
'transcripts',
|
||||
'transcription_status',
|
||||
'transcript_urls',
|
||||
@@ -385,8 +388,8 @@ class VideosHandlerTestCase(
|
||||
(
|
||||
[
|
||||
'edx_video_id', 'client_video_id', 'created', 'duration',
|
||||
'status', 'course_video_image_url', 'transcripts', 'transcription_status',
|
||||
'transcript_urls', 'error_description'
|
||||
'status', 'course_video_image_url', 'file_size', 'download_link',
|
||||
'transcripts', 'transcription_status', 'transcript_urls', 'error_description'
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -402,8 +405,8 @@ class VideosHandlerTestCase(
|
||||
(
|
||||
[
|
||||
'edx_video_id', 'client_video_id', 'created', 'duration',
|
||||
'status', 'course_video_image_url', 'transcripts', 'transcription_status',
|
||||
'transcript_urls', 'error_description'
|
||||
'status', 'course_video_image_url', 'file_size', 'download_link',
|
||||
'transcripts', 'transcription_status', 'transcript_urls', 'error_description'
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -444,8 +447,9 @@ class VideosHandlerTestCase(
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_videos = json.loads(response.content.decode('utf-8'))['videos']
|
||||
self.assertEqual(len(response_videos), len(self.previous_uploads))
|
||||
|
||||
for response_video in response_videos:
|
||||
print(response_video)
|
||||
|
||||
self.assertEqual(set(response_video.keys()), set(expected_video_keys))
|
||||
if response_video['edx_video_id'] == self.previous_uploads[0]['edx_video_id']:
|
||||
self.assertEqual(response_video.get('transcripts', []), expected_transcripts)
|
||||
|
||||
Reference in New Issue
Block a user