From: Benjamin Auder Date: Tue, 13 Feb 2018 22:32:29 +0000 (+0100) Subject: thoughts about time, refactor statements component X-Git-Url: https://git.auder.net/assets/img/css/rpsls.js?a=commitdiff_plain;h=a80c6a3b87f75653725f54caca1f24abc556afc7;p=qomet.git thoughts about time, refactor statements component --- diff --git a/models/assessment.js b/models/assessment.js index c7cb0fd..fd0133c 100644 --- a/models/assessment.js +++ b/models/assessment.js @@ -11,11 +11,11 @@ const AssessmentModel = * _id: BSON id * cid: course ID * name: varchar - * active: boolean true/false - * mode: secure | exam | open (decreasing security) + * active: boolean + * mode: secure | watch | exam | open (decreasing security) * fixed: bool (questions in fixed order; default: false) * display: "one" or "all" (generally "all" for open questions, but...) - * time: 0, //<=0 means "untimed"; otherwise, time in seconds + * time: 0, global (one vaue) or per question (array of integers) * introduction: "", * coefficient: number, default 1 * questions: array of @@ -26,7 +26,6 @@ const AssessmentModel = * 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) - * time: 0 (<=0: untimed) * papers : array of * number: student number * inputs: array of {index,answer[array of integers or html text],startTime} diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js index 93200cc..9275879 100644 --- a/public/javascripts/assessment.js +++ b/public/javascripts/assessment.js @@ -23,19 +23,19 @@ new Vue({ // 2: locked: password set, exam started // 3: completed // 4: show answers + remainingTime: assessment.time, //integer or array stage: assessment.mode != "open" ? 0 : 1, - remainingTime: 0, //global, in seconds warnMsg: "", }, computed: { countdown: function() { - let seconds = this.remainingTime % 60; - let minutes = Math.floor(this.remainingTime / 60); + const remainingTime = assessment.display == "one" && _.isArray(assessment.time) + ? this.remainingTime[this.answers.index] + : this.remainingTime; + let seconds = remainingTime % 60; + let minutes = Math.floor(remainingTime / 60); return this.padWithZero(minutes) + ":" + this.padWithZero(seconds); }, - showAnswers: function() { - return this.stage == 4; - }, }, mounted: function() { $(".modal").modal(); @@ -46,7 +46,7 @@ new Vue({ return; if (assessment.mode == "secure") { - this.trySendCurrentAnswer(); + this.sendAnswer(); document.location.href= "/noblur"; } else //"watch" mode @@ -65,7 +65,7 @@ new Vue({ return; if (assessment.mode == "secure") { - this.trySendCurrentAnswer(); + this.sendAnswer(); document.location.href = "/fullscreen"; } else //"watch" mode @@ -78,7 +78,7 @@ new Vue({ }, false); }, methods: { - // In case of AJAX errors + // In case of AJAX errors (not blur-ing) showWarning: function(message) { this.warnMsg = message; $("#warning").modal("open"); @@ -88,12 +88,8 @@ new Vue({ return "0" + x; return x; }, - trySendCurrentAnswer: function() { - if (this.stage == 2) - this.sendAnswer(); - }, // stage 0 --> 1 - getStudent: function(cb) { + getStudent: function() { $.ajax("/get/student", { method: "GET", data: { @@ -107,8 +103,6 @@ new Vue({ this.stage = 1; this.student = s.student; Vue.nextTick( () => { Materialize.updateTextFields(); }); - if (!!cb) - cb(); }, }); }, @@ -118,18 +112,14 @@ new Vue({ }, // stage 1 --> 2 (get all questions, set password) startAssessment: function() { - let initializeStage2 = (questions,paper) => { + let initializeStage2 = paper => { $("#leftButton, #rightButton").hide(); - if (assessment.time > 0) - { - -// TODO: distinguish total exam time AND question time - - const deltaTime = !!paper ? Date.now() - paper.startTime : 0; - this.remainingTime = assessment.time * 60 - Math.round(deltaTime / 1000); - this.runTimer(); - } // Initialize structured answer(s) based on questions type and nesting (TODO: more general) + + // if display == "all" getQuestionS + // otherwise get first question + + if (!!questions) assessment.questions = questions; this.answers.inputs = [ ]; @@ -148,6 +138,24 @@ new Vue({ let remainingIndices = _.difference( _.range(assessment.questions.length).map(String), indices ); this.answers.indices = indices.concat( _.shuffle(remainingIndices) ); } + + + + + + + + if (assessment.time > 0) + { + +// TODO: distinguish total exam time AND question time + + const deltaTime = !!paper ? Date.now() - paper.startTime : 0; + this.remainingTime = assessment.time * 60 - Math.round(deltaTime / 1000); + this.runTimer(); + } + + this.answers.index = !!paper ? paper.inputs.length : 0; this.answers.displayAll = assessment.display == "all"; this.answers.showSolution = false; diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js index 0fb5124..900a5e7 100644 --- a/public/javascripts/components/statements.js +++ b/public/javascripts/components/statements.js @@ -17,26 +17,37 @@ Imaginary example: (using math.js) */ Vue.component("statements", { - // 'answers' is an object containing - // 'inputs'(array), - // 'displayAll'(bool), //TODO: should be in questions! - // 'showSolution'(bool), - // 'indices': order of appearance - // 'index': current integer index (focused question) - props: ['questions','answers'], - // TODO: general render function for nested exercises - // There should be a questions navigator below, or next (visible if display=='all') + // 'inputs': array of index (as in questions) + input (text or array of ints) + // display: 'all', 'one', 'solution' + // iidx: current level-0 integer index (can match a group of questions / inputs) + props: ['questions','inputs','display','iidx'], + data: function() { + return { + displayStyle: "compact", //or "all": all on same page + }; + } // Full questions tree is rendered, but some parts hidden depending on display settings render(h) { - // TODO: render nothing if answers is empty 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, @@ -44,82 +55,141 @@ Vue.component("statements", { } ) ); - 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; }, - }, - }, - [ '' ] //to work in Firefox 45.9 ESR @ ENSTA... - ) - ); - option.push( - h( - "label", - { - domProps: { - innerHTML: q.options[idx], + 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; }, + }, }, - 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(i,idx), + }, + } + ) + ); + optionList.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.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, - }, - }, - optionList - ) - ); - if (this.answers.displayAll && i < this.questions.length-1) + } + 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, - "hide": !this.answers.displayAll && this.answers.indices[this.answers.index] != i, + "hide": this.display == "one" && this.iidx != i, + "depth" + depth: true, }, }, questionContent ); }); + const navigator = h( + "div", + { + "class": { + "hide": this.displayStyle == "all" + }, + }, + [ + h( + "button", + { + "class": { + "btn": true, + }, + on: { + click: () => { + this.index = Math.max(0, this.index - 1); + }, + }, + }, + [ h("span", { "class": { "material-icon": true } }, "fast_rewind") ] + ), //onclick: index = max(0,index-1) + h("span",{ },(this.iidx+1).toString()), + h( + "button", + { + "class": { + "btn": true, + }, + on: { + click: () => { + this.index = Math.min(this.index+1, this.questions.length-1) + }, + }, + }, + [ h("span", { "class": { "material-icon": true } }, "fast_forward") ] + ) + ] + ); + domTree.push(navigator); + domTree.push( + h( + "button", + { + on: { + click: () => { + this.displayStyle = displayStyle == "compact" ? "all" : "compact"; + }, + }, + }, + this.displayStyle == "compact" ? "Show all" : "Navigator" + ) + ); return h( "div", { @@ -134,8 +204,6 @@ Vue.component("statements", { statementsLibsRefresh(); }, updated: function() { - // TODO: next line shouldn't be required: questions wordings + answer + options - // are processed earlier; their content should be updated at this time. statementsLibsRefresh(); }, methods: {