refactoring, better README (breaking commit...)
[qomet.git] / public / javascripts / course.js
1 /*Draft format (compiled to json)
2
3 > Some global (HTML) intro
4
5 <some html question (or/+ exercise intro)>
6
7 <some html subQuestion>
8 * some answer [trigger input/index in answers]
9
10 <another subquestion>
11
12 <sub-subQuestion>
13 + choix1
14 - choix 2
15 + choix 3
16 - choix4
17
18 <another sub sub>
19 * answer 2 (which can
20 be on
21 several lines)
22
23 <Some second question>
24 * With answer
25 */
26
27 new Vue({
28 el: '#course',
29 data: {
30 display: "assessments", //or "students", or "grades" (admin mode)
31 course: course,
32 mode: "view", //or "edit" (some assessment)
33 monitorPwd: "",
34 newAssessment: { name: "" },
35 assessmentArray: assessmentArray,
36 assessmentIndex: 0, //current edited assessment index
37 assessment: { }, //copy of assessment at editing index in array
38 assessmentText: "", //questions in an assessment, in text format
39 },
40 mounted: function() {
41 $('.modal').each( (i,elem) => {
42 if (elem.id != "assessmentEdit")
43 $(elem).modal();
44 });
45 $('ul.tabs').tabs();
46 $('#assessmentEdit').modal({
47 complete: () => {
48 this.parseAssessment();
49 Vue.nextTick( () => {
50 $("#questionList").find("code[class^=language-]").each( (i,elem) => {
51 Prism.highlightElement(elem);
52 });
53 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
54 });
55 },
56 });
57 },
58 methods: {
59 // GENERAL:
60 toggleDisplay: function(area) {
61 if (this.display == area)
62 this.display = "";
63 else
64 this.display = area;
65 },
66 studentList: function(group) {
67 return this.course.students
68 .filter( s => { return group==0 || s.group == group; })
69 .map( s => { return Object.assign({}, s); }) //not altering initial array
70 .sort( (a,b) => { return a.name.localeCompare(b.name); })
71 },
72 // STUDENTS:
73 uploadTrigger: function() {
74 $("#upload").click();
75 },
76 upload: function(e) {
77 let file = (e.target.files || e.dataTransfer.files)[0];
78 Papa.parse(file, {
79 header: true,
80 skipEmptyLines: true,
81 complete: (results,file) => {
82 let students = [ ];
83 // Post-process: add group/number if missing
84 let number = 1;
85 results.data.forEach( d => {
86 if (!d.group)
87 d.group = 1;
88 if (!d.number)
89 d.number = number++;
90 if (typeof d.number !== "string")
91 d.number = d.number.toString();
92 students.push(d);
93 });
94 $.ajax("/import/students", {
95 method: "POST",
96 data: {
97 cid: this.course._id,
98 students: JSON.stringify(students),
99 },
100 dataType: "json",
101 success: res => {
102 if (!res.errmsg)
103 this.course.students = students;
104 else
105 alert(res.errmsg);
106 },
107 });
108 },
109 });
110 },
111 // ASSESSMENT:
112 addAssessment: function() {
113 if (!admin)
114 return;
115 // modal, fill code and description
116 let error = Validator.checkObject(this.newAssessment, "Assessment");
117 if (!!error)
118 return alert(error);
119 else
120 $('#newAssessment').modal('close');
121 $.ajax("/add/assessment",
122 {
123 method: "GET",
124 data: {
125 name: this.newAssessment.name,
126 cid: course._id,
127 },
128 dataType: "json",
129 success: res => {
130 if (!res.errmsg)
131 {
132 this.newAssessment["name"] = "";
133 this.assessmentArray.push(res);
134 }
135 else
136 alert(res.errmsg);
137 },
138 }
139 );
140 },
141 materialOpenModal: function(id) {
142 $("#" + id).modal("open");
143 Materialize.updateTextFields(); //textareas, time field...
144 },
145 updateAssessment: function() {
146 $.ajax("/update/assessment", {
147 method: "POST",
148 data: {assessment: JSON.stringify(this.assessment)},
149 dataType: "json",
150 success: res => {
151 if (!res.errmsg)
152 {
153 this.assessmentArray[this.assessmentIndex] = this.assessment;
154 this.mode = "view";
155 }
156 else
157 alert(res.errmsg);
158 },
159 });
160 },
161 deleteAssessment: function(assessment) {
162 if (!admin)
163 return;
164 if (confirm("Delete assessment '" + assessment.name + "' ?"))
165 {
166 $.ajax("/remove/assessment",
167 {
168 method: "GET",
169 data: { qid: this.assessment._id },
170 dataType: "json",
171 success: res => {
172 if (!res.errmsg)
173 this.assessmentArray.splice( this.assessmentArray.findIndex( item => {
174 return item._id == assessment._id;
175 }), 1 );
176 else
177 alert(res.errmsg);
178 },
179 }
180 );
181 }
182 },
183 toggleState: function(questionIndex) {
184 // add or remove from activeSet of current assessment
185 let activeIndex = this.assessment.activeSet.findIndex( item => { return item == questionIndex; });
186 if (activeIndex >= 0)
187 this.assessment.activeSet.splice(activeIndex, 1);
188 else
189 this.assessment.activeSet.push(questionIndex);
190 },
191 setAssessmentText: function() {
192 let txt = "";
193 this.assessment.questions.forEach( q => {
194 txt += q.wording; //already ended by \n
195 q.options.forEach( (o,i) => {
196 let symbol = q.answer.includes(i) ? "+" : "-";
197 txt += symbol + " " + o + "\n";
198 });
199 txt += "\n"; //separate questions by new line
200 });
201 this.assessmentText = txt;
202 },
203 parseAssessment: function() {
204 let questions = [ ];
205 let lines = this.assessmentText.split("\n").map( L => { return L.trim(); })
206 lines.push(""); //easier parsing
207 let emptyQuestion = () => {
208 return {
209 wording: "",
210 options: [ ],
211 answer: [ ],
212 active: true, //default
213 };
214 };
215 let q = emptyQuestion();
216 lines.forEach( L => {
217 if (L.length > 0)
218 {
219 if (['+','-'].includes(L.charAt(0)))
220 {
221 if (L.charAt(0) == '+')
222 q.answer.push(q.options.length);
223 q.options.push(L.slice(1).trim());
224 }
225 else if (L.charAt(0) == '*')
226 {
227 // TODO: read current + next lines into q.answer (HTML, 1-elem array)
228 }
229 else
230 q.wording += L + "\n";
231 }
232 else
233 {
234 // Flush current question (if any)
235 if (q.wording.length > 0)
236 {
237 questions.push(q);
238 q = emptyQuestion();
239 }
240 }
241 });
242 this.assessment.questions = questions;
243 },
244 actionAssessment: function(index) {
245 if (admin)
246 {
247 // Edit screen
248 this.assessmentIndex = index;
249 this.assessment = $.extend(true, {}, this.assessmentArray[index]);
250 this.setAssessmentText();
251 this.mode = "edit";
252 Vue.nextTick( () => {
253 $("#questionList").find("code[class^=language-]").each( (i,elem) => {
254 Prism.highlightElement(elem);
255 });
256 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
257 });
258 }
259 else //external user: show assessment
260 this.redirect(this.assessmentArray[index].name);
261 },
262 redirect: function(assessmentName) {
263 document.location.href = "/" + initials + "/" + course.code + "/" + assessmentName;
264 },
265 setPassword: function() {
266 let hashPwd = Sha1.Compute(this.monitorPwd);
267 let error = Validator.checkObject({password:hashPwd}, "Course");
268 if (error.length > 0)
269 return alert(error);
270 $.ajax("/set/password",
271 {
272 method: "GET",
273 data: {
274 cid: this.course._id,
275 pwd: hashPwd,
276 },
277 dataType: "json",
278 success: res => {
279 if (!res.errmsg)
280 alert("Password saved!");
281 else
282 alert(res.errmsg);
283 },
284 }
285 );
286 },
287 // NOTE: artifact required for Vue v-model to behave well
288 checkboxFixedId: function(i) {
289 return "questionFixed" + i;
290 },
291 checkboxActiveId: function(i) {
292 return "questionActive" + i;
293 },
294 },
295 });