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:
kenclary
2022-10-04 14:59:22 -04:00
committed by GitHub
parent cc3a2d8b85
commit a7abab1236
3 changed files with 339 additions and 4 deletions

View File

@@ -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,

View File

@@ -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);

View File

@@ -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',
},
},
});