Skip to content

Commit 0f75799

Browse files
authored
Merge pull request #1443 from mumuki/feature-submissions-store
Feature submissions store
2 parents 944157a + 0a5b4f5 commit 0f75799

File tree

8 files changed

+179
-63
lines changed

8 files changed

+179
-63
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,41 @@
11
/**
2-
* @typedef {{status: string, test_results: [{status: string, title: string}]}} ClientResult
2+
* @typedef {"errored"|"failed"|"passed_with_warnings"|"passed"|"pending"|"aborted"} SubmissionStatus
33
*/
44

55
/**
6-
* @typedef {{solution: object, client_result?: ClientResult}} Submission
6+
* @typedef {{
7+
* status: SubmissionStatus,
8+
* test_results: [{status: SubmissionStatus, title: string}]
9+
* }} SubmissionClientResult
710
*/
811

9-
var mumuki = mumuki || {};
10-
11-
(function (mumuki) {
12-
var lastSubmission = {};
13-
14-
function Laboratory(exerciseId){
15-
this.exerciseId = exerciseId;
16-
}
17-
18-
function asString(json){
19-
return JSON.stringify(json);
20-
}
21-
22-
function sameAsLastSolution(newSolution){
23-
return asString(lastSubmission.content) === asString(newSolution);
24-
}
25-
26-
function lastSubmissionFinishedSuccessfully(){
27-
return lastSubmission.result && lastSubmission.result.status !== 'aborted';
28-
}
29-
30-
function sendNewSolution(submission){
31-
var token = new mumuki.CsrfToken();
32-
var request = token.newRequest({
33-
type: 'POST',
34-
url: window.location.origin + window.location.pathname + '/solutions' + window.location.search,
35-
data: submission
36-
});
37-
38-
return $.ajax(request).then(preRenderResult).done(function (result) {
39-
lastSubmission = { content: {solution: submission.solution}, result: result };
40-
});
41-
}
12+
/**
13+
* @typedef {{
14+
* status: SubmissionStatus,
15+
* class_for_progress_list_item?: string,
16+
* guide_finished_by_solution?: boolean
17+
* }} SubmissionResult
18+
*/
4219

20+
/**
21+
* @typedef {object} Solution
22+
*/
4323

44-
/**
45-
* Pre-renders some html parts of submission UI
46-
* */
47-
function preRenderResult(result) {
48-
result.class_for_progress_list_item = mumuki.renderers.progressListItemClassForStatus(result.status, true)
49-
return result;
50-
}
24+
/**
25+
* @typedef {{
26+
* "solution[content]"?:string,
27+
* solution?: Solution,
28+
* client_result?: SubmissionClientResult
29+
* }} Submission
30+
*/
5131

52-
mumuki.load(function () {
53-
lastSubmission = {};
54-
});
32+
/**
33+
* @typedef {{submission?: Submission, result?: SubmissionResult}} SubmissionAndResult
34+
*/
5535

56-
Laboratory.prototype = {
36+
mumuki.bridge = (() => {
5737

38+
class Laboratory {
5839
// ==========
5940
// Public API
6041
// ==========
@@ -65,9 +46,9 @@ var mumuki = mumuki || {};
6546
*
6647
* @param {object} content the content object
6748
* */
68-
runTests: function(content) {
49+
runTests(content) {
6950
return this._submitSolution({ solution: content });
70-
},
51+
}
7152

7253
// ===========
7354
// Private API
@@ -77,18 +58,46 @@ var mumuki = mumuki || {};
7758
* Sends a solution object
7859
*
7960
* @param {Submission} submission the submission object
61+
* @returns {JQuery.Promise<SubmissionResult>}
8062
*/
81-
_submitSolution: function (submission) {
82-
if(lastSubmissionFinishedSuccessfully() && sameAsLastSolution(submission)){
83-
return $.Deferred().resolve(lastSubmission.result);
63+
_submitSolution(submission) {
64+
const lastSubmission = mumuki.SubmissionsStore.getCachedResultFor(mumuki.currentExerciseId, submission);
65+
if (lastSubmission) {
66+
return $.Deferred().resolve(lastSubmission);
8467
} else {
85-
return sendNewSolution(submission);
68+
return this._sendNewSolution(submission).done((result) => {
69+
mumuki.SubmissionsStore.setLastSubmission(mumuki.currentExerciseId, {submission, result});
70+
});
8671
}
8772
}
88-
};
8973

90-
mumuki.bridge = {
91-
Laboratory: Laboratory
92-
};
74+
/**
75+
* @param {Submission} submission the submission object
76+
* @returns {JQuery.Promise<SubmissionResult>}
77+
*/
78+
_sendNewSolution(submission){
79+
var token = new mumuki.CsrfToken();
80+
var request = token.newRequest({
81+
type: 'POST',
82+
url: window.location.origin + window.location.pathname + '/solutions' + window.location.search,
83+
data: submission
84+
});
85+
return $.ajax(request).then((result) => this._preRenderResult(result));
86+
}
87+
88+
/**
89+
* Pre-renders some html parts of submission UI, adding them to the given result
90+
*
91+
* @param {SubmissionResult} result
92+
* @returns {SubmissionResult}
93+
*/
94+
_preRenderResult(result) {
95+
result.class_for_progress_list_item = mumuki.renderers.progressListItemClassForStatus(result.status, true)
96+
return result;
97+
}
98+
}
9399

94-
}(mumuki));
100+
return {
101+
Laboratory
102+
};
103+
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/** @type {number} */
2+
mumuki.currentExerciseId = null;
3+
(() => {
4+
mumuki.load(() => {
5+
// Set global currentExerciseId
6+
const $muExerciseId = $('#mu-exercise-id');
7+
if ($muExerciseId) {
8+
mumuki.currentExerciseId = Number($muExerciseId.val());
9+
} else {
10+
mumuki.currentExerciseId = null;
11+
}
12+
})
13+
})();

app/assets/javascripts/mumuki_laboratory/application/results-renderer.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// ==========================
77

88
/**
9-
* @param {string} status
9+
* @param {SubmissionStatus} status
1010
* @returns {string}
1111
*/
1212
function iconForStatus(status) {
@@ -20,8 +20,7 @@
2020
}
2121

2222
/**
23-
*
24-
* @param {string} status
23+
* @param {SubmissionStatus} status
2524
* @returns {string}
2625
*/
2726
function classForStatus(status) {
@@ -36,7 +35,7 @@
3635

3736

3837
/**
39-
* @param {string} status
38+
* @param {SubmissionStatus} status
4039
* @param {boolean} [active]
4140
* @returns {string}
4241
*/

app/assets/javascripts/mumuki_laboratory/application/submission.js

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ var mumuki = mumuki || {};
8282
*
8383
* This method will use CustomEditor's sources if availble, or
8484
* standard editor's content sources otherwise
85+
*
86+
* @returns {Submission}
8587
*/
8688
function getContent() {
8789
let content = {};
@@ -97,6 +99,7 @@ var mumuki = mumuki || {};
9799
content[it.name] = it.value;
98100
});
99101

102+
// @ts-ignore
100103
return content;
101104
}
102105

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
mumuki.SubmissionsStore = (() => {
2+
const SubmissionsStore = new class {
3+
/**
4+
* @param {number} exerciseId
5+
* @returns {SubmissionStatus}
6+
*/
7+
getLastSubmissionStatus(exerciseId) {
8+
const submission = this.getLastSubmission(exerciseId);
9+
return submission ? submission.result.status : 'pending';
10+
}
11+
12+
/**
13+
* @param {number} exerciseId
14+
* @returns {SubmissionAndResult}
15+
*/
16+
getLastSubmission(exerciseId) {
17+
const submissionAndResult = window.localStorage.getItem(this._keyFor(exerciseId));
18+
if (!submissionAndResult) return null;
19+
return JSON.parse(submissionAndResult);
20+
}
21+
22+
/**
23+
* @param {number} exerciseId
24+
* @param {SubmissionAndResult} submissionAndResult
25+
*/
26+
setLastSubmission(exerciseId, submissionAndResult) {
27+
window.localStorage.setItem(this._keyFor(exerciseId), this._asString(submissionAndResult));
28+
}
29+
30+
/**
31+
* Retrieves the last cached, non-aborted result for the given submission
32+
*
33+
* @param {number} exerciseId
34+
* @param {Submission} submission
35+
* @returns {SubmissionResult} the cached result for this submission
36+
*/
37+
getCachedResultFor(exerciseId, submission) {
38+
const lastSubmission = this.getLastSubmission(exerciseId);
39+
if (!lastSubmission
40+
|| lastSubmission.result.status === 'aborted'
41+
|| !this.submissionSolutionEquals(lastSubmission.submission, submission)) {
42+
return null;
43+
}
44+
return lastSubmission.result;
45+
}
46+
47+
/**
48+
* Extract the submission's solution content
49+
*
50+
* @param {Submission} submission
51+
* @returns {string}
52+
*/
53+
submissionSolutionContent(submission) {
54+
if (submission.solution) {
55+
return submission.solution.content;
56+
} else {
57+
return submission['solution[content]'];
58+
}
59+
}
60+
61+
/**
62+
* Compares two solutions to determine if they are equivalent
63+
* from the point of view of the code evaluation
64+
*
65+
* @param {Submission} one
66+
* @param {Submission} other
67+
* @returns {boolean}
68+
*/
69+
submissionSolutionEquals(one, other) {
70+
return this.submissionSolutionContent(one) === this.submissionSolutionContent(other);
71+
}
72+
73+
// private API
74+
75+
_asString(object) {
76+
return JSON.stringify(object);
77+
}
78+
79+
_keyFor(exerciseId) {
80+
return `/exercise/${exerciseId}/submission`;
81+
}
82+
};
83+
84+
return SubmissionsStore;
85+
})();

app/views/exercises/show.html.erb

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<%= render_exercise_input_layout(@exercise) %>
4646

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

4950
<div style="display: none" id="processing-template">
5051
<div class="bs-callout bs-callout-info">
+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
<div class="progress-list-flex">
22
<% guide.exercises.each do |e| %>
3-
<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)%>">
3+
<a
4+
<%= turbolinks_enable_for e %>
5+
href="<%= exercise_path(e)%>"
6+
aria-label="<%= e.navigable_name %>"
7+
title="<%= e.navigable_name %>"
8+
data-mu-exercise-id="<%= e.id %>"
9+
class="<%= class_for_progress_list_item(e, e == actual)%>">
410
</a>
511
<% end %>
612
</div>

jsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"compilerOptions": {
3-
"module": "commonjs",
3+
"module": "none",
44
"target": "es6",
55
"checkJs": true,
66
"allowSyntheticDefaultImports": true

0 commit comments

Comments
 (0)