Convert video settings from xblock metadata into the redux store. In-progress draft. TNL-10009. (#119)
* feat: Convert video settings from xblock metadata into the redux store. In-progress draft. TNL-10009. Co-authored-by: rayzhou-bit <rzhou@2u.com>
This commit is contained in:
@@ -1,9 +1,133 @@
|
||||
import { singleVideoData } from '../../services/cms/mockVideoData';
|
||||
import { actions, selectors } from '..';
|
||||
import * as requests from './requests';
|
||||
import * as module from './video';
|
||||
|
||||
export const loadVideoData = () => (dispatch) => {
|
||||
dispatch(actions.video.load(singleVideoData));
|
||||
export const loadVideoData = () => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const rawVideoData = state.app.blockValue.data.metadata ? state.app.blockValue.data.metadata : {};
|
||||
const {
|
||||
videoSource,
|
||||
videoId,
|
||||
fallbackVideos,
|
||||
} = module.determineVideoSource({
|
||||
edxVideoId: rawVideoData.edx_video_id,
|
||||
youtubeId: rawVideoData.youtube_id_1_0,
|
||||
html5Sources: rawVideoData.html5_sources,
|
||||
});
|
||||
|
||||
// we don't appear to want to parse license version
|
||||
const [licenseType, licenseOptions] = module.parseLicense(rawVideoData.license);
|
||||
|
||||
dispatch(actions.video.load({
|
||||
videoSource,
|
||||
videoId,
|
||||
fallbackVideos,
|
||||
allowVideoDownloads: rawVideoData.download_video,
|
||||
transcripts: rawVideoData.transcripts,
|
||||
allowTranscriptDownloads: rawVideoData.download_track,
|
||||
showTranscriptByDefault: rawVideoData.show_captions,
|
||||
duration: { // TODO duration is not always sent so they should be calculated.
|
||||
startTime: rawVideoData.start_time,
|
||||
stopTime: rawVideoData.end_time,
|
||||
total: null, // TODO can we get total duration? if not, probably dropping from widget
|
||||
},
|
||||
handout: rawVideoData.handout,
|
||||
licenseType,
|
||||
licenseDetails: {
|
||||
attribution: licenseOptions.by,
|
||||
noncommercial: licenseOptions.nc,
|
||||
noDerivatives: licenseOptions.nd,
|
||||
shareAlike: licenseOptions.sa,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const determineVideoSource = ({
|
||||
edxVideoId,
|
||||
youtubeId,
|
||||
html5Sources,
|
||||
}) => {
|
||||
// videoSource should be the edx_video_id (if present), or the youtube url (if present), or the first fallback url.
|
||||
// in that order.
|
||||
// if we are falling back to the first fallback url, remove it from the list of fallback urls for display
|
||||
const videoSource = edxVideoId || youtubeId || html5Sources[0] || '';
|
||||
const videoId = edxVideoId || '';
|
||||
const fallbackVideos = (!edxVideoId && !youtubeId)
|
||||
? html5Sources.slice(1)
|
||||
: html5Sources;
|
||||
return {
|
||||
videoSource,
|
||||
videoId,
|
||||
fallbackVideos,
|
||||
};
|
||||
};
|
||||
|
||||
// copied from frontend-app-learning/src/courseware/course/course-license/CourseLicense.jsx
|
||||
// in the long run, should be shared (perhaps one day the learning MFE will depend on this repo)
|
||||
export const parseLicense = (license) => {
|
||||
if (!license) {
|
||||
// Default to All Rights Reserved if no license
|
||||
// is detected
|
||||
return ['all-rights-reserved', {}];
|
||||
}
|
||||
|
||||
// Search for a colon character denoting the end
|
||||
// of the license type and start of the options
|
||||
const colonIndex = license.indexOf(':');
|
||||
if (colonIndex === -1) {
|
||||
// no options, so the entire thing is the license type
|
||||
return [license, {}];
|
||||
}
|
||||
|
||||
// Split the license on the colon
|
||||
const licenseType = license.slice(0, colonIndex).trim();
|
||||
const optionStr = license.slice(colonIndex + 1).trim();
|
||||
|
||||
let options = {};
|
||||
let version = '';
|
||||
|
||||
// Set the defaultVersion to 4.0
|
||||
const defaultVersion = '4.0';
|
||||
optionStr.split(' ').forEach(option => {
|
||||
// Split the option into key and value
|
||||
// Default the value to `true` if no value
|
||||
let key = '';
|
||||
let value = '';
|
||||
if (option.indexOf('=') !== -1) {
|
||||
[key, value] = option.split('=');
|
||||
} else {
|
||||
key = option;
|
||||
value = true;
|
||||
}
|
||||
|
||||
// Check for version
|
||||
if (key === 'ver') {
|
||||
version = value;
|
||||
} else {
|
||||
// Set the option key to lowercase to make
|
||||
// it easier to query
|
||||
options[key.toLowerCase()] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// No options
|
||||
if (Object.keys(options).length === 0) {
|
||||
// If no other options are set for the
|
||||
// license, set version to 1.0
|
||||
version = '1.0';
|
||||
|
||||
// Set the `zero` option so the link
|
||||
// works correctly
|
||||
options = {
|
||||
zero: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Set the version to whatever was included,
|
||||
// using `defaultVersion` as a fallback if unset
|
||||
version = version || defaultVersion;
|
||||
|
||||
return [licenseType, options, version];
|
||||
};
|
||||
|
||||
export const saveVideoData = () => () => {
|
||||
@@ -80,6 +204,8 @@ export const replaceTranscript = ({ newFile, newFilename, language }) => (dispat
|
||||
|
||||
export default {
|
||||
loadVideoData,
|
||||
determineVideoSource,
|
||||
parseLicense,
|
||||
saveVideoData,
|
||||
uploadTranscript,
|
||||
deleteTranscript,
|
||||
|
||||
@@ -2,6 +2,19 @@ import { actions } from '..';
|
||||
import { keyStore } from '../../../utils';
|
||||
import * as thunkActions from './video';
|
||||
|
||||
jest.mock('..', () => ({
|
||||
actions: {
|
||||
video: {
|
||||
load: (args) => ({ load: args }),
|
||||
updateField: (args) => ({ updateField: args }),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
video: {
|
||||
videoId: (state) => ({ videoId: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('./requests', () => ({
|
||||
deleteTranscript: (args) => ({ deleteTranscript: args }),
|
||||
uploadTranscript: (args) => ({ uploadTranscript: args }),
|
||||
@@ -12,6 +25,18 @@ const mockLanguage = 'la';
|
||||
const mockFile = 'soMEtRANscRipT';
|
||||
const mockFilename = 'soMEtRANscRipT.srt';
|
||||
|
||||
const testMetadata = {
|
||||
download_track: 'dOWNlOAdTraCK',
|
||||
download_video: 'downLoaDViDEo',
|
||||
edx_video_id: 'soMEvIDEo',
|
||||
end_time: 'StOpTIMe',
|
||||
handout: 'hANdoUT',
|
||||
html5_sources: [],
|
||||
license: 'liCENse',
|
||||
show_captions: 'shOWcapTIONS',
|
||||
start_time: 'stARtTiME',
|
||||
transcripts: { la: 'test VALUE' },
|
||||
};
|
||||
const testState = { transcripts: { la: 'test VALUE' }, videoId: 'soMEvIDEo' };
|
||||
const testUpload = { transcripts: { la: { filename: mockFilename } } };
|
||||
const testReplaceUpload = {
|
||||
@@ -27,10 +52,154 @@ describe('video thunkActions', () => {
|
||||
beforeEach(() => {
|
||||
dispatch = jest.fn((action) => ({ dispatch: action }));
|
||||
getState = jest.fn(() => ({
|
||||
app: { studioEndpointUrl: 'soMEeNDPoiNT', blockId: 'soMEBloCk' },
|
||||
app: {
|
||||
blockId: 'soMEBloCk',
|
||||
blockValue: { data: { metadata: { ...testMetadata } } },
|
||||
studioEndpointUrl: 'soMEeNDPoiNT',
|
||||
},
|
||||
video: testState,
|
||||
}));
|
||||
});
|
||||
describe('loadVideoData', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
it('dispatches actions.video.load', () => {
|
||||
jest.spyOn(thunkActions, thunkActionsKeys.determineVideoSource).mockReturnValue({
|
||||
videoSource: 'videOsOurce',
|
||||
videoId: 'videOiD',
|
||||
fallbackVideos: 'fALLbACKvIDeos',
|
||||
});
|
||||
jest.spyOn(thunkActions, thunkActionsKeys.parseLicense).mockReturnValue([
|
||||
'liCENSEtyPe',
|
||||
{
|
||||
by: true,
|
||||
nc: true,
|
||||
nd: true,
|
||||
sa: false,
|
||||
},
|
||||
]);
|
||||
thunkActions.loadVideoData()(dispatch, getState);
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.video.load({
|
||||
videoSource: 'videOsOurce',
|
||||
videoId: 'videOiD',
|
||||
fallbackVideos: 'fALLbACKvIDeos',
|
||||
allowVideoDownloads: testMetadata.download_video,
|
||||
transcripts: testMetadata.transcripts,
|
||||
allowTranscriptDownloads: testMetadata.download_track,
|
||||
showTranscriptByDefault: testMetadata.show_captions,
|
||||
duration: {
|
||||
startTime: testMetadata.start_time,
|
||||
stopTime: testMetadata.end_time,
|
||||
total: null,
|
||||
},
|
||||
handout: testMetadata.handout,
|
||||
licenseType: 'liCENSEtyPe',
|
||||
licenseDetails: {
|
||||
attribution: true,
|
||||
noncommercial: true,
|
||||
noDerivatives: true,
|
||||
shareAlike: false,
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('determineVideoSource', () => {
|
||||
const edxVideoId = 'EDxviDEoiD';
|
||||
const youtubeId = 'yOuTuBEiD';
|
||||
const html5Sources = ['htmLOne', 'hTMlTwo', 'htMLthrEE'];
|
||||
describe('when there is an edx video id, youtube id and html5 sources', () => {
|
||||
it('returns the edx video id for video source and html5 sources for fallback videos', () => {
|
||||
expect(thunkActions.determineVideoSource({
|
||||
edxVideoId,
|
||||
youtubeId,
|
||||
html5Sources,
|
||||
})).toEqual({
|
||||
videoSource: edxVideoId,
|
||||
videoId: edxVideoId,
|
||||
fallbackVideos: html5Sources,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when there is no edx video id', () => {
|
||||
it('returns the youtube id for video source and html5 sources for fallback videos', () => {
|
||||
expect(thunkActions.determineVideoSource({
|
||||
edxVideoId: '',
|
||||
youtubeId,
|
||||
html5Sources,
|
||||
})).toEqual({
|
||||
videoSource: youtubeId,
|
||||
videoId: '',
|
||||
fallbackVideos: html5Sources,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when there is no edx video id and no youtube id', () => {
|
||||
it('returns the first html5 source for video source and the rest for fallback videos', () => {
|
||||
expect(thunkActions.determineVideoSource({
|
||||
edxVideoId: '',
|
||||
youtubeId: '',
|
||||
html5Sources,
|
||||
})).toEqual({
|
||||
videoSource: 'htmLOne',
|
||||
videoId: '',
|
||||
fallbackVideos: ['hTMlTwo', 'htMLthrEE'],
|
||||
});
|
||||
});
|
||||
it('returns the html5 source for video source and an empty array for fallback videos', () => {
|
||||
expect(thunkActions.determineVideoSource({
|
||||
edxVideoId: '',
|
||||
youtubeId: '',
|
||||
html5Sources: ['htmlOne'],
|
||||
})).toEqual({
|
||||
videoSource: 'htmlOne',
|
||||
videoId: '',
|
||||
fallbackVideos: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when there is no edx video id, no youtube id and no html5 sources', () => {
|
||||
it('returns an empty string for video source and an empty array for fallback videos', () => {
|
||||
expect(thunkActions.determineVideoSource({
|
||||
edxVideoId: '',
|
||||
youtubeId: '',
|
||||
html5Sources: [],
|
||||
})).toEqual({
|
||||
videoSource: '',
|
||||
videoId: '',
|
||||
fallbackVideos: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('parseLicense', () => {
|
||||
let license;
|
||||
it('returns all-rights-reserved when there is no license', () => {
|
||||
expect(thunkActions.parseLicense(license)).toEqual([
|
||||
'all-rights-reserved',
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it('returns expected values for a license with no options', () => {
|
||||
license = 'sOmeLIcense';
|
||||
expect(thunkActions.parseLicense(license)).toEqual([
|
||||
license,
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it('returns expected type and options for creative commons', () => {
|
||||
license = 'creative-commons: ver=4.0 BY NC ND';
|
||||
expect(thunkActions.parseLicense(license)).toEqual([
|
||||
'creative-commons',
|
||||
{
|
||||
by: true,
|
||||
nc: true,
|
||||
nd: true,
|
||||
},
|
||||
'4.0',
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('deleteTranscript', () => {
|
||||
beforeEach(() => {
|
||||
thunkActions.deleteTranscript({ language: mockLanguage })(dispatch, getState);
|
||||
|
||||
@@ -10,6 +10,26 @@ export const fetchBlockById = ({ blockId, studioEndpointUrl }) => mockPromise({
|
||||
data: {
|
||||
data: '<p>Test prompt content</p>',
|
||||
display_name: 'My Text Prompt',
|
||||
metadata: {
|
||||
display_name: 'Welcome!',
|
||||
download_track: true,
|
||||
download_video: true,
|
||||
edx_video_id: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7',
|
||||
html5_sources: [
|
||||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
],
|
||||
show_captions: true,
|
||||
sub: '',
|
||||
track: '',
|
||||
transcripts: {
|
||||
en: 'my-transcript-url',
|
||||
},
|
||||
xml_attributes: {
|
||||
source: '',
|
||||
},
|
||||
youtube_id_1_0: 'dQw4w9WgXcQ',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,6 +41,26 @@ export const fetchStudioView = ({ blockId, studioEndpointUrl }) => mockPromise({
|
||||
html: blockId.includes('mockRaw') ? 'data-editor="raw"' : '',
|
||||
data: '<p>Test prompt content</p>',
|
||||
display_name: 'My Text Prompt',
|
||||
metadata: {
|
||||
display_name: 'Welcome!',
|
||||
download_track: true,
|
||||
download_video: true,
|
||||
edx_video_id: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7',
|
||||
html5_sources: [
|
||||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
],
|
||||
show_captions: true,
|
||||
sub: '',
|
||||
track: '',
|
||||
transcripts: {
|
||||
en: 'my-transcript-url',
|
||||
},
|
||||
xml_attributes: {
|
||||
source: '',
|
||||
},
|
||||
youtube_id_1_0: 'dQw4w9WgXcQ',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user