From c913a1223f392a9ca1a7ffa3dd67855c0f2c57ef Mon Sep 17 00:00:00 2001 From: Franco Bulgarelli Date: Wed, 5 Aug 2020 22:58:51 -0300 Subject: [PATCH] Extracting sync-mode and editors modules --- README.md | 4 +- .../mumuki_laboratory/application/editors.js | 104 ++++++++++++++++++ .../mumuki_laboratory/application/progress.js | 74 ++----------- .../application/submission.js | 83 +------------- .../application/sync-mode.js | 75 +++++++++++++ spec/javascripts/editors-spec.js | 54 +++++++++ spec/javascripts/sync-mode-spec.js | 15 +++ 7 files changed, 262 insertions(+), 147 deletions(-) create mode 100644 app/assets/javascripts/mumuki_laboratory/application/editors.js create mode 100644 app/assets/javascripts/mumuki_laboratory/application/sync-mode.js create mode 100644 spec/javascripts/editors-spec.js create mode 100644 spec/javascripts/sync-mode-spec.js diff --git a/README.md b/README.md index 2299125e0..d6fb7a323 100644 --- a/README.md +++ b/README.md @@ -369,14 +369,14 @@ contents. There are two different approaches: ```javascript // simplest method - you can register just one -mumuki.submission.registerContentSyncer(() => { +mumuki.editors.registerContentSyncer(() => { // ... write here your custom component content... $('#mu-custom-editor-value').val(/* ... */); }); // alternate method // you can register many sources -mumuki.CustomEditor.addSource({ +mumuki.editors.addCustomSource({ getContent() { return { name: "solution[content]", value: /* ... */ } ; } diff --git a/app/assets/javascripts/mumuki_laboratory/application/editors.js b/app/assets/javascripts/mumuki_laboratory/application/editors.js new file mode 100644 index 000000000..4ac51a597 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/editors.js @@ -0,0 +1,104 @@ +/** + * This module allows to read and write the current editor's contents + * regardless if it is an standard editor or a custom editor + */ +mumuki.editors = { + /** + * @type {() => void} + */ + _contentSyncer: null, + + /** + * Updates the current editor's content + * + * @param {string} content + */ + setContent(content) { + const $customEditor = $('#mu-custom-editor-value'); + if ($customEditor.length) { + $customEditor.val(content); + } else { + mumuki.editor.setContent(content); + } + }, + + + /** + * @returns {EditorProperty[]} + */ + getContents() { + return mumuki.CustomEditor.hasSources ? + mumuki.CustomEditor.getContents() : + this.getStandardEditorContents(); + }, + + /** + * Syncs and returns the content objects of the standard editor form + * + * This content object may include keys like {@code content}, + * {@code content_extra} and {@code content_test} + * + * @returns {EditorProperty[]} + */ + getStandardEditorContents() { + this._syncContent(); + return $('.new_solution').serializeArray(); + }, + + /** + * Answers a submission object with a key for each of the current + * editor sources. + * + * This method will use CustomEditor's sources if availble, or + * standard editor's content sources otherwise + * + * @returns {Submission} + */ + getSubmission() { + let content = {}; + let contents = this.getContents(); + contents.forEach((it) => { + content[it.name] = it.value; + }); + return content; + }, + + /** + * Copies current solution from it native rendering components + * to the appropriate submission form elements. + * + * Both editors and runners with a custom editor that don't register a source should + * register its own syncer function in order to {@link syncContent} work properly. + * + * @see registerContentSyncer + * @see CustomEditor#addSource + */ + _syncContent() { + if (this._contentSyncer) { + this._contentSyncer(); + } + }, + + /** + * Sets a content syncer, that will be used by {@link _syncContent} + * in ordet to dump solution into the submission form fields. + * + * Each editor should have its own syncer registered - otherwise previous or none may be used + * causing unpredicatble behaviours - or cleared by passing {@code null}. + * + * As a particular case, runners with custom editors that don't add sources using {@link CustomEditor#addSource} + * should set the {@code #mu-custom-editor-value} value within its syncer. + * + * @param {() => void} [syncer] the syncer, or null, if no sync'ing is needed + */ + registerContentSyncer(syncer = null) { + this._contentSyncer = syncer; + }, + + /** + * @param {CustomEditorSource} source + */ + addCustomSource(source) { + mumuki.CustomEditor.addSource(source) + } +}; diff --git a/app/assets/javascripts/mumuki_laboratory/application/progress.js b/app/assets/javascripts/mumuki_laboratory/application/progress.js index b86cd3a5d..ee7d773c1 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/progress.js +++ b/app/assets/javascripts/mumuki_laboratory/application/progress.js @@ -1,48 +1,4 @@ mumuki.progress = (() => { - - class LocalSyncMode { - syncProgress() { - $('.progress-list-item').each((_, it) => { - this._updateProgressListItemClass($(it)) - }); - } - - syncExerciseEditorValue() { - const lastSubmission = mumuki.SubmissionsStore.getLastSubmission(mumuki.currentExerciseId); - if (lastSubmission) { - /** @todo reify editors module */ - /** @todo extract core module */ - const content = mumuki.SubmissionsStore.submissionSolutionContent(lastSubmission.submission); - const $customEditor = $('#mu-custom-editor-value'); - if ($customEditor.length) { - $customEditor.val(content); - } else { - mumuki.editor.setContent(content); - } - } - } - - /** - * @param {JQuery} $anchor - */ - _updateProgressListItemClass($anchor) { - const exerciseId = $anchor.data('mu-exercise-id'); - const status = mumuki.SubmissionsStore.getLastSubmissionStatus(exerciseId); - $anchor.attr('class', mumuki.renderers.progressListItemClassForStatus(status, exerciseId == mumuki.currentExerciseId)); - } - - } - - class ServerSyncMode { - syncProgress() { - // nothing - } - - syncExerciseEditorValue() { - // nothing - } - } - /** * Updates the current exercise progress indicator * @@ -53,29 +9,21 @@ mumuki.progress = (() => { if(data.guide_finished_by_solution) $('#guide-done').modal(); }; - - /** Selects the most appropriate sync mode */ - function _selectSyncMode() { - if (mumuki.incognitoMode) { - mumuki.progress._syncMode = new LocalSyncMode(); - } else { - mumuki.progress._syncMode = new ServerSyncMode(); - } + /** + * Update all links in the progress bar with the given function + * + * @param {(anchor: JQuery) => string} f + */ + function updateWholeProgressBar(f) { + $('.progress-list-item').each((_, it) => { + const $anchor = $(it); + $anchor.attr('class', f($anchor)) + }); } - mumuki.load(() => { - /** @todo move to another module & load sync mode lazily */ - mumuki.progress._selectSyncMode(); - mumuki.progress._syncMode.syncProgress(); - mumuki.progress._syncMode.syncExerciseEditorValue(); - }) - return { updateProgressBarAndShowModal, - - /** @type {LocalSyncMode|ServerSyncMode} */ - _syncMode: null, - _selectSyncMode + updateWholeProgressBar }; })(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/submission.js b/app/assets/javascripts/mumuki_laboratory/application/submission.js index 6240055a1..b60e34ab7 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/submission.js +++ b/app/assets/javascripts/mumuki_laboratory/application/submission.js @@ -56,81 +56,6 @@ mumuki.submission = (() => { } } - // ============ - // Content Sync - // ============ - - /** - * Syncs and returns the content objects of the standard editor form - * - * This content object may include keys like {@code content}, - * {@code content_extra} and {@code content_test} - * - * @returns {EditorProperty[]} - */ - function getStandardEditorContents() { - mumuki.submission._syncContent(); - return $('.new_solution').serializeArray(); - } - - /** - * Answers a content object with a key for each of the current - * editor sources. - * - * This method will use CustomEditor's sources if availble, or - * standard editor's content sources otherwise - * - * @returns {Submission} - */ - function getContent() { - let content = {}; - let contents; - - if (mumuki.CustomEditor.hasSources) { - contents = mumuki.CustomEditor.getContents(); - } else { - contents = mumuki.submission.getStandardEditorContents(); - } - - contents.forEach((it) => { - content[it.name] = it.value; - }); - - // @ts-ignore - return content; - } - - /** - * Copies current solution from it native rendering components - * to the appropriate submission form elements. - * - * Both editors and runners with a custom editor that don't register a source should - * register its own syncer function in order to {@link syncContent} work properly. - * - * @see registerContentSyncer - * @see CustomEditor#addSource - */ - function _syncContent() { - if (mumuki.submission._contentSyncer) { - mumuki.submission._contentSyncer(); - } - } - - /** - * Sets a content syncer, that will be used by {@link _syncContent} - * in ordet to dump solution into the submission form fields. - * - * Each editor should have its own syncer registered - otherwise previous or none may be used - * causing unpredicatble behaviours - or cleared by passing {@code null}. - * - * As a particular case, runners with custom editors that don't add sources using {@link CustomEditor#addSource} - * should set the {@code #mu-custom-editor-value} value within its syncer. - * - * @param {() => void} [syncer] the syncer, or null, if no sync'ing is needed - */ - function registerContentSyncer(syncer = null) { - mumuki.submission._contentSyncer = syncer; - } // ========== // Processing @@ -222,8 +147,7 @@ mumuki.submission = (() => { mumuki.submission._selectSolutionProcessor(submitButton, $submissionsResults); submitButton.start(() => { - var solution = mumuki.submission.getContent(); - mumuki.submission.processSolution(solution); + mumuki.submission.processSolution(mumuki.editors.getSubmission()); }); submitButton.checkAttemptsLeft(); @@ -249,11 +173,6 @@ mumuki.submission = (() => { _registerSolutionProcessor, _selectSolutionProcessor, - _syncContent, - registerContentSyncer, - getStandardEditorContents, - getContent, - animateTimeoutError, SubmitButton, }; diff --git a/app/assets/javascripts/mumuki_laboratory/application/sync-mode.js b/app/assets/javascripts/mumuki_laboratory/application/sync-mode.js new file mode 100644 index 000000000..999a86444 --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/sync-mode.js @@ -0,0 +1,75 @@ +mumuki.syncMode = (() => { + + /** + * Syncs progress and solutions + * from local storage + */ + class ClientSyncMode { + syncProgress() { + mumuki.progress.updateWholeProgressBar($anchor => this._getProgressListItemClass($anchor)); + } + + syncEditorContent() { + const lastSubmission = mumuki.SubmissionsStore.getLastSubmission(mumuki.currentExerciseId); + if (lastSubmission) { + /** @todo extract core module */ + const content = mumuki.SubmissionsStore.submissionSolutionContent(lastSubmission.submission); + mumuki.editors.setContent(content); + } + } + + /** + * @param {JQuery} $anchor + */ + _getProgressListItemClass($anchor) { + const exerciseId = $anchor.data('mu-exercise-id'); + const status = mumuki.SubmissionsStore.getLastSubmissionStatus(exerciseId); + return mumuki.renderers.progressListItemClassForStatus(status, exerciseId == mumuki.currentExerciseId); + } + + } + + /** + * Syncs progress and solutions + * from server. + * + * This class does actually nothing + * since that behaviour is actually the default one, son no additional actions + * are nedeed. + */ + class ServerSyncMode { + syncProgress() { + // nothing + } + + syncEditorContent() { + // nothing + } + } + + + /** Selects the most appropriate sync mode */ + function _selectSyncMode() { + if (mumuki.incognitoMode) { + mumuki.syncMode._current = new ClientSyncMode(); + } else { + mumuki.syncMode._current = new ServerSyncMode(); + } + } + + mumuki.load(() => { + mumuki.syncMode._selectSyncMode(); + mumuki.syncMode._current.syncProgress(); + mumuki.syncMode._current.syncEditorContent(); + }) + + return { + ServerSyncMode, + ClientSyncMode, + + _selectSyncMode, + + /** @type {ClientSyncMode|ServerSyncMode}*/ + _current: null + } +})(); diff --git a/spec/javascripts/editors-spec.js b/spec/javascripts/editors-spec.js new file mode 100644 index 000000000..745a8389e --- /dev/null +++ b/spec/javascripts/editors-spec.js @@ -0,0 +1,54 @@ +describe('editors', () => { + + beforeEach(() => { + mumuki.CustomEditor.clearSources() + }) + + it('has initially no sources', () => { + expect(mumuki.CustomEditor.hasSources).toBe(false) + }); + + it('can add a custom soure', () => { + mumuki.editors.addCustomSource({ + getContent() { + return { name: "solution[content]", value: 'the value' } ; + } + }); + + expect(mumuki.CustomEditor.hasSources).toBe(true); + expect(mumuki.CustomEditor.getContents()[0].value).toEqual('the value'); + expect(mumuki.CustomEditor.getContents()[0].name).toEqual('solution[content]'); + }); + + it('reads the custom sources if present, ignoring the form', () => { + $('body').html(` +
+
+ +
+
`) + + mumuki.editors.addCustomSource({ + getContent() { + return { name: "solution[content]", value: 'the custom solution' } ; + } + }); + + expect(mumuki.editors.getSubmission()).toEqual({"solution[content]":"the custom solution"}); + }); + + it('reads the form if no sources', () => { + $('body').html(` +
+
+ +
+
`) + expect(mumuki.editors.getSubmission()).toEqual({"solution[content]":"the solution"}); + }); + + it('produces empty submission if no form nor sources', () => { + $('body').html(``); + expect(mumuki.editors.getSubmission()).toEqual({}); + }); +}) diff --git a/spec/javascripts/sync-mode-spec.js b/spec/javascripts/sync-mode-spec.js new file mode 100644 index 000000000..8eca3ab9a --- /dev/null +++ b/spec/javascripts/sync-mode-spec.js @@ -0,0 +1,15 @@ +describe('sync mode', () => { + it('can choose server mode', () => { + mumuki.incognitoMode = false; + mumuki.syncMode._selectSyncMode(); + + expect(mumuki.syncMode._current instanceof mumuki.syncMode.ServerSyncMode).toBe(true); + }) + + it('can choose local mode', () => { + mumuki.incognitoMode = true; + mumuki.syncMode._selectSyncMode(); + + expect(mumuki.syncMode._current instanceof mumuki.syncMode.ClientSyncMode).toBe(true); + }) +})