// Use open mode for question banks: add setting "nbQuestions" to show nbQuestions
// at random among active questions
-window.onload = function() {
-
- 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,
- },
- group: 1, //for detailed grades tables
- grades: { }, //computed
+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,
},
- 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"]);
+ group: 1, //for detailed grades tables
+ grades: { }, //computed
+ },
+ 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);
});
- },
- });
- },
- 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) => { return a.name.localeCompare(b.name); })
- },
- // 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);
- },
- });
- },
+ MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
});
},
- // 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",
+ });
+ },
+ 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) => { return a.name.localeCompare(b.name); })
+ },
+ // 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: {
- 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", {
- method: "POST",
- data: {assessment: JSON.stringify(this.assessment)},
+ });
+ },
+ });
+ },
+ // 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",
+ data: {
+ name: this.newAssessment.name,
+ cid: course._id,
+ },
dataType: "json",
success: res => {
if (!res.errmsg)
{
- this.assessmentArray[this.assessmentIndex] = this.assessment;
- this.mode = "view";
+ this.newAssessment["name"] = "";
+ this.assessmentArray.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...
+ },
+ updateAssessment: function() {
+ $.ajax("/update/assessment", {
+ method: "POST",
+ data: {assessment: JSON.stringify(this.assessment)},
+ 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.assessmentArray[this.assessmentIndex] = this.assessment;
+ this.mode = "view";
}
else
- {
- // 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",
+ alert(res.errmsg);
+ },
+ });
+ },
+ deleteAssessment: function(assessment) {
+ if (!admin)
+ return;
+ if (confirm("Delete assessment '" + assessment.name + "' ?"))
+ {
+ $.ajax("/remove/assessment",
{
method: "GET",
- data: {
- cid: this.course._id,
- pwd: hashPwd,
- },
+ data: { qid: this.assessment._id },
dataType: "json",
success: res => {
if (!res.errmsg)
- alert("Password saved!");
+ this.assessmentArray.splice( this.assessmentArray.findIndex( item => {
+ return item._id == assessment._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 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; //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.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)
+ {
+ 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,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;
+ 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.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"]);
});
- 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 //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,
+ },
+ 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;
+ },
+ // 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])
+ {
+ 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
+ });
+ }
+ data.push(s); //number,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)
+ },
+ },
+});
-window.onload = function() {
-
- const messages = {
- "login": "Go",
- "register": "Send",
- };
+const messages = {
+ "login": "Go",
+ "register": "Send",
+};
- const ajaxUrl = {
- "login": "/sendtoken",
- "register": "/register",
- };
+const ajaxUrl = {
+ "login": "/sendtoken",
+ "register": "/register",
+};
- const infos = {
- "login": "Connection token sent. Check your emails!",
- "register": "Registration complete! Please check your emails.",
- };
+const infos = {
+ "login": "Connection token sent. Check your emails!",
+ "register": "Registration complete! Please check your emails.",
+};
- const animationDuration = 300; //in milliseconds
+const animationDuration = 300; //in milliseconds
- // Basic anti-bot measure: force at least N seconds between arrival on page, and register form validation:
- const enterTime = Date.now();
+// Basic anti-bot measure: force at least N seconds between arrival on page, and register form validation:
+const enterTime = Date.now();
- new Vue({
- el: '#login',
- data: {
- messages: messages,
- user: {
- name: "",
- email: "",
- },
- stage: "login", //or "register"
+new Vue({
+ el: '#login',
+ data: {
+ messages: messages,
+ user: {
+ name: "",
+ email: "",
},
- mounted: function() {
- // https://laracasts.com/discuss/channels/vue/vuejs-set-focus-on-textfield
- this.$refs.userEmail.focus();
+ stage: "login", //or "register"
+ },
+ mounted: function() {
+ // https://laracasts.com/discuss/channels/vue/vuejs-set-focus-on-textfield
+ this.$refs.userEmail.focus();
+ },
+ methods: {
+ toggleStage: function(stage) {
+ let $form = $("#form");
+ $form.fadeOut(animationDuration);
+ setTimeout( () => {
+ this.stage = stage;
+ $form.show(0);
+ }, animationDuration);
},
- methods: {
- toggleStage: function(stage) {
- let $form = $("#form");
- $form.fadeOut(animationDuration);
- setTimeout( () => {
- this.stage = stage;
- $form.show(0);
- }, animationDuration);
- },
- submit: function() {
- if (this.stage=="register")
+ submit: function() {
+ if (this.stage=="register")
+ {
+ if (Date.now() - enterTime < 5000)
+ return;
+ }
+ let error = Validator.checkObject({email: this.user.email}, "User");
+ if (!error && this.stage == "register")
+ error = Validator.checkObject({name: this.user.name}, "User");
+ let $dialog = $("#dialog");
+ show($dialog);
+ setTimeout(() => {hide($dialog);}, 3000);
+ if (error.length > 0)
+ return showMsg($dialog, "error", error);
+ showMsg($dialog, "process", "Processing... Please wait");
+ $.ajax(ajaxUrl[this.stage],
{
- if (Date.now() - enterTime < 5000)
- return;
- }
- let error = Validator.checkObject({email: this.user.email}, "User");
- if (!error && this.stage == "register")
- error = Validator.checkObject({name: this.user.name}, "User");
- let $dialog = $("#dialog");
- show($dialog);
- setTimeout(() => {hide($dialog);}, 3000);
- if (error.length > 0)
- return showMsg($dialog, "error", error);
- showMsg($dialog, "process", "Processing... Please wait");
- $.ajax(ajaxUrl[this.stage],
+ method: "GET",
+ data:
{
- method: "GET",
- data:
+ email: encodeURIComponent(this.user.email),
+ name: encodeURIComponent(this.user.name), //may be unused
+ },
+ dataType: "json",
+ success: res => {
+ if (!res.errmsg)
{
- email: encodeURIComponent(this.user.email),
- name: encodeURIComponent(this.user.name), //may be unused
- },
- dataType: "json",
- success: res => {
- if (!res.errmsg)
- {
- this.user["name"] = "";
- this.user["email"] = "";
- showMsg($dialog, "info", infos[this.stage]);
- }
- else
- showMsg($dialog, "error", res.errmsg);
- },
- }
- );
- },
- }
- });
-
-};
+ this.user["name"] = "";
+ this.user["email"] = "";
+ showMsg($dialog, "info", infos[this.stage]);
+ }
+ else
+ showMsg($dialog, "error", res.errmsg);
+ },
+ }
+ );
+ },
+ }
+});