diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..3f46d636b --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": "eslint:recommended", + "env": { + "browser": true, + "es6": true, + "mocha": true + }, + "globals": { + "$": "readonly", + "CodeMirror": "readonly", + "expect": "readonly", + "ga": "readonly", + "moment": "readonly", + "mumuki": "writable", + "muvment": "readonly", + "NProgress": "readonly" + }, + "rules": { + "no-unused-vars": [2, {"argsIgnorePattern": "^_"}] + } +} diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index a6a0ce9c4..000000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,277 +0,0 @@ ---- -parserOptions: - sourceType: module - ecmaFeatures: - jsx: true - -env: - amd: true - browser: true - es6: true - jquery: true - node: true - -# http://eslint.org/docs/rules/ -rules: - # Possible Errors - no-await-in-loop: off - no-cond-assign: error - no-console: off - no-constant-condition: error - no-control-regex: error - no-debugger: error - no-dupe-args: error - no-dupe-keys: error - no-duplicate-case: error - no-empty-character-class: error - no-empty: error - no-ex-assign: error - no-extra-boolean-cast: error - no-extra-parens: off - no-extra-semi: error - no-func-assign: error - no-inner-declarations: - - error - - functions - no-invalid-regexp: error - no-irregular-whitespace: error - no-negated-in-lhs: error - no-obj-calls: error - no-prototype-builtins: off - no-regex-spaces: error - no-sparse-arrays: error - no-template-curly-in-string: off - no-unexpected-multiline: error - no-unreachable: error - no-unsafe-finally: off - no-unsafe-negation: off - use-isnan: error - valid-jsdoc: off - valid-typeof: error - - # Best Practices - accessor-pairs: error - array-callback-return: off - block-scoped-var: off - class-methods-use-this: off - complexity: - - error - - 6 - consistent-return: off - curly: off - default-case: off - dot-location: off - dot-notation: off - eqeqeq: error - guard-for-in: error - no-alert: error - no-caller: error - no-case-declarations: error - no-div-regex: error - no-else-return: off - no-empty-function: off - no-empty-pattern: error - no-eq-null: error - no-eval: error - no-extend-native: error - no-extra-bind: error - no-extra-label: off - no-fallthrough: error - no-floating-decimal: off - no-global-assign: off - no-implicit-coercion: off - no-implied-eval: error - no-invalid-this: off - no-iterator: error - no-labels: - - error - - allowLoop: true - allowSwitch: true - no-lone-blocks: error - no-loop-func: error - no-magic-number: off - no-multi-spaces: off - no-multi-str: off - no-native-reassign: error - no-new-func: error - no-new-wrappers: error - no-new: error - no-octal-escape: error - no-octal: error - no-param-reassign: off - no-proto: error - no-redeclare: error - no-restricted-properties: off - no-return-assign: error - no-return-await: off - no-script-url: error - no-self-assign: off - no-self-compare: error - no-sequences: off - no-throw-literal: off - no-unmodified-loop-condition: off - no-unused-expressions: error - no-unused-labels: off - no-useless-call: error - no-useless-concat: error - no-useless-escape: off - no-useless-return: off - no-void: error - no-warning-comments: off - no-with: error - prefer-promise-reject-errors: off - radix: error - require-await: off - vars-on-top: off - wrap-iife: error - yoda: off - - # Strict - strict: off - - # Variables - init-declarations: off - no-catch-shadow: error - no-delete-var: error - no-label-var: error - no-restricted-globals: off - no-shadow-restricted-names: error - no-shadow: off - no-undef-init: error - no-undef: off - no-undefined: off - no-unused-vars: off - no-use-before-define: off - - # Node.js and CommonJS - callback-return: error - global-require: error - handle-callback-err: error - no-mixed-requires: off - no-new-require: off - no-path-concat: error - no-process-env: off - no-process-exit: error - no-restricted-modules: off - no-sync: off - - # Stylistic Issues - array-bracket-spacing: off - block-spacing: off - brace-style: off - camelcase: off - capitalized-comments: off - comma-dangle: - - error - - never - comma-spacing: off - comma-style: off - computed-property-spacing: off - consistent-this: off - eol-last: off - func-call-spacing: off - func-name-matching: off - func-names: off - func-style: off - id-length: off - id-match: off - indent: off - jsx-quotes: off - key-spacing: off - keyword-spacing: off - line-comment-position: off - linebreak-style: off - lines-around-comment: off - lines-around-directive: off - max-depth: off - max-len: off - max-nested-callbacks: off - max-params: off - max-statements-per-line: off - max-statements: - - error - - 30 - multiline-ternary: off - new-cap: off - new-parens: off - newline-after-var: off - newline-before-return: off - newline-per-chained-call: off - no-array-constructor: off - no-bitwise: off - no-continue: off - no-inline-comments: off - no-lonely-if: off - no-mixed-operators: off - no-mixed-spaces-and-tabs: off - no-multi-assign: off - no-multiple-empty-lines: off - no-negated-condition: off - no-nested-ternary: off - no-new-object: off - no-plusplus: off - no-restricted-syntax: off - no-spaced-func: off - no-tabs: off - no-ternary: off - no-trailing-spaces: off - no-underscore-dangle: off - no-unneeded-ternary: off - object-curly-newline: off - object-curly-spacing: off - object-property-newline: off - one-var-declaration-per-line: off - one-var: off - operator-assignment: off - operator-linebreak: off - padded-blocks: off - quote-props: off - quotes: off - require-jsdoc: off - semi-spacing: off - semi: off - sort-keys: off - sort-vars: off - space-before-blocks: off - space-before-function-paren: off - space-in-parens: off - space-infix-ops: off - space-unary-ops: off - spaced-comment: off - template-tag-spacing: off - unicode-bom: off - wrap-regex: off - - # ECMAScript 6 - arrow-body-style: off - arrow-parens: off - arrow-spacing: off - constructor-super: off - generator-star-spacing: off - no-class-assign: off - no-confusing-arrow: off - no-const-assign: off - no-dupe-class-members: off - no-duplicate-imports: off - no-new-symbol: off - no-restricted-imports: off - no-this-before-super: off - no-useless-computed-key: off - no-useless-constructor: off - no-useless-rename: off - no-var: off - object-shorthand: off - prefer-arrow-callback: off - prefer-const: off - prefer-destructuring: off - prefer-numeric-literals: off - prefer-rest-params: off - prefer-reflect: off - prefer-spread: off - prefer-template: off - require-yield: off - rest-spread-spacing: off - sort-imports: off - symbol-description: off - template-curly-spacing: off - yield-star-spacing: off diff --git a/Gemfile b/Gemfile index 767f20ec6..081e6286d 100644 --- a/Gemfile +++ b/Gemfile @@ -35,3 +35,5 @@ group :development, :test do gem 'teaspoon-jasmine' gem "selenium-webdriver" end + +gem 'mumuki-domain', github: 'mumuki/mumuki-domain', branch: 'master' diff --git a/README.md b/README.md index fb74a6822..fea9c70e8 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,12 @@ bundle exec rspec MOZ_HEADLESS=1 bundle exec rake teaspoon ``` +## Running `eslint` + +```bash +yarn run lint +``` + ## JavaScript API Docs In order to be customized by runners, Laboratory exposes the following selectors and methods @@ -208,6 +214,8 @@ which are granted to be safe and stable. * `SpeechBubbleRenderer` * `renderSpeechBubbleResultItem` * `mumuki.locale` +* `mumuki.currentExerciseId`: the `id` of the currently loaded exercise, if any +* `mumuki.incognitoUser`: whether the current user is an incognito user * `mumuki.MultipleScenarios` * `scenarios` * `currentScenarioIndex` @@ -369,14 +377,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/bridge.js b/app/assets/javascripts/mumuki_laboratory/application/bridge.js index 7e2f9efe3..fec14f16c 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/bridge.js +++ b/app/assets/javascripts/mumuki_laboratory/application/bridge.js @@ -61,12 +61,12 @@ mumuki.bridge = (() => { * @returns {JQuery.Promise} */ _submitSolution(submission) { - const lastSubmission = mumuki.SubmissionsStore.getCachedResultFor(mumuki.currentExerciseId, submission); + const lastSubmission = mumuki.SubmissionsStore.getSubmissionResultFor(mumuki.currentExerciseId, submission); if (lastSubmission) { return $.Deferred().resolve(lastSubmission); } else { return this._sendNewSolution(submission).done((result) => { - mumuki.SubmissionsStore.setLastSubmission(mumuki.currentExerciseId, {submission, result}); + mumuki.SubmissionsStore.setSubmissionResultFor(mumuki.currentExerciseId, {submission, result}); }); } } diff --git a/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js b/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js index 4e2e4ecff..a1e0944c1 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js +++ b/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js @@ -96,7 +96,7 @@ createEditor(customOptions) { return CodeMirror.fromTextArea(this.textarea, Object.assign({}, codeMirrorDefaults, customOptions)); } - }; + } mumuki.editor = mumuki.editor || {}; mumuki.editor.CodeMirrorBuilder = CodeMirrorBuilder; diff --git a/app/assets/javascripts/mumuki_laboratory/application/codemirror.js b/app/assets/javascripts/mumuki_laboratory/application/codemirror.js index b83df98e5..8820bb9d4 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +++ b/app/assets/javascripts/mumuki_laboratory/application/codemirror.js @@ -1,3 +1,8 @@ +mumuki.editor = mumuki.editor || {}; +mumuki.page = mumuki.page || {}; +mumuki.page.dynamicEditors = []; +mumuki.page.editors = []; + (() => { function createCodeMirrors() { return $(".editor").map(function (index, textarea) { @@ -66,21 +71,16 @@ }); } - mumuki.editor = mumuki.editor || {}; mumuki.editor.reset = resetEditor; mumuki.editor.toggleFullscreen = toggleFullscreen; mumuki.editor.formatContent = formatContent; mumuki.editor.indentWithSpaces = indentWithSpaces; mumuki.editor.syncContent = syncContent; - mumuki.page = mumuki.page || {}; - mumuki.page.dynamicEditors = []; - mumuki.page.editors = []; - mumuki.load(() => { mumuki.page.editors = createCodeMirrors(); - mumuki.submission.registerContentSyncer(mumuki.editor.syncContent); + mumuki.editors.registerContentSyncer(mumuki.editor.syncContent); updateCodeMirrorLanguage(); onSelectUpdateCodeMirror(); diff --git a/app/assets/javascripts/mumuki_laboratory/application/confirmation.js b/app/assets/javascripts/mumuki_laboratory/application/confirmation.js index c1eb38c02..1fa14a0dd 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/confirmation.js +++ b/app/assets/javascripts/mumuki_laboratory/application/confirmation.js @@ -1,5 +1,5 @@ mumuki.load(() => { - $('.btn-confirmation').on('click change', function (evt) { + $('.btn-confirmation').on('click change', function (_evt) { var token = new mumuki.CsrfToken(); $.ajax(token.newRequest({ diff --git a/app/assets/javascripts/mumuki_laboratory/application/console.js b/app/assets/javascripts/mumuki_laboratory/application/console.js index 38fb1f125..d940ca648 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/console.js +++ b/app/assets/javascripts/mumuki_laboratory/application/console.js @@ -74,7 +74,7 @@ self.preloadQuery(queryWithResults); }); } - }; + } class Query { constructor(line, cookie, console) { @@ -142,7 +142,7 @@ get _requestData() { return {content: this.content, query: this.line, cookie: this.cookie}; } - }; + } mumuki.load(() => { 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/messages.js b/app/assets/javascripts/mumuki_laboratory/application/messages.js index a911f0a2d..33eedd01a 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/messages.js +++ b/app/assets/javascripts/mumuki_laboratory/application/messages.js @@ -61,7 +61,7 @@ mumuki.load(() => { Chat.readMessages(readUrl); } - function error(xhr) { + function error(_xhr) { Chat.tokenRequest({ url: errorUrl, success: renderHTML, diff --git a/app/assets/javascripts/mumuki_laboratory/application/multiple-choice.js b/app/assets/javascripts/mumuki_laboratory/application/multiple-choice.js index 30b262e95..4622ad8ff 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/multiple-choice.js +++ b/app/assets/javascripts/mumuki_laboratory/application/multiple-choice.js @@ -1,5 +1,5 @@ mumuki.load(() => { - function dumpChoices(evt) { + function dumpChoices(_evt) { var indexes = $('.solution-choice:checked').map(function () { return $(this).data('index') }).get().join(':'); diff --git a/app/assets/javascripts/mumuki_laboratory/application/progress.js b/app/assets/javascripts/mumuki_laboratory/application/progress.js index ec5f01205..b161848cb 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/progress.js +++ b/app/assets/javascripts/mumuki_laboratory/application/progress.js @@ -1,5 +1,4 @@ -mumuki.updateProgressBarAndShowModal = (() => { - +mumuki.progress = (() => { /** * Updates the current exercise progress indicator * @@ -8,7 +7,25 @@ mumuki.updateProgressBarAndShowModal = (() => { function updateProgressBarAndShowModal(data) { $('.progress-list-item.active').attr('class', data.class_for_progress_list_item); if(data.guide_finished_by_solution) $('#guide-done').modal(); - }; + } - return updateProgressBarAndShowModal; + /** + * 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)) + }); + } + + return { + updateProgressBarAndShowModal, + updateWholeProgressBar + }; })(); + +/** @deprecated use {@code mumuki.progress.updateProgressBarAndShowModal} instead */ +mumuki.updateProgressBarAndShowModal = mumuki.progress.updateProgressBarAndShowModal; diff --git a/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js b/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js index bb1b3d7cb..b33bed389 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +++ b/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js @@ -1,4 +1,6 @@ -(() => { +mumuki.renderers = mumuki.renderers || {}; +mumuki.renderers.results = (() => { + // ========================== // View function for building @@ -31,7 +33,7 @@ case "passed": return "success"; case "pending": return "muted"; } - }; + } /** @@ -41,10 +43,18 @@ */ function progressListItemClassForStatus(status, active = false) { return `progress-list-item text-center ${classForStatus(status)} ${active ? 'active' : ''}`; - }; + } - mumuki.renderers = mumuki.renderers || {}; - mumuki.renderers.classForStatus = classForStatus; - mumuki.renderers.iconForStatus = iconForStatus; - mumuki.renderers.progressListItemClassForStatus = progressListItemClassForStatus; + return { + classForStatus, + iconForStatus, + progressListItemClassForStatus + } })(); + +/** @deprecated use {@code mumuki.renderers.results.classForStatus} instead */ +mumuki.renderers.classForStatus = mumuki.renderers.results.classForStatus; +/** @deprecated use {@code mumuki.renderers.results.iconForStatus} instead */ +mumuki.renderers.iconForStatus = mumuki.renderers.results.iconForStatus; +/** @deprecated use {@code mumuki.renderers.results.progressListItemClassForStatus} instead */ +mumuki.renderers.progressListItemClassForStatus = mumuki.renderers.results.progressListItemClassForStatus; diff --git a/app/assets/javascripts/mumuki_laboratory/application/speech-bubble-renderer.js b/app/assets/javascripts/mumuki_laboratory/application/speech-bubble-renderer.js index 9d8de8401..96e8e2de2 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/speech-bubble-renderer.js +++ b/app/assets/javascripts/mumuki_laboratory/application/speech-bubble-renderer.js @@ -1,4 +1,5 @@ -((mumuki)=> { +mumuki.renderers = mumuki.renderers || {}; +mumuki.renderers.speechBubble = (()=> { function renderSpeechBubbleResultItem(item) { return ` @@ -93,7 +94,13 @@ } } - mumuki.renderers = mumuki.renderers || {}; - mumuki.renderers.SpeechBubbleRenderer = SpeechBubbleRenderer; - mumuki.renderers.renderSpeechBubbleResultItem = renderSpeechBubbleResultItem; -})(mumuki) + return { + SpeechBubbleRenderer, + renderSpeechBubbleResultItem + } +})(); + +/** @deprecated use {@code mumuki.renderers.speechBubble.SpeechBubbleRenderer} instead */ +mumuki.renderers.SpeechBubbleRenderer = mumuki.renderers.speechBubble.SpeechBubbleRenderer; +/** @deprecated use {@code mumuki.renderers.speechBubble.renderSpeechBubbleResultItem} instead */ +mumuki.renderers.renderSpeechBubbleResultItem = mumuki.renderers.speechBubble.renderSpeechBubbleResultItem; diff --git a/app/assets/javascripts/mumuki_laboratory/application/submission.js b/app/assets/javascripts/mumuki_laboratory/application/submission.js index 72a1d7861..6c8db0160 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/submission.js +++ b/app/assets/javascripts/mumuki_laboratory/application/submission.js @@ -36,7 +36,7 @@ mumuki.submission = (() => { submitButton.updateAttemptsLeft(data); mumuki.pin.scroll(); } - }; + } class SubmitButton extends mumuki.Button { @@ -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/submissions-store.js b/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js index f1ed2d703..db46d9d11 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +++ b/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js @@ -1,47 +1,55 @@ mumuki.SubmissionsStore = (() => { const SubmissionsStore = new class { /** + * Returns the submission's result status for the last submission to + * the given exercise, or pending, if not present + * * @param {number} exerciseId * @returns {SubmissionStatus} */ getLastSubmissionStatus(exerciseId) { - const submission = this.getLastSubmission(exerciseId); + const submission = this.getLastSubmissionAndResult(exerciseId); return submission ? submission.result.status : 'pending'; } /** + * Returns the submission and result for the last submission to + * the given exercise + * * @param {number} exerciseId * @returns {SubmissionAndResult} */ - getLastSubmission(exerciseId) { + getLastSubmissionAndResult(exerciseId) { const submissionAndResult = window.localStorage.getItem(this._keyFor(exerciseId)); if (!submissionAndResult) return null; return JSON.parse(submissionAndResult); } /** + * Saves the result for the given exercise + * * @param {number} exerciseId * @param {SubmissionAndResult} submissionAndResult */ - setLastSubmission(exerciseId, submissionAndResult) { + setSubmissionResultFor(exerciseId, submissionAndResult) { window.localStorage.setItem(this._keyFor(exerciseId), this._asString(submissionAndResult)); } /** - * Retrieves the last cached, non-aborted result for the given submission + * Retrieves the last cached, non-aborted result for the given submission of the given exercise * * @param {number} exerciseId * @param {Submission} submission * @returns {SubmissionResult} the cached result for this submission */ - getCachedResultFor(exerciseId, submission) { - const lastSubmission = this.getLastSubmission(exerciseId); - if (!lastSubmission - || lastSubmission.result.status === 'aborted' - || !this.submissionSolutionEquals(lastSubmission.submission, submission)) { + getSubmissionResultFor(exerciseId, submission) { + const last = this.getLastSubmissionAndResult(exerciseId); + if (!last + || last.result.status === 'aborted' + || !this.submissionSolutionEquals(last.submission, submission)) { return null; } - return lastSubmission.result; + return last.result; } /** 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..e96f225da --- /dev/null +++ b/app/assets/javascripts/mumuki_laboratory/application/sync-mode.js @@ -0,0 +1,75 @@ +/** @type {boolean} */ +mumuki.incognitoUser; +mumuki.syncMode = (() => { + + /** + * Syncs progress and solutions + * from local storage + */ + class ClientSyncMode { + syncProgress() { + mumuki.progress.updateWholeProgressBar($anchor => this._getProgressListItemClass($anchor)); + } + + syncEditorContent() { + const lastSubmission = mumuki.SubmissionsStore.getLastSubmissionAndResult(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 nothing actually, since a server-side behaviour is the default one + * and no additional actions are needed. + */ + class ServerSyncMode { + syncProgress() { + // nothing + } + + syncEditorContent() { + // nothing + } + } + + + /** Selects the most appropriate sync mode */ + function _selectSyncMode() { + if (mumuki.incognitoUser) { + mumuki.syncMode._current = new ClientSyncMode(); + } else { + mumuki.syncMode._current = new ServerSyncMode(); + } + } + + return { + ServerSyncMode, + ClientSyncMode, + + _selectSyncMode, + + /** @type {ClientSyncMode|ServerSyncMode}*/ + _current: null + } +})(); + +mumuki.load(() => { + mumuki.syncMode._selectSyncMode(); + mumuki.syncMode._current.syncProgress(); + mumuki.syncMode._current.syncEditorContent(); +}) diff --git a/app/assets/javascripts/mumuki_laboratory/application/timer.js b/app/assets/javascripts/mumuki_laboratory/application/timer.js index 829b09888..55c90880d 100644 --- a/app/assets/javascripts/mumuki_laboratory/application/timer.js +++ b/app/assets/javascripts/mumuki_laboratory/application/timer.js @@ -15,6 +15,6 @@ mumuki.startTimer = (() => { $('#timer').text(duration.format("HH:mm:ss")); } }, intervalDuration); - }; + } return startTimer })(); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 54ff8877b..7b9fe9969 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base include Mumuki::Laboratory::Controllers::Notifications include Mumuki::Laboratory::Controllers::DynamicErrors include Mumuki::Laboratory::Controllers::EmbeddedMode + include Mumuki::Laboratory::Controllers::IncognitoMode before_action :set_current_organization! before_action :set_locale! diff --git a/app/helpers/locale_helper.rb b/app/helpers/globals_helper.rb similarity index 63% rename from app/helpers/locale_helper.rb rename to app/helpers/globals_helper.rb index ef19ce336..6d80e8ea8 100644 --- a/app/helpers/locale_helper.rb +++ b/app/helpers/globals_helper.rb @@ -1,10 +1,14 @@ -module LocaleHelper - def locale_tags +module GlobalsHelper + def globals_tags %Q{ }.html_safe end diff --git a/app/helpers/version_helper.rb b/app/helpers/version_helper.rb deleted file mode 100644 index a263d9cdc..000000000 --- a/app/helpers/version_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module VersionHelper - def version_tags - %Q{}.html_safe - end -end diff --git a/app/views/layouts/_main.html.erb b/app/views/layouts/_main.html.erb index 9dee5c10e..b59c34985 100644 --- a/app/views/layouts/_main.html.erb +++ b/app/views/layouts/_main.html.erb @@ -11,8 +11,7 @@ <%= open_graph_tags subject %> <%= assets_include_tags %> <%= csrf_meta_tags %> - <%= locale_tags %> - <%= version_tags %> + <%= globals_tags %> <%= login_form.header_html %> diff --git a/app/views/layouts/_progress.html.erb b/app/views/layouts/_progress.html.erb index 4e8a660f3..1decf2dd1 100644 --- a/app/views/layouts/_progress.html.erb +++ b/app/views/layouts/_progress.html.erb @@ -1 +1 @@ -<% content.completion_percentage_for(current_user) %> +<% content.completion_percentage_for(current_user) if current_user? %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index acd2ab5c6..29db042e7 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -17,7 +17,7 @@ <% end %>
- <% if current_user? %> + <% if current_logged_user? %>