Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.
1 change: 1 addition & 0 deletions runestone/activecode/js/activecode-i18n.en.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,6 @@ $.i18n().load({
msg_activecode_assertion_error_fix:
"Check the expression to the right of assert. The expression is False and you will need to determine why that is. You may want to simply print out the individual parts of the expression to understand why it is evaluating to False.",
msg_activecode_load_db: "Loading DB...",
msg_activecode_code_coach: "Code Coach",
},
});
97 changes: 40 additions & 57 deletions runestone/activecode/js/activecode.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import "codemirror/addon/hint/anyword-hint.js";
import "codemirror/addon/edit/matchbrackets.js";
import "./skulpt.min.js";
import "./skulpt-stdlib.js";
import PyflakesCoach from "./coach-python-pyflakes.js";
// Used by Skulpt.
import embed from "vega-embed";
// Adapt for use outside webpack -- see https://github.com/vega/vega-embed.
Expand Down Expand Up @@ -65,6 +66,7 @@ export class ActiveCode extends RunestoneBase {
this.python3 = true;
this.origElem = orig;
this.origText = this.origElem.textContent;
this.codeCoachList = []; //list of CodeCoaches that will be used to provide feedback
this.divid = opts.orig.id;
this.code = $(orig).text() || "\n\n\n\n\n";
this.language = $(orig).data("lang");
Expand Down Expand Up @@ -92,7 +94,7 @@ export class ActiveCode extends RunestoneBase {
}
this.output = null; // create pre for output
this.graphics = null; // create div for turtle graphics
this.codecoach = null;
this.codecoach = null; // div for Code Coaches
this.codelens = null;
this.controlDiv = null;
this.historyScrubber = null;
Expand Down Expand Up @@ -137,6 +139,12 @@ export class ActiveCode extends RunestoneBase {
this.caption = "ActiveCode";
}
this.addCaption("runestone");

//Setup CodeCoaches - add based on language
if (this.language == "python" || this.language == "python3") {
this.codeCoachList.push(new PyflakesCoach());
}

setTimeout(
function () {
this.editor.refresh();
Expand Down Expand Up @@ -274,6 +282,7 @@ export class ActiveCode extends RunestoneBase {
if (this.logResults) {
this.logCurrentAnswer();
}
this.runCoaches();
this.renderFeedback();
// The run is finished; re-enable the button.
this.runButton.disabled = false;
Expand Down Expand Up @@ -722,15 +731,11 @@ export class ActiveCode extends RunestoneBase {
}.bind(this)
);

//Anything that wants to add output to coachdiv can do so after the h3
// all those elements will be cleared with each run and coach display will be
// reset to none. Any component that adds content after a run should set display
// to block to ensure visibility
var coachDiv = document.createElement("div");
coachDiv.classList.add("alert", "alert-warning", "codecoach");
$(coachDiv).css("display", "none");
let coachHead = coachDiv.appendChild(document.createElement("h3"));
coachHead.textContent = "Code Coach";
coachHead.textContent = $.i18n("msg_activecode_code_coach");
this.outerDiv.appendChild(coachDiv);
this.codecoach = coachDiv;

Expand Down Expand Up @@ -1222,6 +1227,35 @@ Yet another is that there is an internal error. The internal error message is:
}
}

async runCoaches() {
//Run all available code coaches and update code coach div

//clear anything after header in codecoach div and hide it
$(this.codecoach).children().slice(1).remove();
$(this.codecoach).css("display", "none");

//get code, run coaches
let code = await this.buildProg(false);
let results = [];
for(let coach of this.codeCoachList) {
results.push(coach.check(code));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a method to add a coach? If I'm thinking about the future and adding a new coach I would want to have a method to do that so I don't have to modify this code...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No one should have to touch that code.

Coaches are added up in the constructor (line 143). I'm not sure where else they would be injected from - that was one of my questions. I don't have a good sense of how to make creating coaches more modular.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm, can't figure out how to quote in those lines without starting a new conversation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was imagining a method on the Activecode instance, but then I'm not sure what other object would call it, so having it in the constructor is fine for now.

}

//once all coaches are done, update div
Promise.allSettled(results).then((promises) => {
for(let p of promises) {
if(p.status === 'fulfilled' && p.value !== null && p.value.trim() !== "") {
let checkDiv = document.createElement("div");
checkDiv.classList.add("python_check_results");
let checkPre = checkDiv.appendChild(document.createElement("pre"));
checkPre.textContent = p.value;
this.codecoach.append(checkDiv);
$(this.codecoach).css("display", "block");
}
}
});
}

renderFeedback() {
// The python unit test code builds the table as it is running the tests
// In "normal" usage this is displayed immediately.
Expand Down Expand Up @@ -1271,51 +1305,6 @@ Yet another is that there is an internal error. The internal error message is:
}
}

