feat: add import transcripts from youtube (#176)

This commit is contained in:
Kristin Aoki
2022-12-23 10:54:07 -05:00
committed by GitHub
parent 2896393c53
commit 09bb1dab2b
22 changed files with 531 additions and 37 deletions

View File

@@ -50,7 +50,7 @@ LicenseDisplay.propTypes = {
shareAlike: PropTypes.bool.isRequired,
}).isRequired,
level: PropTypes.string.isRequired,
licenseDescription: PropTypes.func.isRequired,
licenseDescription: PropTypes.string.isRequired,
};
export default injectIntl(LicenseDisplay);

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Button,
Icon,
IconButton,
Stack,
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import messages from './messages';
import { thunkActions } from '../../../../../../data/redux';
export const ImportTranscriptCard = ({
setOpen,
// redux
importTranscript,
}) => (
<Stack gap={3} className="border rounded border-primary-200 p-4">
<ActionRow className="h5">
<FormattedMessage {...messages.importHeader} />
<ActionRow.Spacer />
<IconButton
src={Close}
iconAs={Icon}
onClick={() => setOpen(false)}
/>
</ActionRow>
<FormattedMessage {...messages.importMessage} />
<Button
variant="outline-primary"
size="sm"
onClick={importTranscript}
>
<FormattedMessage {...messages.importButtonLabel} />
</Button>
</Stack>
);
ImportTranscriptCard.defaultProps = {
setOpen: true,
};
ImportTranscriptCard.propTypes = {
setOpen: PropTypes.func,
// redux
importTranscript: PropTypes.func.isRequired,
};
export const mapStateToProps = () => ({});
export const mapDispatchToProps = {
importTranscript: thunkActions.video.importTranscript,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ImportTranscriptCard));

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Button, IconButton } from '@edx/paragon';
import { thunkActions } from '../../../../../../data/redux';
import * as module from './ImportTranscriptCard';
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(() => ({ transcripts: ['error.transcripts', jest.fn().mockName('error.setTranscripts')] })),
}));
jest.mock('../../../../../../data/redux', () => ({
thunkActions: {
video: {
importTranscript: jest.fn().mockName('thunkActions.video.importTranscript'),
},
},
}));
describe('ImportTranscriptCard', () => {
const props = {
setOpen: jest.fn().mockName('setOpen'),
importTranscript: jest.fn().mockName('args.importTranscript'),
};
let el;
describe('snapshots', () => {
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<module.ImportTranscriptCard {...props} />),
).toMatchSnapshot();
});
});
describe('behavior inspection', () => {
beforeEach(() => {
el = shallow(<module.ImportTranscriptCard {...props} />);
});
test('close behavior is linked to IconButton', () => {
expect(el.find(IconButton)
.props().onClick).toBeDefined();
});
test('import behavior is linked to Button onClick', () => {
expect(el.find(Button)
.props().onClick).toEqual(props.importTranscript);
});
});
describe('mapStateToProps', () => {
it('returns an empty object', () => {
expect(module.mapStateToProps()).toEqual({});
});
});
describe('mapDispatchToProps', () => {
test('updateField from thunkActions.video.importTranscript', () => {
expect(module.mapDispatchToProps.importTranscript).toEqual(thunkActions.video.importTranscript);
});
});
});

View File

