diff --git a/frontend/common/options.ts b/frontend/common/options.ts index de37837f..dab1bad5 100644 --- a/frontend/common/options.ts +++ b/frontend/common/options.ts @@ -231,8 +231,8 @@ export default function(bundle: Bundle) { if (false !== reloadTask) { yield* put({type: StepperActionTypes.StepperExit}); - if (!state.options.tabsEnabled) { - const activeBufferName = state.buffers.activeBufferName; + const activeBufferName = state.buffers.activeBufferName; + if (!state.options.tabsEnabled && null !== activeBufferName) { yield* put(bufferChangePlatform(activeBufferName, newPlatform)); } } diff --git a/frontend/stepper/js/index.ts b/frontend/stepper/js/index.ts index 550dbdce..8b283d71 100644 --- a/frontend/stepper/js/index.ts +++ b/frontend/stepper/js/index.ts @@ -20,10 +20,9 @@ let originalFireNow; let originalSetBackgroundPathVertical_; export function* loadBlocklyHelperSaga(context: QuickAlgoLibrary) { - let blocklyHelper; - - if (!context) { return; } - + if (!context) { + return; + } if (context && context.blocklyHelper && !context.blocklyHelper.fake) { context.blocklyHelper.unloadLevel(); } @@ -73,7 +72,32 @@ export function* loadBlocklyHelperSaga(context: QuickAlgoLibrary) { }; } - blocklyHelper = window.getBlocklyHelper(context.infos.maxInstructions, context); + context.blocklyHelper = createBlocklyHelper(context); + context.onChange = () => {}; + + // There is a setTimeout delay in Blockly lib between blockly program loading and Blockly firing events. + // We overload this function to catch the Blockly firing event instant so that we know when the program + // is successfully reloaded and that the events won't trigger an editor content update which would trigger + // a stepper.exit + if ('main' === state.environment) { + window.Blockly.Events.fireNow_ = () => { + originalFireNow(); + context.blocklyHelper.reloading = false; + }; + } + + const groupsCategory = !!(context && context.infos && context.infos.includeBlocks && context.infos.includeBlocks.groupByCategory); + if (groupsCategory && 'tralalere' === options.app) { + overrideBlocklyFlyoutForCategories(isMobile); + } else if (originalSetBackgroundPathVertical_) { + window.Blockly.Flyout.prototype.setBackgroundPathVertical_ = originalSetBackgroundPathVertical_; + } + + yield* put(taskIncreaseContextId()); +} + +export function createBlocklyHelper(context: QuickAlgoLibrary) { + const blocklyHelper = window.getBlocklyHelper(context.infos.maxInstructions, context); log.getLogger('blockly_runner').debug('[blockly.editor] load blockly helper', context, blocklyHelper); // Override this function to keep handling the display, and avoiding a call to un-highlight the current block // during loadPrograms at the start of the program execution @@ -101,36 +125,15 @@ export function* loadBlocklyHelperSaga(context: QuickAlgoLibrary) { blocklyHelper.startingBlock = false; } - context.blocklyHelper = blocklyHelper; - context.onChange = () => {}; - if (!originalFireNow) { originalFireNow = window.Blockly.Events.fireNow_; } - // There is a setTimeout delay in Blockly lib between blockly program loading and Blockly firing events. - // We overload this function to catch the Blockly firing event instant so that we know when the program - // is successfully reloaded and that the events won't trigger an editor content update which would trigger - // a stepper.exit - if ('main' === state.environment) { - window.Blockly.Events.fireNow_ = () => { - originalFireNow(); - blocklyHelper.reloading = false; - }; - } - log.getLogger('blockly_runner').debug('[blockly.editor] load context into blockly editor'); blocklyHelper.loadContext(context); blocklyHelper.setIncludeBlocks(context.infos.includeBlocks); - const groupsCategory = !!(context && context.infos && context.infos.includeBlocks && context.infos.includeBlocks.groupByCategory); - if (groupsCategory && 'tralalere' === options.app) { - overrideBlocklyFlyoutForCategories(isMobile); - } else if (originalSetBackgroundPathVertical_) { - window.Blockly.Flyout.prototype.setBackgroundPathVertical_ = originalSetBackgroundPathVertical_; - } - - yield* put(taskIncreaseContextId()); + return blocklyHelper; } export const overrideBlocklyFlyoutForCategories = (isMobile: boolean) => { @@ -192,7 +195,7 @@ export const checkBlocklyCode = function (answer: Document, context: QuickAlgoLi let blocks; try { // This method can fail if Blockly is not loaded in the DOM. In this case it's ok we don't make the check - blocks = getBlocksFromXml(blockly); + blocks = getBlocksFromXml(state, context, blockly); } catch (e) { console.error(e); return; @@ -268,7 +271,7 @@ const getBlockCount = function (block, context: QuickAlgoLibrary) { return 1; } -export const getBlocklyBlocksUsage = function (answer: Document, context: QuickAlgoLibrary) { +export const getBlocklyBlocksUsage = function (answer: Document, context: QuickAlgoLibrary, state: AppStore) { // We cannot evaluate blocks as long as the answer has not been loaded into Blockly // Thus we wait that context.blocklyHelper.programs is filled (by BlocklyEditor) const blockly = (answer as unknown as BlockDocument)?.content?.blockly; @@ -284,7 +287,7 @@ export const getBlocklyBlocksUsage = function (answer: Document, context: QuickA let blocks; try { // This method can fail if Blockly is not loaded in the DOM. In this case it's ok we don't make the check - blocks = getBlocksFromXml(blockly); + blocks = getBlocksFromXml(state, context, blockly); } catch (e) { console.error(e); return { @@ -320,13 +323,20 @@ export function blocklyCount(blocks: any[], context: QuickAlgoLibrary): number { return blocksUsed; } -const getBlocksFromXml = function (xmlText) { - const xml = window.Blockly.Xml.textToDom(xmlText) - const tmpOptions = new window.Blockly.Options({}); - const tmpWorkspace = new window.Blockly.Workspace(tmpOptions); - window.Blockly.Xml.domToWorkspace(xml, tmpWorkspace); +const getBlocksFromXml = function (state: AppStore, context: QuickAlgoLibrary, xmlText: string) { + const xml = window.Blockly.Xml.textToDom(xmlText); + + const blocklyHelper = createBlocklyHelper(context); + const language = state.options.language.split('-')[0]; + blocklyHelper.load(language, false, 1, {}); + + if (!window.Blockly.mainWorkspace) { + window.Blockly.mainWorkspace = blocklyHelper.workspace; + } + + window.Blockly.Xml.domToWorkspace(xml, blocklyHelper.workspace); - return tmpWorkspace.getAllBlocks(); + return blocklyHelper.workspace.getAllBlocks(); }; export const blocklyFindLimited = (blocks, limitedUses, context) => { diff --git a/frontend/stepper/platforms.ts b/frontend/stepper/platforms.ts index 2e0e5237..085945b0 100644 --- a/frontend/stepper/platforms.ts +++ b/frontend/stepper/platforms.ts @@ -24,7 +24,7 @@ export interface PlatformData { getSpecificBlocks?: (notionsBag: NotionsBag, includeBlocks?: QuickalgoTaskIncludeBlocks) => Block[], runner?: typeof AbstractRunner, checkCode?: (document: Document, context: QuickAlgoLibrary, state: AppStore, disabledValidations: string[]) => void, - getBlocksUsage?: (document: Document, context: QuickAlgoLibrary) => BlocksUsage, + getBlocksUsage?: (document: Document, context: QuickAlgoLibrary, state?: AppStore) => BlocksUsage, } const platformBundles = { diff --git a/frontend/submission/submission.ts b/frontend/submission/submission.ts index 8d0dc584..8d33ddf2 100644 --- a/frontend/submission/submission.ts +++ b/frontend/submission/submission.ts @@ -241,7 +241,9 @@ export default function (bundle: Bundle) { // Refresh test visualization const currentTestId = yield* appSelect(state => state.task.currentTestId); - yield* put(updateCurrentTestId({testId: currentTestId, record: false})); + if (null !== currentTestId) { + yield* put(updateCurrentTestId({testId: currentTestId, record: false})); + } }); yield* takeEvery(submissionChangeDisplayedError, function* ({payload}) { diff --git a/frontend/task/blocks/blocks.ts b/frontend/task/blocks/blocks.ts index c3690d7d..c07c2463 100644 --- a/frontend/task/blocks/blocks.ts +++ b/frontend/task/blocks/blocks.ts @@ -238,7 +238,7 @@ function* checkSourceSaga() { blocksUsage.error = e.toString(); } - const currentUsage = getBlocksUsage(answer); + const currentUsage = getBlocksUsage(answer, state); if (currentUsage) { const maxInstructions = context.infos.maxInstructions ? context.infos.maxInstructions : Infinity; diff --git a/frontend/task/index.ts b/frontend/task/index.ts index 45df1c20..cc835a54 100644 --- a/frontend/task/index.ts +++ b/frontend/task/index.ts @@ -109,7 +109,7 @@ import { bufferDissociateFromSubmission, bufferEdit, bufferEditPlain, - bufferResetDocument + bufferResetDocument, buffersInitialState } from '../buffers/buffers_slice'; import {getTaskHintsSelector} from './instructions/instructions'; import {selectActiveBufferPlatform, selectSourceBuffers} from '../buffers/buffer_selectors'; @@ -274,7 +274,7 @@ function* taskLoadSaga(app: App, action) { const currentTask = yield* appSelect(state => state.task.currentTask); if (!isServerTask(currentTask)) { - yield* call(subscribePlatformHelper); + yield* fork(subscribePlatformHelper); } if (currentTask) { @@ -796,6 +796,11 @@ export default function (bundle: Bundle) { bundle.addSaga(watchRecordingProgressSaga); + bundle.defineAction(TaskActionTypes.TaskRunExecution); + bundle.addReducer(TaskActionTypes.TaskRunExecution, (state: AppStore) => { + state.buffers = buffersInitialState; + }); + bundle.addSaga(function* (app: App) { log.getLogger('task').debug('INIT TASK SAGAS'); @@ -982,10 +987,10 @@ export default function (bundle: Bundle) { yield* takeEvery(platformAnswerLoaded, function*({payload: {answer}}) { log.getLogger('task').debug('Platform answer loaded', answer); const state = yield* appSelect(); + const currentBuffer = state.buffers.activeBufferName; if (state.options.tabsEnabled || !state.buffers.activeBufferName) { yield* call(createSourceBufferFromDocument, answer.document, answer.platform); - } else { - const currentBuffer = state.buffers.activeBufferName; + } else if (null !== currentBuffer) { if (state.buffers.buffers[currentBuffer].platform !== answer.platform) { yield* put(bufferChangePlatform(currentBuffer, answer.platform, answer.document)); } else { diff --git a/frontend/task/libs/quickalgo_library_factory.ts b/frontend/task/libs/quickalgo_library_factory.ts index a4366614..078356d5 100644 --- a/frontend/task/libs/quickalgo_library_factory.ts +++ b/frontend/task/libs/quickalgo_library_factory.ts @@ -31,7 +31,7 @@ import {selectActiveBufferPlatform} from '../../buffers/buffer_selectors'; import {selectAvailableExecutionModes} from '../../submission/submission_selectors'; import {submissionChangeExecutionMode} from '../../submission/submission_slice'; -export function* createQuickalgoLibrary() { +export function* createQuickalgoLibrary(platformAlreadyChanged: boolean = false) { let state = yield* appSelect(); let oldContext = quickAlgoLibraries.getContext(null, state.environment); log.getLogger('libraries').debug('Create a context', state.environment); @@ -124,9 +124,13 @@ export function* createQuickalgoLibrary() { availablePlatforms = availablePlatforms.filter(platform => -1 !== taskAvailablePlatforms.indexOf(platform)); } if (-1 === availablePlatforms.indexOf(state.options.platform) && availablePlatforms.length) { + if (platformAlreadyChanged) { + throw new Error("Platform has already changed once, cannot converge to a valid platform"); + } + yield* put({type: CommonActionTypes.PlatformChanged, payload: {platform: availablePlatforms[0], reloadTask: true}}); - return false; + return yield* call(createQuickalgoLibrary, true); } yield* put(taskSetAvailablePlatforms(availablePlatforms)); diff --git a/frontend/task/platform/platform.ts b/frontend/task/platform/platform.ts index 521f327d..f25de3a7 100644 --- a/frontend/task/platform/platform.ts +++ b/frontend/task/platform/platform.ts @@ -49,7 +49,7 @@ import {Codecast} from '../../app_types'; import {Document} from '../../buffers/buffer_types'; import {quickAlgoLibraries} from '../libs/quick_algo_libraries_model'; import {ActionTypes} from '../../common/actionTypes'; -import {TaskAnswer} from '../task_types'; +import {isServerTask, TaskAnswer} from '../task_types'; import {RECORDING_FORMAT_VERSION} from '../../version'; import {BlockBufferHandler, uncompressIntoDocument} from '../../buffers/document'; import {CodecastPlatform} from '../../stepper/codecast_platform'; @@ -57,6 +57,7 @@ import {hasBlockPlatform} from '../../stepper/platforms'; import {callPlatformValidate} from '../../submission/submission_actions'; import {loadOptionsFromQuery} from '../../common/options'; import {CodecastOptions} from '../../store'; +import {asyncGetFile} from '../../utils/api'; let getTaskAnswer: () => Generator; let getTaskState: () => Generator; @@ -365,6 +366,31 @@ function* taskGetStateEventSaga ({payload: {success}}: ReturnType state.task.currentTask); + if (isServerTask(task)) { + const taskSettings = yield* call(asyncGetFile, 'taskSettings.json'); + if (!taskSettings) { + return; + } + taskSettingsParsed = JSON.parse(taskSettings); + } + + if (!taskSettingsParsed?.correctSolutions) { + return; + } + + const {correctSolutions} = taskSettingsParsed; + resources.correct_solutions = []; + for (let correctSolution of correctSolutions) { + const {path} = correctSolution; + const correctedPath = path.replace(/\$TASK_PATH\/?/, ''); + const solution = yield* call(asyncGetFile, correctedPath); + resources.correct_solutions.push({type: 'solution', solution, id: path, ...correctSolution}); + } +} + function* taskGetResourcesPostSaga ({payload: {resources, callback}}: ReturnType) { const options = yield* appSelect(state => state.options); let optionsToPreload = {}; @@ -388,6 +414,13 @@ function* taskGetResourcesPostSaga ({payload: {resources, callback}}: ReturnType } }); + try { + yield* call(taskGetResourcesImportCorrectSolutions, resources); + } catch (e) { + // Avoid blocking errors here + console.error(e); + } + // For Castor platform, we need to add custom scripts that will be added to the assets during the generation of the task const castorScriptInject = `window.codecastPreload = JSON.parse('${JSON.stringify(optionsToPreload)}'); document.body.setAttribute('id', 'app'); diff --git a/frontend/task/task_types.ts b/frontend/task/task_types.ts index bcdbf068..00d66639 100644 --- a/frontend/task/task_types.ts +++ b/frontend/task/task_types.ts @@ -223,7 +223,7 @@ export interface TaskServer extends TaskNormalized { export type Task = QuickalgoTask & Partial; export function isServerTask(object: Task|null): boolean { - return (object && null !== object.id && undefined !== object.id) || window.PEMTaskMetaData; + return !!((object && null !== object.id && undefined !== object.id) || window.PEMTaskMetaData); } export function isServerTest(object: TaskTest): boolean { diff --git a/frontend/task/utils.ts b/frontend/task/utils.ts index bb28d336..02dddb61 100644 --- a/frontend/task/utils.ts +++ b/frontend/task/utils.ts @@ -152,16 +152,16 @@ export function checkCompilingCode(answer: TaskAnswer|null, state: AppStore, dis } } -export function getBlocksUsage(answer: TaskAnswer|null) { +export function getBlocksUsage(answer: TaskAnswer|null, state: AppStore) { const context = quickAlgoLibraries.getContext(null, 'main'); - if (!context) { + if (!context || !answer) { return null; } const {document, platform} = answer; if (platformsList[platform].getBlocksUsage) { - return platformsList[platform].getBlocksUsage(document, context); + return platformsList[platform].getBlocksUsage(document, context, state); } return null; diff --git a/frontend/utils/api.ts b/frontend/utils/api.ts index 1e293590..fe328fd1 100644 --- a/frontend/utils/api.ts +++ b/frontend/utils/api.ts @@ -51,3 +51,24 @@ export const asyncGetJson = function (path, withToken: boolean = false) { return promise; }; + +export const asyncGetFile = function(path) { + let req; + const promise = new Promise(function(resolve, reject) { + req = request.get(path); + + req.end(function(err, res) { + if (err || !res.ok) { + return reject({err, res}); + } + + resolve(res.text); + }); + }); + + promise[CANCEL] = () => { + req.abort(); + }; + + return promise; +}; diff --git a/package.json b/package.json index 99044f75..cadf5613 100644 --- a/package.json +++ b/package.json @@ -159,5 +159,6 @@ "webpack-dev-middleware": "^6.1.1", "webpack-hot-middleware": "^2.25.4", "worker-loader": "^3.0.8" - } + }, + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" }