From ba896a3b15b38e8ae3b55df004ac3454f3a4173b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Fri, 21 Feb 2025 13:33:50 -0500 Subject: [PATCH] feat: Enable transcripts for video library [FC-0076] (#1596) * Get updateTranscriptHandlerUrl() and call it when is ready. * Enable LanguageNamesWidget in a library. * Enable add transcripts for libraries. * Enable delete transcripts for libraries. * Enable replace transcripts for libraries. * Enable download transcripts for libraries. * Enable download transcripts from YouTube --- .../components/TranscriptWidget/index.jsx | 9 +- .../TranscriptWidget/index.test.jsx | 4 + .../components/VideoPreviewWidget/index.jsx | 8 +- .../VideoPreviewWidget/index.test.jsx | 4 +- .../components/VideoSettingsModal/index.tsx | 4 +- src/editors/data/constants/requests.ts | 1 + .../data/redux/thunkActions/requests.js | 149 +++++++++++++----- .../data/redux/thunkActions/requests.test.js | 133 +++++++++++++++- src/editors/data/redux/thunkActions/video.js | 11 ++ .../data/redux/thunkActions/video.test.js | 23 +++ src/editors/data/redux/video/reducer.js | 1 + src/editors/data/redux/video/selectors.js | 34 +++- src/editors/data/services/cms/api.test.ts | 57 +++++++ src/editors/data/services/cms/api.ts | 47 +++++- src/editors/data/services/cms/urls.test.ts | 23 +++ src/editors/data/services/cms/urls.ts | 12 ++ 16 files changed, 458 insertions(+), 62 deletions(-) diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx index b734b31ff4..d54fcbd490 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { connect, useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { FormattedMessage, @@ -17,7 +17,7 @@ import { } from '@openedx/paragon'; import { Add, InfoOutline } from '@openedx/paragon/icons'; -import { actions, selectors } from '../../../../../../data/redux'; +import { thunkActions, actions, selectors } from '../../../../../../data/redux'; import messages from './messages'; import { RequestKeys } from '../../../../../../data/constants/requests'; @@ -97,6 +97,11 @@ const TranscriptWidget = ({ const [showImportCard, setShowImportCard] = React.useState(true); const fullTextLanguages = module.hooks.transcriptLanguages(transcripts, intl); const hasTranscripts = module.hooks.hasTranscripts(transcripts); + const isLibrary = useSelector(selectors.app.isLibrary); + const dispatch = useDispatch(); + if (isLibrary) { + dispatch(thunkActions.video.updateTranscriptHandlerUrl()); + } return ( ({ thunkActions: { video: { deleteTranscript: jest.fn().mockName('thunkActions.video.deleteTranscript'), + updateTranscriptHandlerUrl: jest.fn().mockName('thunkActions.video.updateTranscriptHandlerUrl'), }, }, selectors: { + app: { + isLibrary: jest.fn(state => ({ isLibrary: state })), + }, video: { transcripts: jest.fn(state => ({ transcripts: state })), selectedVideoTranscriptUrls: jest.fn(state => ({ selectedVideoTranscriptUrls: state })), diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx index 3377e31f45..912b541dc1 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx @@ -17,7 +17,6 @@ export const VideoPreviewWidget = ({ videoSource, transcripts, blockTitle, - isLibrary, intl, }) => { const imgRef = React.useRef(); @@ -47,10 +46,7 @@ export const VideoPreviewWidget = ({ />

{blockTitle}

- {!isLibrary && ( - // Since content libraries v2 don't support static assets yet, we can't include transcripts. - - )} + {videoType && ( ({ @@ -82,7 +77,6 @@ export const mapStateToProps = (state) => ({ videoSource: selectors.video.videoSource(state), thumbnail: selectors.video.thumbnail(state), blockTitle: selectors.app.blockTitle(state), - isLibrary: selectors.app.isLibrary(state), }); export default injectIntl(connect(mapStateToProps)(VideoPreviewWidget)); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.test.jsx index fc7f89a5cd..131c9b53ba 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.test.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.test.jsx @@ -30,7 +30,7 @@ describe('VideoPreviewWidget', () => { expect(screen.queryByText('No transcripts added')).toBeInTheDocument(); }); - test('hides transcripts section in preview for libraries', () => { + test('renders transcripts section in preview for libraries', () => { render( { thumbnail="" />, ); - expect(screen.queryByText('No transcripts added')).not.toBeInTheDocument(); + expect(screen.queryByText('No transcripts added')).toBeInTheDocument(); }); }); }); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.tsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.tsx index 4761e39320..7edbcb8be2 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.tsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.tsx @@ -49,9 +49,7 @@ const VideoSettingsModal: React.FC = ({ )} - {!isLibrary && ( // Since content libraries v2 don't support static assets yet, we can't include transcripts. - - )} + diff --git a/src/editors/data/constants/requests.ts b/src/editors/data/constants/requests.ts index e12905d588..561965af86 100644 --- a/src/editors/data/constants/requests.ts +++ b/src/editors/data/constants/requests.ts @@ -27,4 +27,5 @@ export const RequestKeys = StrictDict({ uploadAsset: 'uploadAsset', fetchAdvancedSettings: 'fetchAdvancedSettings', fetchVideoFeatures: 'fetchVideoFeatures', + getHandlerUrl: 'getHandlerUrl', } as const); diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index edff3bf875..3dd4663477 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -4,6 +4,7 @@ import { RequestKeys } from '../../constants/requests'; import api, { loadImages } from '../../services/cms/api'; import { actions as requestsActions } from '../requests'; import { selectors as appSelectors } from '../app'; +import { selectors as videoSelectors } from '../video'; // This 'module' self-import hack enables mocking during tests. // See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested @@ -15,7 +16,7 @@ import { acceptedImgKeys } from '../../../sharedComponents/ImageUploadModal/Sele // Similar to `import { actions, selectors } from '..';` but avoid circular imports: const actions = { requests: requestsActions }; -const selectors = { app: appSelectors }; +const selectors = { app: appSelectors, video: videoSelectors }; /** * Wrapper around a network request promise, that sends actions to the redux store to @@ -239,16 +240,30 @@ export const importTranscript = ({ youTubeId, ...rest }) => (dispatch, getState) }; export const deleteTranscript = ({ language, videoId, ...rest }) => (dispatch, getState) => { - dispatch(module.networkRequest({ - requestKey: RequestKeys.deleteTranscript, - promise: api.deleteTranscript({ - blockId: selectors.app.blockId(getState()), - language, - videoId, - studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), - }), - ...rest, - })); + const state = getState(); + const isLibrary = selectors.app.isLibrary(state); + if (isLibrary) { + dispatch(module.networkRequest({ + requestKey: RequestKeys.deleteTranscript, + promise: api.deleteTranscriptV2({ + language, + videoId, + handlerUrl: selectors.video.transcriptHandlerUrl(state), + }), + ...rest, + })); + } else { + dispatch(module.networkRequest({ + requestKey: RequestKeys.deleteTranscript, + promise: api.deleteTranscript({ + blockId: selectors.app.blockId(state), + language, + videoId, + studioEndpointUrl: selectors.app.studioEndpointUrl(state), + }), + ...rest, + })); + } }; export const uploadTranscript = ({ @@ -257,17 +272,32 @@ export const uploadTranscript = ({ language, ...rest }) => (dispatch, getState) => { - dispatch(module.networkRequest({ - requestKey: RequestKeys.uploadTranscript, - promise: api.uploadTranscript({ - blockId: selectors.app.blockId(getState()), - transcript, - videoId, - language, - studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), - }), - ...rest, - })); + const state = getState(); + const isLibrary = selectors.app.isLibrary(state); + if (isLibrary) { + dispatch(module.networkRequest({ + requestKey: RequestKeys.uploadTranscript, + promise: api.uploadTranscriptV2({ + handlerUrl: selectors.video.transcriptHandlerUrl(state), + transcript, + videoId, + language, + }), + ...rest, + })); + } else { + dispatch(module.networkRequest({ + requestKey: RequestKeys.uploadTranscript, + promise: api.uploadTranscript({ + blockId: selectors.app.blockId(state), + transcript, + videoId, + language, + studioEndpointUrl: selectors.app.studioEndpointUrl(state), + }), + ...rest, + })); + } }; export const updateTranscriptLanguage = ({ @@ -277,28 +307,70 @@ export const updateTranscriptLanguage = ({ videoId, ...rest }) => (dispatch, getState) => { - dispatch(module.networkRequest({ - requestKey: RequestKeys.updateTranscriptLanguage, - promise: api.uploadTranscript({ - blockId: selectors.app.blockId(getState()), - transcript: file, - videoId, - language: languageBeforeChange, - newLanguage: newLanguageCode, - studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), - }), - ...rest, - })); + const state = getState(); + const isLibrary = selectors.app.isLibrary(state); + if (isLibrary) { + dispatch(module.networkRequest({ + requestKey: RequestKeys.updateTranscriptLanguage, + promise: api.uploadTranscriptV2({ + handlerUrl: selectors.video.transcriptHandlerUrl(state), + transcript: file, + videoId, + language: languageBeforeChange, + newLanguage: newLanguageCode, + }), + ...rest, + })); + } else { + dispatch(module.networkRequest({ + requestKey: RequestKeys.updateTranscriptLanguage, + promise: api.uploadTranscript({ + blockId: selectors.app.blockId(state), + transcript: file, + videoId, + language: languageBeforeChange, + newLanguage: newLanguageCode, + studioEndpointUrl: selectors.app.studioEndpointUrl(state), + }), + ...rest, + })); + } }; export const getTranscriptFile = ({ language, videoId, ...rest }) => (dispatch, getState) => { + const state = getState(); + const isLibrary = selectors.app.isLibrary(state); + if (isLibrary) { + dispatch(module.networkRequest({ + requestKey: RequestKeys.getTranscriptFile, + promise: api.getTranscriptV2({ + handlerUrl: selectors.video.transcriptHandlerUrl(state), + videoId, + language, + }), + ...rest, + })); + } else { + dispatch(module.networkRequest({ + requestKey: RequestKeys.getTranscriptFile, + promise: api.getTranscript({ + studioEndpointUrl: selectors.app.studioEndpointUrl(state), + blockId: selectors.app.blockId(state), + videoId, + language, + }), + ...rest, + })); + } +}; + +export const getHandlerlUrl = ({ handlerName, ...rest }) => (dispatch, getState) => { dispatch(module.networkRequest({ - requestKey: RequestKeys.getTranscriptFile, - promise: api.getTranscript({ + requestKey: RequestKeys.getHandlerUrl, + promise: api.getHandlerUrl({ studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), blockId: selectors.app.blockId(getState()), - videoId, - language, + handlerName, }), ...rest, })); @@ -368,4 +440,5 @@ export default StrictDict({ fetchAdvancedSettings, fetchVideoFeatures, uploadVideo, + getHandlerlUrl, }); diff --git a/src/editors/data/redux/thunkActions/requests.test.js b/src/editors/data/redux/thunkActions/requests.test.js index ece5411780..27f288685c 100644 --- a/src/editors/data/redux/thunkActions/requests.test.js +++ b/src/editors/data/redux/thunkActions/requests.test.js @@ -6,6 +6,7 @@ import { actions, selectors } from '../index'; const testState = { some: 'data', + isLibrary: false, }; jest.mock('../app/selectors', () => ({ @@ -18,6 +19,11 @@ jest.mock('../app/selectors', () => ({ blockType: (state) => ({ blockType: state }), learningContextId: (state) => ({ learningContextId: state }), blockTitle: (state) => ({ title: state }), + isLibrary: (state) => (state.isLibrary), +})); + +jest.mock('../video/selectors', () => ({ + transcriptHandlerUrl: () => ('transcriptHandlerUrl'), })); jest.mock('../../services/cms/api', () => ({ @@ -34,7 +40,11 @@ jest.mock('../../services/cms/api', () => ({ uploadThumbnail: (args) => args, uploadTranscript: (args) => args, deleteTranscript: (args) => args, + deleteTranscriptV2: (args) => args, getTranscript: (args) => args, + getTranscriptV2: (args) => args, + getHandlerUrl: (args) => args, + uploadTranscriptV2: (args) => args, checkTranscriptsForImport: (args) => args, importTranscript: (args) => args, fetchVideoFeatures: (args) => args, @@ -158,10 +168,11 @@ describe('requests thunkActions module', () => { args, expectedData, expectedString, + state, }) => { let dispatchedAction; beforeEach(() => { - action({ ...args, onSuccess, onFailure })(dispatch, () => testState); + action({ ...args, onSuccess, onFailure })(dispatch, () => state || testState); [[dispatchedAction]] = dispatch.mock.calls; }); it('dispatches networkRequest', () => { @@ -445,6 +456,31 @@ describe('requests thunkActions module', () => { }, }); }); + describe('deleteTranscript V2', () => { + const language = 'SoME laNGUage CoNtent As String'; + const videoId = 'SoME VidEOid CoNtent As String'; + testNetworkRequestAction({ + action: requests.deleteTranscript, + args: { + language, + videoId, + ...fetchParams, + }, + expectedString: 'with deleteTranscript promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.deleteTranscript, + promise: api.deleteTranscriptV2({ + handlerUrl: selectors.video.transcriptHandlerUrl(testState), + videoId, + language, + }), + }, + state: { + isLibrary: true, + }, + }); + }); describe('checkTranscriptsForImport', () => { const youTubeId = 'SoME yOUtUbEiD As String'; const videoId = 'SoME VidEOid As String'; @@ -500,6 +536,44 @@ describe('requests thunkActions module', () => { }, }); }); + describe('getTranscriptFile V2', () => { + const language = 'SoME laNGUage CoNtent As String'; + const videoId = 'SoME VidEOid CoNtent As String'; + testNetworkRequestAction({ + action: requests.getTranscriptFile, + args: { language, videoId, ...fetchParams }, + expectedString: 'with getTranscriptFile promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.getTranscriptFile, + promise: api.getTranscriptV2({ + handlerUrl: selectors.video.transcriptHandlerUrl(testState), + language, + videoId, + }), + }, + state: { + isLibrary: true, + }, + }); + }); + describe('getHandlerUrl', () => { + const handlerName = 'transcript'; + testNetworkRequestAction({ + action: requests.getHandlerlUrl, + args: { handlerName, ...fetchParams }, + expectedString: 'with getHandlerUrl promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.getHandlerUrl, + promise: api.getHandlerUrl({ + studioEndpointUrl: selectors.app.studioEndpointUrl(testState), + blockId: selectors.app.blockId(testState), + handlerName, + }), + }, + }); + }); describe('updateTranscriptLanguage', () => { const languageBeforeChange = 'SoME laNGUage CoNtent As String'; const newLanguageCode = 'SoME NEW laNGUage CoNtent As String'; @@ -526,7 +600,34 @@ describe('requests thunkActions module', () => { }, }); }); - + describe('updateTranscriptLanguage V2', () => { + const languageBeforeChange = 'SoME laNGUage CoNtent As String'; + const newLanguageCode = 'SoME NEW laNGUage CoNtent As String'; + const videoId = 'SoME VidEOid CoNtent As String'; + testNetworkRequestAction({ + action: requests.updateTranscriptLanguage, + args: { + languageBeforeChange, + newLanguageCode, + videoId, + ...fetchParams, + }, + expectedString: 'with uploadTranscript promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.updateTranscriptLanguage, + promise: api.uploadTranscriptV2({ + videoId, + language: languageBeforeChange, + newLanguage: newLanguageCode, + handlerUrl: selectors.video.transcriptHandlerUrl(testState), + }), + }, + state: { + isLibrary: true, + }, + }); + }); describe('uploadTranscript', () => { const language = 'SoME laNGUage CoNtent As String'; const videoId = 'SoME VidEOid CoNtent As String'; @@ -553,6 +654,34 @@ describe('requests thunkActions module', () => { }, }); }); + describe('uploadTranscript V2', () => { + const language = 'SoME laNGUage CoNtent As String'; + const videoId = 'SoME VidEOid CoNtent As String'; + const transcript = 'SoME tRANscRIPt CoNtent As String'; + testNetworkRequestAction({ + action: requests.uploadTranscript, + args: { + transcript, + language, + videoId, + ...fetchParams, + }, + expectedString: 'with uploadTranscript promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.uploadTranscript, + promise: api.uploadTranscriptV2({ + handlerUrl: selectors.video.transcriptHandlerUrl(testState), + transcript, + videoId, + language, + }), + }, + state: { + isLibrary: true, + }, + }); + }); describe('fetchVideoFeatures', () => { testNetworkRequestAction({ action: requests.fetchVideoFeatures, diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 352d989737..0ea4d84cea 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -383,6 +383,16 @@ export const updateTranscriptLanguage = ({ newLanguageCode, languageBeforeChange })); }; +export const updateTranscriptHandlerUrl = () => (dispatch) => { + dispatch(requests.getHandlerlUrl({ + handlerName: 'studio_transcript', + onSuccess: (response) => { + const transcriptHandlerUrl = response.data.handler_url; + dispatch(actions.video.updateField({ transcriptHandlerUrl })); + }, + })); +}; + export const replaceTranscript = ({ newFile, newFilename, language }) => (dispatch, getState) => { const state = getState(); const { videoId } = state.video; @@ -456,4 +466,5 @@ export default { replaceTranscript, uploadHandout, uploadVideo, + updateTranscriptHandlerUrl, }; diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index 75a668d0bc..4c501cc447 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -33,6 +33,7 @@ jest.mock('./requests', () => ({ importTranscript: (args) => ({ importTranscript: args }), fetchVideoFeatures: (args) => ({ fetchVideoFeatures: args }), uploadVideo: (args) => ({ uploadVideo: args }), + getHandlerlUrl: (args) => ({ getHandlerlUrl: args }), })); jest.mock('../../../utils', () => ({ @@ -61,6 +62,12 @@ const mockVideoFeatures = { }; const mockSelectedVideoId = 'ThisIsAVideoId'; const mockSelectedVideoUrl = 'ThisIsAYoutubeUrl'; +const mockUpdateTranscriptHandlerUrl = 'ThisIsAHandler'; +const mockUpdateTranscriptHandlerUrlData = { + data: { + handler_url: mockUpdateTranscriptHandlerUrl, + }, +}; const testMetadata = { download_track: 'dOWNlOAdTraCK', @@ -669,6 +676,22 @@ describe('video thunkActions', () => { expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ transcripts: [] })); }); }); + describe('updateTranscriptHandlerUrl', () => { + beforeEach(() => { + thunkActions.updateTranscriptHandlerUrl()(dispatch); + [[dispatchedAction]] = dispatch.mock.calls; + }); + it('dispatches updateTranscriptHandlerUrl action', () => { + expect(dispatchedAction.getHandlerlUrl).not.toEqual(undefined); + }); + it('dispatches actions.video.updateField on success', () => { + dispatch.mockClear(); + dispatchedAction.getHandlerlUrl.onSuccess(mockUpdateTranscriptHandlerUrlData); + expect(dispatch).toHaveBeenCalledWith( + actions.video.updateField({ transcriptHandlerUrl: mockUpdateTranscriptHandlerUrl }), + ); + }); + }); describe('uploadTranscript', () => { beforeEach(() => { thunkActions.uploadTranscript({ diff --git a/src/editors/data/redux/video/reducer.js b/src/editors/data/redux/video/reducer.js index 04bfbcd99c..8770c306fe 100644 --- a/src/editors/data/redux/video/reducer.js +++ b/src/editors/data/redux/video/reducer.js @@ -19,6 +19,7 @@ const initialState = { videoSharingLearnMoreLink: '', thumbnail: null, transcripts: [], + transcriptHandlerUrl: '', selectedVideoTranscriptUrls: {}, allowTranscriptDownloads: false, duration: { diff --git a/src/editors/data/redux/video/selectors.js b/src/editors/data/redux/video/selectors.js index f71c014cf7..4829321835 100644 --- a/src/editors/data/redux/video/selectors.js +++ b/src/editors/data/redux/video/selectors.js @@ -10,7 +10,12 @@ import { initialState } from './reducer'; // eslint-disable-next-line import/no-self-import import * as module from './selectors'; import * as AppSelectors from '../app/selectors'; -import { downloadVideoTranscriptURL, downloadVideoHandoutUrl, mediaTranscriptURL } from '../../services/cms/urls'; +import { + downloadVideoTranscriptURL, + downloadVideoHandoutUrl, + mediaTranscriptURL, + downloadVideoTranscriptURLV2, +} from '../../services/cms/urls'; const stateKeys = keyStore(initialState); @@ -27,6 +32,7 @@ export const simpleSelectors = [ stateKeys.allowVideoSharing, stateKeys.thumbnail, stateKeys.transcripts, + stateKeys.transcriptHandlerUrl, stateKeys.selectedVideoTranscriptUrls, stateKeys.allowTranscriptDownloads, stateKeys.duration, @@ -53,13 +59,27 @@ export const openLanguages = createSelector( }, ); +/* istanbul ignore next */ export const getTranscriptDownloadUrl = createSelector( - [AppSelectors.simpleSelectors.studioEndpointUrl, AppSelectors.simpleSelectors.blockId], - (studioEndpointUrl, blockId) => ({ language }) => downloadVideoTranscriptURL({ - studioEndpointUrl, - blockId, - language, - }), + [ + AppSelectors.simpleSelectors.studioEndpointUrl, + AppSelectors.simpleSelectors.blockId, + AppSelectors.isLibrary, + simpleSelectors.transcriptHandlerUrl, + ], + (studioEndpointUrl, blockId, isLibrary, transcriptHandlerUrl) => ({ language }) => { + if (isLibrary) { + return downloadVideoTranscriptURLV2({ + transcriptHandlerUrl, + language, + }); + } + return downloadVideoTranscriptURL({ + studioEndpointUrl, + blockId, + language, + }); + }, ); export const buildTranscriptUrl = createSelector( diff --git a/src/editors/data/services/cms/api.test.ts b/src/editors/data/services/cms/api.test.ts index c3df32ccbf..9ae6f9a379 100644 --- a/src/editors/data/services/cms/api.test.ts +++ b/src/editors/data/services/cms/api.test.ts @@ -23,6 +23,8 @@ jest.mock('./urls', () => ({ .mockImplementation( ({ studioEndpointUrl, learningContextId }) => `${studioEndpointUrl}/some_video_upload_url/${learningContextId}`, ), + handlerUrl: jest.fn().mockReturnValue('urls.handlerUrl'), + transcriptXblockV2: jest.fn().mockReturnValue('url.transcriptXblockV2'), })); jest.mock('./utils', () => ({ @@ -152,6 +154,14 @@ describe('cms api', () => { }); }); + describe('getHandlerUrl', () => { + it('should call get with url.handlerUrl', () => { + const handlerName = 'transcript'; + apiMethods.getHandlerUrl({ studioEndpointUrl, blockId, handlerName }); + expect(get).toHaveBeenCalledWith(urls.handlerUrl({ studioEndpointUrl, blockId, handlerName })); + }); + }); + describe('normalizeContent', () => { test('return value for blockType: html', () => { const content = 'Im baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps.'; @@ -410,6 +420,27 @@ describe('cms api', () => { ); }); }); + describe('uploadTranscriptV2', () => { + const transcript = new Blob(['dAta']); + it('should call post with urls.uploadTranscriptV2 and transcript data', () => { + const mockFormdata = new FormData(); + const transcriptHandlerUrl = 'handlerUrl'; + mockFormdata.append('file', transcript); + mockFormdata.append('edx_video_id', videoId); + mockFormdata.append('language_code', language); + mockFormdata.append('new_language_code', language); + apiMethods.uploadTranscriptV2({ + handlerUrl: transcriptHandlerUrl, + transcript, + videoId, + language, + }); + expect(post).toHaveBeenCalledWith( + urls.transcriptXblockV2({ transcriptHandlerUrl }), + mockFormdata, + ); + }); + }); describe('transcript delete', () => { it('should call deleteObject with urls.videoTranscripts and transcript data', () => { const mockDeleteJSON = { data: { lang: language, edx_video_id: videoId } }; @@ -424,6 +455,19 @@ describe('cms api', () => { mockDeleteJSON, ); }); + it('should call deleteObject with urls.transcriptXblockV2 and transcript data', () => { + const mockDeleteJSON = { data: { lang: language, edx_video_id: videoId } }; + const transcriptHandlerUrl = 'handlerUrl'; + apiMethods.deleteTranscriptV2({ + handlerUrl: transcriptHandlerUrl, + videoId, + language, + }); + expect(deleteObject).toHaveBeenCalledWith( + urls.transcriptXblockV2({ transcriptHandlerUrl }), + mockDeleteJSON, + ); + }); }); describe('transcript get', () => { it('should call get with urls.videoTranscripts and transcript data', () => { @@ -439,6 +483,19 @@ describe('cms api', () => { mockJSON, ); }); + it('should call get with urls.transcriptXblockV2 and transcript data', () => { + const mockJSON = { data: { lang: language, edx_video_id: videoId } }; + const transcriptHandlerUrl = 'handlerUrl'; + apiMethods.getTranscriptV2({ + handlerUrl: transcriptHandlerUrl, + videoId, + language, + }); + expect(get).toHaveBeenCalledWith( + `${urls.transcriptXblockV2({ transcriptHandlerUrl })}?language_code=${language}`, + mockJSON, + ); + }); }); }); describe('processVideoIds', () => { diff --git a/src/editors/data/services/cms/api.ts b/src/editors/data/services/cms/api.ts index e979eb4455..ec4cd7d760 100644 --- a/src/editors/data/services/cms/api.ts +++ b/src/editors/data/services/cms/api.ts @@ -215,7 +215,17 @@ export const apiMethods = { getJSON, ); }, - + getTranscriptV2: ({ + handlerUrl, + language, + videoId, + }) => { + const getJSON = { data: { lang: language, edx_video_id: videoId } }; + return get( + `${urls.transcriptXblockV2({ transcriptHandlerUrl: handlerUrl })}?language_code=${language}`, + getJSON, + ); + }, deleteTranscript: ({ studioEndpointUrl, language, @@ -228,6 +238,17 @@ export const apiMethods = { deleteJSON, ); }, + deleteTranscriptV2: ({ + handlerUrl, + language, + videoId, + }) => { + const deleteJSON = { data: { lang: language, edx_video_id: videoId } }; + return deleteObject( + urls.transcriptXblockV2({ transcriptHandlerUrl: handlerUrl }), + deleteJSON, + ); + }, uploadTranscript: ({ blockId, studioEndpointUrl, @@ -246,6 +267,23 @@ export const apiMethods = { data, ); }, + uploadTranscriptV2: ({ + handlerUrl, + transcript, + videoId, + language, + newLanguage = null, + }) => { + const data = new FormData(); + data.append('file', transcript); + data.append('edx_video_id', videoId); + data.append('language_code', language); + data.append('new_language_code', newLanguage || language); + return post( + urls.transcriptXblockV2({ transcriptHandlerUrl: handlerUrl }), + data, + ); + }, normalizeContent: ({ blockId, blockType, @@ -345,6 +383,13 @@ export const apiMethods = { urls.courseVideos({ studioEndpointUrl, learningContextId }), data, ), + getHandlerUrl: ({ + studioEndpointUrl, + blockId, + handlerName, + }) => get( + urls.handlerUrl({ studioEndpointUrl, blockId, handlerName }), + ), }; export default apiMethods; diff --git a/src/editors/data/services/cms/urls.test.ts b/src/editors/data/services/cms/urls.test.ts index 95a3968959..c1f9c1ecda 100644 --- a/src/editors/data/services/cms/urls.test.ts +++ b/src/editors/data/services/cms/urls.test.ts @@ -17,6 +17,9 @@ import { mediaTranscriptURL, videoFeatures, courseVideos, + handlerUrl, + transcriptXblockV2, + downloadVideoTranscriptURLV2, } from './urls'; describe('cms url methods', () => { @@ -32,6 +35,8 @@ describe('cms url methods', () => { const handout = '/aSSet@hANdoUt'; const videoId = '123-SOmeVidEOid-213'; const parameters = 'SomEParAMEterS'; + const handlerName = 'transcript'; + const transcriptHandlerUrl = handlerUrl({ studioEndpointUrl, blockId, handlerName }); describe('return to learning context urls', () => { const unitUrl = { @@ -189,4 +194,22 @@ describe('cms url methods', () => { .toEqual(`${studioEndpointUrl}${transcriptUrl}`); }); }); + describe('handlerUrl', () => { + it('returns url with studioEndpointUrl, blockId and handlerName', () => { + expect(handlerUrl({ studioEndpointUrl, blockId, handlerName })) + .toEqual(`${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/handler_url/transcript/`); + }); + }); + describe('transcriptXblockV2', () => { + it('returns url with transcriptHandlerUrl', () => { + expect(transcriptXblockV2({ transcriptHandlerUrl })) + .toEqual(`${transcriptHandlerUrl}translation`); + }); + }); + describe('downloadVideoTranscriptURLV2', () => { + it('returns url with transcriptHandlerUrl and language', () => { + expect(downloadVideoTranscriptURLV2({ transcriptHandlerUrl, language })) + .toEqual(`${transcriptHandlerUrl}translation?language_code=${language}`); + }); + }); }); diff --git a/src/editors/data/services/cms/urls.ts b/src/editors/data/services/cms/urls.ts index a137ffb9f8..6da1e22038 100644 --- a/src/editors/data/services/cms/urls.ts +++ b/src/editors/data/services/cms/urls.ts @@ -80,6 +80,14 @@ export const downloadVideoTranscriptURL = (({ studioEndpointUrl, blockId, langua `${videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}` )) satisfies UrlFunction; +export const transcriptXblockV2 = (({ transcriptHandlerUrl }) => ( + `${transcriptHandlerUrl}translation` +)) satisfies UrlFunction; + +export const downloadVideoTranscriptURLV2 = (({ transcriptHandlerUrl, language }) => ( + `${transcriptXblockV2({ transcriptHandlerUrl })}?language_code=${language}` +)) satisfies UrlFunction; + export const mediaTranscriptURL = (({ studioEndpointUrl, transcriptUrl }) => ( `${studioEndpointUrl}${transcriptUrl}` )) satisfies UrlFunction; @@ -111,3 +119,7 @@ export const videoFeatures = (({ studioEndpointUrl }) => ( export const courseVideos = (({ studioEndpointUrl, learningContextId }) => ( `${studioEndpointUrl}/videos/${learningContextId}` )) satisfies UrlFunction; + +export const handlerUrl = (({ studioEndpointUrl, blockId, handlerName }) => ( + `${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/handler_url/${handlerName}/` +)) satisfies UrlFunction;