Skip to content

Commit

Permalink
Merge pull request #1443 from mumuki/feature-submissions-store
Browse files Browse the repository at this point in the history
Feature submissions store
  • Loading branch information
flbulgarelli authored Aug 10, 2020
2 parents 944157a + 0a5b4f5 commit 0f75799
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 63 deletions.
123 changes: 66 additions & 57 deletions app/assets/javascripts/mumuki_laboratory/application/bridge.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,41 @@
/**
* @typedef {{status: string, test_results: [{status: string, title: string}]}} ClientResult
* @typedef {"errored"|"failed"|"passed_with_warnings"|"passed"|"pending"|"aborted"} SubmissionStatus
*/

/**
* @typedef {{solution: object, client_result?: ClientResult}} Submission
* @typedef {{
* status: SubmissionStatus,
* test_results: [{status: SubmissionStatus, title: string}]
* }} SubmissionClientResult
*/

var mumuki = mumuki || {};

(function (mumuki) {
var lastSubmission = {};

function Laboratory(exerciseId){
this.exerciseId = exerciseId;
}

function asString(json){
return JSON.stringify(json);
}

function sameAsLastSolution(newSolution){
return asString(lastSubmission.content) === asString(newSolution);
}

function lastSubmissionFinishedSuccessfully(){
return lastSubmission.result && lastSubmission.result.status !== 'aborted';
}

function sendNewSolution(submission){
var token = new mumuki.CsrfToken();
var request = token.newRequest({
type: 'POST',
url: window.location.origin + window.location.pathname + '/solutions' + window.location.search,
data: submission
});

return $.ajax(request).then(preRenderResult).done(function (result) {
lastSubmission = { content: {solution: submission.solution}, result: result };
});
}
/**
* @typedef {{
* status: SubmissionStatus,
* class_for_progress_list_item?: string,
* guide_finished_by_solution?: boolean
* }} SubmissionResult
*/

/**
* @typedef {object} Solution
*/

/**
* Pre-renders some html parts of submission UI
* */
function preRenderResult(result) {
result.class_for_progress_list_item = mumuki.renderers.progressListItemClassForStatus(result.status, true)
return result;
}
/**
* @typedef {{
* "solution[content]"?:string,
* solution?: Solution,
* client_result?: SubmissionClientResult
* }} Submission
*/

mumuki.load(function () {
lastSubmission = {};
});
/**
* @typedef {{submission?: Submission, result?: SubmissionResult}} SubmissionAndResult
*/

Laboratory.prototype = {
mumuki.bridge = (() => {

class Laboratory {
// ==========
// Public API
// ==========
Expand All @@ -65,9 +46,9 @@ var mumuki = mumuki || {};
*
* @param {object} content the content object
* */
runTests: function(content) {
runTests(content) {
return this._submitSolution({ solution: content });
},
}

// ===========
// Private API
Expand All @@ -77,18 +58,46 @@ var mumuki = mumuki || {};
* Sends a solution object
*
* @param {Submission} submission the submission object
* @returns {JQuery.Promise<SubmissionResult>}
*/
_submitSolution: function (submission) {
if(lastSubmissionFinishedSuccessfully() && sameAsLastSolution(submission)){
return $.Deferred().resolve(lastSubmission.result);
_submitSolution(submission) {
const lastSubmission = mumuki.SubmissionsStore.getCachedResultFor(mumuki.currentExerciseId, submission);
if (lastSubmission) {
return $.Deferred().resolve(lastSubmission);
} else {
return sendNewSolution(submission);
return this._sendNewSolution(submission).done((result) => {
mumuki.SubmissionsStore.setLastSubmission(mumuki.currentExerciseId, {submission, result});
});
}
}
};

mumuki.bridge = {
Laboratory: Laboratory
};
/**
* @param {Submission} submission the submission object
* @returns {JQuery.Promise<SubmissionResult>}
*/
_sendNewSolution(submission){
var token = new mumuki.CsrfToken();
var request = token.newRequest({
type: 'POST',
url: window.location.origin + window.location.pathname + '/solutions' + window.location.search,
data: submission
});
return $.ajax(request).then((result) => this._preRenderResult(result));
}

/**
* Pre-renders some html parts of submission UI, adding them to the given result
*
* @param {SubmissionResult} result
* @returns {SubmissionResult}
*/
_preRenderResult(result) {
result.class_for_progress_list_item = mumuki.renderers.progressListItemClassForStatus(result.status, true)
return result;
}
}

}(mumuki));
return {
Laboratory
};
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @type {number} */
mumuki.currentExerciseId = null;
(() => {
mumuki.load(() => {
// Set global currentExerciseId
const $muExerciseId = $('#mu-exercise-id');
if ($muExerciseId) {
mumuki.currentExerciseId = Number($muExerciseId.val());
} else {
mumuki.currentExerciseId = null;
}
})
})();
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// ==========================

/**
* @param {string} status
* @param {SubmissionStatus} status
* @returns {string}
*/
function iconForStatus(status) {
Expand All @@ -20,8 +20,7 @@
}

/**
*
* @param {string} status
* @param {SubmissionStatus} status
* @returns {string}
*/
function classForStatus(status) {
Expand All @@ -36,7 +35,7 @@


/**
* @param {string} status
* @param {SubmissionStatus} status
* @param {boolean} [active]
* @returns {string}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ var mumuki = mumuki || {};
*
* This method will use CustomEditor's sources if availble, or
* standard editor's content sources otherwise
*
* @returns {Submission}
*/
function getContent() {
let content = {};
Expand All @@ -97,6 +99,7 @@ var mumuki = mumuki || {};
content[it.name] = it.value;
});

// @ts-ignore
return content;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
mumuki.SubmissionsStore = (() => {
const SubmissionsStore = new class {
/**
* @param {number} exerciseId
* @returns {SubmissionStatus}
*/
getLastSubmissionStatus(exerciseId) {
const submission = this.getLastSubmission(exerciseId);
return submission ? submission.result.status : 'pending';
}

/**
* @param {number} exerciseId
* @returns {SubmissionAndResult}
*/
getLastSubmission(exerciseId) {
const submissionAndResult = window.localStorage.getItem(this._keyFor(exerciseId));
if (!submissionAndResult) return null;
return JSON.parse(submissionAndResult);
}

/**
* @param {number} exerciseId
* @param {SubmissionAndResult} submissionAndResult
*/
setLastSubmission(exerciseId, submissionAndResult) {
window.localStorage.setItem(this._keyFor(exerciseId), this._asString(submissionAndResult));
}

/**
* Retrieves the last cached, non-aborted result for the given submission
*
* @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)) {
return null;
}
return lastSubmission.result;
}

/**
* Extract the submission's solution content
*
* @param {Submission} submission
* @returns {string}
*/
submissionSolutionContent(submission) {
if (submission.solution) {
return submission.solution.content;
} else {
return submission['solution[content]'];
}
}

/**
* Compares two solutions to determine if they are equivalent
* from the point of view of the code evaluation
*
* @param {Submission} one
* @param {Submission} other
* @returns {boolean}
*/
submissionSolutionEquals(one, other) {
return this.submissionSolutionContent(one) === this.submissionSolutionContent(other);
}

// private API

_asString(object) {
return JSON.stringify(object);
}

_keyFor(exerciseId) {
return `/exercise/${exerciseId}/submission`;
}
};

return SubmissionsStore;
})();
1 change: 1 addition & 0 deletions app/views/exercises/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<%= render_exercise_input_layout(@exercise) %>

<%= hidden_field_tag default_content_tag_id(@exercise), @default_content %>
<%= hidden_field_tag "mu-exercise-id", @exercise.id %>

<div style="display: none" id="processing-template">
<div class="bs-callout bs-callout-info">
Expand Down
8 changes: 7 additions & 1 deletion app/views/layouts/_progress_bar.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<div class="progress-list-flex">
<% guide.exercises.each do |e| %>
<a <%= turbolinks_enable_for e %> href="<%= exercise_path(e)%>" aria-label="<%= e.navigable_name %>" title="<%= e.navigable_name %>" class="<%= class_for_progress_list_item(e, e == actual)%>">
<a
<%= turbolinks_enable_for e %>
href="<%= exercise_path(e)%>"
aria-label="<%= e.navigable_name %>"
title="<%= e.navigable_name %>"
data-mu-exercise-id="<%= e.id %>"
class="<%= class_for_progress_list_item(e, e == actual)%>">
</a>
<% end %>
</div>
2 changes: 1 addition & 1 deletion jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "none",
"target": "es6",
"checkJs": true,
"allowSyntheticDefaultImports": true
Expand Down

0 comments on commit 0f75799

Please sign in to comment.