Skip to content
This repository has been archived by the owner on Mar 7, 2023. It is now read-only.

Commit

Permalink
US 282 depsco avec scoring (#215)
Browse files Browse the repository at this point in the history
depsco works with partial validation, attribution of acquix not possible (yet)
  • Loading branch information
bdavidxyz committed Dec 21, 2016
1 parent ad96e42 commit 6420662
Show file tree
Hide file tree
Showing 36 changed files with 296 additions and 44 deletions.
2 changes: 2 additions & 0 deletions api/lib/domain/models/referential/solution.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const AirtableModel = require('./airtable-model');
const _ = require('../../../utils/lodash-utils');

class Solution extends AirtableModel {

Expand All @@ -11,6 +12,7 @@ class Solution extends AirtableModel {
const fields = this.record.fields;
this.type = fields['Type d\'épreuve'];
this.value = fields['Bonnes réponses'];
this.scoring = _.ensureString(fields['Scoring']).replace(/@/g, ''); // XXX YAML ne supporte pas @

}
}
Expand Down
9 changes: 3 additions & 6 deletions api/lib/domain/services/assessment-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ const _ = require('../../utils/lodash-utils');

function _selectNextInAdaptiveMode(assessment, challenges) {

return new Promise((resolve, reject) => {
return new Promise((resolve) => {

const answerIds = assessment.related('answers').pluck('id');

// Check input
if (challenges.length !== 3) {
reject('Adaptive mode is enabled only for tests with 3 challenges');
}
// Check input
else if (answerIds.length > 1) { // if there is more than one answer, user reached the end of test
// else if (answerIds.length > 1) { // if there is more than one answer, user reached the end of test
if (answerIds.length > 1) { // if there is more than one answer, user reached the end of test
resolve(null);
}
// ADAPTIVE TEST HAPPENS HERE
Expand Down
39 changes: 26 additions & 13 deletions api/lib/domain/services/solution-service-qrocm-dep.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*eslint no-console: ["error", { allow: ["warn", "error"] }] */
const utils = require('./solution-service-utils');
const yaml = require('js-yaml');
const _ = require('lodash');
const _ = require('../../utils/lodash-utils');

// We expect that parsing from airtable returns an Object
// whose all values are array, like this :
Expand All @@ -18,10 +18,11 @@ function _isValidSolution(solution) {

module.exports = {

match (yamlAnswer, yamlSolution) {
match (yamlAnswer, yamlSolution, yamlScoring) {
let result = 'ko';
let answerMap = null;
let solution = null;
let scoring = null;

try {
answerMap = yaml.safeLoad(yamlAnswer);
Expand All @@ -32,18 +33,18 @@ module.exports = {
// solution is
// { Google: [ 'Google', 'google.fr', 'Google Search' ], Yahoo: [ 'Yahoo', 'Yahoo Answer' ] }

scoring = yaml.safeLoad(yamlScoring);
// scoring is
// { 1: 'rechinfo1', 2: 'rechinfo2', 3: 'rechinfo3' }

} catch (e) { // Parse exceptions like script injection could happen. They are detected here.
return 'ko';
}

console.warn(solution);

if (!_isValidSolution(solution)) {
return 'ko';
}

console.warn('en fait ça continue', solution);

const possibleAnswers = {};
_.each(solution, (answerList, solutionKey) => {
_.each(answerList, (answer) => {
Expand All @@ -53,7 +54,7 @@ module.exports = {
// possibleAnswers is
// { Google: 'Google','google.fr': 'Google','Google Search': 'Google',Yahoo: 'Yahoo','Yahoo Answer': 'Yahoo' }

let scoredKeys = [];
const scoredKeys = [];
_.each(answerMap, (answer) => {
_.each(possibleAnswers, (solutionKey, possibleAnswer) => {
if(utils.fuzzyMatchingWithAnswers(answer, [possibleAnswer])) {
Expand All @@ -64,15 +65,27 @@ module.exports = {
// scoredKeys is
// [ 'Google', 'Yahoo' ]

// remove duplicates
scoredKeys = _.uniq(scoredKeys);

const numberOfUserAnswers = Object.keys(answerMap).length;
const numberOfUniqueCorrectAnswers = scoredKeys.length;
const numberOfUniqueCorrectAnswers = _.uniq(scoredKeys).length;

if (_.isNotEmpty(scoring)) {

if (numberOfUniqueCorrectAnswers === numberOfUserAnswers) {
result = 'ok';
const minGrade = _.min(Object.keys(scoring));
const maxGrade = _.max(Object.keys(scoring));

if (numberOfUniqueCorrectAnswers >= maxGrade) {
result = 'ok';
} else if (numberOfUniqueCorrectAnswers >= minGrade) {
result = 'partially';
}

} else {

if (_(numberOfUniqueCorrectAnswers).isEqual(numberOfUserAnswers)) {
result = 'ok';
}
}

return result;

}
Expand Down
3 changes: 2 additions & 1 deletion api/lib/domain/services/solution-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {

const answerValue = answer.get('value');
const solutionValue = solution.value;
const solutionScoring = solution.scoring;

if ('#ABAND#' === answerValue) {
return 'aband';
Expand All @@ -35,7 +36,7 @@ module.exports = {
}

if (solution.type === 'QROCM-dep') {
return solutionServiceQrocmDep.match(answerValue, solutionValue);
return solutionServiceQrocmDep.match(answerValue, solutionValue, solutionScoring);
}

return 'not-implemented';
Expand Down
13 changes: 12 additions & 1 deletion api/lib/infrastructure/repositories/challenge-repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,18 @@ module.exports = {

if (count > 0) logger.debug(`Deleted from cache challenge ${id}`);

return this._fetch(id, reject, cacheKey, resolve);
const cacheSolutionKey = `solution_${id}`;

cache.del(cacheSolutionKey, (err, count) => {

if (err) return reject(err);

if (count > 0) logger.debug(`Deleted from cache solution ${id}`);

return this._fetch(id, reject, cacheKey, resolve);
});

// return this._fetch(id, reject, cacheKey, resolve);
});
});
},
Expand Down
12 changes: 12 additions & 0 deletions api/lib/utils/lodash-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ _.mixin({
return _.nth(array, 2);
},

'isNotEmpty' : function(elt) {
return !_.isEmpty(elt);
},

'ensureString' : function(elt) {
if (elt) {
return elt.toString();
} else {
return '';
}
},

/*
* Returns the element of the array that is after the the one provided.
*
Expand Down
90 changes: 86 additions & 4 deletions api/tests/unit/domain/services/solution-service_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
const service = require('../../../../lib/domain/services/solution-service');
const Answer = require('../../../../lib/domain/models/data/answer');
const Solution = require('../../../../lib/domain/models/referential/solution');
const _ = require('../../../../lib/utils/lodash-utils');

describe('Unit | Service | SolutionService', function () {

function buildSolution(type, value) {
const twoPossibleSolutions = 'Google:\n- Google\n- google.fr\n- Google Search\nYahoo:\n- Yahoo\n- Yahoo Answer';
const threePossibleSolutions = 'Google:\n- Google\n- google.fr\n- Google Search\nYahoo:\n- Yahoo\n- Yahoo Answer\nBing:\n- Bing';

function buildSolution(type, value, scoring) {
const solution = new Solution({ id: 'solution_id' });
solution.type = type;
solution.value = value;
solution.scoring = _.ensureString(scoring).replace(/@/g, ''); // XXX
return solution;
}

Expand Down Expand Up @@ -173,19 +178,19 @@ describe('Unit | Service | SolutionService', function () {

it('should return "ko" when answer is incorrect', function () {
const answer = buildAnswer('num1: Foo\nnum2: Bar');
const solution = buildSolution('QROCM-dep', 'Google:\n- Google\n- google.fr\n- Google Search\nYahoo:\n- Yahoo\n- Yahoo Answer');
const solution = buildSolution('QROCM-dep', twoPossibleSolutions);
expect(service.match(answer, solution)).to.equal('ko');
});

it('should return "ko" when user duplicated a correct answer', function () {
const answer = buildAnswer('num1: google.fr\nnum2: google.fr');
const solution = buildSolution('QROCM-dep', 'Google:\n- Google\n- google.fr\n- Google Search\nYahoo:\n- Yahoo\n- Yahoo Answer');
const solution = buildSolution('QROCM-dep', twoPossibleSolutions);
expect(service.match(answer, solution)).to.equal('ko');
});

const maximalScoreCases = [
{ answer: 'num1: " google.fr"\nnum2: "Yahoo anSwer "',
solution: 'Google:\n- Google\n- google.fr\n- Google Search\nYahoo:\n- Yahoo\n- Yahoo Answer' },
solution: twoPossibleSolutions },
];

maximalScoreCases.forEach(function (testCase) {
Expand All @@ -198,6 +203,83 @@ describe('Unit | Service | SolutionService', function () {

});

describe('if solution type is QROCM-dep with scoring', function () {

it('should return "ko" for badly formatted solution', function () {
const answer = buildAnswer('num1: Google\nnum2: Yahoo');
const solution = buildSolution('QROCM-dep', 'solution like a QCU', '1: @acquix');
expect(service.match(answer, solution)).to.equal('ko');
});

it('should return "ko" when answer is incorrect', function () {
const answer = buildAnswer('num1: Foo\nnum2: Bar');
const solution = buildSolution('QROCM-dep', twoPossibleSolutions, '1: @acquix');
expect(service.match(answer, solution)).to.equal('ko');
});

const maximalScoreCases = [
{ when: '3 correct answers are given, and scoring is 1-3',
answer: 'num1: " google.fr"\nnum2: "Yahoo anSwer "\nnum3: bing',
solution: threePossibleSolutions,
scoring: '1: @acquix\n2: @acquix\n3: @acquix' },
{ when: '3 correct answers are given, and scoring is 1-2',
answer: 'num1: " google.fr"\nnum2: "Yahoo anSwer "\nnum3: bing',
solution: threePossibleSolutions,
scoring: '1: @acquix\n2: @acquix' },
];

maximalScoreCases.forEach(function (testCase) {
it('should return "ok" when ' + testCase.when, function () {
const answer = buildAnswer(testCase.answer);
const solution = buildSolution('QROCM-dep', testCase.solution, testCase.scoring);
expect(service.match(answer, solution)).to.equal('ok');
});
});

const partialScoreCases = [
{ when: '1 correct answers are given + 2 wrong, and scoring is 1-3',
answer: 'num1: " google.fr"\nnum2: "bad answer"\nnum3: "bad answer"',
solution: threePossibleSolutions,
scoring: '1: @acquix\n2: @acquix\n3: @acquix' },
{ when: '2 correct answers are given + 1 empty, and scoring is 1-3',
answer: 'num1: " google.fr"\nnum2: "Yahoo anSwer "\nnum3: ""',
solution: threePossibleSolutions,
scoring: '1: @acquix\n2: @acquix\n3: @acquix' },
];

partialScoreCases.forEach(function (testCase) {
it('should return "partially" when ' + testCase.when, function () {
const answer = buildAnswer(testCase.answer);
const solution = buildSolution('QROCM-dep', testCase.solution, testCase.scoring);
expect(service.match(answer, solution)).to.equal('partially');
});
});

const failedCases = [
{ when: '2 correct answers are given but scoring requires 3 correct answers',
answer: 'num1: " google.fr"\nnum2: "Yahoo anSwer "',
solution: twoPossibleSolutions,
scoring: '3: @acquix' },
{ when: 'no correct answer is given and scoring is 1-3',
answer: 'num1: " tristesse"\nnum2: "bad answer"',
solution: twoPossibleSolutions,
scoring: '1: @acquix\n2: @acquix\n3: @acquix' },
{ when: 'duplicate good answer is given and scoring is 2-3',
answer: 'num1: "google"\nnum2: "google.fr"',
solution: twoPossibleSolutions,
scoring: '2: @acquix\n3: @acquix' },
];

failedCases.forEach(function (testCase) {
it('should return "ko" when ' + testCase.when, function () {
const answer = buildAnswer(testCase.answer);
const solution = buildSolution('QROCM-dep', testCase.solution, testCase.scoring);
expect(service.match(answer, solution)).to.equal('ko');
});
});

});

describe('if solution type is none of the above ones', function () {

it('should return "not-implemented"', function () {
Expand Down
32 changes: 32 additions & 0 deletions api/tests/unit/utils/lodash-utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,36 @@ describe('Unit | Utils | lodash-utils', function () {
done();
});
});


describe('ensureString', function () {
it('when no input, return an empty String', function (done) {
expect(_.ensureString()).to.equal('');
done();
});
it('when input is explicitly undefined, return an empty String', function (done) {
expect(_.ensureString(undefined)).to.equal('');
done();
});
it('when input is explicitly null, return an empty String', function (done) {
expect(_.ensureString(null)).to.equal('');
done();
});
it('when input is a number (typeof meaning), it returns a JSON.stringify version of the input', function (done) {
expect(_.ensureString(42)).to.equal('42');
done();
});
it('when input is a string (typeof meaning), it returns a JSON.stringify version of the input', function (done) {
expect(_.ensureString('42')).to.equal('42');
done();
});
it('when input is an object (typeof meaning), it returns a JSON.stringify version of the input', function (done) {
expect(_.ensureString(/[aeiou]+/g)).to.equal('/[aeiou]+/g');
done();
});
it('when input is an boolean (typeof meaning), it returns a JSON.stringify version of the input', function (done) {
expect(_.ensureString(true)).to.equal('true');
done();
});
});
});
3 changes: 3 additions & 0 deletions live/app/models/answer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export default Model.extend(ValueAsArrayOfBoolean, ValueAsArrayOfString, {
isResultWithoutAnswer: computed('result', function () {
return this.get('result') === 'aband';
}),
isResultPartiallyOk: computed('result', function () {
return this.get('result') === 'partially';
}),
isResultNotOk: computed('result', function () {
return this.get('result') === 'ko';
})
Expand Down
5 changes: 5 additions & 0 deletions live/app/templates/components/get-result.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M8,8L13,12L8,16M14,8H16V16H14" fill="#3e4149"/></svg>
</div>

{{else if answer.isResultPartiallyOk}}
<div data-toggle="tooltip" data-placement="top" title="Réponse partielle">
<svg width="22" height="22" viewBox="0 0 24 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M941,28.7873535 C944.182598,28.7873535 947.234845,30.0516356 949.485281,32.3020721 C951.735718,34.5525087 953,37.6047556 953,40.7873535 C953,47.4147705 947.627417,52.7873535 941,52.7873535 C937.817402,52.7873535 934.765155,51.5230714 932.514719,49.2726349 C930.264282,47.0221983 929,43.9699514 929,40.7873535 C929,37.6047556 930.264282,34.5525087 932.514719,32.3020721 C934.765155,30.0516356 937.817402,28.7873535 941,28.7873535 L941,28.7873535 Z" id="path-1"></path><mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="24" height="24" fill="white"><use xlink:href="#path-1"></use></mask></defs><g id="Résultats" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="test-fin-1" transform="translate(-1155.000000, -532.000000)" stroke="#FFBE00" stroke-width="14"><g id="Group3" transform="translate(226.000000, 504.000000)"><use id="Shape-Copy-3" mask="url(#mask-2)" xlink:href="#path-1"></use></g></g></g></svg>
</div>

{{else}}
<div data-toggle="tooltip" data-placement="top" data-lines="2" title="Correction automatique en cours de développement ;)">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" fill="#446eff"/></svg>
Expand Down
12 changes: 6 additions & 6 deletions live/app/utils/get-challenge-type.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import _ from 'lodash/lodash';
import _ from './lodash-custom';


export default function getChallengeType(challengeTypeFromAirtable) {
let result = 'qcu'; // qcu by default, no error thrown

const challengeType = challengeTypeFromAirtable.toUpperCase();

if (_.contains(['QCUIMG', 'QCU', 'QRU'], challengeType)) {
if (_(challengeType).isAmongst(['QCUIMG', 'QCU', 'QRU'])) {
result = 'qcu';
} else if (_.contains(['QCMIMG', 'QCM'], challengeType)) {
} else if (_(challengeType).isAmongst(['QCMIMG', 'QCM'])) {
result = 'qcm';
} else if (_.contains(['QROC'], challengeType)) {
} else if (_(challengeType).isAmongst(['QROC'])) {
result = 'qroc';
} else if (_.contains(['QROCM', 'QROCM-IND', 'QROCM-DEP'], challengeType)) {
} else if (_(challengeType).isAmongst(['QROCM', 'QROCM-IND', 'QROCM-DEP'])) {
result = 'qrocm';
}

Expand Down

0 comments on commit 6420662

Please sign in to comment.