async checkPythonSyntax() {
let code = this.editor.getValue();
fetch('/ns/coach/python_check', {
method: 'POST',
body: code
})
.then((response) => {
return response.json();
})
.then((data) => {
if(data.trim() !== '') {
//clean up returned text
let errorLines = data.split("\n");
let codeLines = code.split("\n");
let message = "";
for(let line of errorLines) {
if(line.indexOf(".py:") != -1) {
//old pyflakes returns "file:line:col error"
//new pyflakes returns "file:line:col: error"
//handle either
const cleaner = /[^.]*.py:(\d+):(\d+):? (.*)/i;
let lineParts = line.match(cleaner)
message += "Line " + lineParts[1] + ": " + lineParts[3] + "\n";
message += codeLines[lineParts[1] - 1] + "\n";
message += " ".repeat(lineParts[2] - 1) + "^\n";
} else {
message += line + "\n";
}
}
message = message.slice(0,-1); //remove trailing newline

//Render
let checkDiv = document.createElement("div");
checkDiv.classList.add("python_check_results");
let checkPre = checkDiv.appendChild(document.createElement("pre"));
checkPre.textContent = message;
this.codecoach.append(checkDiv);
$(this.codecoach).css("display", "block");
}
})
.catch(err => {
console.log("Error with ajax python check:", err);
});
}

/* runProg has several async elements to it.
* 1. Skulpt runs the python program asynchronously
* 2. The history is restored asynchronously
Expand All @@ -1342,9 +1331,6 @@ Yet another is that there is an internal error. The internal error message is:
this.saveCode = "True";
$(this.output).text("");

//clear anything after header in codecoach
$(this.codecoach).children().slice(1).remove();

while ($(`#${this.divid}_errinfo`).length > 0) {
$(`#${this.divid}_errinfo`).remove();
}
Expand Down Expand Up @@ -1386,9 +1372,6 @@ Yet another is that there is an internal error. The internal error message is:
queue: false,
});
}
if (this.language == "python" || this.language == "python3") {
this.checkPythonSyntax();
}
try {
await Sk.misceval.asyncToPromise(function () {
return Sk.importMainWithBody("<stdin>", false, prog, true);
Expand Down
55 changes: 55 additions & 0 deletions runestone/activecode/js/coach-python-pyflakes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
$.i18n().load({
en: {
msd_pyflakes_coach_line: "Line",
},
});

export default class PyflakesCoach {
async check(code) {
let promise = new Promise(function (resolve, reject) {
fetch('/ns/coach/python_check', {
method: 'POST',
body: code
})
.then((response) => {
return response.json();
})
.then((data) => {
if(data.trim() !== '') {
let message = "";
//clean up returned text
let errorLines = data.split("\n");
let codeLines = code.split("\n");
for(let line of errorLines) {
if(line.indexOf(".py:") != -1) {
//old pyflakes returns "file:line:col error"
//new pyflakes returns "file:line:col: error"
//handle either
const cleaner = /[^.]*.py:(\d+):(\d+):? (.*)/i;
let lineParts = line.match(cleaner); //[1]: line, [2]: col, [3]: error

//for now, filter messages about star imports
if(!lineParts[3].includes("defined from star imports")
&& !lineParts[3].includes("*' used; unable to detect undefined names"))
{
message += $.i18n("msd_pyflakes_coach_line") + lineParts[1] + ": " + lineParts[3] + "\n";
message += codeLines[lineParts[1] - 1] + "\n";
message += " ".repeat(lineParts[2] - 1) + "^\n";
}
} else {
message += line + "\n";
}
}
message = message.slice(0,-1); //remove trailing newline
resolve(message);
}
resolve(null);
})
.catch(err => {
reject("Error in Pyflakes Coach: " + err);
})
});
return promise;
}
}