From 25d2b69bfe0642e03255c0280b6b6ee2065bfda8 Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 15:16:48 +0200 Subject: [PATCH 01/26] Format all files using prettier --- index.js | 11 +- jspm.config.js | 20 +- resources/fixtures/data/all.json | 19 +- .../data/cover-with-title-no-last-card.json | 17 +- ...single-map-point-guess-high-zoomlevel.json | 8 +- .../fixtures/data/single-map-point-guess.json | 5 +- resources/helpers/constants.js | 8 +- resources/helpers/db.js | 10 +- resources/helpers/itemTransformer.js | 10 +- resources/helpers/scoreHelpers.js | 94 +-- resources/helpers/statsCalculators.js | 113 ++-- resources/helpers/utils.js | 48 +- resources/schema.json | 94 +-- routes/answer-service/answer.js | 29 +- routes/answer-service/map-point-guess.js | 103 ++-- routes/answer-service/number-guess.js | 445 ++++++++------- routes/answer-service/score.js | 43 +- routes/answer-service/stats.js | 77 +-- routes/health.js | 10 +- routes/routes.js | 29 +- routes/schema.js | 10 +- routes/scripts.js | 16 +- routes/stylesheet.js | 15 +- script_src/AnswerStore.js | 39 +- script_src/MultiQuizPositionHandler.js | 97 +++- script_src/MultipleChoiceHandler.js | 145 ++--- script_src/NumberGuessHandler.js | 235 +++++--- script_src/QuestionHandler.js | 160 ++++-- script_src/Scale.js | 16 +- script_src/answerHelpers.js | 169 +++--- server-plugins.js | 24 +- server.js | 8 +- styles_src/cover.scss | 1 - styles_src/numberGuess.scss | 5 +- styles_src/q-quiz-button.scss | 3 +- styles_src/q-quiz-multi-container.scss | 2 - styles_src/q-quiz-multi-header.scss | 1 - tasks/build.js | 83 +-- test/e2e-tests.js | 535 ++++++++++-------- test/mock/answers.js | 68 +-- test/mock/couchdb.js | 57 +- test/mock/qserver.js | 14 +- test/mock/views.js | 36 +- test/plugins.js | 4 +- test/server.js | 2 +- 45 files changed, 1681 insertions(+), 1257 deletions(-) diff --git a/index.js b/index.js index 413704d..a116699 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,14 @@ -const server = require('./server.js'); -const plugins = require('./server-plugins.js'); -const routes = require('./routes/routes.js') +const server = require("./server.js"); +const plugins = require("./server-plugins.js"); +const routes = require("./routes/routes.js"); const start = async function() { - await server.register(plugins); server.route(routes); await server.start(); - console.log('Server running at: ' + server.info.uri); -} + console.log("Server running at: " + server.info.uri); +}; start(); diff --git a/jspm.config.js b/jspm.config.js index f69582a..ed322cb 100644 --- a/jspm.config.js +++ b/jspm.config.js @@ -5,20 +5,20 @@ SystemJS.config({ "github:": "jspm_packages/github/" }, browserConfig: { - "baseURL": "/" + baseURL: "/" }, devConfig: { - "map": { + map: { "plugin-babel": "npm:systemjs-plugin-babel@0.0.21" } }, transpiler: "plugin-babel", packages: { "q-quiz": { - "main": "q-quiz.js", - "meta": { + main: "q-quiz.js", + meta: { "*.js": { - "loader": "plugin-babel" + loader: "plugin-babel" } } } @@ -26,14 +26,10 @@ SystemJS.config({ }); SystemJS.config({ - packageConfigPaths: [ - "npm:@*/*.json", - "npm:*.json", - "github:*/*.json" - ], + packageConfigPaths: ["npm:@*/*.json", "npm:*.json", "github:*/*.json"], map: { - "leaflet": "github:Leaflet/Leaflet@1.0.3", - "text": "github:systemjs/plugin-text@0.0.9" + leaflet: "github:Leaflet/Leaflet@1.0.3", + text: "github:systemjs/plugin-text@0.0.9" }, packages: {} }); diff --git a/resources/fixtures/data/all.json b/resources/fixtures/data/all.json index e58362f..e75450e 100644 --- a/resources/fixtures/data/all.json +++ b/resources/fixtures/data/all.json @@ -9,11 +9,7 @@ "id": "quiz-0-1503673205043-233776299", "type": "multipleChoice", "image": {}, - "choices": [ - "vielleicht", - "falsch", - "absolut falsch" - ], + "choices": ["vielleicht", "falsch", "absolut falsch"], "articleRecommendations": [], "question": "Was ist die richtige Antwort?", "answer": "richtig" @@ -47,10 +43,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 8.548714, - 47.3664 - ] + "coordinates": [8.548714, 47.3664] }, "properties": { "pointLabel": "NZZ Red", @@ -74,10 +67,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 13.42632293701172, - 51.456574106519724 - ] + "coordinates": [13.42632293701172, 51.456574106519724] }, "properties": { "pointLabel": "Kuhkaff", @@ -98,7 +88,8 @@ "id": "quiz-0-1503673750136-872919612", "type": "lastCard", "title": "Geschafft. Danke fürs Mitspielen.", - "quizLink": "https://www.nzz.ch/feuilleton/nzz-quizz-twin-peaks-kehrt-zurueck-ld.1294313", + "quizLink": + "https://www.nzz.ch/feuilleton/nzz-quizz-twin-peaks-kehrt-zurueck-ld.1294313", "quizTitle": "Was wissen Sie über Twin Peaks", "isFinalScoreShown": true, "articleRecommendations": [], diff --git a/resources/fixtures/data/cover-with-title-no-last-card.json b/resources/fixtures/data/cover-with-title-no-last-card.json index 9cdeae2..2e66a89 100644 --- a/resources/fixtures/data/cover-with-title-no-last-card.json +++ b/resources/fixtures/data/cover-with-title-no-last-card.json @@ -1,5 +1,6 @@ { - "title": "FIXTURE: Ein Quiz mit Cover mit Titel, alle Fragetypen, ohne letzte Seite", + "title": + "FIXTURE: Ein Quiz mit Cover mit Titel, alle Fragetypen, ohne letzte Seite", "elements": [ { "id": "quiz-1-1503673199200-897399321", @@ -10,17 +11,14 @@ "id": "quiz-1-1503673205043-233776299", "type": "multipleChoice", "image": { - "url": "https://nzz-q-assets-stage.s3.eu-west-1.amazonaws.com/2018/02/05/ea4995ff-38d9-4ba2-a6b4-f8707f98b57c.png", + "url": + "https://nzz-q-assets-stage.s3.eu-west-1.amazonaws.com/2018/02/05/ea4995ff-38d9-4ba2-a6b4-f8707f98b57c.png", "key": "2018/02/05/ea4995ff-38d9-4ba2-a6b4-f8707f98b57c.png", "size": 14577, "width": 902, "height": 168 }, - "choices": [ - "vielleicht", - "falsch", - "absolut falsch" - ], + "choices": ["vielleicht", "falsch", "absolut falsch"], "articleRecommendations": [], "question": "Was ist die richtige Antwort?", "answer": "richtig" @@ -43,10 +41,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 8.548714, - 47.3664 - ] + "coordinates": [8.548714, 47.3664] }, "properties": { "pointLabel": "NZZ Red", diff --git a/resources/fixtures/data/single-map-point-guess-high-zoomlevel.json b/resources/fixtures/data/single-map-point-guess-high-zoomlevel.json index 13ce885..3aafeee 100644 --- a/resources/fixtures/data/single-map-point-guess-high-zoomlevel.json +++ b/resources/fixtures/data/single-map-point-guess-high-zoomlevel.json @@ -8,10 +8,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -5.3541295, - 36.140807 - ] + "coordinates": [-5.3541295, 36.140807] }, "properties": { "pointLabel": "Gibraltar", @@ -25,7 +22,8 @@ ] }, "image": { - "url": "https://nzz-q-assets-stage.s3.eu-west-1.amazonaws.com/2018/02/05/d93b5299-a21f-4bb0-bc9d-cd08b61674a9.jpeg", + "url": + "https://nzz-q-assets-stage.s3.eu-west-1.amazonaws.com/2018/02/05/d93b5299-a21f-4bb0-bc9d-cd08b61674a9.jpeg", "key": "2018/02/05/d93b5299-a21f-4bb0-bc9d-cd08b61674a9.jpeg", "size": 24361, "width": 380, diff --git a/resources/fixtures/data/single-map-point-guess.json b/resources/fixtures/data/single-map-point-guess.json index 0635b84..9e9a89a 100644 --- a/resources/fixtures/data/single-map-point-guess.json +++ b/resources/fixtures/data/single-map-point-guess.json @@ -8,10 +8,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 8.548714, - 47.3664 - ] + "coordinates": [8.548714, 47.3664] }, "properties": { "pointLabel": "NZZ Red", diff --git a/resources/helpers/constants.js b/resources/helpers/constants.js index b37c616..436c73f 100644 --- a/resources/helpers/constants.js +++ b/resources/helpers/constants.js @@ -1,12 +1,8 @@ module.exports = { - questionTypes: [ - 'multipleChoice', - 'numberGuess', - 'mapPointGuess' - ], + questionTypes: ["multipleChoice", "numberGuess", "mapPointGuess"], multiplicator: { multipleChoice: 5, numberGuess: 10, mapPointGuess: 10 } -} +}; diff --git a/resources/helpers/db.js b/resources/helpers/db.js index d9c82a3..e66cb73 100644 --- a/resources/helpers/db.js +++ b/resources/helpers/db.js @@ -1,17 +1,17 @@ -const PouchDB = require('pouchdb'); +const PouchDB = require("pouchdb"); const options = {}; if (process.env.COUCH_DB_USER && process.env.COUCH_DB_PASS) { options.auth = { username: process.env.COUCH_DB_USER, password: process.env.COUCH_DB_PASS - } + }; } const quizDb = new PouchDB(process.env.COUCH_DB_URL_Q_QUIZ, options); -console.log('connected to ' + process.env.COUCH_DB_URL_Q_QUIZ); +console.log("connected to " + process.env.COUCH_DB_URL_Q_QUIZ); module.exports = { - quizDb: quizDb, -} + quizDb: quizDb +}; diff --git a/resources/helpers/itemTransformer.js b/resources/helpers/itemTransformer.js index 902ba8b..d87556f 100644 --- a/resources/helpers/itemTransformer.js +++ b/resources/helpers/itemTransformer.js @@ -1,24 +1,24 @@ -const questionTypes = require('./constants.js').questionTypes; +const questionTypes = require("./constants.js").questionTypes; function transform(item) { // extract only one of the possibly existing cover elements, undefined otherwise const coverElement = item.elements.filter(element => { - return element.type === 'cover'; + return element.type === "cover"; })[0]; const hasCover = coverElement !== undefined; // extract only one of the possibly existing last card elements, undefined otherwise const lastCardElement = item.elements.filter(element => { - return element.type === 'lastCard'; + return element.type === "lastCard"; })[0]; const hasLastCard = lastCardElement !== undefined; - + // extract question elements const questionElements = item.elements.filter(element => { return questionTypes.includes(element.type); - }) + }); let numberElements = questionElements.length; if (hasCover) { diff --git a/resources/helpers/scoreHelpers.js b/resources/helpers/scoreHelpers.js index e88f434..84885b6 100644 --- a/resources/helpers/scoreHelpers.js +++ b/resources/helpers/scoreHelpers.js @@ -1,31 +1,39 @@ -const turf = require('@turf/turf'); -const constants = require('./constants.js'); +const turf = require("@turf/turf"); +const constants = require("./constants.js"); function calculateWorstAnswerDifference(question) { - if (question.type === 'numberGuess') { - return Math.max((question.answer - question.min), (question.max - question.answer)); + if (question.type === "numberGuess") { + return Math.max( + question.answer - question.min, + question.max - question.answer + ); } - if (question.type === 'mapPointGuess') { + if (question.type === "mapPointGuess") { const bbox = question.answer.bbox; const correctAnswerCoord = question.answer.geometry.coordinates; - + const pointNorthWest = turf.point([bbox[3], bbox[0]]); const pointSouthWest = turf.point([bbox[1], bbox[0]]); const pointNorthEast = turf.point([bbox[3], bbox[2]]); const pointSouthEast = turf.point([bbox[1], bbox[2]]); - const correctAnswer = turf.point([correctAnswerCoord[1], correctAnswerCoord[0]]); - - // there is no meter unit in turf, we use kilometers and multiply with + const correctAnswer = turf.point([ + correctAnswerCoord[1], + correctAnswerCoord[0] + ]); + + // there is no meter unit in turf, we use kilometers and multiply with // 1000 afterwards to get the needed meters const distanceOptions = { - units: 'kilometers' - } - return Math.max( - turf.distance(pointNorthWest, correctAnswer, distanceOptions), - turf.distance(pointSouthWest, correctAnswer, distanceOptions), - turf.distance(pointNorthEast, correctAnswer, distanceOptions), - turf.distance(pointSouthEast, correctAnswer, distanceOptions) - ) * 1000; + units: "kilometers" + }; + return ( + Math.max( + turf.distance(pointNorthWest, correctAnswer, distanceOptions), + turf.distance(pointSouthWest, correctAnswer, distanceOptions), + turf.distance(pointNorthEast, correctAnswer, distanceOptions), + turf.distance(pointSouthEast, correctAnswer, distanceOptions) + ) * 1000 + ); } return undefined; } @@ -35,11 +43,11 @@ function calculateAchievedScore(answerQuality, questionType) { // slope was pre-calculated with a defined point of worst estimation quality of (0.08, 0) const lowerSlope = 0.6 / 0.52; const lowerConstant = -(lowerSlope * turningPoint) + turningPoint; // y = ax + b, for a = lowerSlope and (x,y) = turningPoint - + // slope was pre-calculated with a defined point of best estimation quality of (0.9, 0.98) const upperSlope = 0.38 / 0.3; const upperConstant = -(upperSlope * turningPoint) + turningPoint; // y = ax + b, for a = upperSlope and (x,y) = turningPoint - + const lowerBoundX = (0 - lowerConstant) / lowerSlope; // 0 = ax + b, for a = lowerSlope, b = lowerConstant and y = 0 -> no negative values const upperBoundX = (1 - upperConstant) / upperSlope; // 0 = ax + b, for a = upperSlope, b = upperConstant and y = 1 -> no values over 1 @@ -48,9 +56,15 @@ function calculateAchievedScore(answerQuality, questionType) { if (answerQuality < lowerBoundX) { return 0; } else if (lowerBoundX <= answerQuality && answerQuality <= turningPoint) { - return multiplicator * (lowerSlope * answerQuality - ((lowerSlope * turningPoint) - turningPoint)); + return ( + multiplicator * + (lowerSlope * answerQuality - (lowerSlope * turningPoint - turningPoint)) + ); } else if (turningPoint < answerQuality && answerQuality <= upperBoundX) { - return multiplicator * (upperSlope * answerQuality - ((upperSlope * turningPoint) - turningPoint)); + return ( + multiplicator * + (upperSlope * answerQuality - (upperSlope * turningPoint - turningPoint)) + ); } else if (answerQuality > upperBoundX) { return multiplicator; } @@ -58,29 +72,38 @@ function calculateAchievedScore(answerQuality, questionType) { function getAnswerQuality(question) { let worstAnswerDifference = calculateWorstAnswerDifference(question); - if (question.type === 'multipleChoice' && question.userAnswer === question.answer) { + if ( + question.type === "multipleChoice" && + question.userAnswer === question.answer + ) { return 1; } - if (question.type === 'numberGuess' && worstAnswerDifference !== undefined) { - return 1 - (Math.abs(question.userAnswer - question.answer) / worstAnswerDifference); + if (question.type === "numberGuess" && worstAnswerDifference !== undefined) { + return ( + 1 - + Math.abs(question.userAnswer - question.answer) / worstAnswerDifference + ); } - if (question.type === 'mapPointGuess' && worstAnswerDifference !== undefined) { - return 1 - (question.userAnswer.distance / worstAnswerDifference); + if ( + question.type === "mapPointGuess" && + worstAnswerDifference !== undefined + ) { + return 1 - question.userAnswer.distance / worstAnswerDifference; } return 0; } function getScoreTitle(scorePercentage) { if (scorePercentage < 0.2) { - return '😱'; + return "😱"; } else if (scorePercentage < 0.5) { - return '😕'; + return "😕"; } else if (scorePercentage < 0.8) { - return '🙂'; + return "🙂"; } else if (scorePercentage < 1.0) { - return '👏👏👏'; + return "👏👏👏"; } else { - return '👏👏👏👏👏'; + return "👏👏👏👏👏"; } } @@ -88,14 +111,17 @@ function calculateScore(questions) { let score = { maxScore: 0, achievedScore: 0 - } + }; questions.forEach(question => { score.maxScore += constants.multiplicator[question.type]; if (question.userAnswer !== undefined) { - score.achievedScore += calculateAchievedScore(getAnswerQuality(question), question.type) + score.achievedScore += calculateAchievedScore( + getAnswerQuality(question), + question.type + ); } - }) + }); score.achievedScore = Math.round(score.achievedScore); if (score.achievedScore > score.maxScore) { diff --git a/resources/helpers/statsCalculators.js b/resources/helpers/statsCalculators.js index 5c1fc8f..c0a8e14 100644 --- a/resources/helpers/statsCalculators.js +++ b/resources/helpers/statsCalculators.js @@ -1,5 +1,4 @@ class NumberGuessStatsCalculator { - constructor(answersStats, correctAnswer, userAnswer) { this.answersStats = answersStats; this.correctAnswer = correctAnswer; @@ -11,41 +10,51 @@ class NumberGuessStatsCalculator { let numberOfSameAnswers; let diffPercentage; - let totalAnswers = this.answersStats.reduce((prev, current) => { return prev + current.count }, 0) + let totalAnswers = this.answersStats.reduce((prev, current) => { + return prev + current.count; + }, 0); if (this.userAnswer) { - betterThanCount = this.answersStats - .reduce((prev, current) => { - if (Math.abs(this.correctAnswer - current.value) > Math.abs(this.correctAnswer - this.userAnswer.value)) { - return prev + current.count - } - return prev - }, 0) - - numberOfSameAnswers = this.answersStats - .reduce((prev, current) => { - if (this.userAnswer.value === current.value) { - return prev + current.count - 1 - } - return prev - }, 0) - - diffPercentage = Math.abs(Math.round(Math.abs(this.correctAnswer - this.userAnswer.value) / this.correctAnswer * 100)) + betterThanCount = this.answersStats.reduce((prev, current) => { + if ( + Math.abs(this.correctAnswer - current.value) > + Math.abs(this.correctAnswer - this.userAnswer.value) + ) { + return prev + current.count; + } + return prev; + }, 0); + + numberOfSameAnswers = this.answersStats.reduce((prev, current) => { + if (this.userAnswer.value === current.value) { + return prev + current.count - 1; + } + return prev; + }, 0); + + diffPercentage = Math.abs( + Math.round( + (Math.abs(this.correctAnswer - this.userAnswer.value) / + this.correctAnswer) * + 100 + ) + ); } return { - betterThanPercentage: betterThanCount !== undefined ? Math.round(betterThanCount / totalAnswers * 100) : undefined, + betterThanPercentage: + betterThanCount !== undefined + ? Math.round((betterThanCount / totalAnswers) * 100) + : undefined, betterThanCount: betterThanCount, diffPercentage: diffPercentage, numberOfSameAnswers: numberOfSameAnswers, totalAnswers: totalAnswers - } + }; } - } class MultipleChoiceStatsCalculator { - constructor(answersStats, correctAnswer, userAnswer) { this.answersStats = answersStats; this.correctAnswer = correctAnswer; @@ -55,23 +64,22 @@ class MultipleChoiceStatsCalculator { getStats() { let numberOfAnswersPerChoice = {}; - let totalAnswers = this.answersStats.reduce((prev, current) => { return prev + current.count }, 0) + let totalAnswers = this.answersStats.reduce((prev, current) => { + return prev + current.count; + }, 0); - this.answersStats - .map(answer => { - numberOfAnswersPerChoice[answer.value] = answer.count - }) + this.answersStats.map(answer => { + numberOfAnswersPerChoice[answer.value] = answer.count; + }); return { totalAnswers: totalAnswers, numberOfAnswersPerChoice: numberOfAnswersPerChoice - } + }; } - } class MapPointGuessStatsCalculator { - constructor(answersStats, correctAnswer, userAnswer) { this.answersStats = answersStats; this.correctAnswer = correctAnswer; @@ -79,40 +87,39 @@ class MapPointGuessStatsCalculator { } getStats() { - let betterThanCount - let numberOfSameAnswers - let totalAnswers = this.answersStats.reduce((prev, current) => { return prev + current.count }, 0) + let betterThanCount; + let numberOfSameAnswers; + let totalAnswers = this.answersStats.reduce((prev, current) => { + return prev + current.count; + }, 0); if (this.userAnswer) { - betterThanCount = this.answersStats - .reduce((prev, current) => { - if (current.value > this.userAnswer.value.distance) { - return prev + current.count - } - return prev - }, 0) - - numberOfSameAnswers = this.answersStats - .reduce((prev, current) => { - if (this.userAnswer.value === current.value.distance) { - return prev + current.count - 1 - } - return prev - }, 0) + betterThanCount = this.answersStats.reduce((prev, current) => { + if (current.value > this.userAnswer.value.distance) { + return prev + current.count; + } + return prev; + }, 0); + + numberOfSameAnswers = this.answersStats.reduce((prev, current) => { + if (this.userAnswer.value === current.value.distance) { + return prev + current.count - 1; + } + return prev; + }, 0); } return { betterThanCount: betterThanCount, - betterThanPercentage: Math.floor(betterThanCount / totalAnswers * 100), + betterThanPercentage: Math.floor((betterThanCount / totalAnswers) * 100), numberOfSameAnswers: numberOfSameAnswers, totalAnswers: totalAnswers - } + }; } - } module.exports = { numberGuess: NumberGuessStatsCalculator, multipleChoice: MultipleChoiceStatsCalculator, mapPointGuess: MapPointGuessStatsCalculator -} +}; diff --git a/resources/helpers/utils.js b/resources/helpers/utils.js index c36fdb9..79e3683 100644 --- a/resources/helpers/utils.js +++ b/resources/helpers/utils.js @@ -1,24 +1,27 @@ -const quizDb = require('./db.js').quizDb; -const fetch = require('node-fetch'); +const quizDb = require("./db.js").quizDb; +const fetch = require("node-fetch"); function getAnswers(type, questionId, options) { - if (typeof options === 'undefined') { + if (typeof options === "undefined") { options = {}; } - if (type === 'numberGuess' || type === 'mapPointGuess' || type === 'multipleChoice') { - options['start_key'] = [questionId]; - options['end_key'] = [questionId,{}]; - options['group'] = true; + if ( + type === "numberGuess" || + type === "mapPointGuess" || + type === "multipleChoice" + ) { + options["start_key"] = [questionId]; + options["end_key"] = [questionId, {}]; + options["group"] = true; } - return quizDb.query('stats/answers-' + type, options); + return quizDb.query("stats/answers-" + type, options); } function getAnswer(id) { - return quizDb.get(id) - .then(answer => { - return answer.data - }); + return quizDb.get(id).then(answer => { + return answer.data; + }); } function getItem(itemId) { @@ -27,7 +30,7 @@ function getItem(itemId) { if (response.ok && response.status < 400) { return response.json(); } - throw(new Error(response)); + throw new Error(response); }) .then(item => { // if the type is set directly on the top level, we have a legacy datastructure @@ -51,23 +54,22 @@ function getItem(itemId) { delete item.type; item.data = { elements: elements - } + }; } return item; - }) + }); } function getNumberOfAnswers(questionId, options) { - if (typeof options === 'undefined') { + if (typeof options === "undefined") { options = {}; } - options['key'] = questionId; - options['reduce'] = true; + options["key"] = questionId; + options["reduce"] = true; - return quizDb.query('docs/byQuestionId', options) - .then(data => { - return data.rows[0].value - }) + return quizDb.query("docs/byQuestionId", options).then(data => { + return data.rows[0].value; + }); } function getPrecision(n) { @@ -82,4 +84,4 @@ module.exports = { getAnswer: getAnswer, getItem: getItem, getPrecision: getPrecision -} +}; diff --git a/resources/schema.json b/resources/schema.json index 8a4fa00..d047baa 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -17,30 +17,29 @@ "availabilityChecks": [ { "type": "ItemHasId", - "unavailableMessage": "Bitte speichere deinen Titel um fortzufahren." + "unavailableMessage": + "Bitte speichere deinen Titel um fortzufahren." } ] }, "items": { "oneOf": [ - {"$ref": "#/definitions/cover"}, - {"$ref": "#/definitions/multipleChoice"}, - {"$ref": "#/definitions/numberGuess"}, - {"$ref": "#/definitions/mapPointGuess"}, - {"$ref": "#/definitions/lastCard"} + { "$ref": "#/definitions/cover" }, + { "$ref": "#/definitions/multipleChoice" }, + { "$ref": "#/definitions/numberGuess" }, + { "$ref": "#/definitions/mapPointGuess" }, + { "$ref": "#/definitions/lastCard" } ] } } - }, - "required": [ - "title" - ], + }, + "required": ["title"], "definitions": { "cover": { "type": "object", "title": "Cover", "properties": { - "id": {"$ref": "#/definitions/elementId"}, + "id": { "$ref": "#/definitions/elementId" }, "type": { "type": "string", "default": "cover", @@ -60,7 +59,7 @@ "type": "object", "title": "Multiple Choice", "properties": { - "id": {"$ref": "#/definitions/elementId"}, + "id": { "$ref": "#/definitions/elementId" }, "type": { "type": "string", "default": "multipleChoice", @@ -73,7 +72,7 @@ "type": "string", "title": "Frage" }, - "image": {"$ref": "#/definitions/image"}, + "image": { "$ref": "#/definitions/image" }, "answer": { "type": "string", "title": "Korrekte Antwort" @@ -94,25 +93,25 @@ "type": "string", "title": "Ausformulierte Antwort", "Q:options": { - "placeholder": "Sagt dem Nutzer in Prosa, welche Antwort richtig ist und liefert idealerweise noch etwas Erklärung dazu." + "placeholder": + "Sagt dem Nutzer in Prosa, welche Antwort richtig ist und liefert idealerweise noch etwas Erklärung dazu." } }, "notes": { "type": "string", "title": "Anmerkungen" }, - "articleRecommendations": {"$ref": "#/definitions/articleRecommendations"} + "articleRecommendations": { + "$ref": "#/definitions/articleRecommendations" + } }, - "required":[ - "question", - "answer" - ] + "required": ["question", "answer"] }, "numberGuess": { "type": "object", "title": "Zahl schätzen", "properties": { - "id": {"$ref": "#/definitions/elementId"}, + "id": { "$ref": "#/definitions/elementId" }, "type": { "type": "string", "default": "numberGuess", @@ -125,7 +124,7 @@ "type": "string", "title": "Frage" }, - "image": {"$ref": "#/definitions/image"}, + "image": { "$ref": "#/definitions/image" }, "answer": { "type": "number", "title": "Korrekte Antwort" @@ -134,7 +133,8 @@ "type": "string", "title": "Einheit Plural", "Q:options": { - "placeholder": "Was für eine Zahl ist zu schätzen? Meter? Personen? Franken? etc." + "placeholder": + "Was für eine Zahl ist zu schätzen? Meter? Personen? Franken? etc." } }, "unit_singular": { @@ -156,35 +156,33 @@ "type": "number", "title": "Grösse der Zwischenschritte beim Verschieben des Sliders", "Q:options": { - "placeholder": "Sollte so gewählt sein, dass sich zwischen Minimal- und Maximalwert etwa 100-500 Zwischenschritte ergeben." + "placeholder": + "Sollte so gewählt sein, dass sich zwischen Minimal- und Maximalwert etwa 100-500 Zwischenschritte ergeben." } }, "answerText": { "type": "string", "title": "Ausformulierte Antwort", "Q:options": { - "placeholder": "Sagt dem Nutzer in Prosa, welche Antwort richtig ist und liefert idealerweise noch etwas Erklärung dazu." + "placeholder": + "Sagt dem Nutzer in Prosa, welche Antwort richtig ist und liefert idealerweise noch etwas Erklärung dazu." } }, "notes": { "type": "string", "title": "Anmerkungen" }, - "articleRecommendations": {"$ref": "#/definitions/articleRecommendations"} + "articleRecommendations": { + "$ref": "#/definitions/articleRecommendations" + } }, - "required": [ - "question", - "answer", - "min", - "max", - "step" - ] + "required": ["question", "answer", "min", "max", "step"] }, "mapPointGuess": { "type": "object", "title": "Ort schätzen", "properties": { - "id": {"$ref": "#/definitions/elementId"}, + "id": { "$ref": "#/definitions/elementId" }, "type": { "type": "string", "default": "mapPointGuess", @@ -208,16 +206,18 @@ "type": "string", "title": "Anmerkungen" }, - "image": {"$ref": "#/definitions/image"}, - "articleRecommendations": {"$ref": "#/definitions/articleRecommendations"} + "image": { "$ref": "#/definitions/image" }, + "articleRecommendations": { + "$ref": "#/definitions/articleRecommendations" + } }, - "required":[ "question" ] + "required": ["question"] }, "lastCard": { "type": "object", "title": "Letzte Seite", "properties": { - "id": {"$ref": "#/definitions/elementId"}, + "id": { "$ref": "#/definitions/elementId" }, "type": { "type": "string", "default": "lastCard", @@ -244,12 +244,15 @@ "type": "string", "title": "Abmoderation" }, - "articleRecommendations": {"$ref": "#/definitions/articleRecommendations"}, + "articleRecommendations": { + "$ref": "#/definitions/articleRecommendations" + }, "quizLink": { "type": "string", "title": "Link zu Anschlussquiz", "Q:options": { - "placeholder": "https://www.nzz.ch/... (am besten ein thematisch ähnliches Quiz, das ein ähnliches Zielpublikum ansprechen dürfte)" + "placeholder": + "https://www.nzz.ch/... (am besten ein thematisch ähnliches Quiz, das ein ähnliches Zielpublikum ansprechen dürfte)" } }, "quizTitle": { @@ -285,7 +288,7 @@ "title": "Artikel ID", "Q:options": { "placeholder": "ld.123456" - } + } } } } @@ -339,7 +342,7 @@ } } }, - "bbox": { + "bbox": { "oneOf": [ { "$ref": "#/definitions/bbox" @@ -364,14 +367,19 @@ "description": "A single position", "type": "array", "minItems": 2, - "items": [ { "type": "number" }, { "type": "number" } ], + "items": [{ "type": "number" }, { "type": "number" }], "additionalItems": false }, "bbox": { "description": "A bounding box", "type": "array", "minItems": 4, - "items": [ { "type": "number" }, { "type": "number" }, { "type": "number" }, { "type": "number" } ], + "items": [ + { "type": "number" }, + { "type": "number" }, + { "type": "number" }, + { "type": "number" } + ], "additionalItems": false } } diff --git a/routes/answer-service/answer.js b/routes/answer-service/answer.js index 192c13d..30b6128 100644 --- a/routes/answer-service/answer.js +++ b/routes/answer-service/answer.js @@ -1,33 +1,34 @@ -const Joi = require('joi'); -const Boom = require('boom'); -const quizDb = require('../../resources/helpers/db.js').quizDb; +const Joi = require("joi"); +const Boom = require("boom"); +const quizDb = require("../../resources/helpers/db.js").quizDb; module.exports = [ { - method: 'POST', - path: '/answer', + method: "POST", + path: "/answer", options: { - tags: ['api'], + tags: ["api"], payload: { - allow: ['application/json'] + allow: ["application/json"] }, validate: { payload: { - data: Joi.object().required(), + data: Joi.object().required() } }, cors: true }, handler: function(request, h) { var doc = request.payload; - if (typeof request.payload !== 'object') { + if (typeof request.payload !== "object") { doc = JSON.parse(request.payload); } // we want some properties on every document that goes into user-store doc.created_at = new Date().toISOString(); - - return quizDb.post(doc) + + return quizDb + .post(doc) .then(function(response) { if (response.ok) { return response; @@ -38,12 +39,12 @@ module.exports = [ .catch(function(couchError) { console.log(couchError); return Boom.badRequest(couchError.message); - }) - }, + }); + } /* plugins: { yaral: { buckets: ['maxPerIp'] } } */ } -] +]; diff --git a/routes/answer-service/map-point-guess.js b/routes/answer-service/map-point-guess.js index 9fe589a..139238f 100644 --- a/routes/answer-service/map-point-guess.js +++ b/routes/answer-service/map-point-guess.js @@ -1,12 +1,12 @@ -'use strict'; +"use strict"; -var heatmap = require('sparseheatmap'); +var heatmap = require("sparseheatmap"); heatmap.FILTER = heatmap.FILTERS.LOWPASS; -const Joi = require('joi'); -const Boom = require('boom'); +const Joi = require("joi"); +const Boom = require("boom"); -const quizDb = require('../../resources/helpers/db.js').quizDb; +const quizDb = require("../../resources/helpers/db.js").quizDb; // red-ish heatmap, not used for now but keep for future use @@ -25,66 +25,99 @@ const quizDb = require('../../resources/helpers/db.js').quizDb; module.exports = [ { - method: 'GET', - path: '/map/{questionId}/heatmap/{width}/{height}/{bbox}', + method: "GET", + path: "/map/{questionId}/heatmap/{width}/{height}/{bbox}", options: { - tags: ['api'], + tags: ["api"], validate: { params: { questionId: Joi.string().required(), width: Joi.number().required(), height: Joi.number().required(), - bbox: Joi.string().required(), + bbox: Joi.string().required() } } }, handler: async function(request, h) { var data = []; try { - const points = await quizDb.query('mapPointGuess/points', { 'reduce': false, 'keys': [request.params.questionId] }) + const points = await quizDb + .query("mapPointGuess/points", { + reduce: false, + keys: [request.params.questionId] + }) .then(answers => { if (answers && answers.rows && answers.rows.length) { return answers.rows.map(answer => answer.value); } - throw('invalid answer'); + throw "invalid answer"; }) .then(userPoints => { let points = []; - let bbox = request.params.bbox.split(',').map(val => parseFloat(val)); + let bbox = request.params.bbox + .split(",") + .map(val => parseFloat(val)); for (var i = 0; i < userPoints.length; i++) { - let lat = parseFloat(userPoints[i].lat) - let lng = parseFloat(userPoints[i].lng) - + let lat = parseFloat(userPoints[i].lat); + let lng = parseFloat(userPoints[i].lng); + let lngExtent = bbox[2] - bbox[0]; let fromLeft = lng - bbox[0]; - + let latExtent = bbox[3] - bbox[1]; let fromTop = bbox[3] - lat; - - let x = request.params.width * fromLeft / lngExtent; - - let mapLatBottomDegree = bbox[1] * Math.PI / 180; + + let x = (request.params.width * fromLeft) / lngExtent; + + let mapLatBottomDegree = (bbox[1] * Math.PI) / 180; let d = Math.PI / 180; - let mapWidth = ((request.params.width / lngExtent) * 360) / (2 * Math.PI); - let mapOffsetY = (mapWidth / 2 * Math.log((1 + Math.sin(mapLatBottomDegree)) / (1 - Math.sin(mapLatBottomDegree)))); - let y = request.params.height - ((mapWidth / 2 * Math.log((1 + Math.sin(lat * d)) / (1 - Math.sin(lat * d)))) - mapOffsetY); - - points = points.concat([1,x,y]); + let mapWidth = + ((request.params.width / lngExtent) * 360) / (2 * Math.PI); + let mapOffsetY = + (mapWidth / 2) * + Math.log( + (1 + Math.sin(mapLatBottomDegree)) / + (1 - Math.sin(mapLatBottomDegree)) + ); + let y = + request.params.height - + ((mapWidth / 2) * + Math.log((1 + Math.sin(lat * d)) / (1 - Math.sin(lat * d))) - + mapOffsetY); + + points = points.concat([1, x, y]); } return points; }); - data.push(new heatmap.SparseArray(heatmap.LAYOUTS.CENTERFIXEDWIDTH, request.params.width, request.params.height, points)); - - let heatmapStream = await Promise.resolve(new Promise(resolve => { - new heatmap(request.params.width, request.params.height, request.params.width, heatmap.LAYOUTS.CENTERFIXEDWIDTH, data, heatmap.BLOBTYPE.SMALL, function(heatmapStream) { - resolve(heatmapStream); + data.push( + new heatmap.SparseArray( + heatmap.LAYOUTS.CENTERFIXEDWIDTH, + request.params.width, + request.params.height, + points + ) + ); + + let heatmapStream = await Promise.resolve( + new Promise(resolve => { + new heatmap( + request.params.width, + request.params.height, + request.params.width, + heatmap.LAYOUTS.CENTERFIXEDWIDTH, + data, + heatmap.BLOBTYPE.SMALL, + function(heatmapStream) { + resolve(heatmapStream); + } + ); }) - })); - return h.response(heatmapStream).type('image/png'); + ); + return h.response(heatmapStream).type("image/png"); } catch (e) { - console.log(e) - return Boom.badRequest(e) + console.log(e); + return Boom.badRequest(e); } } } -] +]; diff --git a/routes/answer-service/number-guess.js b/routes/answer-service/number-guess.js index bd0f0ac..ac1f7c0 100644 --- a/routes/answer-service/number-guess.js +++ b/routes/answer-service/number-guess.js @@ -1,269 +1,291 @@ -'use strict'; +"use strict"; -const Boom = require('boom'); -const Joi = require('joi'); -const getAnswers = require('../../resources/helpers/utils.js').getAnswers; -const getItem = require('../../resources/helpers/utils.js').getItem; -const getPrecision = require('../../resources/helpers/utils.js').getPrecision; +const Boom = require("boom"); +const Joi = require("joi"); +const getAnswers = require("../../resources/helpers/utils.js").getAnswers; +const getItem = require("../../resources/helpers/utils.js").getItem; +const getPrecision = require("../../resources/helpers/utils.js").getPrecision; -var jsdom = require('jsdom'); +var jsdom = require("jsdom"); let d3 = { - scaleLinear: require('d3-scale').scaleLinear, - scaleBand: require('d3-scale').scaleBand, - max: require('d3-array').max, - select: require('d3-selection').select -} + scaleLinear: require("d3-scale").scaleLinear, + scaleBand: require("d3-scale").scaleBand, + max: require("d3-array").max, + select: require("d3-selection").select +}; function getStripplotSvg(data, stats, plotWidth) { return new Promise((resolve, reject) => { // in order to work with jsdom like that let jsdom version at 9.5 // should be rebuilt and upgraded in the future jsdom.env({ - html: '', - features: { QuerySelector:true }, //you need query selector for D3 to work + html: "", + features: { QuerySelector: true }, //you need query selector for D3 to work done: (errors, window) => { - if (errors) { reject(errors); return; } if (!stats) { - reject('stats is undefined') + reject("stats is undefined"); return; } - let margin = {top: 0, right: 0, bottom: 30, left: 0}, - width = plotWidth - margin.left - margin.right, - height = 90 - margin.top - margin.bottom; - - let xScale = d3.scaleLinear() - .domain([data.min, data.max]) - .range([0, width]); - - let yScale = d3.scaleLinear() - .domain([0, d3.max(stats, d => parseInt(d.count))]) - .range([height, 0]); - - let element = window.document.createElement('div'); - - let svg = d3.select(element).append('svg') - .datum(stats) - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + let margin = { top: 0, right: 0, bottom: 30, left: 0 }, + width = plotWidth - margin.left - margin.right, + height = 90 - margin.top - margin.bottom; + + let xScale = d3 + .scaleLinear() + .domain([data.min, data.max]) + .range([0, width]); + + let yScale = d3 + .scaleLinear() + .domain([0, d3.max(stats, d => parseInt(d.count))]) + .range([height, 0]); + + let element = window.document.createElement("div"); + + let svg = d3 + .select(element) + .append("svg") + .datum(stats) + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr( + "transform", + "translate(" + margin.left + "," + margin.top + ")" + ); svg - .append('rect') - .attr('x', 0) - .attr('y', margin.top) - .attr('width', width) - .attr('height', 60) - .attr('fill', 'transparent') + .append("rect") + .attr("x", 0) + .attr("y", margin.top) + .attr("width", width) + .attr("height", 60) + .attr("fill", "transparent"); svg - .append('text') - .text(data.min) - .attr('class', 's-font-note-s s-font-note-s--light') - .attr('text-anchor', 'start') - .attr('x', 3) - .attr('y', margin.top + 60) - .attr('dy', '1em') + .append("text") + .text(data.min) + .attr("class", "s-font-note-s s-font-note-s--light") + .attr("text-anchor", "start") + .attr("x", 3) + .attr("y", margin.top + 60) + .attr("dy", "1em"); svg - .append('line') - .attr('class', 's-color-gray-6 q-quiz-answer-chart-min-line') - .attr('x1', 0) - .attr('x2', 0) - .attr('y1', margin.top + height + 2) - .attr('y2', margin.top + height + 8) + .append("line") + .attr("class", "s-color-gray-6 q-quiz-answer-chart-min-line") + .attr("x1", 0) + .attr("x2", 0) + .attr("y1", margin.top + height + 2) + .attr("y2", margin.top + height + 8); svg - .append('text') - .text(data.max) - .attr('class', 's-font-note-s s-font-note-s--light') - .attr('text-anchor', 'end') - .attr('x', width - 3) - .attr('y', margin.top + 60) - .attr('dy', '1em') + .append("text") + .text(data.max) + .attr("class", "s-font-note-s s-font-note-s--light") + .attr("text-anchor", "end") + .attr("x", width - 3) + .attr("y", margin.top + 60) + .attr("dy", "1em"); svg - .append('line') - .attr('class', 's-color-gray-6 q-quiz-answer-chart-max-line') - .attr('x1', width) - .attr('x2', width) - .attr('y1', margin.top + height + 2) - .attr('y2', margin.top + height + 8) - - let lines = svg.selectAll('line.q-quiz-answer-chart-line') + .append("line") + .attr("class", "s-color-gray-6 q-quiz-answer-chart-max-line") + .attr("x1", width) + .attr("x2", width) + .attr("y1", margin.top + height + 2) + .attr("y2", margin.top + height + 8); + + let lines = svg + .selectAll("line.q-quiz-answer-chart-line") .data(stats) .enter() - .append('line') - .attr('class', 'q-quiz-answer-chart-line s-color-gray-8') - .attr('style', d => { return `opacity: ${0.1 * d.count > 0.9 ? 0.9 : 0.1 * d.count}`}) - .attr('x1', d => xScale(d.value)) - .attr('x2', d => xScale(d.value)) - .attr('y1', margin.top) - .attr('y2', margin.top + 60) - - - resolve(element.innerHTML) + .append("line") + .attr("class", "q-quiz-answer-chart-line s-color-gray-8") + .attr("style", d => { + return `opacity: ${0.1 * d.count > 0.9 ? 0.9 : 0.1 * d.count}`; + }) + .attr("x1", d => xScale(d.value)) + .attr("x2", d => xScale(d.value)) + .attr("y1", margin.top) + .attr("y2", margin.top + 60); + + resolve(element.innerHTML); } - - }) - }) + }); + }); } function getBarchartSvg(data, stats, chartWidth) { return new Promise((resolve, reject) => { jsdom.env({ - html: '', - features: { QuerySelector:true }, //you need query selector for D3 to work - done: (errors, window) => { + html: "", + features: { QuerySelector: true }, //you need query selector for D3 to work + done: (errors, window) => { + if (errors) { + reject(errors); + return; + } - if (errors) { - reject(errors); - return; - } + if (!stats) { + reject("stats is undefined"); + return; + } - if (!stats) { - reject('stats is undefined') - return; - } + let margin = { top: 0, right: 0, bottom: 30, left: 0 }, + width = chartWidth - margin.left - margin.right, + height = 90 - margin.top - margin.bottom; - let margin = {top: 0, right: 0, bottom: 30, left: 0}, - width = chartWidth - margin.left - margin.right, - height = 90 - margin.top - margin.bottom; + let precision = getPrecision(data.step); - let precision = getPrecision(data.step) + let xDomain = []; + for ( + let i = data.min; + i <= data.max; + i = parseFloat((i + data.step).toFixed(precision)) + ) { + xDomain.push(i); + } - let xDomain = []; - for (let i = data.min; i <= data.max; i = parseFloat((i + data.step).toFixed(precision))) { - xDomain.push(i) - } + let xScale = d3 + .scaleBand() + .domain(xDomain) + // we probably want to have this padding, its removed to have it easier on the client to calculate the position for the overlaying correct and users answer + // .paddingInner(0.1) + .range([0, width]); + + let yScale = d3 + .scaleLinear() + .domain([ + 0, + Math.max(200 / stats.length, d3.max(stats, d => parseInt(d.count))) + ]) + .range([height, 0]); + + let element = window.document.createElement("div"); + + let svg = d3 + .select(element) + .append("svg") + .datum(stats) + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr( + "transform", + "translate(" + margin.left + "," + margin.top + ")" + ); - let xScale = d3.scaleBand() - .domain(xDomain) - // we probably want to have this padding, its removed to have it easier on the client to calculate the position for the overlaying correct and users answer - // .paddingInner(0.1) - .range([0, width]); - - let yScale = d3.scaleLinear() - .domain([0, Math.max(200/stats.length, d3.max(stats, d => parseInt(d.count)))]) - .range([height, 0]); - - let element = window.document.createElement('div'); - - let svg = d3.select(element).append('svg') - .datum(stats) - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - svg - .append('rect') - .attr('x', 0) - .attr('y', margin.top) - .attr('width', width) - .attr('height', 60) - .attr('fill', 'transparent') - - svg - .append('text') - .text(data.min) - .attr('class', 's-font-note-s s-font-note-s--light') - .attr('text-anchor', 'start') - .attr('x', 3) - .attr('y', margin.top + 60) - .attr('dy', '1em') - svg - .append('line') - .attr('class', 's-color-gray-6 q-quiz-answer-chart-min-line') - .attr('x1', 0) - .attr('x2', 0) - .attr('y1', margin.top + height + 2) - .attr('y2', margin.top + height + 8) - - svg - .append('text') - .text(data.max) - .attr('class', 's-font-note-s s-font-note-s--light') - .attr('text-anchor', 'end') - .attr('x', width - 3) - .attr('y', margin.top + 60) - .attr('dy', '1em') - svg - .append('line') - .attr('class', 's-color-gray-6 q-quiz-answer-chart-max-line') - .attr('x1', width) - .attr('x2', width) - .attr('y1', margin.top + height + 2) - .attr('y2', margin.top + height + 8) - - let bars = svg.selectAll(".bar") - .data(stats) - .enter().append("rect") - .attr("class", "s-color-gray-4") - .attr("fill", "currentColor") - .attr("x", function(d) { - return xScale(d.value); - }) - .attr("width", xScale.bandwidth()) - .attr("y", function(d) { - return yScale(d.count); - }) - .attr("height", function(d) { - return Math.max(1, height - yScale(d.count)); - }); - - - resolve(element.innerHTML) - } - }) - }) + svg + .append("rect") + .attr("x", 0) + .attr("y", margin.top) + .attr("width", width) + .attr("height", 60) + .attr("fill", "transparent"); + + svg + .append("text") + .text(data.min) + .attr("class", "s-font-note-s s-font-note-s--light") + .attr("text-anchor", "start") + .attr("x", 3) + .attr("y", margin.top + 60) + .attr("dy", "1em"); + svg + .append("line") + .attr("class", "s-color-gray-6 q-quiz-answer-chart-min-line") + .attr("x1", 0) + .attr("x2", 0) + .attr("y1", margin.top + height + 2) + .attr("y2", margin.top + height + 8); + + svg + .append("text") + .text(data.max) + .attr("class", "s-font-note-s s-font-note-s--light") + .attr("text-anchor", "end") + .attr("x", width - 3) + .attr("y", margin.top + 60) + .attr("dy", "1em"); + svg + .append("line") + .attr("class", "s-color-gray-6 q-quiz-answer-chart-max-line") + .attr("x1", width) + .attr("x2", width) + .attr("y1", margin.top + height + 2) + .attr("y2", margin.top + height + 8); + + let bars = svg + .selectAll(".bar") + .data(stats) + .enter() + .append("rect") + .attr("class", "s-color-gray-4") + .attr("fill", "currentColor") + .attr("x", function(d) { + return xScale(d.value); + }) + .attr("width", xScale.bandwidth()) + .attr("y", function(d) { + return yScale(d.count); + }) + .attr("height", function(d) { + return Math.max(1, height - yScale(d.count)); + }); + + resolve(element.innerHTML); + } + }); + }); } module.exports = [ { - method: 'GET', - path: '/number-guess/{itemId}/{questionId}/plot/{width}', + method: "GET", + path: "/number-guess/{itemId}/{questionId}/plot/{width}", options: { - tags: ['api'], + tags: ["api"], validate: { params: { itemId: Joi.string().required(), questionId: Joi.string().required(), - width: Joi.number().required(), + width: Joi.number().required() } }, cors: true }, handler: async function(request, h) { - return await Promise.all([getItem(request.params.itemId), getAnswers('numberGuess', request.params.questionId)]) + return await Promise.all([ + getItem(request.params.itemId), + getAnswers("numberGuess", request.params.questionId) + ]) .then(data => { let item = data[0]; let answers = data[1]; let stats; if (answers.rows && answers.rows.length > 0) { - stats = answers.rows - .map(row => { - return { - value: row.key[1], - count: row.value - } - }) + stats = answers.rows.map(row => { + return { + value: row.key[1], + count: row.value + }; + }); } - let question = item.elements - .filter(element => { - return element.id === request.params.questionId - })[0] + let question = item.elements.filter(element => { + return element.id === request.params.questionId; + })[0]; return { stats: stats, question: question - } + }; }) .then(async data => { let stats = data.stats; @@ -275,30 +297,37 @@ module.exports = [ let numberOfPossibleAnswers = 0; for (let i = question.min; i <= question.max; i = i + question.step) { - numberOfPossibleAnswers++ + numberOfPossibleAnswers++; } if (numberOfPossibleAnswers <= 100) { try { - return await getBarchartSvg(question, stats, request.params.width); + return await getBarchartSvg( + question, + stats, + request.params.width + ); } catch (errMessage) { - console.log(errMessage) + console.log(errMessage); return Boom.badRequest(errMessage); } } else { try { - return await getStripplotSvg(question, stats, request.params.width); + return await getStripplotSvg( + question, + stats, + request.params.width + ); } catch (errMessage) { - console.log(errMessage) + console.log(errMessage); return Boom.badRequest(errMessage); } } - }) .catch(couchError => { - console.log(couchError) + console.log(couchError); return Boom.badRequest(couchError.message); - }) + }); } } -] +]; diff --git a/routes/answer-service/score.js b/routes/answer-service/score.js index fc265e2..b8db44d 100644 --- a/routes/answer-service/score.js +++ b/routes/answer-service/score.js @@ -1,28 +1,33 @@ -const fs = require('fs'); -const Enjoi = require('enjoi'); -const Joi = require('joi'); -const Boom = require('boom'); -const resourcesDir = __dirname + '/../../resources/'; +const fs = require("fs"); +const Enjoi = require("enjoi"); +const Joi = require("joi"); +const Boom = require("boom"); +const resourcesDir = __dirname + "/../../resources/"; const scoreHelpers = require(`${resourcesDir}helpers/scoreHelpers.js`); -const questionTypes = require(`${resourcesDir}helpers/constants.js`).questionTypes; -const answerDb = require('../../resources/helpers/db.js').quizDb; +const questionTypes = require(`${resourcesDir}helpers/constants.js`) + .questionTypes; +const answerDb = require("../../resources/helpers/db.js").quizDb; -const schemaString = JSON.parse(fs.readFileSync(`${resourcesDir}schema.json`, { - encoding: 'utf-8' -})); +const schemaString = JSON.parse( + fs.readFileSync(`${resourcesDir}schema.json`, { + encoding: "utf-8" + }) +); const schema = Enjoi(schemaString).required(); module.exports = { - method: 'POST', - path: '/score', + method: "POST", + path: "/score", options: { validate: { payload: { item: schema, - userAnswers: Joi.array().items(Joi.object({ - questionId: Joi.string() - })) + userAnswers: Joi.array().items( + Joi.object({ + questionId: Joi.string() + }) + ) }, options: { allowUnknown: true @@ -39,16 +44,18 @@ module.exports = { const userAnswers = request.payload.userAnswers; questions.forEach(question => { if (userAnswers) { - const relevantAnswers = userAnswers.filter(userAnswer => userAnswer.questionId === question.id); + const relevantAnswers = userAnswers.filter( + userAnswer => userAnswer.questionId === question.id + ); if (relevantAnswers.length > 0) { question.userAnswer = relevantAnswers[0].value; } } - }) + }); return scoreHelpers.calculateScore(questions); } catch (e) { return Boom.internal(e); } } -} +}; diff --git a/routes/answer-service/stats.js b/routes/answer-service/stats.js index 5472b01..b02e0ee 100644 --- a/routes/answer-service/stats.js +++ b/routes/answer-service/stats.js @@ -1,27 +1,30 @@ -'use strict'; +"use strict"; -const Joi = require('joi'); -const Boom = require('boom'); +const Joi = require("joi"); +const Boom = require("boom"); -const getAnswers = require('../../resources/helpers/utils.js').getAnswers; -const getNumberOfAnswers = require('../../resources/helpers/utils.js').getNumberOfAnswers; -const getAnswer = require('../../resources/helpers/utils.js').getAnswer; -const getItem = require('../../resources/helpers/utils.js').getItem; +const getAnswers = require("../../resources/helpers/utils.js").getAnswers; +const getNumberOfAnswers = require("../../resources/helpers/utils.js") + .getNumberOfAnswers; +const getAnswer = require("../../resources/helpers/utils.js").getAnswer; +const getItem = require("../../resources/helpers/utils.js").getItem; -const statsCalculators = require('../../resources/helpers/statsCalculators.js'); +const statsCalculators = require("../../resources/helpers/statsCalculators.js"); module.exports = [ { - method: 'GET', - path: '/stats/answers/{type}/{itemId}/{questionId}/{answerId?}', + method: "GET", + path: "/stats/answers/{type}/{itemId}/{questionId}/{answerId?}", options: { - tags: ['api'], + tags: ["api"], validate: { params: { - type: Joi.any().valid(Object.keys(statsCalculators)).required(), + type: Joi.any() + .valid(Object.keys(statsCalculators)) + .required(), itemId: Joi.string().required(), questionId: Joi.string().required(), - answerId: Joi.string().optional(), + answerId: Joi.string().optional() } }, cors: true @@ -30,9 +33,11 @@ module.exports = [ try { let options = {}; - let validQueryOptions = Object.keys(request.query).filter(function(optionName) { + let validQueryOptions = Object.keys(request.query).filter(function( + optionName + ) { return validCouchDBViewOptions.indexOf(optionName) >= 0; - }) + }); validQueryOptions.forEach(function(optionName) { options[optionName] = request.query[optionName]; @@ -40,11 +45,11 @@ module.exports = [ let dataPromises = [ getAnswers(request.params.type, request.params.questionId, options), - getItem(request.params.itemId), - ] + getItem(request.params.itemId) + ]; if (request.params.answerId !== undefined) { - dataPromises.push(getAnswer(request.params.answerId)) + dataPromises.push(getAnswer(request.params.answerId)); } return await Promise.all(dataPromises) @@ -58,34 +63,36 @@ module.exports = [ let answersStats = []; if (answers.rows && answers.rows.length > 0) { - answersStats = answers.rows - .map(row => { - return { - value: row.key[1], - count: row.value - } - }) + answersStats = answers.rows.map(row => { + return { + value: row.key[1], + count: row.value + }; + }); } - let question = item.elements - .filter(quizElement => { - return quizElement.id === request.params.questionId - })[0] + let question = item.elements.filter(quizElement => { + return quizElement.id === request.params.questionId; + })[0]; - let statsCalculator = new statsCalculators[request.params.type](answersStats, question.answer, userAnswer); + let statsCalculator = new statsCalculators[request.params.type]( + answersStats, + question.answer, + userAnswer + ); let stats = statsCalculator.getStats(); return stats; }) .catch(couchError => { - console.log('error', couchError) + console.log("error", couchError); return Boom.badRequest(couchError.message); - }) - } catch(e) { - console.log('error in stats route: ' + e); + }); + } catch (e) { + console.log("error in stats route: " + e); return Boom.badRequest(e); } } } -] +]; diff --git a/routes/health.js b/routes/health.js index c461eb4..15da71a 100644 --- a/routes/health.js +++ b/routes/health.js @@ -1,10 +1,10 @@ module.exports = { - path: '/health', - method: 'GET', + path: "/health", + method: "GET", options: { - tags: ['api', 'health'] + tags: ["api", "health"] }, handler: (request, h) => { - return 'ok'; + return "ok"; } -} +}; diff --git a/routes/routes.js b/routes/routes.js index 36cc298..47e67b7 100644 --- a/routes/routes.js +++ b/routes/routes.js @@ -1,16 +1,15 @@ module.exports = [ - require('./rendering-info/html-js.js'), - require('./stylesheet.js'), - require('./schema.js'), - require('./locales.js'), - require('./health.js'), - require('./fixtures/data.js') -] -.concat( - require('./scripts.js'), - require('./answer-service/answer.js'), - require('./answer-service/stats.js'), - require('./answer-service/map-point-guess.js'), - require('./answer-service/number-guess.js'), - require('./answer-service/score.js') -) + require("./rendering-info/html-js.js"), + require("./stylesheet.js"), + require("./schema.js"), + require("./locales.js"), + require("./health.js"), + require("./fixtures/data.js") +].concat( + require("./scripts.js"), + require("./answer-service/answer.js"), + require("./answer-service/stats.js"), + require("./answer-service/map-point-guess.js"), + require("./answer-service/number-guess.js"), + require("./answer-service/score.js") +); diff --git a/routes/schema.js b/routes/schema.js index 7d0e63d..e714dba 100644 --- a/routes/schema.js +++ b/routes/schema.js @@ -1,9 +1,9 @@ -const resourcesDir = __dirname + '/../resources/'; +const resourcesDir = __dirname + "/../resources/"; module.exports = { - method: 'GET', - path:'/schema.json', + method: "GET", + path: "/schema.json", handler: function(request, h) { - return h.file(resourcesDir + 'schema.json'); + return h.file(resourcesDir + "schema.json"); } -} +}; diff --git a/routes/scripts.js b/routes/scripts.js index 5cb8628..87a667e 100644 --- a/routes/scripts.js +++ b/routes/scripts.js @@ -1,20 +1,20 @@ -const path = require('path'); +const path = require("path"); module.exports = [ { - method: 'GET', - path: '/script/{filename}.{hash}.{extension}', + method: "GET", + path: "/script/{filename}.{hash}.{extension}", options: { cors: true, files: { - relativeTo: path.join(__dirname, '/../scripts/') + relativeTo: path.join(__dirname, "/../scripts/") } }, handler: function(request, h) { - return h.file(`${request.params.filename}.${request.params.extension}`) - .type('text/javascript') - .header('cache-control', `max-age=${60 * 60 * 24 * 365}, immutable`); // 1 year + return h + .file(`${request.params.filename}.${request.params.extension}`) + .type("text/javascript") + .header("cache-control", `max-age=${60 * 60 * 24 * 365}, immutable`); // 1 year } } ]; - diff --git a/routes/stylesheet.js b/routes/stylesheet.js index c36fb45..ab0c892 100644 --- a/routes/stylesheet.js +++ b/routes/stylesheet.js @@ -1,17 +1,18 @@ -const path = require('path'); +const path = require("path"); module.exports = { - method: 'GET', - path: '/stylesheet/{filename}.{hash}.{extension}', + method: "GET", + path: "/stylesheet/{filename}.{hash}.{extension}", options: { cors: true, files: { - relativeTo: path.join(__dirname, '/../styles/') + relativeTo: path.join(__dirname, "/../styles/") } }, handler: function(request, h) { - return h.file(`${request.params.filename}.${request.params.extension}`) - .type('text/css') - .header('cache-control', `max-age=${60 * 60 * 24 * 365}, immutable`); // 1 year + return h + .file(`${request.params.filename}.${request.params.extension}`) + .type("text/css") + .header("cache-control", `max-age=${60 * 60 * 24 * 365}, immutable`); // 1 year } }; diff --git a/script_src/AnswerStore.js b/script_src/AnswerStore.js index 2d39cf8..144e733 100644 --- a/script_src/AnswerStore.js +++ b/script_src/AnswerStore.js @@ -1,40 +1,43 @@ export default class AnswerStore { - constructor(toolBaseUrl) { this.toolBaseUrl = toolBaseUrl; } saveAnswer(answer) { return fetch(`${this.toolBaseUrl}/answer`, { - method: 'POST', + method: "POST", headers: { - 'content-type': 'application/json' + "content-type": "application/json" }, body: JSON.stringify({ data: answer }) }) - .then(response => { - return response.json() - }) - .then(data => { - return data; - }) + .then(response => { + return response.json(); + }) + .then(data => { + return data; + }); } getStats(itemId, questionData, answerId = undefined) { - if (questionData.type === 'numberGuess' || questionData.type === 'mapPointGuess' || questionData.type === 'multipleChoice') { - let statsServiceUrl = `${this.toolBaseUrl}/stats/answers/${questionData.type}/${itemId}/${questionData.id}` + if ( + questionData.type === "numberGuess" || + questionData.type === "mapPointGuess" || + questionData.type === "multipleChoice" + ) { + let statsServiceUrl = `${this.toolBaseUrl}/stats/answers/${ + questionData.type + }/${itemId}/${questionData.id}`; if (answerId !== undefined) { - statsServiceUrl += `/${answerId}` + statsServiceUrl += `/${answerId}`; } - return fetch(statsServiceUrl) - .then(response => { - return response.json() - }) + return fetch(statsServiceUrl).then(response => { + return response.json(); + }); } else { - return Promise.resolve(undefined) + return Promise.resolve(undefined); } } - } diff --git a/script_src/MultiQuizPositionHandler.js b/script_src/MultiQuizPositionHandler.js index 1ba6619..6f5e0ad 100644 --- a/script_src/MultiQuizPositionHandler.js +++ b/script_src/MultiQuizPositionHandler.js @@ -1,11 +1,14 @@ -import MapPointGuessHandler from './MapPointGuessHandler.js'; +import MapPointGuessHandler from "./MapPointGuessHandler.js"; export default class MultiQuizPositionHandler { - constructor(quizRootElement, data) { - this.quizContainerElement = quizRootElement.querySelector('.q-quiz-multi-container'); - this.headerElement = quizRootElement.querySelector('.q-quiz-multi-header'); - this.quizElements = this.quizContainerElement.querySelectorAll('.q-quiz-element-container'); + this.quizContainerElement = quizRootElement.querySelector( + ".q-quiz-multi-container" + ); + this.headerElement = quizRootElement.querySelector(".q-quiz-multi-header"); + this.quizElements = this.quizContainerElement.querySelectorAll( + ".q-quiz-element-container" + ); this.questionElementData = data.questionElementData; this.numberElements = data.numberElements; @@ -14,7 +17,7 @@ export default class MultiQuizPositionHandler { this.isFinalScoreShown = data.isFinalScoreShown; this.position = 0; } - + getPosition() { return this.position; } @@ -30,16 +33,29 @@ export default class MultiQuizPositionHandler { } setQuizElement() { - this.quizContainerElement.style.transform = `translateX(${this.position * -100 / this.numberElements}%)`; - this.quizContainerElement.style.webkitTransform = `translateX(${this.position * -100 / this.numberElements}%)`; + this.quizContainerElement.style.transform = `translateX(${(this.position * + -100) / + this.numberElements}%)`; + this.quizContainerElement.style.webkitTransform = `translateX(${(this + .position * + -100) / + this.numberElements}%)`; for (let i = 0; i < this.quizElements.length; i++) { if (i === this.position) { - this.quizElements.item(i).classList.remove('q-quiz-element-container--is-inactive'); - this.quizElements.item(i).classList.add('q-quiz-element-container--is-active'); + this.quizElements + .item(i) + .classList.remove("q-quiz-element-container--is-inactive"); + this.quizElements + .item(i) + .classList.add("q-quiz-element-container--is-active"); } else { - this.quizElements.item(i).classList.remove('q-quiz-element-container--is-active'); - this.quizElements.item(i).classList.add('q-quiz-element-container--is-inactive'); + this.quizElements + .item(i) + .classList.remove("q-quiz-element-container--is-active"); + this.quizElements + .item(i) + .classList.add("q-quiz-element-container--is-inactive"); } } } @@ -50,12 +66,15 @@ export default class MultiQuizPositionHandler { this.changeButtonText(questionNumber); this.setButtonVisibility(); - if (this.headerElement.querySelector('.q-quiz-header__title').textContent === '' && this.isHeaderButtonHidden()) { - this.headerElement.classList.add('q-quiz-header--is-empty'); + if ( + this.headerElement.querySelector(".q-quiz-header__title").textContent === + "" && + this.isHeaderButtonHidden() + ) { + this.headerElement.classList.add("q-quiz-header--is-empty"); } else { - this.headerElement.classList.remove('q-quiz-header--is-empty'); + this.headerElement.classList.remove("q-quiz-header--is-empty"); } - } getQuestionNumber() { @@ -67,44 +86,64 @@ export default class MultiQuizPositionHandler { } setTitle(questionNumber) { - let title = ''; - if (!this.quizElements.item(this.position).classList.contains('q-quiz__cover') && !this.quizElements.item(this.position).classList.contains('q-quiz__last-card')) { + let title = ""; + if ( + !this.quizElements + .item(this.position) + .classList.contains("q-quiz__cover") && + !this.quizElements + .item(this.position) + .classList.contains("q-quiz__last-card") + ) { if (questionNumber === this.numberQuestions) { - title = 'letzte Frage'; + title = "letzte Frage"; } else { title = `Frage ${questionNumber}/${this.numberQuestions}`; } } else if (this.position === this.numberElements - 1) { if (this.isFinalScoreShown) { - title = 'Auswertung'; + title = "Auswertung"; } else { - title = 'Fertig!'; + title = "Fertig!"; } } - this.headerElement.querySelector('.q-quiz-header__title').textContent = title; + this.headerElement.querySelector( + ".q-quiz-header__title" + ).textContent = title; } changeButtonText(questionNumber) { if (questionNumber === this.numberQuestions) { if (this.isFinalScoreShown) { - this.headerElement.querySelector('.q-quiz-button__content span').textContent = 'zur Auswertung'; + this.headerElement.querySelector( + ".q-quiz-button__content span" + ).textContent = + "zur Auswertung"; } else { - this.headerElement.querySelector('.q-quiz-button__content span').textContent = 'Thema vertiefen'; + this.headerElement.querySelector( + ".q-quiz-button__content span" + ).textContent = + "Thema vertiefen"; } - } + } } isHeaderButtonHidden() { - return this.headerElement.querySelector('.q-quiz-button').classList.contains('q-quiz-button--hidden'); + return this.headerElement + .querySelector(".q-quiz-button") + .classList.contains("q-quiz-button--hidden"); } setButtonVisibility() { const isButtonHidden = this.isHeaderButtonHidden(); if (this.position < this.numberElements - 1 && isButtonHidden) { - this.headerElement.querySelector('.q-quiz-button').classList.remove('q-quiz-button--hidden'); + this.headerElement + .querySelector(".q-quiz-button") + .classList.remove("q-quiz-button--hidden"); } else if (this.position === this.numberElements - 1) { - this.headerElement.querySelector('.q-quiz-button').classList.add('q-quiz-button--hidden'); + this.headerElement + .querySelector(".q-quiz-button") + .classList.add("q-quiz-button--hidden"); } } - } diff --git a/script_src/MultipleChoiceHandler.js b/script_src/MultipleChoiceHandler.js index b8b7a04..07a8cfa 100644 --- a/script_src/MultipleChoiceHandler.js +++ b/script_src/MultipleChoiceHandler.js @@ -1,8 +1,7 @@ -import * as answerHelpers from './answerHelpers.js'; +import * as answerHelpers from "./answerHelpers.js"; export default class MultipleChoiceHandler { - - constructor (questionElement, data, quizId, toolBaseUrl) { + constructor(questionElement, data, quizId, toolBaseUrl) { this.questionElement = questionElement; this.data = data; this.quizId = quizId; @@ -14,97 +13,117 @@ export default class MultipleChoiceHandler { } renderResult(answer) { - const checkmark = ''; - const crossmark = ''; + const checkmark = + ''; + const crossmark = + ''; this.choices = []; - this.questionElement.querySelectorAll('.q-quiz-result__answer > span').forEach(element => { - this.choices.push(element.textContent); - let parentNode = element.parentNode; - let bar = parentNode.querySelector('.q-quiz-result__multiple-choice-bar'); + this.questionElement + .querySelectorAll(".q-quiz-result__answer > span") + .forEach(element => { + this.choices.push(element.textContent); + let parentNode = element.parentNode; + let bar = parentNode.querySelector( + ".q-quiz-result__multiple-choice-bar" + ); - // is answer element the correct answer - if (element.textContent === this.data.correctAnswer) { - element.classList.add('s-font-note--strong'); - element.classList.add('s-color-positive'); + // is answer element the correct answer + if (element.textContent === this.data.correctAnswer) { + element.classList.add("s-font-note--strong"); + element.classList.add("s-color-positive"); - let correctAnswerElement = document.createElement('span'); - correctAnswerElement.classList.add('s-color-positive'); - correctAnswerElement.innerText = 'korrekte Antwort'; - parentNode.insertBefore(correctAnswerElement, element.nextSibling); + let correctAnswerElement = document.createElement("span"); + correctAnswerElement.classList.add("s-color-positive"); + correctAnswerElement.innerText = "korrekte Antwort"; + parentNode.insertBefore(correctAnswerElement, element.nextSibling); - bar.classList.add('s-font-note-s--strong'); - bar.classList.add('s-color-positive'); + bar.classList.add("s-font-note-s--strong"); + bar.classList.add("s-color-positive"); - // is user input correct - if (this.data.correctAnswer === answer) { - let checkmarkElement = document.createElement('span'); - checkmarkElement.innerHTML = checkmark; - parentNode.insertBefore(checkmarkElement, element); - } - } else { - // is user input not correct - if (element.textContent === answer) { - let crossmarkElement = document.createElement('span'); - crossmarkElement.innerHTML = crossmark; - parentNode.insertBefore(crossmarkElement, element); - element.classList.add('s-font-note--strong'); - element.classList.add('s-color-negative'); + // is user input correct + if (this.data.correctAnswer === answer) { + let checkmarkElement = document.createElement("span"); + checkmarkElement.innerHTML = checkmark; + parentNode.insertBefore(checkmarkElement, element); + } + } else { + // is user input not correct + if (element.textContent === answer) { + let crossmarkElement = document.createElement("span"); + crossmarkElement.innerHTML = crossmark; + parentNode.insertBefore(crossmarkElement, element); + element.classList.add("s-font-note--strong"); + element.classList.add("s-color-negative"); - let wrongAnswerElement = document.createElement('span'); - wrongAnswerElement.classList.add('s-color-negative'); - wrongAnswerElement.innerText = 'falsche Antwort'; - parentNode.insertBefore(wrongAnswerElement, element.nextSibling); + let wrongAnswerElement = document.createElement("span"); + wrongAnswerElement.classList.add("s-color-negative"); + wrongAnswerElement.innerText = "falsche Antwort"; + parentNode.insertBefore(wrongAnswerElement, element.nextSibling); - bar.classList.add('s-font-note-s--strong'); - bar.classList.add('s-color-negative'); - } else { - // all other answer elements - element.classList.add('s-font-note--light'); - bar.classList.add('s-color-gray-4'); + bar.classList.add("s-font-note-s--strong"); + bar.classList.add("s-color-negative"); + } else { + // all other answer elements + element.classList.add("s-font-note--light"); + bar.classList.add("s-color-gray-4"); + } } - } - }) - } + }); + } getMaxAnswersCount(answersStats) { let maxAnswersCount = this.choices .map(choice => { - if (answersStats.hasOwnProperty('numberOfAnswersPerChoice') && answersStats.numberOfAnswersPerChoice.hasOwnProperty(choice)) { - return parseInt(answersStats.numberOfAnswersPerChoice[choice]) + if ( + answersStats.hasOwnProperty("numberOfAnswersPerChoice") && + answersStats.numberOfAnswersPerChoice.hasOwnProperty(choice) + ) { + return parseInt(answersStats.numberOfAnswersPerChoice[choice]); } - return 0 + return 0; }) .reduce((prev, current) => { if (current > prev) { - return current + return current; } - return prev - },0) - return maxAnswersCount + return prev; + }, 0); + return maxAnswersCount; } renderResultStats(answer, answersStats) { - let resultVisualElement = this.questionElement.querySelector('.q-quiz-result-visual'); - let maxAnswersCount = this.getMaxAnswersCount(answersStats) + let resultVisualElement = this.questionElement.querySelector( + ".q-quiz-result-visual" + ); + let maxAnswersCount = this.getMaxAnswersCount(answersStats); this.choices.forEach((choice, index) => { - let bar = resultVisualElement.querySelectorAll('.q-quiz-result__multiple-choice-bar').item(index); + let bar = resultVisualElement + .querySelectorAll(".q-quiz-result__multiple-choice-bar") + .item(index); let percentageWithThisAnswer = 0; let barWidthPercentage = 0; if (answersStats.numberOfAnswersPerChoice.hasOwnProperty(choice)) { - percentageWithThisAnswer = (answersStats.numberOfAnswersPerChoice[choice] / answersStats.totalAnswers * 100).toFixed(0); - barWidthPercentage = (answersStats.numberOfAnswersPerChoice[choice] / maxAnswersCount * 100).toFixed(0); + percentageWithThisAnswer = ( + (answersStats.numberOfAnswersPerChoice[choice] / + answersStats.totalAnswers) * + 100 + ).toFixed(0); + barWidthPercentage = ( + (answersStats.numberOfAnswersPerChoice[choice] / maxAnswersCount) * + 100 + ).toFixed(0); } - bar.style.width = `${barWidthPercentage}%` + bar.style.width = `${barWidthPercentage}%`; if (percentageWithThisAnswer === 0) { bar.innerHTML = `${percentageWithThisAnswer} %`; } else { bar.innerHTML = `${percentageWithThisAnswer} %`; } - }) - this.questionElement.querySelectorAll('.q-quiz-result__answer > span').forEach((element, index) => { - - }) + }); + this.questionElement + .querySelectorAll(".q-quiz-result__answer > span") + .forEach((element, index) => {}); } } diff --git a/script_src/NumberGuessHandler.js b/script_src/NumberGuessHandler.js index 6e1460d..a05aa6a 100644 --- a/script_src/NumberGuessHandler.js +++ b/script_src/NumberGuessHandler.js @@ -1,23 +1,29 @@ -import Scale from './Scale.js'; -import { getAnswerTextElement } from './answerHelpers.js'; +import Scale from "./Scale.js"; +import { getAnswerTextElement } from "./answerHelpers.js"; function getUnit(value, data) { let unit = data.unit; - if (parseFloat(value) === 1 && data.unitSingular && data.unitSingular !== '') { + if ( + parseFloat(value) === 1 && + data.unitSingular && + data.unitSingular !== "" + ) { unit = data.unitSingular; } return unit; } export default class NumberGuessHandler { - constructor(questionElement, data, quizId, toolBaseUrl) { this.questionElement = questionElement; - this.inputElement = this.questionElement.querySelector('.q-quiz-input input[type="range"]'); - this.min = parseFloat(this.inputElement.getAttribute('min')); - this.max = parseFloat(this.inputElement.getAttribute('max')); - this.step = parseFloat(this.inputElement.getAttribute('step')); - this.defaultInputValue = ((parseFloat(this.max) - parseFloat(this.min)) / 2) + parseFloat(this.min); + this.inputElement = this.questionElement.querySelector( + '.q-quiz-input input[type="range"]' + ); + this.min = parseFloat(this.inputElement.getAttribute("min")); + this.max = parseFloat(this.inputElement.getAttribute("max")); + this.step = parseFloat(this.inputElement.getAttribute("step")); + this.defaultInputValue = + (parseFloat(this.max) - parseFloat(this.min)) / 2 + parseFloat(this.min); this.data = data; this.quizId = quizId; this.toolBaseUrl = toolBaseUrl; @@ -26,21 +32,38 @@ export default class NumberGuessHandler { renderInput() { const labelContainer = this.inputElement.parentNode.firstChild; - let label = labelContainer.querySelector('.q-quiz-input-range-position-label'); - this.inputElement.setAttribute('value', this.defaultInputValue); + let label = labelContainer.querySelector( + ".q-quiz-input-range-position-label" + ); + this.inputElement.setAttribute("value", this.defaultInputValue); this.defaultInputValue = this.inputElement.value; - this.inputElement.addEventListener('input', () => { + this.inputElement.addEventListener("input", () => { label.textContent = this.inputElement.value; - label.setAttribute('style', `left: calc(${(this.inputElement.value - this.min) / (this.max - this.min) * 100}% - 1px);`); + label.setAttribute( + "style", + `left: calc(${((this.inputElement.value - this.min) / + (this.max - this.min)) * + 100}% - 1px);` + ); }); - this.inputElement.addEventListener('change', () => { + this.inputElement.addEventListener("change", () => { label.textContent = this.inputElement.value; - label.setAttribute('style', `left: calc(${(this.inputElement.value - this.min) / (this.max - this.min) * 100}% - 1px);`); + label.setAttribute( + "style", + `left: calc(${((this.inputElement.value - this.min) / + (this.max - this.min)) * + 100}% - 1px);` + ); }); label.innerHTML = this.defaultInputValue; - label.setAttribute('style', `left: calc(${(this.inputElement.value - this.min) / (this.max - this.min) * 100}% - 1px);`); + label.setAttribute( + "style", + `left: calc(${((this.inputElement.value - this.min) / + (this.max - this.min)) * + 100}% - 1px);` + ); } getValue(event) { @@ -49,43 +72,55 @@ export default class NumberGuessHandler { isAnswerValid() { let element = this.inputElement.parentNode.nextElementSibling; - return element.classList.contains('q-quiz-invalid-input-message') - || (parseFloat(this.defaultInputValue) !== parseFloat(this.inputElement.value)); + return ( + element.classList.contains("q-quiz-invalid-input-message") || + parseFloat(this.defaultInputValue) !== parseFloat(this.inputElement.value) + ); } handleInvalidAnswer() { let answerButton = this.inputElement.parentNode.nextSibling; - let defaultInputValueMessageElement = document.createElement('div') - defaultInputValueMessageElement.classList.add('q-quiz-invalid-input-message') - defaultInputValueMessageElement.classList.add('s-font-text-s') - defaultInputValueMessageElement.classList.add('s-font-text-s--strong') - defaultInputValueMessageElement.innerHTML = 'Sie haben den Schieberegeler nicht bewegt, trotzdem die Position speichern?' - answerButton.parentNode.insertBefore(defaultInputValueMessageElement, answerButton) + let defaultInputValueMessageElement = document.createElement("div"); + defaultInputValueMessageElement.classList.add( + "q-quiz-invalid-input-message" + ); + defaultInputValueMessageElement.classList.add("s-font-text-s"); + defaultInputValueMessageElement.classList.add("s-font-text-s--strong"); + defaultInputValueMessageElement.innerHTML = + "Sie haben den Schieberegeler nicht bewegt, trotzdem die Position speichern?"; + answerButton.parentNode.insertBefore( + defaultInputValueMessageElement, + answerButton + ); } renderResult(answer) { - this.resultElement = this.questionElement.querySelector('.q-quiz-result__number-guess-visual'); + this.resultElement = this.questionElement.querySelector( + ".q-quiz-result__number-guess-visual" + ); const unitData = { - unit: this.resultElement.getAttribute('unit'), - unitSingular: this.resultElement.getAttribute('unit-singular') + unit: this.resultElement.getAttribute("unit"), + unitSingular: this.resultElement.getAttribute("unit-singular") }; let steppedValues = []; for (let i = this.min; i <= this.max; i = i + this.step) { - steppedValues.push((100/(this.max - this.min + 1)) * i); + steppedValues.push((100 / (this.max - this.min + 1)) * i); } - let range = [] + let range = []; for (let i = 0; i < steppedValues.length; i++) { - range.push(steppedValues[i] - ((100/steppedValues.length)/2)); + range.push(steppedValues[i] - 100 / steppedValues.length / 2); } let xScale = new Scale([this.min, this.max], range); - let stepWidth = this.resultElement.getBoundingClientRect().width/steppedValues.length; + let stepWidth = + this.resultElement.getBoundingClientRect().width / steppedValues.length; - let additionalMarkerClass = ''; - let additionalMarkerAttributes = ''; + let additionalMarkerClass = ""; + let additionalMarkerAttributes = ""; if (steppedValues.length <= 100) { - additionalMarkerClass = 'q-quiz-result__number-guess-visual__text__marker--few-answers'; + additionalMarkerClass = + "q-quiz-result__number-guess-visual__text__marker--few-answers"; additionalMarkerAttributes = `style="width: ${stepWidth}px;"`; } @@ -93,51 +128,83 @@ export default class NumberGuessHandler { let correctAnswerHtml = `
Korrekte Antwort -
${this.correctAnswer || ''} ${getUnit(this.correctAnswer, unitData)}
+
${this.correctAnswer || ""} ${getUnit( + this.correctAnswer, + unitData + )}
`; - let correctAnswerElement = document.createElement('div'); - correctAnswerElement.classList.add('q-quiz-result__number-guess-visual__text'); - correctAnswerElement.classList.add('q-quiz-result__number-guess-visual__text--bottom'); - correctAnswerElement.classList.add('s-color-gray-8'); + let correctAnswerElement = document.createElement("div"); + correctAnswerElement.classList.add( + "q-quiz-result__number-guess-visual__text" + ); + correctAnswerElement.classList.add( + "q-quiz-result__number-guess-visual__text--bottom" + ); + correctAnswerElement.classList.add("s-color-gray-8"); correctAnswerElement.innerHTML = correctAnswerHtml; - if ((this.correctAnswer - this.min) > (this.max - this.min)/2) { - let rightPos = (((xScale.range.length - xScale.getIndexOnScale(this.correctAnswer) - 1) * stepWidth) + stepWidth / 2 + 1).toFixed(1); + if (this.correctAnswer - this.min > (this.max - this.min) / 2) { + let rightPos = ( + (xScale.range.length - xScale.getIndexOnScale(this.correctAnswer) - 1) * + stepWidth + + stepWidth / 2 + + 1 + ).toFixed(1); correctAnswerElement.style.right = `${rightPos}px`; - correctAnswerElement.classList.add('q-quiz-result__number-guess-visual__text--right'); + correctAnswerElement.classList.add( + "q-quiz-result__number-guess-visual__text--right" + ); } else { - let leftPos = (((xScale.getIndexOnScale(this.correctAnswer) + 1) * stepWidth) - stepWidth / 2 + 1).toFixed(1); + let leftPos = ( + (xScale.getIndexOnScale(this.correctAnswer) + 1) * stepWidth - + stepWidth / 2 + + 1 + ).toFixed(1); correctAnswerElement.style.left = `${leftPos}px`; } this.resultElement.appendChild(correctAnswerElement); - // show the users answer let answerHtml = `
Ihre Schätzung -
${answer} ${getUnit(answer, unitData)}
+
${answer} ${getUnit( + answer, + unitData + )}
`; - let answerElement = document.createElement('div'); - answerElement.classList.add('q-quiz-result__number-guess-visual__text'); - answerElement.classList.add('q-quiz-result__number-guess-visual__text--top'); - answerElement.classList.add('s-color-primary-7'); + let answerElement = document.createElement("div"); + answerElement.classList.add("q-quiz-result__number-guess-visual__text"); + answerElement.classList.add( + "q-quiz-result__number-guess-visual__text--top" + ); + answerElement.classList.add("s-color-primary-7"); answerElement.innerHTML = answerHtml; - answerElement.style.position = 'absolute'; + answerElement.style.position = "absolute"; - if ((answer - this.min) > (this.max - this.min)/2) { - let rightPos = (((xScale.range.length - xScale.getIndexOnScale(answer) - 1) * stepWidth) + stepWidth / 2 + 1).toFixed(1); + if (answer - this.min > (this.max - this.min) / 2) { + let rightPos = ( + (xScale.range.length - xScale.getIndexOnScale(answer) - 1) * stepWidth + + stepWidth / 2 + + 1 + ).toFixed(1); answerElement.style.right = `${rightPos}px`; - answerElement.classList.add('q-quiz-result__number-guess-visual__text--right'); + answerElement.classList.add( + "q-quiz-result__number-guess-visual__text--right" + ); } else { - let leftPos = (((xScale.getIndexOnScale(answer) + 1) * stepWidth) - stepWidth / 2 + 1).toFixed(1); + let leftPos = ( + (xScale.getIndexOnScale(answer) + 1) * stepWidth - + stepWidth / 2 + + 1 + ).toFixed(1); answerElement.style.left = `${leftPos}px`; } this.resultElement.appendChild(answerElement); @@ -145,36 +212,54 @@ export default class NumberGuessHandler { renderResultStats(answer, answersStats) { let isCorrectAnswer = answer === this.correctAnswer; - let resultVisualElement = this.questionElement.querySelector('.q-quiz-result .q-quiz-result__number-guess-visual'); - let resultTextElement = this.questionElement.querySelector('.q-quiz-result .q-quiz-result-answer-text'); + let resultVisualElement = this.questionElement.querySelector( + ".q-quiz-result .q-quiz-result__number-guess-visual" + ); + let resultTextElement = this.questionElement.querySelector( + ".q-quiz-result .q-quiz-result-answer-text" + ); - if (answersStats !== undefined && answersStats.diffPercentage !== undefined && answersStats.diffPercentage !== null) { - let textElement = getAnswerTextElement(answersStats, isCorrectAnswer, () => { - return `Ihre Schätzung liegt um ${answersStats.diffPercentage} Prozent zu ${answer > this.correctAnswer ? 'hoch' : 'tief'}.` - }); + if ( + answersStats !== undefined && + answersStats.diffPercentage !== undefined && + answersStats.diffPercentage !== null + ) { + let textElement = getAnswerTextElement( + answersStats, + isCorrectAnswer, + () => { + return `Ihre Schätzung liegt um ${ + answersStats.diffPercentage + } Prozent zu ${answer > this.correctAnswer ? "hoch" : "tief"}.`; + } + ); resultTextElement.appendChild(textElement); } - + this.renderStatsVisual(resultVisualElement); } getStatsPlot(width) { - return fetch(`${this.toolBaseUrl}/number-guess/${this.quizId}/${this.data.id}/plot/${width}`) - .then(response => { - if (response.ok) { - return response.text() - } - return '' - }) + return fetch( + `${this.toolBaseUrl}/number-guess/${this.quizId}/${ + this.data.id + }/plot/${width}` + ).then(response => { + if (response.ok) { + return response.text(); + } + return ""; + }); } renderStatsVisual(element) { - this.getStatsPlot(element.getBoundingClientRect().width) - .then(svgString => { - let statsVisualContainerElement = document.createElement('div') - statsVisualContainerElement.classList.add('q-quiz-result__number-guess-visual__stats-graphic-container') - statsVisualContainerElement.innerHTML = svgString - element.appendChild(statsVisualContainerElement) - }); + this.getStatsPlot(element.getBoundingClientRect().width).then(svgString => { + let statsVisualContainerElement = document.createElement("div"); + statsVisualContainerElement.classList.add( + "q-quiz-result__number-guess-visual__stats-graphic-container" + ); + statsVisualContainerElement.innerHTML = svgString; + element.appendChild(statsVisualContainerElement); + }); } } diff --git a/script_src/QuestionHandler.js b/script_src/QuestionHandler.js index 0c32d9e..a49473e 100644 --- a/script_src/QuestionHandler.js +++ b/script_src/QuestionHandler.js @@ -1,15 +1,15 @@ -import NumberGuess from './NumberGuessHandler.js'; -import MultipleChoice from './MultipleChoiceHandler.js'; -import MapPointGuess from './MapPointGuessHandler.js'; -import * as answerHelpers from './answerHelpers.js'; -import MultiQuizPositionHandler from './MultiQuizPositionHandler.js'; -import AnswerStore from './AnswerStore.js'; +import NumberGuess from "./NumberGuessHandler.js"; +import MultipleChoice from "./MultipleChoiceHandler.js"; +import MapPointGuess from "./MapPointGuessHandler.js"; +import * as answerHelpers from "./answerHelpers.js"; +import MultiQuizPositionHandler from "./MultiQuizPositionHandler.js"; +import AnswerStore from "./AnswerStore.js"; const questionTypes = { - 'numberGuess': NumberGuess, - 'multipleChoice': MultipleChoice, - 'mapPointGuess': MapPointGuess -} + numberGuess: NumberGuess, + multipleChoice: MultipleChoice, + mapPointGuess: MapPointGuess +}; export default class QuestionHandler { constructor(quizRootElement, data) { @@ -18,7 +18,10 @@ export default class QuestionHandler { this.userAnswers = []; this.isSingleQuestionQuiz = data.numberElements === 1; if (!this.isSingleQuestionQuiz) { - this.multiQuizPositionHandler = new MultiQuizPositionHandler(quizRootElement, data); + this.multiQuizPositionHandler = new MultiQuizPositionHandler( + quizRootElement, + data + ); } this.answerStore = new AnswerStore(this.data.toolBaseUrl); } @@ -26,10 +29,13 @@ export default class QuestionHandler { renderInputElement(position) { if (this.isSingleQuestionQuiz) { this.questionPosition = 0; - this.quizElement = this.quizRootElement.querySelector('.q-quiz-element-container--is-active'); + this.quizElement = this.quizRootElement.querySelector( + ".q-quiz-element-container--is-active" + ); } else { this.multiQuizPositionHandler.setPosition(position); - this.questionPosition = this.multiQuizPositionHandler.getQuestionNumber() - 1; + this.questionPosition = + this.multiQuizPositionHandler.getQuestionNumber() - 1; this.quizElement = this.multiQuizPositionHandler.getQuizElement(); } @@ -41,17 +47,24 @@ export default class QuestionHandler { } renderQuestion() { - this.questionType = this.data.questionElementData[this.questionPosition].type; - this.questionRenderer = new questionTypes[this.questionType](this.quizElement, this.data.questionElementData[this.questionPosition], this.data.itemId, this.data.toolBaseUrl); - if (typeof this.questionRenderer.renderInput === 'function') { + this.questionType = this.data.questionElementData[ + this.questionPosition + ].type; + this.questionRenderer = new questionTypes[this.questionType]( + this.quizElement, + this.data.questionElementData[this.questionPosition], + this.data.itemId, + this.data.toolBaseUrl + ); + if (typeof this.questionRenderer.renderInput === "function") { this.questionRenderer.renderInput(); } } handleAnswer(event) { - const answerValue = this.questionRenderer.getValue(event); + const answerValue = this.questionRenderer.getValue(event); - if (typeof this.questionRenderer.isAnswerValid === 'function') { + if (typeof this.questionRenderer.isAnswerValid === "function") { if (!this.questionRenderer.isAnswerValid()) { this.questionRenderer.handleInvalidAnswer(); return; @@ -60,7 +73,7 @@ export default class QuestionHandler { // dispatch a custom event for tracking system to track the answer // and others if they are interested - let answerEvent = new CustomEvent('q-quiz-answer', { + let answerEvent = new CustomEvent("q-quiz-answer", { bubbles: true, detail: { id: this.data.questionElementData[this.questionPosition].id @@ -82,10 +95,14 @@ export default class QuestionHandler { return answerId; }) .then(answerId => { - return this.answerStore.getStats(this.data.itemId, this.data.questionElementData[this.questionPosition], answerId); + return this.answerStore.getStats( + this.data.itemId, + this.data.questionElementData[this.questionPosition], + answerId + ); }) .then(stats => { - if (typeof this.questionRenderer.renderResultStats === 'function') { + if (typeof this.questionRenderer.renderResultStats === "function") { this.questionRenderer.renderResultStats(answerValue, stats); } }) @@ -94,7 +111,10 @@ export default class QuestionHandler { }); // if this was the last question, get score result promise - if (this.questionPosition === this.data.questionElementData.length - 1 && this.data.hasLastCard) { + if ( + this.questionPosition === this.data.questionElementData.length - 1 && + this.data.hasLastCard + ) { this.getScore(); } this.renderAdditionalInformation(); @@ -107,8 +127,8 @@ export default class QuestionHandler { questionId: this.data.questionElementData[this.questionPosition].id, type: this.questionType, value: answerValue - } - + }; + if (!this.data.isPure) { return this.answerStore.saveAnswer(answerData); } else { @@ -119,59 +139,99 @@ export default class QuestionHandler { getScore() { // either scorePromise is already set with answering the last question or it will be set whenever needed e.g. when rendering last card if (this.scorePromise === undefined) { - this.scorePromise = fetch(`${this.data.toolBaseUrl}/score?appendItemToPayload=${this.data.itemId}`, { - method: 'POST', - body: JSON.stringify({ - userAnswers: this.userAnswers, - }) - }) - .then(response => { + this.scorePromise = fetch( + `${this.data.toolBaseUrl}/score?appendItemToPayload=${ + this.data.itemId + }`, + { + method: "POST", + body: JSON.stringify({ + userAnswers: this.userAnswers + }) + } + ).then(response => { if (response.ok) { return response.json(); } - }) + }); } return this.scorePromise; } renderAdditionalInformation() { - answerHelpers.renderAdditionalInformationForQuestion(this.quizElement, this.data.questionElementData[this.questionPosition]); + answerHelpers.renderAdditionalInformationForQuestion( + this.quizElement, + this.data.questionElementData[this.questionPosition] + ); } displayResult() { - this.quizElement.querySelector('.q-quiz-input').classList.add('state-hidden'); - this.quizElement.querySelector('.q-quiz-result').classList.remove('state-hidden'); - this.quizElement.querySelector('.q-quiz-result').classList.add('state-visible'); + this.quizElement + .querySelector(".q-quiz-input") + .classList.add("state-hidden"); + this.quizElement + .querySelector(".q-quiz-result") + .classList.remove("state-hidden"); + this.quizElement + .querySelector(".q-quiz-result") + .classList.add("state-visible"); if (this.hasNoFurtherQuizElements()) { - this.quizElement.querySelector('.q-quiz-button').classList.add('state-hidden'); + this.quizElement + .querySelector(".q-quiz-button") + .classList.add("state-hidden"); } else if (this.isNextQuizElementLastCard()) { if (this.data.isFinalScoreShown) { - this.quizElement.querySelector('.q-quiz-button__content span').textContent = 'zur Auswertung'; + this.quizElement.querySelector( + ".q-quiz-button__content span" + ).textContent = + "zur Auswertung"; } else { - this.quizElement.querySelector('.q-quiz-button__content span').textContent = 'Thema vertiefen'; + this.quizElement.querySelector( + ".q-quiz-button__content span" + ).textContent = + "Thema vertiefen"; } } } hasNoFurtherQuizElements() { - return this.isSingleQuestionQuiz || this.multiQuizPositionHandler.getPosition() === this.data.numberElements - 1; + return ( + this.isSingleQuestionQuiz || + this.multiQuizPositionHandler.getPosition() === + this.data.numberElements - 1 + ); } isNextQuizElementLastCard() { - return this.multiQuizPositionHandler.getQuestionNumber() === this.data.questionElementData.length && this.data.hasLastCard + return ( + this.multiQuizPositionHandler.getQuestionNumber() === + this.data.questionElementData.length && this.data.hasLastCard + ); } - renderLastCard() { - if (this.data.lastCardData && this.data.lastCardData.articleRecommendations) { - answerHelpers.renderAdditionalInformationForLastCard(this.quizElement, this.data.lastCardData.articleRecommendations); + renderLastCard() { + if ( + this.data.lastCardData && + this.data.lastCardData.articleRecommendations + ) { + answerHelpers.renderAdditionalInformationForLastCard( + this.quizElement, + this.data.lastCardData.articleRecommendations + ); } if (this.data.isFinalScoreShown) { - this.getScore() - .then(score => { - let lastCardTitleElement = this.quizElement.querySelector('.q-quiz-last-card-title'); - lastCardTitleElement.innerHTML = `Sie haben ${score.achievedScore} von ${score.maxScore} möglichen Punkten erzielt.` - this.quizRootElement.querySelector('.q-quiz-header__title').textContent = score.lastCardTitle; - }) + this.getScore().then(score => { + let lastCardTitleElement = this.quizElement.querySelector( + ".q-quiz-last-card-title" + ); + lastCardTitleElement.innerHTML = `Sie haben ${ + score.achievedScore + } von ${score.maxScore} möglichen Punkten erzielt.`; + this.quizRootElement.querySelector( + ".q-quiz-header__title" + ).textContent = + score.lastCardTitle; + }); } } } diff --git a/script_src/Scale.js b/script_src/Scale.js index ad181f1..b124914 100644 --- a/script_src/Scale.js +++ b/script_src/Scale.js @@ -1,28 +1,32 @@ export default class Scale { - constructor(domain, range) { this.domain = domain; this.range = range; } getValueOnScale(value) { - return this.range[this.getIndexOnScale(value)] + return this.range[this.getIndexOnScale(value)]; } getIndexOnScale(value) { - let ratio = (this.range[this.range.length - 1] - this.range[0]) / (this.domain[1] - this.domain[0]); + let ratio = + (this.range[this.range.length - 1] - this.range[0]) / + (this.domain[1] - this.domain[0]); let result = this.range[0] + ratio * (value - this.domain[0]); let indexOnScale; this.range.forEach((possibleValue, index) => { - if (result >= possibleValue && this.range[index + 1] && result < this.range[index + 1]) { + if ( + result >= possibleValue && + this.range[index + 1] && + result < this.range[index + 1] + ) { indexOnScale = index; } else if (result >= possibleValue && index === this.range.length - 1) { indexOnScale = index; } - }) + }); return indexOnScale; } - } diff --git a/script_src/answerHelpers.js b/script_src/answerHelpers.js index bc21f42..563368f 100644 --- a/script_src/answerHelpers.js +++ b/script_src/answerHelpers.js @@ -1,101 +1,122 @@ -import * as helpers from './helpers.js' +import * as helpers from "./helpers.js"; export function getAnswerTextElement(stats, isCorrectAnswer, getDiffText) { - let statsTextHtml = '' + let statsTextHtml = ''; if (isCorrectAnswer) { - statsTextHtml += 'Wir ziehen den Hut. Das ist nicht gut geschätzt, sondern exakt richtig.' - + statsTextHtml += + 'Wir ziehen den Hut. Das ist nicht gut geschätzt, sondern exakt richtig.'; } else if (stats.totalAnswers - stats.betterThanCount === 1) { statsTextHtml += ` - Gratulation! So gut hat bis jetzt noch niemand geschätzt.` + Gratulation! So gut hat bis jetzt noch niemand geschätzt.`; if (stats.totalAnswers === 1) { - statsTextHtml += ' Etwas relativieren müssen wir das Lob allerdings: Sie waren so schnell, dass noch niemand vor Ihnen mitgemacht hat.' + statsTextHtml += + " Etwas relativieren müssen wir das Lob allerdings: Sie waren so schnell, dass noch niemand vor Ihnen mitgemacht hat."; } else if (stats.totalAnswers === 2) { - statsTextHtml += ' Etwas relativieren müssen wir das Lob allerdings: Sie waren so schnell, dass erst eine andere Person mitgemacht hat.' + statsTextHtml += + " Etwas relativieren müssen wir das Lob allerdings: Sie waren so schnell, dass erst eine andere Person mitgemacht hat."; } else if (stats.totalAnswers <= 11) { - statsTextHtml += ` Etwas relativieren müssen wir das Lob allerdings: Sie waren so schnell, dass erst ${stats.totalAnswers - 1} andere mitgemacht haben.` + statsTextHtml += ` Etwas relativieren müssen wir das Lob allerdings: Sie waren so schnell, dass erst ${stats.totalAnswers - + 1} andere mitgemacht haben.`; } - statsTextHtml += ''; - + statsTextHtml += ""; } else if (stats.betterThanPercentage > 90) { statsTextHtml += ` - Hervorragend. Nur ${100 - stats.betterThanPercentage} Prozent aller anderen haben noch besser geschätzt als Sie. - ` - + Hervorragend. Nur ${100 - + stats.betterThanPercentage} Prozent aller anderen haben noch besser geschätzt als Sie. + `; } else if (stats.betterThanPercentage > 20) { statsTextHtml += ` - Sie haben damit besser geschätzt als ${stats.betterThanPercentage} Prozent aller anderen. - ` - - } else if (stats.betterThanPercentage === 0 && stats.numberOfSameAnswers === 0) { - statsTextHtml += `Niemand lag so weit daneben wie Sie.` + Sie haben damit besser geschätzt als ${ + stats.betterThanPercentage + } Prozent aller anderen. + `; + } else if ( + stats.betterThanPercentage === 0 && + stats.numberOfSameAnswers === 0 + ) { + statsTextHtml += `Niemand lag so weit daneben wie Sie.`; if (stats.totalAnswers === 2) { - statsTextHtml += ' Das klingt schlimmer als es ist. Sie waren so schnell, dass erst eine andere Person mitgemacht hat. Ihre Schätzung ist also auch die Zweitbeste.' + statsTextHtml += + " Das klingt schlimmer als es ist. Sie waren so schnell, dass erst eine andere Person mitgemacht hat. Ihre Schätzung ist also auch die Zweitbeste."; } else if (stats.totalAnswers <= 11) { - statsTextHtml += ` Das klingt schlimmer als es ist. Sie waren so schnell, dass erst ${stats.totalAnswers - 1} andere mitgemacht haben.` + statsTextHtml += ` Das klingt schlimmer als es ist. Sie waren so schnell, dass erst ${stats.totalAnswers - + 1} andere mitgemacht haben.`; } else { - statsTextHtml += ' Dürfen wir Ihnen, damit das nicht mehr vorkommt, ein NZZ-Abo empfehlen?' + statsTextHtml += + ' Dürfen wir Ihnen, damit das nicht mehr vorkommt, ein NZZ-Abo empfehlen?'; } - statsTextHtml += '' - + statsTextHtml += ""; } else if (stats.betterThanPercentage === 0) { statsTextHtml += ` Schlechter hat bisher noch niemand geschätzt. - ` - - } else if (stats.betterThanPercentage !== undefined && stats.betterThanPercentage !== null) { + `; + } else if ( + stats.betterThanPercentage !== undefined && + stats.betterThanPercentage !== null + ) { statsTextHtml += ` - Nur ${stats.betterThanPercentage} Prozent aller anderen lagen noch weiter daneben als Sie. - ` + Nur ${ + stats.betterThanPercentage + } Prozent aller anderen lagen noch weiter daneben als Sie. + `; } - statsTextHtml += '' - + statsTextHtml += ""; - let textAnswerHtml = '' + let textAnswerHtml = ""; if (!isCorrectAnswer && getDiffText) { - textAnswerHtml += ` ${getDiffText()}` + textAnswerHtml += ` ${getDiffText()}`; } - textAnswerHtml += ` ${statsTextHtml}` + textAnswerHtml += ` ${statsTextHtml}`; - let element = document.createElement('span') - element.innerHTML = textAnswerHtml - return element + let element = document.createElement("span"); + element.innerHTML = textAnswerHtml; + return element; } function getRecommendationsElement(articleRecommendations) { - let recommendationsElement = document.createElement('p'); - recommendationsElement.classList.add('s-font-text-s'); - let recommendationsHtml = ''; + let recommendationsElement = document.createElement("p"); + recommendationsElement.classList.add("s-font-text-s"); + let recommendationsHtml = ""; if (articleRecommendations && articleRecommendations.length) { - - let punctuation = ['!', '?', '.']; - - helpers.loadAdditionalArticles(articleRecommendations.map(r => r.articleId)) + let punctuation = ["!", "?", "."]; + + helpers + .loadAdditionalArticles(articleRecommendations.map(r => r.articleId)) .then(articles => { articles .filter(article => { return article !== undefined; }) .forEach((article, index) => { - let recommendationText = ''; - if (articleRecommendations[index].text && articleRecommendations[index].text.length && articleRecommendations[index].text.length > 0) { - recommendationText = articleRecommendations[index].text + ' '; + let recommendationText = ""; + if ( + articleRecommendations[index].text && + articleRecommendations[index].text.length && + articleRecommendations[index].text.length > 0 + ) { + recommendationText = articleRecommendations[index].text + " "; } recommendationsHtml += ` - ${recommendationText}${article.metadata.title}${punctuation.indexOf(article.metadata.title.slice(-1)) === -1 ? '.' : ''} + ${recommendationText}${article.metadata.title}${ + punctuation.indexOf(article.metadata.title.slice(-1)) === -1 + ? "." + : "" + } `; - }) + }); }) .then(() => { - recommendationsElement.innerHTML = recommendationsHtml + recommendationsElement.innerHTML = recommendationsHtml; }); } - return recommendationsElement + return recommendationsElement; } export function getDistanceText(distance) { @@ -106,24 +127,40 @@ export function getDistanceText(distance) { } } -export function renderAdditionalInformationForLastCard(element, articleRecommendations) { - let articleRecommendationsContainer = element.querySelector('.q-quiz-article-recommendations'); - let articleRecommendationsElement = getRecommendationsElement(articleRecommendations); +export function renderAdditionalInformationForLastCard( + element, + articleRecommendations +) { + let articleRecommendationsContainer = element.querySelector( + ".q-quiz-article-recommendations" + ); + let articleRecommendationsElement = getRecommendationsElement( + articleRecommendations + ); articleRecommendationsContainer.appendChild(articleRecommendationsElement); } export function renderAdditionalInformationForQuestion(element, correctAnswer) { - let detailedAnswer = element.querySelector('.q-quiz-result .q-quiz-result-answer-text'); - let detailedAnswerSpan = document.createElement('span'); - detailedAnswerSpan.classList.add('s-font-text-s'); - if (correctAnswer.answerText) { - detailedAnswerSpan.innerHTML = correctAnswer.answerText; - } - detailedAnswer.appendChild(detailedAnswerSpan); - - let articleRecommendationsElement = getRecommendationsElement(correctAnswer.articleRecommendations); - detailedAnswer.parentNode.insertBefore(articleRecommendationsElement, detailedAnswer.nextSibling); - - let nextQuestionButton = element.querySelector('button.q-quiz-button.q-quiz-button--horizontal.q-quiz-button--right'); - nextQuestionButton.classList.remove('state-hidden'); + let detailedAnswer = element.querySelector( + ".q-quiz-result .q-quiz-result-answer-text" + ); + let detailedAnswerSpan = document.createElement("span"); + detailedAnswerSpan.classList.add("s-font-text-s"); + if (correctAnswer.answerText) { + detailedAnswerSpan.innerHTML = correctAnswer.answerText; } + detailedAnswer.appendChild(detailedAnswerSpan); + + let articleRecommendationsElement = getRecommendationsElement( + correctAnswer.articleRecommendations + ); + detailedAnswer.parentNode.insertBefore( + articleRecommendationsElement, + detailedAnswer.nextSibling + ); + + let nextQuestionButton = element.querySelector( + "button.q-quiz-button.q-quiz-button--horizontal.q-quiz-button--right" + ); + nextQuestionButton.classList.remove("state-hidden"); +} diff --git a/server-plugins.js b/server-plugins.js index bb57678..2025396 100644 --- a/server-plugins.js +++ b/server-plugins.js @@ -1,15 +1,15 @@ -const noir = require('pino-noir'); +const noir = require("pino-noir"); module.exports = [ - require('inert'), + require("inert"), { - plugin: require('hapi-pino'), + plugin: require("hapi-pino"), options: { serializers: { - req: (req) => { + req: req => { // this is from https://github.com/pinojs/hapi-pino/blob/master/index.js#L164-L174 // and should be changed once there is an agreement on: https://github.com/pinojs/hapi-pino/pull/34 - const raw = req.raw.req + const raw = req.raw.req; const normalizedReq = { id: req.info.id, method: raw.method, @@ -17,15 +17,15 @@ module.exports = [ headers: raw.headers, remoteAddress: raw.connection.remoteAddress, remotePort: raw.connection.remotePort - } - return noir(['req.headers.authorization']).req(normalizedReq); + }; + return noir(["req.headers.authorization"]).req(normalizedReq); } }, - prettyPrint: process.env.APP_ENV !== 'production' && process.env.APP_ENV !== 'staging', + prettyPrint: + process.env.APP_ENV !== "production" && + process.env.APP_ENV !== "staging", logRouteTags: true, - ignorePaths: [ - '/health' - ] + ignorePaths: ["/health"] } } // should be added again as soon as yaral and hapi 17 are compatible @@ -47,4 +47,4 @@ module.exports = [ default: [] } }, */ -] +]; diff --git a/server.js b/server.js index beab061..1882827 100644 --- a/server.js +++ b/server.js @@ -1,13 +1,13 @@ -const Hapi = require('hapi'); +const Hapi = require("hapi"); const hapiOptions = { cache: [ { - name: 'memoryCache', - engine: require('catbox-memory') + name: "memoryCache", + engine: require("catbox-memory") } ], port: process.env.PORT || 3000 -} +}; module.exports = new Hapi.Server(hapiOptions); diff --git a/styles_src/cover.scss b/styles_src/cover.scss index 91b316c..3e44f9b 100644 --- a/styles_src/cover.scss +++ b/styles_src/cover.scss @@ -15,4 +15,3 @@ margin-bottom: 30px; padding: 0 5px; } - diff --git a/styles_src/numberGuess.scss b/styles_src/numberGuess.scss index f657a66..58d6ad2 100644 --- a/styles_src/numberGuess.scss +++ b/styles_src/numberGuess.scss @@ -27,7 +27,7 @@ $q-quiz-result__number-guess-visual__stats-graphic-height: 60px; // contains the small line towards the correct answer / user answer line &::before { - content: ''; + content: ""; display: block; position: absolute; width: 1px; @@ -88,7 +88,8 @@ $q-quiz-result__number-guess-visual__stats-graphic-height: 60px; } } -.q-quiz-answer-chart-min-line, .q-quiz-answer-chart-max-line { +.q-quiz-answer-chart-min-line, +.q-quiz-answer-chart-max-line { stroke-width: 1px; stroke: currentColor; shape-rendering: crispEdges; diff --git a/styles_src/q-quiz-button.scss b/styles_src/q-quiz-button.scss index 0a41c6a..5c06bbe 100644 --- a/styles_src/q-quiz-button.scss +++ b/styles_src/q-quiz-button.scss @@ -11,11 +11,10 @@ } .q-quiz-button { - &.q-quiz-button__icon { margin: 0 auto; } - + &:hover { text-decoration: none; } diff --git a/styles_src/q-quiz-multi-container.scss b/styles_src/q-quiz-multi-container.scss index 0e7eba9..6102be0 100644 --- a/styles_src/q-quiz-multi-container.scss +++ b/styles_src/q-quiz-multi-container.scss @@ -10,13 +10,11 @@ row-wrap: nowrap; align-items: flex-start; - .q-quiz-element-container { display: block !important; white-space: normal; flex-grow: 0; - &--is-active { height: auto; } diff --git a/styles_src/q-quiz-multi-header.scss b/styles_src/q-quiz-multi-header.scss index 4cb78e3..cfbfea9 100644 --- a/styles_src/q-quiz-multi-header.scss +++ b/styles_src/q-quiz-multi-header.scss @@ -1,5 +1,4 @@ .q-quiz-multi-header { - display: flex; justify-content: flex-end; diff --git a/tasks/build.js b/tasks/build.js index 242f860..93de2cc 100644 --- a/tasks/build.js +++ b/tasks/build.js @@ -1,23 +1,24 @@ -const fs = require('fs'); -const crypto = require('crypto'); +const fs = require("fs"); +const crypto = require("crypto"); -const Builder = require('systemjs-builder'); -const builder = new Builder('', 'jspm.config.js'); +const Builder = require("systemjs-builder"); +const builder = new Builder("", "jspm.config.js"); -const sass = require('node-sass'); -const postcss = require('postcss'); -const postcssImport = require('postcss-import'); -const autoprefixer = require('autoprefixer'); -const cssnano = require('cssnano'); +const sass = require("node-sass"); +const postcss = require("postcss"); +const postcssImport = require("postcss-import"); +const autoprefixer = require("autoprefixer"); +const cssnano = require("cssnano"); // const autoprefixerPlugin = autoprefixer({ // browsers: ['ie > 9', 'last 3 versions'] // }); -const stylesDir = __dirname + '/../styles_src/'; +const stylesDir = __dirname + "/../styles_src/"; builder.config({ map: { - 'systemjs-babel-build': 'jspm_packages/npm/systemjs-plugin-babel@0.0.20/systemjs-babel-node.js' + "systemjs-babel-build": + "jspm_packages/npm/systemjs-plugin-babel@0.0.20/systemjs-babel-node.js" } }); @@ -25,13 +26,16 @@ function writeHashmap(hashmapPath, files, fileext) { const hashMap = {}; files .map(file => { - const hash = crypto.createHash('md5'); - hash.update(file.content, { encoding: 'utf8'} ); - file.hash = hash.digest('hex'); + const hash = crypto.createHash("md5"); + hash.update(file.content, { encoding: "utf8" }); + file.hash = hash.digest("hex"); return file; }) .map(file => { - hashMap[file.name] = `${file.name}.${file.hash.substring(0, 8)}.${fileext}`; + hashMap[file.name] = `${file.name}.${file.hash.substring( + 0, + 8 + )}.${fileext}`; }); fs.writeFileSync(hashmapPath, JSON.stringify(hashMap)); @@ -39,27 +43,34 @@ function writeHashmap(hashmapPath, files, fileext) { async function buildScripts() { return builder - .bundle('q-quiz/quiz.js', { normalize: true, runtime: false, minify: true, mangle: false }) + .bundle("q-quiz/quiz.js", { + normalize: true, + runtime: false, + minify: true, + mangle: false + }) .then(bundle => { - const fileName = 'quiz'; + const fileName = "quiz"; fs.writeFileSync(`scripts/${fileName}.js`, bundle.source); - return [{ - name: fileName, - content: bundle.source - }]; + return [ + { + name: fileName, + content: bundle.source + } + ]; }) - .then((files) => { - writeHashmap('scripts/hashMap.json', files, 'js'); + .then(files => { + writeHashmap("scripts/hashMap.json", files, "js"); }) .then(() => { /* eslint-disable */ - console.log('Build complete'); + console.log("Build complete"); /* eslint-enable */ process.exit(0); }) - .catch((err) => { + .catch(err => { /* eslint-disable */ - console.log('Build error', err); + console.log("Build error", err); /* eslint-enable */ process.exit(1); }); @@ -68,7 +79,7 @@ async function buildScripts() { async function compileStylesheet(name) { return new Promise((resolve, reject) => { const filePath = stylesDir + `${name}.scss`; - fs.exists(filePath, (exists) => { + fs.exists(filePath, exists => { if (!exists) { reject(`stylesheet not found ${filePath}`); process.exit(1); @@ -76,8 +87,8 @@ async function compileStylesheet(name) { sass.render( { file: filePath, - includePaths: [__dirname + '/../jspm_packages/npm'], - outputStyle: 'compressed' + includePaths: [__dirname + "/../jspm_packages/npm"], + outputStyle: "compressed" }, (err, sassResult) => { if (err) { @@ -107,8 +118,8 @@ async function buildStyles() { // compile styles const styleFiles = [ { - name: 'default', - content: await compileStylesheet('default') + name: "default", + content: await compileStylesheet("default") } ]; @@ -116,16 +127,12 @@ async function buildStyles() { fs.writeFileSync(`styles/${file.name}.css`, file.content); }); - writeHashmap('styles/hashMap.json', styleFiles, 'css'); + writeHashmap("styles/hashMap.json", styleFiles, "css"); } -Promise.all( - [ - buildScripts(), - buildStyles() - ]) +Promise.all([buildScripts(), buildStyles()]) .then(res => { - console.log('build complete'); + console.log("build complete"); }) .catch(err => { console.log(err); diff --git a/test/e2e-tests.js b/test/e2e-tests.js index 67c3d15..b18e58b 100644 --- a/test/e2e-tests.js +++ b/test/e2e-tests.js @@ -1,37 +1,41 @@ -const Lab = require('lab'); -const Code = require('code'); -const Hapi = require('hapi'); -const Boom = require('boom'); -const lab = exports.lab = Lab.script(); -const PouchDb = require('pouchdb'); +const Lab = require("lab"); +const Code = require("code"); +const Hapi = require("hapi"); +const Boom = require("boom"); +const lab = (exports.lab = Lab.script()); +const PouchDb = require("pouchdb"); const expect = Code.expect; const before = lab.before; const after = lab.after; const it = lab.it; -process.env.COUCH_DB_URL_Q_QUIZ = 'http://localhost:5984/answer-store'; -process.env.COUCH_DB_URL_Q_ITEMS = 'http://localhost:9999/item' +process.env.COUCH_DB_URL_Q_QUIZ = "http://localhost:5984/answer-store"; +process.env.COUCH_DB_URL_Q_ITEMS = "http://localhost:9999/item"; -let server = require('./server.js').getServer(); -const plugins = require('./plugins'); -const routes = require('../routes/routes.js'); +let server = require("./server.js").getServer(); +const plugins = require("./plugins"); +const routes = require("../routes/routes.js"); before(async () => { - const { spawn } = require('child_process'); - pouchdbServer = spawn('./node_modules/pouchdb-server/bin/pouchdb-server', ['-c', 'test/pouchdb-server-config.json', '--in-memory']); + const { spawn } = require("child_process"); + pouchdbServer = spawn("./node_modules/pouchdb-server/bin/pouchdb-server", [ + "-c", + "test/pouchdb-server-config.json", + "--in-memory" + ]); // wait a second to give pouchdbServer time to boot await new Promise(resolve => { setTimeout(resolve, 1000); }); - console.log('started pouchdb server with pid', pouchdbServer.pid); - const setupCouch = await require('./mock/couchdb.js').setupCouch; + console.log("started pouchdb server with pid", pouchdbServer.pid); + const setupCouch = await require("./mock/couchdb.js").setupCouch; await setupCouch(); - await require('./mock/qserver.js').start(); + await require("./mock/qserver.js").start(); await server.register(plugins); server.route(routes); @@ -39,162 +43,184 @@ before(async () => { }); after(async () => { - console.log('\ngoing to kill pouchdb server with pid', pouchdbServer.pid); - pouchdbServer.kill('SIGHUP'); - console.log('killed?', pouchdbServer.killed, '\n'); + console.log("\ngoing to kill pouchdb server with pid", pouchdbServer.pid); + pouchdbServer.kill("SIGHUP"); + console.log("killed?", pouchdbServer.killed, "\n"); if (!pouchdbServer.killed) { - console.log('somehow i could not kill your pouchdb server. maybe another one is still running. check with "lsof -i :5984" and kill it yourself'); + console.log( + 'somehow i could not kill your pouchdb server. maybe another one is still running. check with "lsof -i :5984" and kill it yourself' + ); } await server.stop({ timeout: 2000 }); server = null; return; }); -lab.experiment('basic routes', () => { - it('starts the server', () => { +lab.experiment("basic routes", () => { + it("starts the server", () => { expect(server.info.created).to.be.a.number(); }); - it('is healthy', async () => { - const response = await server.inject('/health'); - expect(response.payload).to.be.equal('ok'); + it("is healthy", async () => { + const response = await server.inject("/health"); + expect(response.payload).to.be.equal("ok"); }); }); -lab.experiment('schema route', () => { - it('returns existing schema', async () => { +lab.experiment("schema route", () => { + it("returns existing schema", async () => { const response = await server.inject(`/schema.json`); expect(response.statusCode).to.be.equal(200); }); - it('returns Not Found when requesting an inexisting schema', async () => { - const response = await server.inject('/inexisting.json'); + it("returns Not Found when requesting an inexisting schema", async () => { + const response = await server.inject("/inexisting.json"); expect(response.statusCode).to.be.equal(404); }); }); -lab.experiment('stylesheets route', () => { - it('returns existing stylesheet with right cache control header', { plan: 2 }, async () => { - const filename = require('../styles/hashMap.json').default; - const response = await server.inject(`/stylesheet/${filename}`); - expect(response.statusCode).to.be.equal(200); - expect(response.headers['cache-control']).to.be.equal('max-age=31536000, immutable'); - }); +lab.experiment("stylesheets route", () => { + it( + "returns existing stylesheet with right cache control header", + { plan: 2 }, + async () => { + const filename = require("../styles/hashMap.json").default; + const response = await server.inject(`/stylesheet/${filename}`); + expect(response.statusCode).to.be.equal(200); + expect(response.headers["cache-control"]).to.be.equal( + "max-age=31536000, immutable" + ); + } + ); - it('returns Not Found when requesting an inexisting stylesheet', async () => { - const response = await server.inject('/stylesheet/inexisting.123.css'); + it("returns Not Found when requesting an inexisting stylesheet", async () => { + const response = await server.inject("/stylesheet/inexisting.123.css"); expect(response.statusCode).to.be.equal(404); }); }); -lab.experiment('scripts route', () => { - it('returns existing script with right cache control header', { plan: 2 }, async () => { - const filename = require('../scripts/hashMap.json').quiz; - const response = await server.inject(`/script/${filename}`); - expect(response.statusCode).to.be.equal(200); - expect(response.headers['cache-control']).to.be.equal('max-age=31536000, immutable'); - }); +lab.experiment("scripts route", () => { + it( + "returns existing script with right cache control header", + { plan: 2 }, + async () => { + const filename = require("../scripts/hashMap.json").quiz; + const response = await server.inject(`/script/${filename}`); + expect(response.statusCode).to.be.equal(200); + expect(response.headers["cache-control"]).to.be.equal( + "max-age=31536000, immutable" + ); + } + ); - it('returns Not Found when requesting an inexisting script', async () => { - const response = await server.inject('/script/inexisting.123.css'); + it("returns Not Found when requesting an inexisting script", async () => { + const response = await server.inject("/script/inexisting.123.css"); expect(response.statusCode).to.be.equal(404); }); }); -lab.experiment('locales route', () => { - it('returns existing english translation', async () => { +lab.experiment("locales route", () => { + it("returns existing english translation", async () => { const response = await server.inject(`/locales/en/translation.json`); expect(response.statusCode).to.be.equal(200); }); - it('returns Not Found when requesting an inexisting translation', async () => { - const response = await server.inject('/locales/inexisting/translation.json'); + it("returns Not Found when requesting an inexisting translation", async () => { + const response = await server.inject( + "/locales/inexisting/translation.json" + ); expect(response.statusCode).to.be.equal(404); }); }); -lab.experiment('rendering info route', async () => { - it('renders a quiz', { plan: 4 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); +lab.experiment("rendering info route", async () => { + it("renders a quiz", { plan: 4 }, async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const fixtureData = fixtureResponse.result; const response = await server.inject({ - method: 'POST', - url: '/rendering-info/html-js', + method: "POST", + url: "/rendering-info/html-js", payload: { item: JSON.stringify(fixtureData[0]), toolRuntimeConfig: { - toolBaseUrl: 'http://localhost:3000' + toolBaseUrl: "http://localhost:3000" } } - }) + }); expect(response.statusCode).to.be.equal(200); - expect(response.result.markup).startsWith(`
{ - const fixtureResponse = await server.inject('/fixtures/data'); - const fixtureData = fixtureResponse.result; - const response = await server.inject({ - method: 'POST', - url: '/rendering-info/html-js', - payload: { - item: JSON.stringify(fixtureData[0]), - toolRuntimeConfig: { - toolBaseUrl: 'http://localhost:3000', - isPure: true + it( + "renders quiz for which answers will not be saved (= is pure)", + { plan: 3 }, + async () => { + const fixtureResponse = await server.inject("/fixtures/data"); + const fixtureData = fixtureResponse.result; + const response = await server.inject({ + method: "POST", + url: "/rendering-info/html-js", + payload: { + item: JSON.stringify(fixtureData[0]), + toolRuntimeConfig: { + toolBaseUrl: "http://localhost:3000", + isPure: true + } } - } - }) - expect(response.statusCode).to.be.equal(200); - expect(response.result.scripts.length).to.be.equal(2); - expect(response.result.scripts[1].content).contains('"isPure":true'); - }); + }); + expect(response.statusCode).to.be.equal(200); + expect(response.result.scripts.length).to.be.equal(2); + expect(response.result.scripts[1].content).contains('"isPure":true'); + } + ); - // id from answer and random id? + // id from answer and random id? // no last card? // isFinalScoreShown? - it('returns 400 if no payload is given', async () => { + it("returns 400 if no payload is given", async () => { const response = await server.inject({ - method: 'POST', - url: '/rendering-info/html-js', + method: "POST", + url: "/rendering-info/html-js" }); expect(response.statusCode).to.be.equal(400); }); - it('returns 400 if no item is given in payload', async () => { + it("returns 400 if no item is given in payload", async () => { const response = await server.inject({ - method: 'POST', - url: '/rendering-info/html-js', + method: "POST", + url: "/rendering-info/html-js", payload: { toolRuntimeConfig: { - toolBaseUrl: 'http://localhost:3000' + toolBaseUrl: "http://localhost:3000" } } }); expect(response.statusCode).to.be.equal(400); }); - it('returns 400 if no toolRuntimeConfig is given in payload', async () => { - const fixtureResponse = await server.inject('/fixtures/data'); + it("returns 400 if no toolRuntimeConfig is given in payload", async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const fixtureData = fixtureResponse.result; const response = await server.inject({ - method: 'POST', - url: '/rendering-info/html-js', + method: "POST", + url: "/rendering-info/html-js", payload: { - item: JSON.stringify(fixtureData[0]), + item: JSON.stringify(fixtureData[0]) } }); expect(response.statusCode).to.be.equal(400); }); }); -lab.experiment('answer store route', () => { - it('returns id for stored answer', { plan: 2 }, async () => { +lab.experiment("answer store route", () => { + it("returns id for stored answer", { plan: 2 }, async () => { // only quick test at the moment with fixed value, should be random values in possible domain (min, max, max distance..) and should be done several times, maybe also use for testing stats route - const fixtureResponse = await server.inject('/fixtures/data'); + const fixtureResponse = await server.inject("/fixtures/data"); const fixtureData = fixtureResponse.result; const fixtureIndex = 0; const data = { @@ -202,56 +228,68 @@ lab.experiment('answer store route', () => { questionId: fixtureData[fixtureIndex].elements[2].id, type: fixtureData[fixtureIndex].elements[2].type, value: 7 - } + }; const answerStoreResponse = await server.inject({ - method: 'POST', - url: '/answer', + method: "POST", + url: "/answer", payload: { data: JSON.stringify(data) - }, - }) + } + }); expect(answerStoreResponse.statusCode).to.be.equal(200); expect(answerStoreResponse.result.id).to.be.a.string(); }); - it('returns 400 if payload is empty', async () => { + it("returns 400 if payload is empty", async () => { const response = await server.inject({ - method: 'POST', - url: '/answer', + method: "POST", + url: "/answer", payload: {} - }) + }); expect(response.statusCode).to.be.equal(400); }); }); -lab.experiment('answer stats route', () => { - it('returns stats for a multiple choice question of first fixture quiz', { plan: 3 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); - const fixtureData = fixtureResponse.result; - const fixtureIndex = 0; - const mcQuestion = fixtureData[fixtureIndex].elements[1]; - const statsReponse = await server.inject(`/stats/answers/${mcQuestion.type}/${fixtureIndex}/${mcQuestion.id}`); - expect(statsReponse.statusCode).to.be.equal(200); - expect(statsReponse.result.totalAnswers).to.be.equal(5); - expect(statsReponse.result.numberOfAnswersPerChoice['richtig']).to.be.equal(3); - }); +lab.experiment("answer stats route", () => { + it( + "returns stats for a multiple choice question of first fixture quiz", + { plan: 3 }, + async () => { + const fixtureResponse = await server.inject("/fixtures/data"); + const fixtureData = fixtureResponse.result; + const fixtureIndex = 0; + const mcQuestion = fixtureData[fixtureIndex].elements[1]; + const statsReponse = await server.inject( + `/stats/answers/${mcQuestion.type}/${fixtureIndex}/${mcQuestion.id}` + ); + expect(statsReponse.statusCode).to.be.equal(200); + expect(statsReponse.result.totalAnswers).to.be.equal(5); + expect( + statsReponse.result.numberOfAnswersPerChoice["richtig"] + ).to.be.equal(3); + } + ); - it('returns 400 if question type is not valid', async () => { - const fixtureResponse = await server.inject('/fixtures/data'); + it("returns 400 if question type is not valid", async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const fixtureData = fixtureResponse.result; const fixtureIndex = 0; const mcQuestion = fixtureData[fixtureIndex].elements[1]; - const response = await server.inject(`/stats/answers/singleChoice/${fixtureIndex}/${mcQuestion.id}`); + const response = await server.inject( + `/stats/answers/singleChoice/${fixtureIndex}/${mcQuestion.id}` + ); expect(response.statusCode).to.be.equal(400); }); }); -lab.experiment('score route', () => { - const questionTypes = require('../resources/helpers/constants.js').questionTypes; - const worstAnswerDifference = require('../resources/helpers/scoreHelpers.js').worstAnswerDifference; +lab.experiment("score route", () => { + const questionTypes = require("../resources/helpers/constants.js") + .questionTypes; + const worstAnswerDifference = require("../resources/helpers/scoreHelpers.js") + .worstAnswerDifference; - it('returns 100% score for right answers only', { plan: 2 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); + it("returns 100% score for right answers only", { plan: 2 }, async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const quiz = fixtureResponse.result[0]; const questions = quiz.elements.filter(element => { return questionTypes.includes(element.type); @@ -259,21 +297,24 @@ lab.experiment('score route', () => { let userAnswers = []; for (const question of questions) { let userAnswer = { - questionId: question.id, - } - if (question.type === 'multipleChoice' || question.type === 'numberGuess') { + questionId: question.id + }; + if ( + question.type === "multipleChoice" || + question.type === "numberGuess" + ) { userAnswer.value = question.answer; - } else if (question.type === 'mapPointGuess') { + } else if (question.type === "mapPointGuess") { userAnswer.value = { distance: 0 - } + }; } userAnswers.push(userAnswer); - }; + } const response = await server.inject({ - method: 'POST', - url: '/score', + method: "POST", + url: "/score", payload: { item: JSON.stringify(quiz), userAnswers: userAnswers @@ -284,8 +325,8 @@ lab.experiment('score route', () => { expect(response.result.maxScore).to.be.equal(response.result.achievedScore); }); - it('returns score for mixed answers', { plan: 2 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); + it("returns score for mixed answers", { plan: 2 }, async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const quiz = fixtureResponse.result[0]; const questions = quiz.elements.filter(element => { return questionTypes.includes(element.type); @@ -293,26 +334,27 @@ lab.experiment('score route', () => { let userAnswers = []; for (const question of questions) { let userAnswer = { - questionId: question.id, - } - if (question.type === 'multipleChoice') { + questionId: question.id + }; + if (question.type === "multipleChoice") { userAnswer.value = question.choices[0]; - } else if (question.type === 'numberGuess') { + } else if (question.type === "numberGuess") { // random answer between min and max - userAnswer.value = Math.random() + (question.max - question.min) + question.min; - } else if (question.type === 'mapPointGuess') { + userAnswer.value = + Math.random() + (question.max - question.min) + question.min; + } else if (question.type === "mapPointGuess") { // random answer between 0 distance and worst distance const worst = worstAnswerDifference(question); userAnswer.value = { distance: Math.random() * worst - } + }; } userAnswers.push(userAnswer); - }; + } const response = await server.inject({ - method: 'POST', - url: '/score', + method: "POST", + url: "/score", payload: { item: JSON.stringify(quiz), userAnswers: userAnswers @@ -320,52 +362,61 @@ lab.experiment('score route', () => { }); expect(response.statusCode).to.be.equal(200); - expect(response.result.maxScore).to.be.greaterThan(response.result.achievedScore); + expect(response.result.maxScore).to.be.greaterThan( + response.result.achievedScore + ); }); - it('returns score for partially answered questions', { plan: 2 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); - const quiz = fixtureResponse.result[0]; - const questions = quiz.elements.filter(element => { - return questionTypes.includes(element.type); - }); - let userAnswers = []; - for (const [index, question] of questions.entries()) { - if (index % 2 === 0) { - let userAnswer = { - questionId: question.id, - } - if (question.type === 'multipleChoice') { - userAnswer.value = question.answer; - } else if (question.type === 'numberGuess') { - // random answer between min and max - userAnswer.value = Math.random() + (question.max - question.min) + question.min; - } else if (question.type === 'mapPointGuess') { - // random answer between 0 distance and worst distance - const worst = worstAnswerDifference(question); - userAnswer.value = { - distance: Math.random() * worst + it( + "returns score for partially answered questions", + { plan: 2 }, + async () => { + const fixtureResponse = await server.inject("/fixtures/data"); + const quiz = fixtureResponse.result[0]; + const questions = quiz.elements.filter(element => { + return questionTypes.includes(element.type); + }); + let userAnswers = []; + for (const [index, question] of questions.entries()) { + if (index % 2 === 0) { + let userAnswer = { + questionId: question.id + }; + if (question.type === "multipleChoice") { + userAnswer.value = question.answer; + } else if (question.type === "numberGuess") { + // random answer between min and max + userAnswer.value = + Math.random() + (question.max - question.min) + question.min; + } else if (question.type === "mapPointGuess") { + // random answer between 0 distance and worst distance + const worst = worstAnswerDifference(question); + userAnswer.value = { + distance: Math.random() * worst + }; } + userAnswers.push(userAnswer); } - userAnswers.push(userAnswer); } - }; - const response = await server.inject({ - method: 'POST', - url: '/score', - payload: { - item: JSON.stringify(quiz), - userAnswers: userAnswers - } - }); + const response = await server.inject({ + method: "POST", + url: "/score", + payload: { + item: JSON.stringify(quiz), + userAnswers: userAnswers + } + }); - expect(response.statusCode).to.be.equal(200); - expect(response.result.maxScore).to.be.greaterThan(response.result.achievedScore); - }) + expect(response.statusCode).to.be.equal(200); + expect(response.result.maxScore).to.be.greaterThan( + response.result.achievedScore + ); + } + ); - it('returns 0 score for wrong/worst answers only', { plan: 2 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); + it("returns 0 score for wrong/worst answers only", { plan: 2 }, async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const quiz = fixtureResponse.result[0]; const questions = quiz.elements.filter(element => { return questionTypes.includes(element.type); @@ -373,28 +424,28 @@ lab.experiment('score route', () => { let userAnswers = []; for (const question of questions) { let userAnswer = { - questionId: question.id, - } - if (question.type === 'multipleChoice') { + questionId: question.id + }; + if (question.type === "multipleChoice") { userAnswer.value = question.choices[0]; - } else if (question.type === 'numberGuess') { + } else if (question.type === "numberGuess") { const worst = worstAnswerDifference(question); if (worst === question.max) { userAnswer.value = question.max; } else { userAnswer.value = question.min; } - } else if (question.type === 'mapPointGuess') { + } else if (question.type === "mapPointGuess") { userAnswer.value = { distance: worstAnswerDifference(question) - } + }; } userAnswers.push(userAnswer); - }; + } const response = await server.inject({ - method: 'POST', - url: '/score', + method: "POST", + url: "/score", payload: { item: JSON.stringify(quiz), userAnswers: userAnswers @@ -405,38 +456,40 @@ lab.experiment('score route', () => { expect(response.result.achievedScore).to.be.equal(0); }); - it('returns 400 if payload is empty', async () => { + it("returns 400 if payload is empty", async () => { const response = await server.inject({ - method: 'POST', - url: '/score', + method: "POST", + url: "/score", payload: {} }); expect(response.statusCode).to.be.equal(400); }); - it('returns 400 if item is missing in payload', async () => { + it("returns 400 if item is missing in payload", async () => { const response = await server.inject({ - method: 'POST', - url: '/score', + method: "POST", + url: "/score", payload: { - userAnswers: [{ - questionId: 'inexisting', - value: 'inexisting' - }] + userAnswers: [ + { + questionId: "inexisting", + value: "inexisting" + } + ] } }); expect(response.statusCode).to.be.equal(400); }); - it('returns 400 if user answers is of wrong type in payload', async () => { - const fixtureResponse = await server.inject('/fixtures/data'); + it("returns 400 if user answers is of wrong type in payload", async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const quiz = fixtureResponse.result[0]; const response = await server.inject({ - method: 'POST', - url: '/score', + method: "POST", + url: "/score", payload: { item: JSON.stringify(quiz), userAnswers: {} @@ -447,44 +500,62 @@ lab.experiment('score route', () => { }); }); -lab.experiment('number guess plot route', () => { - it('returns number guess plot', { plan: 2 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); +lab.experiment("number guess plot route", () => { + it("returns number guess plot", { plan: 2 }, async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const fixtureIndex = 0; const quiz = fixtureResponse.result[fixtureIndex]; - const response = await server.inject(`/number-guess/${fixtureIndex}/${quiz.elements[2].id}/plot/${560}`); + const response = await server.inject( + `/number-guess/${fixtureIndex}/${quiz.elements[2].id}/plot/${560}` + ); expect(response.statusCode).to.be.equal(200); - expect(response.result).startsWith(' { - const fixtureResponse = await server.inject('/fixtures/data'); - const fixtureIndex = 0; - const quiz = fixtureResponse.result[fixtureIndex]; - const response = await server.inject(`/number-guess/${fixtureIndex}/${quiz.elements[1].id}/plot/${560}`); - expect(response.statusCode).to.be.equal(400); - expect(response.result.message).to.be.equal('stats is undefined'); - }) -}) + it( + "returns 400 for requesting number guess plot with multiple choice question", + { plan: 2 }, + async () => { + const fixtureResponse = await server.inject("/fixtures/data"); + const fixtureIndex = 0; + const quiz = fixtureResponse.result[fixtureIndex]; + const response = await server.inject( + `/number-guess/${fixtureIndex}/${quiz.elements[1].id}/plot/${560}` + ); + expect(response.statusCode).to.be.equal(400); + expect(response.result.message).to.be.equal("stats is undefined"); + } + ); +}); -lab.experiment('map point guess plot route', () => { - it('returns heatmap for map point guess', { plan: 2 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); +lab.experiment("map point guess plot route", () => { + it("returns heatmap for map point guess", { plan: 2 }, async () => { + const fixtureResponse = await server.inject("/fixtures/data"); const quiz = fixtureResponse.result[0]; const mapPointQuestion = quiz.elements[4]; - const bboxString = `${mapPointQuestion.answer.bbox[0]}, ${mapPointQuestion.answer.bbox[1]}, ${mapPointQuestion.answer.bbox[3]}, ${mapPointQuestion.answer.bbox[4]}` - const response = await server.inject(`/map/${mapPointQuestion.id}/heatmap/${560}/${500}/${bboxString}`); + const bboxString = `${mapPointQuestion.answer.bbox[0]}, ${ + mapPointQuestion.answer.bbox[1] + }, ${mapPointQuestion.answer.bbox[3]}, ${mapPointQuestion.answer.bbox[4]}`; + const response = await server.inject( + `/map/${mapPointQuestion.id}/heatmap/${560}/${500}/${bboxString}` + ); expect(response.statusCode).to.be.equal(200); - expect(response.headers['content-type']).to.be.equal('image/png'); - }) + expect(response.headers["content-type"]).to.be.equal("image/png"); + }); - it('returns 400 for requesting number guess plot with multiple choice question', { plan: 2 }, async () => { - const fixtureResponse = await server.inject('/fixtures/data'); - const quiz = fixtureResponse.result[0]; - const response = await server.inject(`/map/${quiz.elements[1].id}/heatmap/${560}/${500}/1, 2, 3, 4`); - expect(response.statusCode).to.be.equal(400); - expect(response.result.message).to.be.equal('invalid answer'); - }) -}) + it( + "returns 400 for requesting number guess plot with multiple choice question", + { plan: 2 }, + async () => { + const fixtureResponse = await server.inject("/fixtures/data"); + const quiz = fixtureResponse.result[0]; + const response = await server.inject( + `/map/${quiz.elements[1].id}/heatmap/${560}/${500}/1, 2, 3, 4` + ); + expect(response.statusCode).to.be.equal(400); + expect(response.result.message).to.be.equal("invalid answer"); + } + ); +}); // check further very specific points in code (see coverage result) if they should/can be tested, maybe also with unit tests diff --git a/test/mock/answers.js b/test/mock/answers.js index 48e9a93..596acc8 100644 --- a/test/mock/answers.js +++ b/test/mock/answers.js @@ -1,65 +1,65 @@ module.exports = [ { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673205043-233776299', - type: 'multipleChoice', - value: 'richtig' + itemId: "quiz-0", + questionId: "quiz-0-1503673205043-233776299", + type: "multipleChoice", + value: "richtig" } }, - { + { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673205043-233776299', - type: 'multipleChoice', - value: 'richtig' + itemId: "quiz-0", + questionId: "quiz-0-1503673205043-233776299", + type: "multipleChoice", + value: "richtig" } }, { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673205043-233776299', - type: 'multipleChoice', - value: 'richtig' + itemId: "quiz-0", + questionId: "quiz-0-1503673205043-233776299", + type: "multipleChoice", + value: "richtig" } }, { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673205043-233776299', - type: 'multipleChoice', - value: 'vielleicht' + itemId: "quiz-0", + questionId: "quiz-0-1503673205043-233776299", + type: "multipleChoice", + value: "vielleicht" } }, { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673205043-233776299', - type: 'multipleChoice', - value: 'falsch' + itemId: "quiz-0", + questionId: "quiz-0-1503673205043-233776299", + type: "multipleChoice", + value: "falsch" } }, { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673244888-827300797', - type: 'numberGuess', + itemId: "quiz-0", + questionId: "quiz-0-1503673244888-827300797", + type: "numberGuess", value: 7 } }, { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673375520-572013568', - type: 'numberGuess', + itemId: "quiz-0", + questionId: "quiz-0-1503673375520-572013568", + type: "numberGuess", value: 30 } }, { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673602641-999859280', - type: 'mapPointGuess', + itemId: "quiz-0", + questionId: "quiz-0-1503673602641-999859280", + type: "mapPointGuess", value: { latLng: { lat: 47.36553654530629, @@ -71,9 +71,9 @@ module.exports = [ }, { data: { - itemId: 'quiz-0', - questionId: 'quiz-0-1503673653497-139543834', - type: 'mapPointGuess', + itemId: "quiz-0", + questionId: "quiz-0-1503673653497-139543834", + type: "mapPointGuess", value: { latLng: { lat: 51.28940590271679, @@ -83,4 +83,4 @@ module.exports = [ } } } -] +]; diff --git a/test/mock/couchdb.js b/test/mock/couchdb.js index 8208746..7ed0d6b 100644 --- a/test/mock/couchdb.js +++ b/test/mock/couchdb.js @@ -1,60 +1,69 @@ -const wreck = require('wreck'); +const wreck = require("wreck"); -const views = require('./views.js'); -const answers = require('./answers.js'); +const views = require("./views.js"); +const answers = require("./answers.js"); const fixtureData = [ - require('../../resources/fixtures/data/all.json'), - require('../../resources/fixtures/data/cover-with-title-no-last-card.json') + require("../../resources/fixtures/data/all.json"), + require("../../resources/fixtures/data/cover-with-title-no-last-card.json") ]; module.exports = { setupCouch: async function() { try { - await wreck.put('http://localhost:5984/answer-store'); + await wreck.put("http://localhost:5984/answer-store"); } catch (err) { - console.log('failed to create database answer-store', err); + console.log("failed to create database answer-store", err); process.exit(1); } try { for (const view of views) { - const viewsResponse = await wreck.post('http://localhost:5984/answer-store', { - payload: view - }); + const viewsResponse = await wreck.post( + "http://localhost:5984/answer-store", + { + payload: view + } + ); console.log(`all ${views.length} views successfully saved`); } } catch (err) { - console.log('failed to add views to answer-store', err); + console.log("failed to add views to answer-store", err); process.exit(1); } try { for (const answer of answers) { - const createAnswerResponse = await wreck.post('http://localhost:5984/answer-store', { - payload: answer - }); + const createAnswerResponse = await wreck.post( + "http://localhost:5984/answer-store", + { + payload: answer + } + ); } console.log(`all ${answers.length} answers successfully saved`); } catch (err) { - console.log('failed to add documents to answer-store', err); + console.log("failed to add documents to answer-store", err); process.exit(1); } try { - await wreck.put('http://localhost:5984/q-items'); - console.log('q items created'); + await wreck.put("http://localhost:5984/q-items"); + console.log("q items created"); } catch (err) { - console.log('failed to create database q-items', err); + console.log("failed to create database q-items", err); process.exit(1); } try { for (const quiz of fixtureData) { - const createQuizResponse = await wreck.post('http://localhost:5984/q-items', { - payload: quiz - }) + const createQuizResponse = await wreck.post( + "http://localhost:5984/q-items", + { + payload: quiz + } + ); } console.log(`all ${fixtureData.length} quizzes successfully saved`); - return; + return; } catch (err) { - console.log('failed to add fixture quizzes to q-items', err); + console.log("failed to add fixture quizzes to q-items", err); process.exit(1); } } -} +}; diff --git a/test/mock/qserver.js b/test/mock/qserver.js index 55f81de..dea0de4 100644 --- a/test/mock/qserver.js +++ b/test/mock/qserver.js @@ -1,9 +1,9 @@ -const Hapi = require('hapi'); -const Boom = require('boom'); -const Joi = require('joi'); +const Hapi = require("hapi"); +const Boom = require("boom"); +const Joi = require("joi"); // provide every fixture data file present in ../../resources/fixtures/data -const fixtureDataDirectory = '../../resources/fixtures/data'; +const fixtureDataDirectory = "../../resources/fixtures/data"; const fixtureData = [ require(`${fixtureDataDirectory}/all.json`), require(`${fixtureDataDirectory}/cover-with-title-no-last-card.json`) @@ -17,8 +17,8 @@ const server = Hapi.server({ }); server.route({ - method: 'GET', - path: '/item/{id}', + method: "GET", + path: "/item/{id}", options: { validate: { params: { @@ -39,4 +39,4 @@ module.exports = { start: async function() { await server.start(); } -} +}; diff --git a/test/mock/views.js b/test/mock/views.js index ef47ad9..cce3dd1 100644 --- a/test/mock/views.js +++ b/test/mock/views.js @@ -1,29 +1,33 @@ module.exports = [ - { - "_id": "_design/stats", - "views": { + { + _id: "_design/stats", + views: { "answers-numberGuess": { - "map": "function (doc) {\n if (doc.data && doc.data.type === 'numberGuess' && typeof doc.data.value !== 'undefined') {\n var questionId;\n if (doc.data.questionId) {\n questionId = doc.data.questionId;\n } else if (doc.data.itemId) {\n questionId = doc.data.itemId;\n }\n if (questionId) {\n emit([questionId, doc.data.value], 1);\n }\n }\n}", - "reduce": "_count" + map: + "function (doc) {\n if (doc.data && doc.data.type === 'numberGuess' && typeof doc.data.value !== 'undefined') {\n var questionId;\n if (doc.data.questionId) {\n questionId = doc.data.questionId;\n } else if (doc.data.itemId) {\n questionId = doc.data.itemId;\n }\n if (questionId) {\n emit([questionId, doc.data.value], 1);\n }\n }\n}", + reduce: "_count" }, "answers-mapPointGuess": { - "map": "function (doc) {\n if (doc.data && doc.data.type === 'mapPointGuess' && typeof doc.data.value !== 'undefined') {\n var questionId;\n if (doc.data.questionId) {\n questionId = doc.data.questionId;\n } else if (doc.data.itemId) {\n questionId = doc.data.itemId;\n }\n if (questionId) {\n emit([questionId, doc.data.value.distance], 1);\n }\n }\n}", - "reduce": "_count" + map: + "function (doc) {\n if (doc.data && doc.data.type === 'mapPointGuess' && typeof doc.data.value !== 'undefined') {\n var questionId;\n if (doc.data.questionId) {\n questionId = doc.data.questionId;\n } else if (doc.data.itemId) {\n questionId = doc.data.itemId;\n }\n if (questionId) {\n emit([questionId, doc.data.value.distance], 1);\n }\n }\n}", + reduce: "_count" }, "answers-multipleChoice": { - "reduce": "_count", - "map": "function (doc) {\n if (doc.data && doc.data.type === 'multipleChoice' && typeof doc.data.value !== 'undefined') {\n var questionId;\n if (doc.data.questionId) {\n questionId = doc.data.questionId;\n } else if (doc.data.itemId) {\n questionId = doc.data.itemId;\n }\n if (questionId) {\n emit([questionId, doc.data.value], 1);\n }\n }\n}" + reduce: "_count", + map: + "function (doc) {\n if (doc.data && doc.data.type === 'multipleChoice' && typeof doc.data.value !== 'undefined') {\n var questionId;\n if (doc.data.questionId) {\n questionId = doc.data.questionId;\n } else if (doc.data.itemId) {\n questionId = doc.data.itemId;\n }\n if (questionId) {\n emit([questionId, doc.data.value], 1);\n }\n }\n}" } }, - "language": "javascript" + language: "javascript" }, { - "_id": "_design/mapPointGuess", - "views": { - "points": { - "map": "function (doc) {\n if (doc.data && doc.data.type === 'mapPointGuess' && typeof doc.data.value.latLng !== 'undefined') {\n var questionId;\n if (doc.data.questionId) {\n questionId = doc.data.questionId;\n } else if (doc.data.itemId) {\n questionId = doc.data.itemId;\n }\n if (questionId) {\n emit(questionId, doc.data.value.latLng);\n }\n }\n}" + _id: "_design/mapPointGuess", + views: { + points: { + map: + "function (doc) {\n if (doc.data && doc.data.type === 'mapPointGuess' && typeof doc.data.value.latLng !== 'undefined') {\n var questionId;\n if (doc.data.questionId) {\n questionId = doc.data.questionId;\n } else if (doc.data.itemId) {\n questionId = doc.data.itemId;\n }\n if (questionId) {\n emit(questionId, doc.data.value.latLng);\n }\n }\n}" } }, - "language": "javascript" + language: "javascript" } -] +]; diff --git a/test/plugins.js b/test/plugins.js index ff64553..e57760a 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -1,3 +1 @@ -module.exports = [ - require('inert') -] +module.exports = [require("inert")]; diff --git a/test/server.js b/test/server.js index d9f12e1..41ee904 100644 --- a/test/server.js +++ b/test/server.js @@ -1,4 +1,4 @@ -const Hapi = require('hapi'); +const Hapi = require("hapi"); function getServer() { let server = Hapi.server({ From 3dd8a9dc9f71f38deb7fc7ed73be185a508d6332 Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 15:18:19 +0200 Subject: [PATCH 02/26] Update readme --- README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 326051f..3949afb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,32 @@ **Maintainer**: [manuelroth](https://github.com/manuelroth) -Q quiz is one tool of the Q toolbox to render quizzes containing questions of type multiple choice, number guess and map point guess. It also includes the rendering of answer statistics for each question type. +Q Quiz is one tool of the Q toolbox to render quizzes containing questions of type multiple choice, number guess and map point guess. It also includes the rendering of answer statistics for each question type. + +## Installation + +```bash +$ npm install +$ npm run build +``` + +## Development + +Install the [Q cli](https://github.com/nzzdev/Q-cli) and start the Q dev server: + +``` +$ Q server +``` + +Run the Q tool: +``` +$ node index.js +``` ## Implementation details The tool structure follows the general structure of each Q tool. Further information can be found in [Q server documentation - Developing tools](https://nzzdev.github.io/Q-server/developing-tools.html). + +## License +Copyright (c) 2018 Neue Zürcher Zeitung. All rights reserved. + +This software is published under the MIT license. From d83ae8a438836c3f2b485a156771c402cf53eb99 Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 15:21:27 +0200 Subject: [PATCH 03/26] Fix typos in answer text --- script_src/answerHelpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script_src/answerHelpers.js b/script_src/answerHelpers.js index 563368f..dfe71cf 100644 --- a/script_src/answerHelpers.js +++ b/script_src/answerHelpers.js @@ -40,9 +40,9 @@ export function getAnswerTextElement(stats, isCorrectAnswer, getDiffText) { if (stats.totalAnswers === 2) { statsTextHtml += - " Das klingt schlimmer als es ist. Sie waren so schnell, dass erst eine andere Person mitgemacht hat. Ihre Schätzung ist also auch die Zweitbeste."; + " Das klingt schlimmer, als es ist. Sie waren so schnell, dass erst eine andere Person mitgemacht hat. Ihre Schätzung ist also auch die zweitbeste."; } else if (stats.totalAnswers <= 11) { - statsTextHtml += ` Das klingt schlimmer als es ist. Sie waren so schnell, dass erst ${stats.totalAnswers - + statsTextHtml += ` Das klingt schlimmer, als es ist. Sie waren so schnell, dass erst ${stats.totalAnswers - 1} andere mitgemacht haben.`; } else { statsTextHtml += From fe64a251073446f9f8af9445d2be3b4cc2d144e7 Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 15:34:49 +0200 Subject: [PATCH 04/26] (mappointguess) Use new map style where roads and bridges are shown --- script_src/MapPointGuessHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script_src/MapPointGuessHandler.js b/script_src/MapPointGuessHandler.js index 6e95251..e0f1194 100644 --- a/script_src/MapPointGuessHandler.js +++ b/script_src/MapPointGuessHandler.js @@ -6,7 +6,7 @@ Leaflet.Icon.Default.imagePath = "jspm_packages/github/Leaflet/Leaflet@1.0.3/dist/images"; const tileUrl = - "https://api.mapbox.com/styles/v1/neuezuercherzeitung/ciqkqfx6g002rc5m9obtblxlk/tiles/256/{z}/{x}/{y}?access_token=pk.eyJ1IjoibmV1ZXp1ZXJjaGVyemVpdHVuZyIsImEiOiJjaXFnbWpvbmMwMDk4aHptY2RiYjM0dHc2In0.Y3HeaE0zhj9OaFEoDIrxRA"; + "https://api.mapbox.com/styles/v1/neuezuercherzeitung/cj3yj33bk1w5t2rmyy0jty3bb/tiles/256/{z}/{x}/{y}?access_token=pk.eyJ1IjoibmV1ZXp1ZXJjaGVyemVpdHVuZyIsImEiOiJjaXFnbWpvbmMwMDk4aHptY2RiYjM0dHc2In0.Y3HeaE0zhj9OaFEoDIrxRA"; const mapOptions = { boxZoom: false, From e719495a5235247cd9b6f7aaa6f7d6ecc0414bd5 Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 16:32:38 +0200 Subject: [PATCH 05/26] Replace all string concatenation with template strings --- index.js | 2 +- resources/helpers/db.js | 2 +- resources/helpers/utils.js | 2 +- routes/answer-service/number-guess.js | 10 ++-------- routes/answer-service/score.js | 2 +- routes/answer-service/stats.js | 2 +- routes/locales.js | 4 ++-- routes/rendering-info/html-js.js | 16 ++++++++-------- routes/schema.js | 4 ++-- routes/scripts.js | 4 +--- routes/stylesheet.js | 4 +--- script_src/MapPointGuessHandler.js | 2 +- script_src/answerHelpers.js | 2 +- tasks/build.js | 4 ++-- 14 files changed, 25 insertions(+), 35 deletions(-) diff --git a/index.js b/index.js index a116699..badd9bf 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,7 @@ const start = async function() { server.route(routes); await server.start(); - console.log("Server running at: " + server.info.uri); + console.log(`Server running at: ${server.info.uri}`); }; start(); diff --git a/resources/helpers/db.js b/resources/helpers/db.js index e66cb73..f36e5af 100644 --- a/resources/helpers/db.js +++ b/resources/helpers/db.js @@ -10,7 +10,7 @@ if (process.env.COUCH_DB_USER && process.env.COUCH_DB_PASS) { const quizDb = new PouchDB(process.env.COUCH_DB_URL_Q_QUIZ, options); -console.log("connected to " + process.env.COUCH_DB_URL_Q_QUIZ); +console.log(`connected to ${process.env.COUCH_DB_URL_Q_QUIZ}`); module.exports = { quizDb: quizDb diff --git a/resources/helpers/utils.js b/resources/helpers/utils.js index 79e3683..64628c6 100644 --- a/resources/helpers/utils.js +++ b/resources/helpers/utils.js @@ -15,7 +15,7 @@ function getAnswers(type, questionId, options) { options["group"] = true; } - return quizDb.query("stats/answers-" + type, options); + return quizDb.query(`stats/answers-${type}`, options); } function getAnswer(id) { diff --git a/routes/answer-service/number-guess.js b/routes/answer-service/number-guess.js index ac1f7c0..8f5f6c2 100644 --- a/routes/answer-service/number-guess.js +++ b/routes/answer-service/number-guess.js @@ -56,10 +56,7 @@ function getStripplotSvg(data, stats, plotWidth) { .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") - .attr( - "transform", - "translate(" + margin.left + "," + margin.top + ")" - ); + .attr("transform", `translate(${margin.left},${margin.top})`); svg .append("rect") @@ -176,10 +173,7 @@ function getBarchartSvg(data, stats, chartWidth) { .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") - .attr( - "transform", - "translate(" + margin.left + "," + margin.top + ")" - ); + .attr("transform", `translate(${margin.left},${margin.top})`); svg .append("rect") diff --git a/routes/answer-service/score.js b/routes/answer-service/score.js index b8db44d..e546d47 100644 --- a/routes/answer-service/score.js +++ b/routes/answer-service/score.js @@ -2,7 +2,7 @@ const fs = require("fs"); const Enjoi = require("enjoi"); const Joi = require("joi"); const Boom = require("boom"); -const resourcesDir = __dirname + "/../../resources/"; +const resourcesDir = `${__dirname}/../../resources/`; const scoreHelpers = require(`${resourcesDir}helpers/scoreHelpers.js`); const questionTypes = require(`${resourcesDir}helpers/constants.js`) .questionTypes; diff --git a/routes/answer-service/stats.js b/routes/answer-service/stats.js index b02e0ee..501abc5 100644 --- a/routes/answer-service/stats.js +++ b/routes/answer-service/stats.js @@ -90,7 +90,7 @@ module.exports = [ return Boom.badRequest(couchError.message); }); } catch (e) { - console.log("error in stats route: " + e); + console.log(`error in stats route: ${e}`); return Boom.badRequest(e); } } diff --git a/routes/locales.js b/routes/locales.js index 3ab707b..8a8bbb0 100644 --- a/routes/locales.js +++ b/routes/locales.js @@ -1,5 +1,5 @@ const Joi = require("joi"); -const localesDir = __dirname + "/../resources/locales/"; +const localesDir = `${__dirname}/../resources/locales/`; module.exports = { path: "/locales/{lng}/translation.json", @@ -15,7 +15,7 @@ module.exports = { }, handler: (request, h) => { return h - .file(localesDir + request.params.lng + "/translation.json") + .file(`${localesDir}${request.params.lng}/translation.json`) .type("application/json"); } }; diff --git a/routes/rendering-info/html-js.js b/routes/rendering-info/html-js.js index 1a12749..9813dcc 100644 --- a/routes/rendering-info/html-js.js +++ b/routes/rendering-info/html-js.js @@ -1,17 +1,17 @@ const fs = require("fs"); const Enjoi = require("enjoi"); const Joi = require("joi"); -const resourcesDir = __dirname + "/../../resources/"; -const viewsDir = __dirname + "/../../views/"; -const scriptsDir = __dirname + "/../../scripts/"; -const stylesDir = __dirname + "/../../styles/"; -const transform = require(resourcesDir + "helpers/itemTransformer.js"); +const resourcesDir = `${__dirname}/../../resources/`; +const viewsDir = `${__dirname}/../../views/`; +const scriptsDir = `${__dirname}/../../scripts/`; +const stylesDir = `${__dirname}/../../styles/`; +const transform = require(`${resourcesDir}helpers/itemTransformer.js`); const getExactPixelWidth = require(resourcesDir + "helpers/toolRuntimeConfig.js").getExactPixelWidth; -const getImageUrls = require(resourcesDir + "helpers/images.js").getImageUrls; +const getImageUrls = require(`${resourcesDir}helpers/images.js`).getImageUrls; const schemaString = JSON.parse( - fs.readFileSync(resourcesDir + "schema.json", { + fs.readFileSync(`${resourcesDir}schema.json`, { encoding: "utf-8" }) ); @@ -22,7 +22,7 @@ const scriptHashMap = require(`${scriptsDir}/hashMap.json`); const styleHashMap = require(`${stylesDir}/hashMap.json`); require("svelte/ssr/register"); -const staticTemplate = require(viewsDir + "HtmlJs.html"); +const staticTemplate = require(`${viewsDir}HtmlJs.html`); function getTransformedItemForClientSideScript(item, toolRuntimeConfig) { const questionElementData = item.questions.map(element => { diff --git a/routes/schema.js b/routes/schema.js index e714dba..152a1ac 100644 --- a/routes/schema.js +++ b/routes/schema.js @@ -1,9 +1,9 @@ -const resourcesDir = __dirname + "/../resources/"; +const resourcesDir = `${__dirname}/../resources/`; module.exports = { method: "GET", path: "/schema.json", handler: function(request, h) { - return h.file(resourcesDir + "schema.json"); + return h.file(`${resourcesDir}schema.json`); } }; diff --git a/routes/scripts.js b/routes/scripts.js index 87a667e..ca5905c 100644 --- a/routes/scripts.js +++ b/routes/scripts.js @@ -1,5 +1,3 @@ -const path = require("path"); - module.exports = [ { method: "GET", @@ -7,7 +5,7 @@ module.exports = [ options: { cors: true, files: { - relativeTo: path.join(__dirname, "/../scripts/") + relativeTo: `${__dirname}/../scripts/` } }, handler: function(request, h) { diff --git a/routes/stylesheet.js b/routes/stylesheet.js index ab0c892..e673ca5 100644 --- a/routes/stylesheet.js +++ b/routes/stylesheet.js @@ -1,12 +1,10 @@ -const path = require("path"); - module.exports = { method: "GET", path: "/stylesheet/{filename}.{hash}.{extension}", options: { cors: true, files: { - relativeTo: path.join(__dirname, "/../styles/") + relativeTo: `${__dirname}/../styles/` } }, handler: function(request, h) { diff --git a/script_src/MapPointGuessHandler.js b/script_src/MapPointGuessHandler.js index e0f1194..974bcc3 100644 --- a/script_src/MapPointGuessHandler.js +++ b/script_src/MapPointGuessHandler.js @@ -86,7 +86,7 @@ export default class MapPointGuessHandler { setMapSize(map) { let container = map.getContainer(); - container.style.height = map.getSize()["x"] * (9 / 16) + "px"; + container.style.height = `${map.getSize()["x"] * (9 / 16)}px`; map.invalidateSize(); } diff --git a/script_src/answerHelpers.js b/script_src/answerHelpers.js index dfe71cf..9ae4a4b 100644 --- a/script_src/answerHelpers.js +++ b/script_src/answerHelpers.js @@ -99,7 +99,7 @@ function getRecommendationsElement(articleRecommendations) { articleRecommendations[index].text.length && articleRecommendations[index].text.length > 0 ) { - recommendationText = articleRecommendations[index].text + " "; + recommendationText = `${articleRecommendations[index].text} `; } recommendationsHtml += ` ${recommendationText} 9', 'last 3 versions'] // }); -const stylesDir = __dirname + "/../styles_src/"; +const stylesDir = `${__dirname}/../styles_src/`; builder.config({ map: { @@ -87,7 +87,7 @@ async function compileStylesheet(name) { sass.render( { file: filePath, - includePaths: [__dirname + "/../jspm_packages/npm"], + includePaths: [`${__dirname}/../jspm_packages/npm`], outputStyle: "compressed" }, (err, sassResult) => { From 75b612a60d257846922466b4c95b254851b11876 Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 16:46:33 +0200 Subject: [PATCH 06/26] Update npm and jspm packages to latest version --- jspm.config.js | 6 +- package-lock.json | 192 +++++------------------------ package.json | 8 +- script_src/MapPointGuessHandler.js | 2 +- tasks/build.js | 2 +- 5 files changed, 37 insertions(+), 173 deletions(-) diff --git a/jspm.config.js b/jspm.config.js index ed322cb..948d4b4 100644 --- a/jspm.config.js +++ b/jspm.config.js @@ -9,7 +9,7 @@ SystemJS.config({ }, devConfig: { map: { - "plugin-babel": "npm:systemjs-plugin-babel@0.0.21" + "plugin-babel": "npm:systemjs-plugin-babel@0.0.25" } }, transpiler: "plugin-babel", @@ -28,8 +28,8 @@ SystemJS.config({ SystemJS.config({ packageConfigPaths: ["npm:@*/*.json", "npm:*.json", "github:*/*.json"], map: { - leaflet: "github:Leaflet/Leaflet@1.0.3", - text: "github:systemjs/plugin-text@0.0.9" + leaflet: "github:Leaflet/Leaflet@1.3.3", + text: "github:systemjs/plugin-text@0.0.11" }, packages: {} }); diff --git a/package-lock.json b/package-lock.json index e95987b..1521453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5197,19 +5197,6 @@ "globule": "^1.0.0" } }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "requires": { - "is-property": "^1.0.0" - } - }, "geojson-equality": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/geojson-equality/-/geojson-equality-0.1.6.tgz", @@ -5229,9 +5216,9 @@ } }, "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" }, "get-closest": { "version": "0.0.4", @@ -5737,9 +5724,9 @@ } }, "hosted-git-info": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.1.tgz", - "integrity": "sha512-Ba4+0M4YvIDUUsprMjhVTU1yN9F2/LJSAl69ZpzaLT4l4j5mwTS6jqqW9Ojvj6lKz/veqPzpJBqGbXspOb533A==" + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" }, "html-comment-regex": { "version": "1.1.1", @@ -6197,23 +6184,6 @@ "to-string-symbols-supported-x": "^1.0.0" } }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==" - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, "is-nan-x": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-nan-x/-/is-nan-x-1.0.1.tgz", @@ -6319,11 +6289,6 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" - }, "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -6560,11 +6525,6 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" - }, "jspm": { "version": "0.17.0-beta.41", "resolved": "https://registry.npmjs.org/jspm/-/jspm-0.17.0-beta.41.tgz", @@ -7584,9 +7544,9 @@ "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" }, "node-sass": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz", - "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz", + "integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==", "requires": { "async-foreach": "^0.1.3", "chalk": "^1.1.1", @@ -7603,7 +7563,7 @@ "nan": "^2.10.0", "node-gyp": "^3.3.1", "npmlog": "^4.0.0", - "request": "~2.79.0", + "request": "2.87.0", "sass-graph": "^2.2.4", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" @@ -7614,29 +7574,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "requires": { - "hoek": "2.x.x" - } - }, - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" - }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -7658,115 +7595,42 @@ "which": "^1.2.9" } }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "requires": { - "boom": "2.x.x" - } - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" - } - }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "requires": { - "chalk": "^1.1.1", - "commander": "^2.9.0", - "is-my-json-valid": "^2.12.4", - "pinkie-promise": "^2.0.0" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "requires": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, "nan": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" }, - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=" - }, "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.11.0", + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", "combined-stream": "~1.0.5", - "extend": "~3.0.0", + "extend": "~3.0.1", "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~2.0.6", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "qs": "~6.3.0", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "~0.4.1", - "uuid": "^3.0.0" - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "requires": { - "hoek": "2.x.x" + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" } }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" } } }, diff --git a/package.json b/package.json index cf9dd09..8d78f1a 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "jspm": "0.17.0-beta.41", "lab": "^15.5.0", "node-fetch": "^1.7.3", - "node-sass": "^4.9.0", + "node-sass": "^4.9.2", "pino-noir": "^2.2.1", "postcss-import": "^11.1.0", "pouchdb": "^6.4.3", @@ -62,11 +62,11 @@ "name": "@nzz/q-quiz", "main": "@nzz/q-quiz.js", "dependencies": { - "leaflet": "github:Leaflet/Leaflet@^1.0.3", - "text": "github:systemjs/plugin-text@^0.0.9" + "leaflet": "github:Leaflet/Leaflet@^1.3.3", + "text": "github:systemjs/plugin-text@^0.0.11" }, "devDependencies": { - "plugin-babel": "npm:systemjs-plugin-babel@^0.0.21" + "plugin-babel": "npm:systemjs-plugin-babel@^0.0.25" } } } diff --git a/script_src/MapPointGuessHandler.js b/script_src/MapPointGuessHandler.js index 974bcc3..151ff8f 100644 --- a/script_src/MapPointGuessHandler.js +++ b/script_src/MapPointGuessHandler.js @@ -3,7 +3,7 @@ import iconPinSvg from "./resources/icon-pin.svg!text"; import { getAnswerTextElement, getDistanceText } from "./answerHelpers.js"; Leaflet.Icon.Default.imagePath = - "jspm_packages/github/Leaflet/Leaflet@1.0.3/dist/images"; + "jspm_packages/github/Leaflet/Leaflet@1.3.3/dist/images"; const tileUrl = "https://api.mapbox.com/styles/v1/neuezuercherzeitung/cj3yj33bk1w5t2rmyy0jty3bb/tiles/256/{z}/{x}/{y}?access_token=pk.eyJ1IjoibmV1ZXp1ZXJjaGVyemVpdHVuZyIsImEiOiJjaXFnbWpvbmMwMDk4aHptY2RiYjM0dHc2In0.Y3HeaE0zhj9OaFEoDIrxRA"; diff --git a/tasks/build.js b/tasks/build.js index 20b4a3f..f56ef60 100644 --- a/tasks/build.js +++ b/tasks/build.js @@ -18,7 +18,7 @@ const stylesDir = `${__dirname}/../styles_src/`; builder.config({ map: { "systemjs-babel-build": - "jspm_packages/npm/systemjs-plugin-babel@0.0.20/systemjs-babel-node.js" + "jspm_packages/npm/systemjs-plugin-babel@0.0.25/systemjs-babel-node.js" } }); From 96d7158737451bbabe741b5dd54067d9b06fd7a2 Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 17:28:39 +0200 Subject: [PATCH 07/26] Step value should never be below 1 --- resources/schema.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/schema.json b/resources/schema.json index d047baa..5b0e331 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -154,6 +154,7 @@ }, "step": { "type": "number", + "minimum": 1, "title": "Grösse der Zwischenschritte beim Verschieben des Sliders", "Q:options": { "placeholder": From 94a9a3f5f07c10d94c86f01302c3a2a37a688ee3 Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 18:08:51 +0200 Subject: [PATCH 08/26] Add field introduction and reorder fields --- resources/schema.json | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/resources/schema.json b/resources/schema.json index 5b0e331..fcd5ed9 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -68,11 +68,15 @@ "hideInEditor": true } }, + "intoduction": { + "type": "string", + "title": "Einleitung" + }, + "image": { "$ref": "#/definitions/image" }, "question": { "type": "string", "title": "Frage" }, - "image": { "$ref": "#/definitions/image" }, "answer": { "type": "string", "title": "Korrekte Antwort" @@ -120,11 +124,15 @@ "hideInEditor": true } }, + "intoduction": { + "type": "string", + "title": "Einleitung" + }, + "image": { "$ref": "#/definitions/image" }, "question": { "type": "string", "title": "Frage" }, - "image": { "$ref": "#/definitions/image" }, "answer": { "type": "number", "title": "Korrekte Antwort" @@ -192,6 +200,11 @@ "hideInEditor": true } }, + "intoduction": { + "type": "string", + "title": "Einleitung" + }, + "image": { "$ref": "#/definitions/image" }, "question": { "type": "string", "title": "Frage" @@ -207,7 +220,6 @@ "type": "string", "title": "Anmerkungen" }, - "image": { "$ref": "#/definitions/image" }, "articleRecommendations": { "$ref": "#/definitions/articleRecommendations" } From f4db1a67fc315622b4fc9b15e8ba043c8ff143ec Mon Sep 17 00:00:00 2001 From: Manuel Roth Date: Thu, 19 Jul 2018 18:41:41 +0200 Subject: [PATCH 09/26] Add introduction field to template and change order to introduction, image, title --- styles_src/default.scss | 1 - views/Question.html | 39 +++++++++++++++++---------------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/styles_src/default.scss b/styles_src/default.scss index 38c35da..08688d6 100644 --- a/styles_src/default.scss +++ b/styles_src/default.scss @@ -118,7 +118,6 @@ .q-quiz-question-image { display: block; - margin: 10px auto; width: 100%; } diff --git a/views/Question.html b/views/Question.html index 1308176..d919022 100644 --- a/views/Question.html +++ b/views/Question.html @@ -1,30 +1,25 @@ +

{{#if question.intoduction}}{{question.intoduction}}{{/if}}

+{{#if question.image && question.image.key}} {{#if width }} + + + + + +{{ else }} + +{{/if}} {{/if}}

{{#if question.question}}{{question.question}}{{/if}}

-{{#if question.image && question.image.key}} - {{#if width }} - - - - - - {{ else }} - - {{/if}} -{{/if}}
{{#if isMultipleChoice}} - - {{/if}} - {{#if isNumberGuess}} - - {{/if}} - {{#if isMapPointGuess}} - - {{/if}} + {{/if}} {{#if isNumberGuess}} + {{/if}} {{#if isMapPointGuess}} + {{/if}}