def create(self, validated_data): """ Create the video and its nested resources. """ courses = validated_data.pop("courses", []) encoded_videos = validated_data.pop("encoded_videos", []) subtitles = validated_data.pop("subtitles", []) video = Video.objects.create(**validated_data) EncodedVideo.objects.bulk_create( EncodedVideo(video=video, **video_data) for video_data in encoded_videos) Subtitle.objects.bulk_create( Subtitle(video=video, **subtitle_data) for subtitle_data in subtitles) # The CourseSerializer will already have converted the course data # to CourseVideo models, so we can just set the video and save. # Also create VideoImage objects if an image filename is present for course_video, image_name in courses: course_video.video = video course_video.save() if image_name: VideoImage.create_or_update(course_video, image_name) return video
def update(self, instance, validated_data): """ Update an existing video resource. """ instance.status = validated_data["status"] instance.client_video_id = validated_data["client_video_id"] instance.duration = validated_data["duration"] instance.save() # Set encoded videos instance.encoded_videos.all().delete() EncodedVideo.objects.bulk_create( EncodedVideo(video=instance, **video_data) for video_data in validated_data.get("encoded_videos", [])) # Set subtitles instance.subtitles.all().delete() Subtitle.objects.bulk_create( Subtitle(video=instance, **subtitle_data) for subtitle_data in validated_data.get("subtitles", [])) # Set courses # NOTE: for backwards compatibility with the DRF v2 behavior, # we do NOT delete existing course videos during the update. # Also update VideoImage objects if an image filename is present for course_video, image_name in validated_data.get("courses", []): course_video.video = instance course_video.save() if image_name: VideoImage.create_or_update(course_video, image_name) return instance
def update(self, instance, validated_data): """ Update an existing video resource. """ instance.status = validated_data["status"] instance.client_video_id = validated_data["client_video_id"] instance.duration = validated_data["duration"] instance.save() # Set encoded videos instance.encoded_videos.all().delete() EncodedVideo.objects.bulk_create( EncodedVideo(video=instance, **video_data) for video_data in validated_data.get("encoded_videos", []) ) # Set courses # NOTE: for backwards compatibility with the DRF v2 behavior, # we do NOT delete existing course videos during the update. # Also update VideoImage objects if an image filename is present for course_video, image_name in validated_data.get("courses", []): course_video.video = instance course_video.save() if image_name: VideoImage.create_or_update(course_video, image_name) return instance
def copy_course_videos(source_course_id, destination_course_id): """ Adds the destination_course_id to the videos taken from the source_course_id Args: source_course_id: The original course_id destination_course_id: The new course_id where the videos will be copied """ if source_course_id == destination_course_id: return course_videos = CourseVideo.objects.select_related('video', 'video_image').filter( course_id=str(source_course_id) ) for course_video in course_videos: destination_course_video, __ = CourseVideo.objects.get_or_create( video=course_video.video, course_id=destination_course_id ) if hasattr(course_video, 'video_image'): VideoImage.create_or_update( course_video=destination_course_video, file_name=course_video.video_image.image.name )
def copy_course_videos(source_course_id, destination_course_id): """ Adds the destination_course_id to the videos taken from the source_course_id Args: source_course_id: The original course_id destination_course_id: The new course_id where the videos will be copied """ if source_course_id == destination_course_id: return course_videos = CourseVideo.objects.select_related('video', 'video_image').filter( course_id=six.text_type(source_course_id) ) for course_video in course_videos: destination_course_video, __ = CourseVideo.objects.get_or_create( video=course_video.video, course_id=destination_course_id ) if hasattr(course_video, 'video_image'): VideoImage.create_or_update( course_video=destination_course_video, file_name=course_video.video_image.image.name )
def update_video_image(edx_video_id, course_id, image_data, file_name): """ Update video image for an existing video. NOTE: If `image_data` is None then `file_name` value will be used as it is, otherwise a new file name is constructed based on uuid and extension from `file_name` value. `image_data` will be None in case of course re-run and export. Arguments: image_data (InMemoryUploadedFile): Image data to be saved for a course video. Returns: course video image url Raises: Raises ValVideoNotFoundError if the CourseVideo cannot be retrieved. """ try: course_video = CourseVideo.objects.select_related('video').get( course_id=course_id, video__edx_video_id=edx_video_id) except ObjectDoesNotExist: error_message = u'VAL: CourseVideo not found for edx_video_id: {0} and course_id: {1}'.format( edx_video_id, course_id) raise ValVideoNotFoundError(error_message) video_image, _ = VideoImage.create_or_update(course_video, file_name, image_data) return video_image.image_url()
def update_video_image(edx_video_id, course_id, image_data, file_name): """ Update video image for an existing video. NOTE: If `image_data` is None then `file_name` value will be used as it is, otherwise a new file name is constructed based on uuid and extension from `file_name` value. `image_data` will be None in case of course re-run and export. Arguments: image_data (InMemoryUploadedFile): Image data to be saved for a course video. Returns: course video image url Raises: Raises ValVideoNotFoundError if the CourseVideo cannot be retrieved. """ try: course_video = CourseVideo.objects.select_related('video').get( course_id=course_id, video__edx_video_id=edx_video_id ) except ObjectDoesNotExist: error_message = u'VAL: CourseVideo not found for edx_video_id: {0} and course_id: {1}'.format( edx_video_id, course_id ) raise ValVideoNotFoundError(error_message) video_image, _ = VideoImage.create_or_update(course_video, file_name, image_data) return video_image.image_url()
def test_generated_images_when_no_image_exists(self): """ Test that if generated_images of video image are updated when no previous manual upload exists, then first generated_image is set as the thumbnail. """ video_image, _ = VideoImage.create_or_update( self.course_video, generated_images=self.generated_images) self.assertEqual(video_image.image, self.generated_images[0])
def post(self, request): """ Update a course video image instance with auto generated image names. """ attrs = ('course_id', 'edx_video_id', 'generated_images') 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} must be specified to update a video image.'. format(missing=' and '.join(missing)) }) course_id = request.data['course_id'] edx_video_id = request.data['edx_video_id'] generated_images = request.data['generated_images'] try: validate_generated_images(generated_images, LIST_MAX_ITEMS) except Exception as e: # pylint: disable=broad-except return Response(status=status.HTTP_400_BAD_REQUEST, data={'message': str(e)}) try: course_video = CourseVideo.objects.select_related( 'video_image').get(course_id=str(course_id), video__edx_video_id=edx_video_id) except CourseVideo.DoesNotExist: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': u'CourseVideo not found for course_id: {course_id}'.format( course_id=course_id) }) try: VideoImage.create_or_update(course_video, generated_images=generated_images) except ValidationError as ex: return Response(status=status.HTTP_400_BAD_REQUEST, data={'message': str(ex)}) return Response()
def test_generated_images_when_image_exists(self): """ Test that if generated_images of video image are updated when previous manual upload exists, then image field does not change. """ manually_uploaded_img = 'manual-upload.jpeg' self.video_image.image = manually_uploaded_img self.video_image.save() video_image, _ = VideoImage.create_or_update( self.course_video, generated_images=self.generated_images) self.assertNotEqual(video_image.image, self.generated_images[0]) self.assertEqual(video_image.image, manually_uploaded_img)
def post(self, request): """ Update a course video image instance with auto generated image names. """ attrs = ('course_id', 'edx_video_id', 'generated_images') 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} must be specified to update a video image.'.format( missing=' and '.join(missing) ) } ) course_id = request.data['course_id'] edx_video_id = request.data['edx_video_id'] generated_images = request.data['generated_images'] try: course_video = CourseVideo.objects.select_related('video_image').get( course_id=six.text_type(course_id), video__edx_video_id=edx_video_id ) except CourseVideo.DoesNotExist: return Response( status=status.HTTP_400_BAD_REQUEST, data={'message': u'CourseVideo not found for course_id: {course_id}'.format(course_id=course_id)} ) try: VideoImage.create_or_update(course_video, generated_images=generated_images) except ValidationError as ex: return Response( status=status.HTTP_400_BAD_REQUEST, data={'message': str(ex)} ) return Response()
def create(self, validated_data): """ Create the video and its nested resources. """ courses = validated_data.pop("courses", []) encoded_videos = validated_data.pop("encoded_videos", []) video = Video.objects.create(**validated_data) EncodedVideo.objects.bulk_create( EncodedVideo(video=video, **video_data) for video_data in encoded_videos ) # The CourseSerializer will already have converted the course data # to CourseVideo models, so we can just set the video and save. # Also create VideoImage objects if an image filename is present for course_video, image_name in courses: course_video.video = video course_video.save() if image_name: VideoImage.create_or_update(course_video, image_name) return video
def import_from_xml(xml, edx_video_id, course_id=None): """ Imports data from a video_asset element about the given video_id. If the edx_video_id already exists, then no changes are made. If an unknown profile is referenced by an encoded video, that encoding will be ignored. Arguments: xml (Element): An lxml video_asset element containing import data edx_video_id (str): val video id course_id (str): The ID of a course to associate the video with Raises: ValCannotCreateError: if there is an error importing the video """ if xml.tag != 'video_asset': raise ValCannotCreateError('Invalid XML') # if edx_video_id does not exist then create video transcripts only if not edx_video_id: return create_transcript_objects(xml) # If video with edx_video_id already exists, associate it with the given course_id. try: video = Video.objects.get(edx_video_id=edx_video_id) logger.info( "edx_video_id '%s' present in course '%s' not imported because it exists in VAL.", edx_video_id, course_id, ) if course_id: course_video, __ = CourseVideo.get_or_create_with_validation( video=video, course_id=course_id) image_file_name = xml.get('image', '').strip() if image_file_name: VideoImage.create_or_update(course_video, image_file_name) # import transcripts create_transcript_objects(xml) return except ValidationError as err: logger.exception(err.message) raise ValCannotCreateError(err.message_dict) except Video.DoesNotExist: pass # Video with edx_video_id did not exist, so create one from xml data. data = { 'edx_video_id': edx_video_id, 'client_video_id': xml.get('client_video_id'), 'duration': xml.get('duration'), 'status': 'imported', 'encoded_videos': [], 'courses': [{ course_id: xml.get('image') }] if course_id else [], } for encoded_video_el in xml.iterfind('encoded_video'): profile_name = encoded_video_el.get('profile') try: Profile.objects.get(profile_name=profile_name) except Profile.DoesNotExist: logger.info( "Imported edx_video_id '%s' contains unknown profile '%s'.", edx_video_id, profile_name) continue data['encoded_videos'].append({ 'profile': profile_name, 'url': encoded_video_el.get('url'), 'file_size': encoded_video_el.get('file_size'), 'bitrate': encoded_video_el.get('bitrate'), }) create_video(data) create_transcript_objects(xml)
def import_from_xml(xml, edx_video_id, resource_fs, static_dir, external_transcripts=None, course_id=None): """ Imports data from a video_asset element about the given video_id. If the edx_video_id already exists, then no changes are made. If an unknown profile is referenced by an encoded video, that encoding will be ignored. Arguments: xml (Element): An lxml video_asset element containing import data edx_video_id (str): val video id resource_fs (OSFS): Import file system. static_dir (str): The Directory to retrieve transcript file. external_transcripts : A dict containing the list of names of the external transcripts. course_id: course id Example: { 'en': ['The_Flash.srt', 'Harry_Potter.srt'], 'es': ['Green_Arrow.srt'] } course_id (str): The ID of a course to associate the video with Raises: ValCannotCreateError: if there is an error importing the video Returns: edx_video_id (str): val video id. """ if external_transcripts is None: external_transcripts = {} if xml.tag != 'video_asset': raise ValCannotCreateError('Invalid XML') # If video with edx_video_id already exists, associate it with the given course_id. try: if not edx_video_id: raise Video.DoesNotExist video = Video.objects.get(edx_video_id=edx_video_id) logger.info( "edx_video_id '%s' present in course '%s' not imported because it exists in VAL.", edx_video_id, course_id, ) # We don't want to link an existing video to course if its an external video. # External videos do not have any playback profiles associated, these are just to track video # transcripts for those video components who do not use edx hosted videos for playback. if course_id and video.status != EXTERNAL_VIDEO_STATUS: course_video, __ = CourseVideo.get_or_create_with_validation(video=video, course_id=course_id) image_file_name = xml.get('image', '').strip() if image_file_name: VideoImage.create_or_update(course_video, image_file_name) # Make sure transcripts are imported when video exists create_transcript_objects( xml, edx_video_id, resource_fs, static_dir, external_transcripts ) return edx_video_id except ValidationError as err: logger.exception(xml) raise ValCannotCreateError(err.message_dict) from err except Video.DoesNotExist: pass if edx_video_id: # Video with edx_video_id did not exist, so create one from xml data. data = { 'edx_video_id': edx_video_id, 'client_video_id': xml.get('client_video_id'), 'duration': xml.get('duration'), 'status': 'imported', 'encoded_videos': [], 'courses': [{course_id: xml.get('image')}] if course_id else [], } for encoded_video_el in xml.iterfind('encoded_video'): profile_name = encoded_video_el.get('profile') try: Profile.objects.get(profile_name=profile_name) except Profile.DoesNotExist: logger.info( "Imported edx_video_id '%s' contains unknown profile '%s'.", edx_video_id, profile_name ) continue data['encoded_videos'].append({ 'profile': profile_name, 'url': encoded_video_el.get('url'), 'file_size': encoded_video_el.get('file_size'), 'bitrate': encoded_video_el.get('bitrate'), }) if not data['encoded_videos']: # Video's status does not get included in video xml at the time of export. So, at this point, # we cannot tell from xml that whether a video had an external status. But if encoded videos # are not set, the chances are, the video was an external one, in which case, we will not link # it to the course(s). Even if the video wasn't an external one and it is having 0 encodes in # xml, it does not have a side effect if not linked to a course, since the video was already # non-playable. data['status'] = EXTERNAL_VIDEO_STATUS data['courses'] = [] # Create external video if no edx_video_id. edx_video_id = create_video(data) else: edx_video_id = create_external_video('External Video') create_transcript_objects(xml, edx_video_id, resource_fs, static_dir, external_transcripts) return edx_video_id
def import_from_xml(xml, edx_video_id, resource_fs, static_dir, external_transcripts=dict(), course_id=None): """ Imports data from a video_asset element about the given video_id. If the edx_video_id already exists, then no changes are made. If an unknown profile is referenced by an encoded video, that encoding will be ignored. Arguments: xml (Element): An lxml video_asset element containing import data edx_video_id (str): val video id resource_fs (OSFS): Import file system. static_dir (str): The Directory to retrieve transcript file. external_transcripts (dict): A dict containing the list of names of the external transcripts. Example: { 'en': ['The_Flash.srt', 'Harry_Potter.srt'], 'es': ['Green_Arrow.srt'] } course_id (str): The ID of a course to associate the video with Raises: ValCannotCreateError: if there is an error importing the video Returns: edx_video_id (str): val video id. """ if xml.tag != 'video_asset': raise ValCannotCreateError('Invalid XML') # If video with edx_video_id already exists, associate it with the given course_id. try: if not edx_video_id: raise Video.DoesNotExist video = Video.objects.get(edx_video_id=edx_video_id) logger.info( "edx_video_id '%s' present in course '%s' not imported because it exists in VAL.", edx_video_id, course_id, ) # We don't want to link an existing video to course if its an external video. # External videos do not have any playback profiles associated, these are just to track video # transcripts for those video components who do not use edx hosted videos for playback. if course_id and video.status != EXTERNAL_VIDEO_STATUS: course_video, __ = CourseVideo.get_or_create_with_validation(video=video, course_id=course_id) image_file_name = xml.get('image', '').strip() if image_file_name: VideoImage.create_or_update(course_video, image_file_name) return edx_video_id except ValidationError as err: logger.exception(err.message) raise ValCannotCreateError(err.message_dict) except Video.DoesNotExist: pass if edx_video_id: # Video with edx_video_id did not exist, so create one from xml data. data = { 'edx_video_id': edx_video_id, 'client_video_id': xml.get('client_video_id'), 'duration': xml.get('duration'), 'status': 'imported', 'encoded_videos': [], 'courses': [{course_id: xml.get('image')}] if course_id else [], } for encoded_video_el in xml.iterfind('encoded_video'): profile_name = encoded_video_el.get('profile') try: Profile.objects.get(profile_name=profile_name) except Profile.DoesNotExist: logger.info( "Imported edx_video_id '%s' contains unknown profile '%s'.", edx_video_id, profile_name ) continue data['encoded_videos'].append({ 'profile': profile_name, 'url': encoded_video_el.get('url'), 'file_size': encoded_video_el.get('file_size'), 'bitrate': encoded_video_el.get('bitrate'), }) if not data['encoded_videos']: # Video's status does not get included in video xml at the time of export. So, at this point, # we cannot tell from xml that whether a video had an external status. But if encoded videos # are not set, the chances are, the video was an external one, in which case, we will not link # it to the course(s). Even if the video wasn't an external one and it is having 0 encodes in # xml, it does not have a side effect if not linked to a course, since the video was already # non-playable. data['status'] = EXTERNAL_VIDEO_STATUS data['courses'] = [] # Create external video if no edx_video_id. edx_video_id = create_video(data) else: edx_video_id = create_external_video('External Video') create_transcript_objects(xml, edx_video_id, resource_fs, static_dir, external_transcripts) return edx_video_id