@@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportTranscriptCard snapshots snapshots: renders as expected with default props 1`] = `
<Stack
className="border rounded border-primary-200 p-4"
gap={3}
>
<ActionRow
className="h5"
>
<FormattedMessage
defaultMessage="Import transcript from YouTube?"
description="Header for import transcript card"
id="authoring.videoEditor.transcripts.importCard.header"
/>
<ActionRow.Spacer />
<IconButton
iconAs="Icon"
onClick={[Function]}
src={[MockFunction icons.Close]}
/>
</ActionRow>
<FormattedMessage
defaultMessage="We found transcript for this video on YouTube. Would you like to import it now?"
description="Message for import transcript card asking user if they want to import transcript"
id="authoring.videoEditor.transcrtipts.importCard.message"
/>
<Button
onClick={[MockFunction args.importTranscript]}
size="sm"
variant="outline-primary"
>
<FormattedMessage
defaultMessage="Import Transcript"
description="Label for youTube import transcript button"
id="authoring.videoEditor.transcripts.importButton.label"
/>
</Button>
</Stack>
`;

View File

@@ -30,9 +30,11 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with
/>
</ErrorAlert>
<Stack
gap={2.5}
gap={3}
>
<Form.Group>
<Form.Group
className="border-primary-100 border-bottom"
>
<Transcript
index={0}
language="en"
@@ -98,7 +100,7 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with
</Form.Checkbox>
</Form.Group>
<div
className="border-primary-100 border-top pt-4"
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
@@ -147,9 +149,11 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with
/>
</ErrorAlert>
<Stack
gap={2.5}
gap={3}
>
<Form.Group>
<Form.Group
className="border-primary-100 border-bottom"
>
<Transcript
index={0}
language="en"
@@ -219,7 +223,7 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with
</Form.Checkbox>
</Form.Group>
<div
className="border-primary-100 border-top pt-4"
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
@@ -268,9 +272,11 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
/>
</ErrorAlert>
<Stack
gap={2.5}
gap={3}
>
<Form.Group>
<Form.Group
className="border-primary-100 border-bottom"
>
<Transcript
index={0}
language="en"
@@ -336,7 +342,67 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
</Form.Checkbox>
</Form.Group>
<div
className="border-primary-100 border-top pt-4"
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
onClick={[Function]}
size="sm"
variant="link"
>
<FormattedMessage
defaultMessage="Add a transcript"
description="Label for upload button"
id="authoring.videoeditor.transcripts.upload.label"
/>
</Button>
</div>
</Stack>
</CollapsibleFormWidget>
`;
exports[`TranscriptWidget component snapshots snapshots: renders as expected with allowTranscriptImport true 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="None"
title="Transcripts"
>
<ErrorAlert
dismissError={null}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="Failed to upload transcript. Please try again."
description="Message presented to user when transcript fails to upload"
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
/>
</ErrorAlert>
<ErrorAlert
dismissError={null}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="Failed to delete transcript. Please try again."
description="Message presented to user when transcript fails to delete"
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
/>
</ErrorAlert>
<Stack
gap={3}
>
<FormattedMessage
defaultMessage="Add video transcripts (.srt files only) for improved accessibility."
description="Message for adding first transcript"
id="authoring.videoeditor.transcripts.upload.firstTranscriptMessage"
/>
<injectIntl(ShimmedIntlComponent)
setOpen={[Function]}
/>
<div
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
@@ -385,7 +451,7 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
/>
</ErrorAlert>
<Stack
gap={2.5}
gap={3}
>
<FormattedMessage
defaultMessage="Add video transcripts (.srt files only) for improved accessibility."
@@ -393,7 +459,7 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
id="authoring.videoeditor.transcripts.upload.firstTranscriptMessage"
/>
<div
className="border-primary-100 border-top pt-4"
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
@@ -442,9 +508,11 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
/>
</ErrorAlert>
<Stack
gap={2.5}
gap={3}
>
<Form.Group>
<Form.Group
className="border-primary-100 border-bottom"
>
<Transcript
index={0}
language="en"
@@ -510,7 +578,7 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
</Form.Checkbox>
</Form.Group>
<div
className="border-primary-100 border-top pt-4"
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
@@ -559,9 +627,11 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
/>
</ErrorAlert>
<Stack
gap={2.5}
gap={3}
>
<Form.Group>
<Form.Group
className="border-primary-100 border-bottom"
>
<Transcript
index={0}
language="en"
@@ -627,7 +697,7 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
</Form.Checkbox>
</Form.Group>
<div
className="border-primary-100 border-top pt-4"
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"

View File

@@ -26,6 +26,7 @@ import { in8lTranscriptLanguages } from '../../../../../../data/constants/video'
import ErrorAlert from '../../../../../../sharedComponents/ErrorAlerts/ErrorAlert';
import CollapsibleFormWidget from '../CollapsibleFormWidget';
import ImportTranscriptCard from './ImportTranscriptCard';
import Transcript from './Transcript';
import { ErrorContext } from '../../../../hooks';
import * as module from './index';
@@ -79,15 +80,18 @@ export const TranscriptWidget = ({
transcripts,
allowTranscriptDownloads,
showTranscriptByDefault,
allowTranscriptImport,
updateField,
isUploadError,
isDeleteError,
// intl
// injected
intl,
}) => {
const [error] = React.useContext(ErrorContext).transcripts;
const [showImportCard, setShowImportCard] = React.useState(true);
const fullTextLanguages = module.hooks.transcriptLanguages(transcripts, intl);
const hasTranscripts = module.hooks.hasTranscripts(transcripts);
return (
<CollapsibleFormWidget
fontSize="x-small"
@@ -107,10 +111,10 @@ export const TranscriptWidget = ({
>
<FormattedMessage {...messages.deleteTranscriptError} />
</ErrorAlert>
<Stack gap={2.5}>
<Stack gap={3}>
{hasTranscripts ? (
<Form.Group>
{ transcripts.map((language, index) => (
<Form.Group className="border-primary-100 border-bottom">
{transcripts.map((language, index) => (
<Transcript
language={language}
index={index}
@@ -152,9 +156,12 @@ export const TranscriptWidget = ({
) : (
<>
<FormattedMessage {...messages.addFirstTranscript} />
{showImportCard && allowTranscriptImport
? <ImportTranscriptCard setOpen={setShowImportCard} />
: null}
</>
)}
<div className="border-primary-100 border-top pt-4">
<div className="mt-2">
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
size="sm"
@@ -177,6 +184,7 @@ TranscriptWidget.propTypes = {
transcripts: PropTypes.arrayOf(PropTypes.string).isRequired,
allowTranscriptDownloads: PropTypes.bool.isRequired,
showTranscriptByDefault: PropTypes.bool.isRequired,
allowTranscriptImport: PropTypes.bool.isRequired,
updateField: PropTypes.func.isRequired,
isUploadError: PropTypes.bool.isRequired,
isDeleteError: PropTypes.bool.isRequired,
@@ -186,6 +194,7 @@ export const mapStateToProps = (state) => ({
transcripts: selectors.video.transcripts(state),
allowTranscriptDownloads: selectors.video.allowTranscriptDownloads(state),
showTranscriptByDefault: selectors.video.showTranscriptByDefault(state),
allowTranscriptImport: selectors.video.allowTranscriptImport(state),
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadTranscript }),
isDeleteError: selectors.requests.isFailed(state, { requestKey: RequestKeys.deleteTranscript }),
});

View File

@@ -29,7 +29,7 @@ jest.mock('../../../../../../data/redux', () => ({
transcripts: jest.fn(state => ({ transcripts: state })),
allowTranscriptDownloads: jest.fn(state => ({ allowTranscriptDownloads: state })),
showTranscriptByDefault: jest.fn(state => ({ showTranscriptByDefault: state })),
allowTranscriptImport: jest.fn(state => ({ allowTranscriptImport: state })),
},
requests: {
isFailed: jest.fn(state => ({ isFailed: state })),
@@ -90,6 +90,7 @@ describe('TranscriptWidget', () => {
transcripts: [],
allowTranscriptDownloads: false,
showTranscriptByDefault: false,
allowTranscriptImport: false,
updateField: jest.fn().mockName('args.updateField'),
isUploadError: false,
isDeleteError: false,
@@ -101,6 +102,11 @@ describe('TranscriptWidget', () => {
shallow(<module.TranscriptWidget {...props} />),
).toMatchSnapshot();
});
test('snapshots: renders as expected with allowTranscriptImport true', () => {
expect(
shallow(<module.TranscriptWidget {...props} allowTranscriptImport />),
).toMatchSnapshot();
});
test('snapshots: renders as expected with transcripts', () => {
expect(
shallow(<module.TranscriptWidget {...props} transcripts={['en']} />),
@@ -144,6 +150,11 @@ describe('TranscriptWidget', () => {
module.mapStateToProps(testState).showTranscriptByDefault,
).toEqual(selectors.video.showTranscriptByDefault(testState));
});
test('allowTranscriptImport from video.allowTranscriptImport', () => {
expect(
module.mapStateToProps(testState).allowTranscriptImport,
).toEqual(selectors.video.allowTranscriptImport(testState));
});
test('isUploadError from requests.isFinished', () => {
expect(
module.mapStateToProps(testState).isUploadError,

View File

@@ -99,6 +99,21 @@ export const messages = {
defaultMessage: 'Only SRT files can be uploaded. Please select a file ending in .srt to upload.',
description: 'Message warning users to only upload .srt files',
},
importButtonLabel: {
id: 'authoring.videoEditor.transcripts.importButton.label',
defaultMessage: 'Import Transcript',
description: 'Label for youTube import transcript button',
},
importHeader: {
id: 'authoring.videoEditor.transcripts.importCard.header',
defaultMessage: 'Import transcript from YouTube?',
description: 'Header for import transcript card',
},
importMessage: {
id: 'authoring.videoEditor.transcrtipts.importCard.message',
defaultMessage: 'We found transcript for this video on YouTube. Would you like to import it now?',
description: 'Message for import transcript card asking user if they want to import transcript',
},
};
export default messages;

View File

@@ -22,6 +22,8 @@ export const RequestKeys = StrictDict({
fetchCourseDetails: 'fetchCourseDetails',
updateTranscriptLanguage: 'updateTranscriptLanguage',
getTranscriptFile: 'getTranscriptFile',
checkTranscriptsForImport: 'checkTranscriptsForImport',
importTranscript: 'importTranscript',
uploadImage: 'uploadImage',
fetchAdvanceSettings: 'fetchAdvanceSettings',
});

View File

@@ -16,7 +16,8 @@ const initialState = {
[RequestKeys.deleteTranscript]: { status: RequestStates.inactive },
[RequestKeys.fetchCourseDetails]: { status: RequestStates.inactive },
[RequestKeys.fetchAssets]: { status: RequestStates.inactive },
[RequestKeys.checkTranscriptsForImport]: { status: RequestStates.inactive },
[RequestKeys.importTranscript]: { status: RequestStates.inactive },
};
// eslint-disable-next-line no-unused-vars

View File

@@ -159,6 +159,31 @@ export const uploadThumbnail = ({ thumbnail, videoId, ...rest }) => (dispatch, g
}));
};
export const checkTranscriptsForImport = ({ videoId, youTubeId, ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.checkTranscriptsForImport,
promise: api.checkTranscriptsForImport({
blockId: selectors.app.blockId(getState()),
videoId,
youTubeId,
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
}),
...rest,
}));
};
export const importTranscript = ({ youTubeId, ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.importTranscript,
promise: api.importTranscript({
blockId: selectors.app.blockId(getState()),
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
youTubeId,
}),
...rest,
}));
};
export const deleteTranscript = ({ language, videoId, ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.deleteTranscript,
@@ -261,5 +286,7 @@ export default StrictDict({
updateTranscriptLanguage,
fetchCourseDetails,
getTranscriptFile,
checkTranscriptsForImport,
importTranscript,
fetchAdvanceSettings,
});

View File

@@ -29,11 +29,13 @@ jest.mock('../../services/cms/api', () => ({
fetchAssets: ({ id, url }) => ({ id, url }),
uploadAsset: (args) => args,
loadImages: jest.fn(),
allowThumbnailUpload: jest.fn(),
uploadThumbnail: jest.fn(),
uploadTranscript: jest.fn(),
deleteTranscript: jest.fn(),
getTranscript: jest.fn(),
allowThumbnailUpload: (args) => args,
uploadThumbnail: (args) => args,
uploadTranscript: (args) => args,
deleteTranscript: (args) => args,
getTranscript: (args) => args,
checkTranscriptsForImport: (args) => args,
importTranscript: (args) => args,
}));
const apiKeys = keyStore(api);
@@ -352,6 +354,42 @@ describe('requests thunkActions module', () => {
},
});
});
describe('checkTranscriptsForImport', () => {
const youTubeId = 'SoME yOUtUbEiD As String';
const videoId = 'SoME VidEOid As String';
testNetworkRequestAction({
action: requests.checkTranscriptsForImport,
args: { youTubeId, videoId, ...fetchParams },
expectedString: 'with checkTranscriptsForImport promise',
expectedData: {
...fetchParams,
requestKey: RequestKeys.checkTranscriptsForImport,
promise: api.checkTranscriptsForImport({
blockId: selectors.app.blockId(testState),
youTubeId,
videoId,
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
}),
},
});
});
describe('importTranscript', () => {
const youTubeId = 'SoME yOUtUbEiD As String';
testNetworkRequestAction({
action: requests.importTranscript,
args: { youTubeId, ...fetchParams },
expectedString: 'with importTranscript promise',
expectedData: {
...fetchParams,
requestKey: RequestKeys.importTranscript,
promise: api.importTranscript({
blockId: selectors.app.blockId(testState),
youTubeId,
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
}),
},
});
});
describe('getTranscriptFile', () => {
const language = 'SoME laNGUage CoNtent As String';
const videoId = 'SoME VidEOid CoNtent As String';

View File

@@ -3,6 +3,7 @@ import { removeItemOnce } from '../../../utils';
import * as requests from './requests';
import * as module from './video';
import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/duration';
import { parseYoutubeId } from '../../services/cms/api';
export const loadVideoData = () => (dispatch, getState) => {
const state = getState();
@@ -60,6 +61,20 @@ export const loadVideoData = () => (dispatch, getState) => {
allowThumbnailUpload: response.data.allowThumbnailUpload,
})),
}));
const youTubeId = parseYoutubeId(videoSource);
if (youTubeId) {
dispatch(requests.checkTranscriptsForImport({
videoId,
youTubeId,
onSuccess: (response) => {
if (response.data.command === 'import') {
dispatch(actions.video.updateField({
allowTranscriptImport: true,
}));
}
},
}));
}
};
export const determineVideoSource = ({
@@ -224,6 +239,30 @@ export const uploadHandout = ({ file }) => (dispatch) => {
// Transcript Thunks:
export const importTranscript = () => (dispatch, getState) => {
const state = getState();
const { transcripts, videoSource } = state.video;
// Remove the placeholder '' from the unset language from the list of transcripts.
const transcriptsPlaceholderRemoved = (transcripts === []) ? transcripts : removeItemOnce(transcripts, '');
dispatch(requests.importTranscript({
youTubeId: parseYoutubeId(videoSource),
onSuccess: (response) => {
dispatch(actions.video.updateField({
transcripts: [
...transcriptsPlaceholderRemoved,
'en'],
}));
if (selectors.video.videoId(state) === '') {
dispatch(actions.video.updateField({
videoId: response.data.edx_video_id,
}));
}
},
}));
};
export const uploadTranscript = ({ language, file }) => (dispatch, getState) => {
const state = getState();
const { transcripts, videoId } = state.video;
@@ -308,6 +347,7 @@ export default {
parseLicense,
saveVideoData,
uploadThumbnail,
importTranscript,
uploadTranscript,
deleteTranscript,
updateTranscriptLanguage,

View File

@@ -28,21 +28,28 @@ jest.mock('./requests', () => ({
uploadTranscript: (args) => ({ uploadTranscript: args }),
getTranscriptFile: (args) => ({ getTranscriptFile: args }),
updateTranscriptLanguage: (args) => ({ updateTranscriptLanguage: args }),
checkTranscriptsForImport: (args) => ({ checkTranscriptsForImport: args }),
importTranscript: (args) => ({ importTranscript: args }),
}));
jest.mock('../../../utils', () => ({
removeItemOnce: (args) => (args),
}));
jest.mock('../../services/cms/api', () => ({
parseYoutubeId: (args) => (args),
}));
const thunkActionsKeys = keyStore(thunkActions);
const mockLanguage = 'na';
const mockLanguage = 'en';
const mockFile = 'soMEtRANscRipT';
const mockFilename = 'soMEtRANscRipT.srt';
const mockThumbnail = 'sOMefILE';
const mockThumbnailResponse = { data: { image_url: 'soMEimAGEUrL' } };
const thumbnailUrl = 'soMEimAGEUrL';
const mockAllowThumbnailUpload = { data: { allowThumbnailUpload: 'soMEbOolEAn' } };
const mockAllowTranscriptImport = { data: { command: 'import' } };
const testMetadata = {
download_track: 'dOWNlOAdTraCK',
@@ -63,7 +70,7 @@ const testState = {
originalThumbnail: null,
videoId: 'soMEvIDEo',
};
const testUpload = { transcripts: ['la', 'na'] };
const testUpload = { transcripts: ['la', 'en'] };
const testReplaceUpload = {
file: mockFile,
language: mockLanguage,
@@ -89,6 +96,8 @@ describe('video thunkActions', () => {
});
describe('loadVideoData', () => {
let dispatchedLoad;
let dispatchedAction1;
let dispatchedAction2;
beforeEach(() => {
jest.spyOn(thunkActions, thunkActionsKeys.determineVideoSource).mockReturnValue({
videoSource: 'videOsOurce',
@@ -108,14 +117,18 @@ describe('video thunkActions', () => {
testMetadata.transcripts,
);
thunkActions.loadVideoData()(dispatch, getState);
[[dispatchedLoad], [dispatchedAction]] = dispatch.mock.calls;
[[dispatchedLoad], [dispatchedAction1], [dispatchedAction2]] = dispatch.mock.calls;
});
afterEach(() => {
jest.restoreAllMocks();
});
it('dispatches allowThumbnailUpload action', () => {
expect(dispatchedLoad).not.toEqual(undefined);
expect(dispatchedAction.allowThumbnailUpload).not.toEqual(undefined);
expect(dispatchedAction1.allowThumbnailUpload).not.toEqual(undefined);
});
it('dispatches checkTranscriptsForImport action', () => {
expect(dispatchedLoad).not.toEqual(undefined);
expect(dispatchedAction2.checkTranscriptsForImport).not.toEqual(undefined);
});
it('dispatches actions.video.load', () => {
expect(dispatchedLoad.load).toEqual({
@@ -151,10 +164,16 @@ describe('video thunkActions', () => {
});
it('dispatches actions.video.updateField on success', () => {
dispatch.mockClear();
dispatchedAction.allowThumbnailUpload.onSuccess(mockAllowThumbnailUpload);
dispatchedAction1.allowThumbnailUpload.onSuccess(mockAllowThumbnailUpload);
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({
allowThumbnailUpload: mockAllowThumbnailUpload.data.allowThumbnailUpload,
}));
dispatch.mockClear();
dispatchedAction2.checkTranscriptsForImport.onSuccess(mockAllowTranscriptImport);
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({
allowTranscriptImport: true,
}));
});
});
describe('determineVideoSource', () => {
@@ -350,6 +369,20 @@ describe('video thunkActions', () => {
expect(dispatchedAction.uploadThumbnail).not.toEqual(undefined);
});
});
describe('importTranscript', () => {
beforeEach(() => {
thunkActions.importTranscript()(dispatch, getState);
[[dispatchedAction]] = dispatch.mock.calls;
});
it('dispatches uploadTranscript action', () => {
expect(dispatchedAction.importTranscript).not.toEqual(undefined);
});
it('dispatches actions.video.updateField on success', () => {
dispatch.mockClear();
dispatchedAction.importTranscript.onSuccess();
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField(testUpload));
});
});
describe('deleteTranscript', () => {
beforeEach(() => {
thunkActions.deleteTranscript({ language: 'la' })(dispatch, getState);

View File

@@ -35,6 +35,7 @@ const initialState = {
shareAlike: false,
},
allowThumbnailUpload: null,
allowTranscriptImport: false,
};
// eslint-disable-next-line no-unused-vars

View File

@@ -28,6 +28,7 @@ export const simpleSelectors = [
stateKeys.courseLicenseType,
stateKeys.courseLicenseDetails,
stateKeys.allowThumbnailUpload,
stateKeys.allowTranscriptImport,
].reduce((obj, key) => ({ ...obj, [key]: state => state.video[key] }), {});
export const openLanguages = createSelector(

View File

@@ -54,6 +54,33 @@ export const apiMethods = {
data,
);
},
checkTranscriptsForImport: ({
studioEndpointUrl,
blockId,
youTubeId,
videoId,
}) => {
const getJSON = `{"locator":"${blockId}","videos":[{"mode":"youtube","video":"${youTubeId}","type":"youtube"},{"mode":"edx_video_id","type":"edx_video_id","video":"${videoId}"}]}`;
return get(
urls.checkTranscriptsForImport({
studioEndpointUrl,
parameters: encodeURIComponent(getJSON),
}),
);
},
importTranscript: ({
studioEndpointUrl,
blockId,
youTubeId,
}) => {
const getJSON = `{"locator":"${blockId}","videos":[{"mode":"youtube","video":"${youTubeId}","type":"youtube"}]}`;
return get(
urls.replaceTranscript({
studioEndpointUrl,
parameters: encodeURIComponent(getJSON),
}),
);
},
getTranscript: ({
studioEndpointUrl,
language,

View File

@@ -20,6 +20,8 @@ jest.mock('./urls', () => ({
videoTranscripts: jest.fn().mockName('urls.videoTranscripts'),
allowThumbnailUpload: jest.fn().mockName('urls.allowThumbnailUpload'),
thumbnailUpload: jest.fn().mockName('urls.thumbnailUpload'),
checkTranscriptsForImport: jest.fn().mockName('urls.checkTranscriptsForImport'),
replaceTranscript: jest.fn().mockName('urls.replaceTranscript'),
}));
jest.mock('./utils', () => ({
@@ -251,6 +253,32 @@ describe('cms api', () => {
describe('videoTranscripts', () => {
const language = 'la';
const videoId = 'sOmeVIDeoiD';
const youTubeId = 'SOMeyoutUBeid';
describe('checkTranscriptsForImport', () => {
const getJSON = `{"locator":"${blockId}","videos":[{"mode":"youtube","video":"${youTubeId}","type":"youtube"},{"mode":"edx_video_id","type":"edx_video_id","video":"${videoId}"}]}`;
it('should call get with url.checkTranscriptsForImport', () => {
apiMethods.checkTranscriptsForImport({
studioEndpointUrl,
blockId,
videoId,
youTubeId,
});
expect(get).toHaveBeenCalledWith(urls.checkTranscriptsForImport({
studioEndpointUrl,
parameters: encodeURIComponent(getJSON),
}));
});
});
describe('importTranscript', () => {
const getJSON = `{"locator":"${blockId}","videos":[{"mode":"youtube","video":"${youTubeId}","type":"youtube"}]}`;
it('should call get with url.replaceTranscript', () => {
apiMethods.importTranscript({ studioEndpointUrl, blockId, youTubeId });
expect(get).toHaveBeenCalledWith(urls.replaceTranscript({
studioEndpointUrl,
parameters: encodeURIComponent(getJSON),
}));
});
});
describe('uploadTranscript', () => {
const transcript = { transcript: 'dAta' };
it('should call post with urls.videoTranscripts and transcript data', () => {

View File

@@ -126,6 +126,18 @@ export const allowThumbnailUpload = ({ studioEndpointUrl }) => mockPromise({
data: true,
});
// eslint-disable-next-line
export const checkTranscripts = ({youTubeId, studioEndpointUrl, blockId, videoId}) => mockPromise({
data: {
command: 'import',
},
});
// eslint-disable-next-line
export const importTranscript = ({youTubeId, studioEndpointUrl, blockId}) => mockPromise({
data: {
edx_video_id: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7',
},
});
// eslint-disable-next-line
export const fetchAdvanceSettings = ({ studioEndpointUrl, learningContextId }) => mockPromise({
data: { allow_unsupported_xblocks: { value: true } },
});

View File

@@ -23,7 +23,6 @@ export const videoDataProps = {
noDerivatives: PropTypes.bool,
shareAlike: PropTypes.bool,
}),
originalThumbnail: PropTypes.string,
};
export const singleVideoData = {
@@ -53,5 +52,4 @@ export const singleVideoData = {
noDerivatives: false,
shareAlike: false,
},
originalThumbnail: 'someString',
};

View File

@@ -55,6 +55,14 @@ export const courseDetailsUrl = ({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/settings/details/${learningContextId}`
);
export const checkTranscriptsForImport = ({ studioEndpointUrl, parameters }) => (
`${studioEndpointUrl}/transcripts/check?data=${parameters}`
);
export const replaceTranscript = ({ studioEndpointUrl, parameters }) => (
`${studioEndpointUrl}/transcripts/replace?data=${parameters}`
);
export const courseAdvanceSettings = ({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/api/contentstore/v0/advanced_settings/${learningContextId}`
);

View File

@@ -12,6 +12,8 @@ import {
videoTranscripts,
downloadVideoHandoutUrl,
courseDetailsUrl,
checkTranscriptsForImport,
replaceTranscript,
} from './urls';
describe('cms url methods', () => {
@@ -23,6 +25,8 @@ describe('cms url methods', () => {
const language = 'la';
const handout = '/aSSet@hANdoUt';
const videoId = '123-SOmeVidEOid-213';
const parameters = 'SomEParAMEterS';
describe('return to learning context urls', () => {
const unitUrl = {
data: {
@@ -115,4 +119,16 @@ describe('cms url methods', () => {
.toEqual(`${studioEndpointUrl}/settings/details/${learningContextId}`);
});
});
describe('checkTranscriptsForImport', () => {
it('returns url with studioEndpointUrl and parameters', () => {
expect(checkTranscriptsForImport({ studioEndpointUrl, parameters }))
.toEqual(`${studioEndpointUrl}/transcripts/check?data=${parameters}`);
});
});
describe('replaceTranscript', () => {
it('returns url with studioEndpointUrl and parameters', () => {
expect(replaceTranscript({ studioEndpointUrl, parameters }))
.toEqual(`${studioEndpointUrl}/transcripts/replace?data=${parameters}`);
});
});
});