-// TODO: YAML format for questions, parsed from text (nested questions)
-// Then yaml parsed to json --> array of indexed questions
-// Use open mode for question banks: add setting "nbQuestions" to show nbQuestions
-// at random among active questions
+/*Draft format (compiled to json)
+ * TODO: separate questions and answers in Evaluation object
-window.onload = function() {
+<some html question (or/+ exercise intro)>
- V = new Vue({
- el: '#course',
- data: {
- display: "assessments", //or "students", or "grades" (admin mode)
- course: course,
- mode: "view", //or "edit" (some assessment)
- // assessment data:
- monitorPwd: "",
- newAssessment: { name: "" },
- assessmentArray: assessmentArray,
- assessmentIndex: 0, //current edited assessment index
- assessment: { }, //copy of assessment at editing index in array
- assessmentText: "", //questions in an assessment, in text format
- // grades data:
- settings: {
- totalPoints: 20,
- halfPoints: false,
- zeroSum: false,
+ <some html subQuestion>
+ * some answer [trigger input/index in answers]
+
+ <another subquestion>
+
+ <sub-subQuestion>
+ + choix1
+ - choix 2
+ + choix 3
+ - choix4
+
+ <another sub sub> with params interpolation £ (tout simplement)
+ / params: javascript parameter generator (another function, body only)
+ * answer 2 (which can
+ be on
+ several lines)
+
+<Some second question>
+* With answer
+
+ dans le cas de parametetrized, answer est une fonction javascript !! qui prend en arg le(s) param(s)
+ coté serveur on stock parameterized question + body func
+ une fois côté client, extra work first to 1) execute each func 2) replace (and store!!!) all params
+https://stackoverflow.com/questions/7650071/is-there-a-way-to-create-a-function-from-a-string-with-javascript
+
+ */
+
+new Vue({
+ el: '#course',
+ data: {
+ display: "evaluations", //or "students" (in admin mode)
+ course: course,
+ monitorPwd: "",
+ newEvaluation: { name: "" },
+ evaluationArray: evaluationArray,
+ mode: "view", //or "edit" (some evaluation)
+ evaluationIndex: 0, //current edited evaluation index
+ evaluation: { }, //copy of evaluation at editing index in array
+ questionsText: "", //questions in an evaluation, in text format
+ },
+ mounted: function() {
+
+
+
+ $('.modal').each( (i,elem) => {
+ if (elem.id != "evaluationEdit")
+ $(elem).modal();
+ });
+ $('ul.tabs').tabs(); //--> migrate to grade.js
+
+
+
+ $('#evaluationEdit').modal({
+ complete: () => {
+ this.parseEvaluation();
+ Vue.nextTick(statementsLibsRefresh);
},
- group: 1, //for detailed grades tables
- grades: { }, //computed
+ });
+ },
+ methods: {
+ // GENERAL:
+ toggleDisplay: function(area) {
+ if (this.display == area)
+ this.display = "";
+ else
+ this.display = area;
},
- mounted: function() {
- $('.modal').each( (i,elem) => {
- if (elem.id != "assessmentEdit")
- $(elem).modal();
- });
- $('ul.tabs').tabs();
- $('#assessmentEdit').modal({
- complete: () => {
- this.parseAssessment();
- Vue.nextTick( () => {
- $("#questionList").find("code[class^=language-]").each( (i,elem) => {
- Prism.highlightElement(elem);
- });
- MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
- });
- },
- });
+ studentList: function(group) {
+ return this.course.students
+ .filter( s => { return group==0 || s.group == group; })
+ .map( s => { return Object.assign({}, s); }) //not altering initial array
+ .sort( (a,b) => { return a.name.localeCompare(b.name); })
},
- methods: {
- // GENERAL:
- toggleDisplay: function(area) {
- if (this.display == area)
- this.display = "";
- else
- this.display = area;
- },
- studentList: function(group) {
- return this.course.students
- .filter( s => { return group==0 || s.group == group; })
- .map( s => { return Object.assign({}, s); }) //not altering initial array
- .sort( (a,b) => {
- let res = a.name.localeCompare(b.name);
- if (res == 0)
- res += a.forename.localeCompare(b.forename);
- return res;
+ // STUDENTS:
+ uploadTrigger: function() {
+ $("#upload").click();
+ },
+ upload: function(e) {
+ let file = (e.target.files || e.dataTransfer.files)[0];
+ Papa.parse(file, {
+ header: true,
+ skipEmptyLines: true,
+ complete: (results,file) => {
+ let students = [ ];
+ // Post-process: add group/number if missing
+ let number = 1;
+ results.data.forEach( d => {
+ if (!d.group)
+ d.group = 1;
+ if (!d.number)
+ d.number = number++;
+ if (typeof d.number !== "string")
+ d.number = d.number.toString();
+ students.push(d);
});
- },
- // STUDENTS:
- uploadTrigger: function() {
- $("#upload").click();
- },
- upload: function(e) {
- let file = (e.target.files || e.dataTransfer.files)[0];
- Papa.parse(file, {
- header: true,
- skipEmptyLines: true,
- complete: (results,file) => {
- let students = [ ];
- // Post-process: add group/number if missing
- let number = 1;
- results.data.forEach( d => {
- if (!d.group)
- d.group = 1;
- if (!d.number)
- d.number = number++;
- if (typeof d.number !== "string")
- d.number = d.number.toString();
- students.push(d);
- });
- $.ajax("/import/students", {
- method: "POST",
- data: {
- cid: this.course._id,
- students: JSON.stringify(students),
- },
- dataType: "json",
- success: res => {
- if (!res.errmsg)
- this.course.students = students;
- else
- alert(res.errmsg);
- },
- });
- },
- });
- },
- // ASSESSMENT:
- addAssessment: function() {
- if (!admin)
- return;
- // modal, fill code and description
- let error = Validator.checkObject(this.newAssessment, "Assessment");
- if (!!error)
- return alert(error);
- else
- $('#newAssessment').modal('close');
- $.ajax("/add/assessment",
- {
- method: "GET",
+ $.ajax("/courses/student-list", {
+ method: "PUT",
data: {
- name: this.newAssessment.name,
- cid: course._id,
+ cid: this.course._id,
+ students: JSON.stringify(students),
},
dataType: "json",
success: res => {
if (!res.errmsg)
- {
- this.newAssessment["name"] = "";
- this.assessmentArray.push(res);
- }
+ this.course.students = students;
else
alert(res.errmsg);
},
- }
- );
- },
- materialOpenModal: function(id) {
- $("#" + id).modal("open");
- Materialize.updateTextFields(); //textareas, time field...
- },
- updateAssessment: function() {
- $.ajax("/update/assessment", {
+ });
+ },
+ });
+ },
+ // evaluation:
+ addEvaluation: function() {
+ if (!admin)
+ return;
+ // modal, fill code and description
+ let error = Validator.checkObject(this.newEvaluation, "Evaluation");
+ if (!!error)
+ return alert(error);
+ else
+ $('#newEvaluation').modal('close');
+ $.ajax("/evaluations",
+ {
method: "POST",
- data: {assessment: JSON.stringify(this.assessment)},
+ data: {
+ name: this.newEvaluation.name,
+ cid: course._id,
+ },
dataType: "json",
success: res => {
if (!res.errmsg)
{
- this.assessmentArray[this.assessmentIndex] = this.assessment;
- this.mode = "view";
+ this.newEvaluation["name"] = "";
+ this.evaluationArray.push(res);
}
else
alert(res.errmsg);
},
- });
- },
- deleteAssessment: function(assessment) {
- if (!admin)
- return;
- if (confirm("Delete assessment '" + assessment.name + "' ?"))
- {
- $.ajax("/remove/assessment",
- {
- method: "GET",
- data: { qid: this.assessment._id },
- dataType: "json",
- success: res => {
- if (!res.errmsg)
- this.assessmentArray.splice( this.assessmentArray.findIndex( item => {
- return item._id == assessment._id;
- }), 1 );
- else
- alert(res.errmsg);
- },
- }
- );
}
- },
- toggleState: function(questionIndex) {
- // add or remove from activeSet of current assessment
- let activeIndex = this.assessment.activeSet.findIndex( item => { return item == questionIndex; });
- if (activeIndex >= 0)
- this.assessment.activeSet.splice(activeIndex, 1);
- else
- this.assessment.activeSet.push(questionIndex);
- },
- setAssessmentText: function() {
- let txt = "";
- this.assessment.questions.forEach( q => {
- txt += q.wording + "\n";
- q.options.forEach( (o,i) => {
- let symbol = q.answer.includes(i) ? "+" : "-";
- txt += symbol + " " + o + "\n";
- });
- txt += "\n"; //separate questions by new line
- });
- this.assessmentText = txt;
- },
- parseAssessment: function() {
- let questions = [ ];
- let lines = this.assessmentText.split("\n").map( L => { return L.trim(); })
- lines.push(""); //easier parsing
- let emptyQuestion = () => {
- return {
- wording: "",
- options: [ ],
- answer: [ ],
- active: true, //default
- };
- };
- let q = emptyQuestion();
- lines.forEach( L => {
- if (L.length > 0)
+ );
+ },
+ materialOpenModal: function(id) {
+ $("#" + id).modal("open");
+ Materialize.updateTextFields(); //textareas, time field...
+ },
+ updateEvaluation: function() {
+ $.ajax("/evaluations", {
+ method: "PUT",
+ data: {evaluation: JSON.stringify(this.evaluation)},
+ dataType: "json",
+ success: res => {
+ if (!res.errmsg)
{
- if (['+','-'].includes(L.charAt(0)))
- {
- if (L.charAt(0) == '+')
- q.answer.push(q.options.length);
- q.options.push(L.slice(1).trim());
- }
- else if (L.charAt(0) == '*')
- {
- // TODO: read current + next lines into q.answer (HTML, 1-elem array)
- }
- else
- q.wording += L + " "; //space required at line breaks, generally
+ this.evaluationArray[this.evaluationIndex] = this.evaluation;
+ this.mode = "view";
}
else
+ alert(res.errmsg);
+ },
+ });
+ },
+ deleteEvaluation: function(evaluation) {
+ if (!admin)
+ return;
+ if (confirm("Delete evaluation '" + evaluation.name + "' ?"))
+ {
+ $.ajax("/evaluations",
{
- // Flush current question (if any)
- if (q.wording.length > 0)
- {
- questions.push(q);
- q = emptyQuestion();
- }
- }
- });
- this.assessment.questions = questions;
- },
- actionAssessment: function(index) {
- if (admin)
- {
- // Edit screen
- this.assessmentIndex = index;
- this.assessment = $.extend(true, {}, this.assessmentArray[index]);
- this.setAssessmentText();
- this.mode = "edit";
- Vue.nextTick( () => {
- $("#questionList").find("code[class^=language-]").each( (i,elem) => {
- Prism.highlightElement(elem);
- });
- MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
- });
- }
- else //external user: show assessment
- this.redirect(this.assessmentArray[index].name);
- },
- redirect: function(assessmentName) {
- document.location.href = "/" + initials + "/" + course.code + "/" + assessmentName;
- },
- setPassword: function() {
- let hashPwd = Sha1.Compute(this.monitorPwd);
- let error = Validator.checkObject({password:hashPwd}, "Course");
- if (error.length > 0)
- return alert(error);
- $.ajax("/set/password",
- {
- method: "GET",
- data: {
- cid: this.course._id,
- pwd: hashPwd,
- },
+ method: "DELETE",
+ data: { qid: this.evaluation._id },
dataType: "json",
success: res => {
if (!res.errmsg)
- alert("Password saved!");
+ this.evaluationArray.splice( this.evaluationArray.findIndex( item => {
+ return item._id == evaluation._id;
+ }), 1 );
else
alert(res.errmsg);
},
}
);
- },
- // NOTE: artifact required for Vue v-model to behave well
- checkBoxFixedId: function(i) {
- return "questionFixed" + i;
- },
- checkBoxActiveId: function(i) {
- return "questionActive" + i;
- },
- // GRADES:
- gradeSettings: function() {
- $("#gradeSettings").modal("open");
- Materialize.updateTextFields(); //total points field in grade settings overlap
- },
- download: function() {
- // Download (all) grades as a CSV file
- let data = [ ];
- this.studentList(0).forEach( s => {
- let finalGrade = 0.;
- let gradesCount = 0;
- if (!!this.grades[s.number])
+ }
+ },
+ toggleState: function(questionIndex) {
+ // add or remove from activeSet of current evaluation
+ let activeIndex = this.evaluation.activeSet.findIndex( item => { return item == questionIndex; });
+ if (activeIndex >= 0)
+ this.evaluation.activeSet.splice(activeIndex, 1);
+ else
+ this.evaluation.activeSet.push(questionIndex);
+ },
+ setEvaluationText: function() {
+ let txt = "";
+ this.evaluation.questions.forEach( q => {
+ txt += q.wording; //already ended by \n
+ q.options.forEach( (o,i) => {
+ let symbol = q.answer.includes(i) ? "+" : "-";
+ txt += symbol + " " + o + "\n";
+ });
+ txt += "\n"; //separate questions by new line
+ });
+ this.questionsText = txt;
+ },
+ parseEvaluation: function() {
+ let questions = [ ];
+ let lines = this.questionsText.split("\n").map( L => { return L.trim(); })
+ lines.push(""); //easier parsing
+ let emptyQuestion = () => {
+ return {
+ wording: "",
+ options: [ ],
+ answer: [ ],
+ active: true, //default
+ };
+ };
+ let q = emptyQuestion();
+ lines.forEach( L => {
+ if (L.length > 0)
+ {
+ if (['+','-'].includes(L.charAt(0)))
{
- Object.keys(this.grades[s.number]).forEach( assessmentName => {
- s[assessmentName] = this.grades[s.number][assessmentName];
- if (_.isNumeric(s[assessmentName]) && !isNaN(s[assessmentName]))
- {
- finalGrade += s[assessmentName];
- gradesCount++;
- }
- if (gradesCount >= 1)
- finalGrade /= gradesCount;
- s["final"] = finalGrade; //TODO: forbid "final" as assessment name
- });
+ if (L.charAt(0) == '+')
+ q.answer.push(q.options.length);
+ q.options.push(L.slice(1).trim());
}
- data.push(s); //number,forename,name,group,assessName1...assessNameN,final
- });
- let csv = Papa.unparse(data, {
- quotes: true,
- header: true,
- });
- let downloadAnchor = $("#download");
- downloadAnchor.attr("download", this.course.code + "_results.csv");
- downloadAnchor.attr("href", "data:text/plain;charset=utf-8," + encodeURIComponent(csv));
- this.$refs.download.click()
- //downloadAnchor.click(); //fails
- },
- showDetails: function(group) {
- this.group = group;
- $("#detailedGrades").modal("open");
- },
- groupList: function() {
- let maxGrp = 1;
- this.course.students.forEach( s => {
- if (s.group > maxGrp)
- maxGrp = s.group;
- });
- return _.range(1,maxGrp+1);
- },
- grade: function(assessmentIndex, studentNumber) {
- if (!this.grades[assessmentIndex] || !this.grades[assessmentIndex][studentNumber])
- return ""; //no grade yet
- return this.grades[assessmentIndex][studentNumber];
- },
- groupId: function(group, prefix) {
- return (prefix || "") + "group" + group;
- },
- togglePresence: function(number, index) {
- // UNIMPLEMENTED
- // TODO: if no grade (thus automatic 0), toggle "exempt" state on student for current exam
- // --> automatic update of grades view (just a few number to change)
- },
- computeGrades: function() {
- // UNIMPLEMENTED
- // TODO: compute all grades using settings (points, coefficients, bonus/malus...).
- // If some questions with free answers (open), display answers and ask teacher action.
- // TODO: need a setting for that too (by student, by exercice, by question)
- },
+ else if (L.charAt(0) == '*')
+ {
+ // TODO: read current + next lines into q.answer (HTML, 1-elem array)
+ }
+ else
+ q.wording += L + "\n";
+ }
+ else
+ {
+ // Flush current question (if any)
+ if (q.wording.length > 0)
+ {
+ questions.push(q);
+ q = emptyQuestion();
+ }
+ }
+ });
+ this.evaluation.questions = questions;
},
- });
-
-};
+ actionEvaluation: function(index) {
+ if (admin)
+ {
+ // Edit screen
+ this.evaluationIndex = index;
+ this.evaluation = $.extend(true, {}, this.evaluationArray[index]);
+ this.setEvaluationText();
+ this.mode = "edit";
+ Vue.nextTick(statementsLibsRefresh);
+ }
+ else //external user: show evaluation
+ this.redirect(this.evaluationArray[index].name);
+ },
+ redirect: function(evaluationName) {
+ document.location.href = "/" + initials + "/" + course.code + "/" + evaluationName;
+ },
+ setPassword: function() {
+ let hashPwd = Sha1.Compute(this.monitorPwd);
+ let error = Validator.checkObject({password:hashPwd}, "Course");
+ if (error.length > 0)
+ return alert(error);
+ $.ajax("/courses/password",
+ {
+ method: "PUT",
+ data: {
+ cid: this.course._id,
+ pwd: hashPwd,
+ },
+ dataType: "json",
+ success: res => {
+ if (!res.errmsg)
+ alert("Password saved!");
+ else
+ alert(res.errmsg);
+ },
+ }
+ );
+ },
+ // NOTE: artifact required for Vue v-model to behave well
+ checkboxFixedId: function(i) {
+ return "questionFixed" + i;
+ },
+ checkboxActiveId: function(i) {
+ return "questionActive" + i;
+ },
+ },
+});