--- /dev/null
+views: course, monitor, grade
+js: monitor (see details), assessment, course, grade
* answer: array of integers (for quiz) or html text (for paper); striped in exam mode
* active: boolean, is question in current assessment?
* points: points for this question (default 1)
+ * param: parameter (if applicable)
* papers : array of
* number: student number
* inputs: array of {index,answer[array of integers or html text],startTime}
},
// stage 0 --> 1
getStudent: function() {
- $.ajax("/get/student", {
+ $.ajax("/courses/student", {
method: "GET",
data: {
number: this.student.number,
};
if (assessment.mode == "open")
return initializeStage2();
- $.ajax("/start/assessment", {
- method: "GET",
+ $.ajax("/assessments/start", {
+ method: "PUT",
data: {
number: this.student.number,
aid: assessment._id
number: this.student.number,
password: this.student.password,
};
- $.ajax("/send/answer", {
- method: "GET",
+ $.ajax("/assessments/answer", {
+ method: "PUT",
data: answerData,
dataType: "json",
success: ret => {
this.answers.displayAll = true;
return;
}
- $.ajax("/end/assessment", {
- method: "GET",
+ $.ajax("/assessments/end", {
+ method: "PUT",
data: {
aid: assessment._id,
number: this.student.number,
<div>Calculer le déterminant de
$$\begin{matrix}7 & x\\y & -3\end{matrix}$$</div>
* ...
+
+--> input of type text (number, or vector, or matrix e.g. in R syntax)
+--> parameter stored in question.param (TODO)
+
*/
Vue.component("statements", {
- // 'inputs': array of index (as in questions) + input (text or array of ints)
+ // 'inputs': object with key = question index and value = text or boolean array
// display: 'all', 'one', 'solution'
// iidx: current level-0 integer index (can match a group of questions / inputs)
props: ['questions','inputs','display','iidx'],
}
// Full questions tree is rendered, but some parts hidden depending on display settings
render(h) {
- let domTree = (this.questions || [ ]).map( (q,i) => {
- let questionContent = [ ];
- questionContent.push(
- h(
- "h4",
- {
- "class": {
- "questionIndex": true,
- }
- },
- q.index
- )
- );
- questionContent.push(
- h(
- "div",
- {
- "class": {
- wording: true,
-
- },
- domProps: {
- innerHTML: q.wording,
+ // Prepare questions groups, ordered
+ let questions = this.questions || [ ]
+ let questionGroups = _.groupBy(questions, q => {
+ const dotPos = q.index.indexOf(".");
+ return dotPos === -1 ? q.index : q.index.substring(0,dotPos);
+ });
+ let domTree = questionGroups.map( (qg,i) => {
+ // Re-order questions 1.1.1 then 1.1.2 then...
+ const orderedQg = qg.sort( (a,b) => {
+ let aParts = a.split('.').map(Number);
+ let bParts = b.split('.').map(Number);
+ const La = aParts.length, Lb = bParts.length;
+ for (let i=0; i<Math.min(La,Lb); i++)
+ {
+ if (aParts[i] != bParts[i])
+ return aParts[i] - bParts[i];
+ }
+ return La - Lb; //the longer should appear after
+ });
+ let qgDom = orderedQg.map( q => {
+ let questionContent = [ ];
+ questionContent.push(
+ h(
+ "h4",
+ {
+ "class": {
+ "questionIndex": true,
+ }
},
- }
- )
- );
- if (!!q.options)
- {
- // quiz-like question
- let optionsOrder = _.range(q.options.length);
- if (!q.fixed)
- optionsOrder = _.shuffle(optionsOrder);
- let optionList = [ ];
- optionsOrder.forEach( idx => {
- let option = [ ];
- option.push(
- h(
- "input",
- {
- domProps: {
- checked: this.answers.inputs.length > 0 && this.answers.inputs[i][idx],
- disabled: monitoring,
- },
- attrs: {
- id: this.inputId(i,idx),
- type: "checkbox",
- },
- on: {
- change: e => { this.answers.inputs[i][idx] = e.target.checked; },
- },
+ q.index
+ )
+ );
+ questionContent.push(
+ h(
+ "div",
+ {
+ "class": {
+ wording: true,
},
- [ '' ] //to work in Firefox 45.9 ESR @ ENSTA...
- )
- );
- option.push(
- h(
- "label",
- {
- domProps: {
- innerHTML: q.options[idx],
+ domProps: {
+ innerHTML: q.wording,
+ },
+ }
+ )
+ );
+ if (!!q.options)
+ {
+ // quiz-like question
+ let optionsOrder = _.range(q.options.length);
+ if (!q.fixed)
+ optionsOrder = _.shuffle(optionsOrder);
+ let optionList = [ ];
+ optionsOrder.forEach( idx => {
+ let option = [ ];
+ option.push(
+ h(
+ "input",
+ {
+ domProps: {
+ checked: this.inputs[q.index][idx],
+ disabled: monitoring,
+ },
+ attrs: {
+ id: this.inputId(q.index,idx),
+ type: "checkbox",
+ },
+ on: {
+ change: e => { this.inputs[q.index][idx] = e.target.checked; },
+ },
},
- attrs: {
- "for": this.inputId(i,idx),
+ [ '' ] //to work in Firefox 45.9 ESR @ ENSTA...
+ )
+ );
+ option.push(
+ h(
+ "label",
+ {
+ domProps: {
+ innerHTML: q.options[idx],
+ },
+ attrs: {
+ "for": this.inputId(q.index,idx),
+ },
+ }
+ )
+ );
+ optionList.push(
+ h(
+ "div",
+ {
+ "class": {
+ option: true,
+ choiceCorrect: this.display == "solution" && q.answer.includes(idx),
+ choiceWrong: this.display == "solution" && this.inputs[q.index][idx] && !q.answer.includes(idx),
+ },
},
- }
- )
- );
- optionList.push(
+ option
+ )
+ );
+ });
+ questionContent.push(
h(
"div",
{
"class": {
- option: true,
- choiceCorrect: this.answers.showSolution && this.questions[i].answer.includes(idx),
- choiceWrong: this.answers.showSolution && this.answers.inputs[i][idx] && !q.answer.includes(idx),
+ optionList: true,
},
},
- option
+ optionList
)
);
- });
- questionContent.push(
- h(
- "div",
- {
- "class": {
- optionList: true,
- },
+ }
+ const depth = (q.index.match(/\./g) || []).length;
+ return h(
+ "div",
+ {
+ "class": {
+ "question": true,
+ "depth" + depth: true,
},
- optionList
- )
+ },
+ questionContent
);
- }
- if (this.display == "all" && !this.navigator && i < this.questions.length-1)
- questionContent.push( h("hr") );
- const depth = (q.index.match(/\./g) || []).length;
+ });
return h(
"div",
{
"class": {
- "question": true,
+ "questionGroup": true,
"hide": this.display == "one" && this.iidx != i,
- "depth" + depth: true,
},
- },
- questionContent
+ }
+ qgDom
);
});
const navigator = h(
},
},
[ h("span", { "class": { "material-icon": true } }, "fast_rewind") ]
- ), //onclick: index = max(0,index-1)
+ ),
h("span",{ },(this.iidx+1).toString()),
h(
"button",
d.number = d.number.toString();
students.push(d);
});
- $.ajax("/import/students", {
- method: "POST",
+ $.ajax("/courses/student-list", {
+ method: "PUT",
data: {
cid: this.course._id,
students: JSON.stringify(students),
return alert(error);
else
$('#newAssessment').modal('close');
- $.ajax("/add/assessment",
+ $.ajax("/assessments",
{
- method: "GET",
+ method: "POST",
data: {
name: this.newAssessment.name,
cid: course._id,
Materialize.updateTextFields(); //textareas, time field...
},
updateAssessment: function() {
- $.ajax("/update/assessment", {
- method: "POST",
+ $.ajax("/assessments", {
+ method: "PUT",
data: {assessment: JSON.stringify(this.assessment)},
dataType: "json",
success: res => {
return;
if (confirm("Delete assessment '" + assessment.name + "' ?"))
{
- $.ajax("/remove/assessment",
+ $.ajax("/assessments",
{
- method: "GET",
+ method: "DELETE",
data: { qid: this.assessment._id },
dataType: "json",
success: res => {
let error = Validator.checkObject({password:hashPwd}, "Course");
if (error.length > 0)
return alert(error);
- $.ajax("/set/password",
+ $.ajax("/courses/password",
{
- method: "GET",
+ method: "PUT",
data: {
cid: this.course._id,
pwd: hashPwd,
return alert(error);
else
$('#newCourse').modal('close');
- $.ajax("/add/course",
+ $.ajax("/courses",
{
- method: "GET",
+ method: "POST",
data: this.newCourse,
dataType: "json",
success: res => {
if (!admin)
return;
if (confirm("Delete course '" + course.code + "' ?"))
- $.ajax("/remove/course",
+ $.ajax("/courses",
{
- method: "GET",
+ method: "DELETE",
data: { cid: course._id },
dataType: "json",
success: res => {
"register": "/register",
};
+const ajaxMethod = {
+ "login": "PUT",
+ "register": "POST",
+};
+
const infos = {
"login": "Connection token sent. Check your emails!",
"register": "Registration complete! Please check your emails.",
showMsg($dialog, "process", "Processing... Please wait");
$.ajax(ajaxUrl[this.stage],
{
- method: "GET",
+ method: ajaxMethod[this.stage],
data:
{
email: encodeURIComponent(this.user.email),
},
// stage 0 --> 1
startMonitoring: function() {
- $.ajax("/start/monitoring", {
+ $.ajax("/assessments/monitor", {
method: "GET",
data: {
password: Sha1.Compute(this.password),
margin: 0 auto;
}
+.questionGroup {
+ /* TODO */
+}
+
.question {
margin: 20px 5px;
padding: 15px 0;
}
+.question:not(:last-child) {
+ border-bottom: 1px solid black;
+}
+
.question label {
color: black;
}
},
};
-router.get("/add/assessment", access.ajax, access.logged, (req,res) => {
- const name = req.query["name"];
- const cid = req.query["cid"];
+router.post("/assessments", access.ajax, access.logged, (req,res) => {
+ const name = req.body["name"];
+ const cid = req.body["cid"];
let error = validator({cid:cid, name:name}, "Assessment");
if (error.length > 0)
return res.json({errmsg:error});
});
});
-router.post("/update/assessment", access.ajax, access.logged, (req,res) => {
+router.put("/assessments", access.ajax, access.logged, (req,res) => {
const assessment = JSON.parse(req.body["assessment"]);
let error = validator(assessment, "Assessment");
if (error.length > 0)
});
// Generate and set student password, return it
-router.get("/start/assessment", access.ajax, (req,res) => {
- let number = req.query["number"];
- let aid = req.query["aid"];
+router.put("/assessments/start", access.ajax, (req,res) => {
+ let number = req.body["number"];
+ let aid = req.body["aid"];
let password = req.cookies["password"]; //potentially from cookies, resuming
let error = validator({ _id:aid, papers:[{number:number,password:password || "samplePwd"}] }, "Assessment");
if (error.length > 0)
});
});
-router.get("/start/monitoring", access.ajax, (req,res) => {
+router.get("/assessments/monitor", access.ajax, (req,res) => {
const password = req.query["password"];
const examName = req.query["aname"];
const courseCode = req.query["ccode"];
});
});
-router.get("/send/answer", access.ajax, (req,res) => {
- let aid = req.query["aid"];
- let number = req.query["number"];
- let password = req.query["password"];
- let input = JSON.parse(req.query["answer"]);
+router.put("/assessments/answer", access.ajax, (req,res) => {
+ let aid = req.body["aid"];
+ let number = req.body["number"];
+ let password = req.body["password"];
+ let input = JSON.parse(req.body["answer"]);
let error = validator({ _id:aid, papers:[{number:number,password:password,inputs:[input]}] }, "Assessment");
if (error.length > 0)
return res.json({errmsg:error});
});
});
-router.get("/end/assessment", access.ajax, (req,res) => {
- let aid = req.query["aid"];
- let number = req.query["number"];
- let password = req.query["password"];
+router.put("/assessments/end", access.ajax, (req,res) => {
+ let aid = req.body["aid"];
+ let number = req.body["number"];
+ let password = req.body["password"];
let error = validator({ _id:aid, papers:[{number:number,password:password}] }, "Assessment");
if (error.length > 0)
return res.json({errmsg:error});
const ObjectId = require("bson-objectid");
const CourseModel = require("../models/course");
-router.get('/add/course', access.ajax, access.logged, (req,res) => {
- let code = req.query["code"];
- let description = sanitizeHtml(req.query["description"]);
+router.post('/courses', access.ajax, access.logged, (req,res) => {
+ let code = req.body["code"];
+ let description = sanitizeHtml(req.body["description"]);
let error = validator({code:code}, "Course");
if (error.length > 0)
return res.json({errmsg:error});
});
});
-router.get("/set/password", access.ajax, access.logged, (req,res) => {
- let cid = req.query["cid"];
- let pwd = req.query["pwd"];
+router.put("/courses/password", access.ajax, access.logged, (req,res) => {
+ let cid = req.body["cid"];
+ let pwd = req.body["pwd"];
let error = validator({password:pwd, _id:cid}, "Course");
if (error.length > 0)
return res.json({errmsg:error});
});
});
-router.post('/import/students', access.ajax, access.logged, (req,res) => {
+router.put('/courses/student-list', access.ajax, access.logged, (req,res) => {
let cid = req.body["cid"];
let students = JSON.parse(req.body["students"]);
let error = validator({_id:cid, students: students}, "Course");
});
});
-router.get('/get/student', access.ajax, (req,res) => {
- let number = req.query["number"];
+router.get('/courses/student', access.ajax, (req,res) => {
let cid = req.query["cid"];
+ let number = req.query["number"];
let error = validator({ _id: cid, students: [{number:number}] }, "Course");
if (error.length > 0)
return res.json({errmsg:error});
});
});
-router.get('/remove/course', access.ajax, access.logged, (req,res) => {
+router.delete('/courses', access.ajax, access.logged, (req,res) => {
let cid = req.query["cid"];
let error = validator({_id:cid}, "Course");
if (error.length > 0)
const params = require("../config/parameters");
// to: object user
-function sendLoginToken(subject, to, res)
+function setAndSendLoginToken(subject, to, res)
{
// Set login token and send welcome(back) email with auth link
let token = TokenGen.generate(params.token.length);
});
}
-router.get('/register', access.ajax, access.unlogged, (req,res) => {
- let email = decodeURIComponent(req.query.email);
- let name = decodeURIComponent(req.query.name);
+router.post('/register', access.ajax, access.unlogged, (req,res) => {
const newUser = {
- email: email,
- name: name,
+ email: req.body.email,
+ name: req.body.name,
};
let error = validator(newUser, "User");
if (error.length > 0)
UserModel.create(newUser, (err,user) => {
access.checkRequest(res, err, user, "Registration failed", () => {
user.ip = req.ip;
- sendLoginToken("Welcome to " + params.siteURL, user, res);
+ setAndSendLoginToken("Welcome to " + params.siteURL, user, res);
});
});
});
});
// Login:
-router.get('/sendtoken', access.ajax, access.unlogged, (req,res) => {
- let email = decodeURIComponent(req.query.email);
+router.put('/sendtoken', access.ajax, access.unlogged, (req,res) => {
+ const email = req.body.email;
let error = validator({email:email}, "User");
if (error.length > 0)
return res.json({errmsg:error});
UserModel.getByEmail(email, (err,user) => {
access.checkRequest(res, err, user, "Unknown user", () => {
user.ip = req.ip;
- sendLoginToken("Token for " + params.siteURL, user, res);
+ setAndSendLoginToken("Token for " + params.siteURL, user, res);
});
});
});
// Authentication process, optionally with email changing:
-router.get('/authenticate', access.unlogged, (req,res) => {
- let loginToken = req.query.token;
- let error = validator({token:loginToken}, "User");
- if (error.length > 0)
- return res.json({errmsg:error});
+router.put('/authenticate/:token([a-z0-9]+)', access.unlogged, (req,res) => {
+ const loginToken = req.params.token;
UserModel.getByLoginToken(loginToken, (err,user) => {
access.checkRequest(res, err, user, "Invalid token", () => {
if (user.loginToken.ip != req.ip)
});
});
-router.get('/logout', access.logged, (req,res) => {
+router.put('/logout', access.logged, (req,res) => {
UserModel.removeToken(req.user._id, req.cookies.token, (err,ret) => {
access.checkRequest(res, err, ret, "Logout failed", () => {
res.clearCookie("initials");