Skip to content

Commit

Permalink
Merge pull request #495 from France-ioi/evaluate-correct-submissions
Browse files Browse the repository at this point in the history
Evaluate correct solutions
  • Loading branch information
SebastienTainon authored Jun 5, 2024
2 parents 3ba4d65 + e06edd9 commit c4e1a8f
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 53 deletions.
4 changes: 2 additions & 2 deletions frontend/common/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand Down
82 changes: 46 additions & 36 deletions frontend/stepper/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/stepper/platforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 3 additions & 1 deletion frontend/submission/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/task/blocks/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
13 changes: 9 additions & 4 deletions frontend/task/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions frontend/task/libs/quickalgo_library_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down
35 changes: 34 additions & 1 deletion frontend/task/platform/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ 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';
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<unknown, TaskAnswer>;
let getTaskState: () => Generator;
Expand Down Expand Up @@ -365,6 +366,31 @@ function* taskGetStateEventSaga ({payload: {success}}: ReturnType<typeof taskGet
yield* call(success, strDump);
}

function* taskGetResourcesImportCorrectSolutions(resources) {
let taskSettingsParsed = getTaskMetadata();
const task = yield* appSelect(state => 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<typeof taskGetResourcesPost>) {
const options = yield* appSelect(state => state.options);
let optionsToPreload = {};
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion frontend/task/task_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export interface TaskServer extends TaskNormalized {
export type Task = QuickalgoTask & Partial<TaskServer>;

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 {
Expand Down
6 changes: 3 additions & 3 deletions frontend/task/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 21 additions & 0 deletions frontend/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(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;
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,6 @@
"webpack-dev-middleware": "^6.1.1",
"webpack-hot-middleware": "^2.25.4",
"worker-loader": "^3.0.8"
}
},
"packageManager": "[email protected]+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

0 comments on commit c4e1a8f

Please sign in to comment.