Skip to content

Commit

Permalink
[FEATURE] Affiche les bulles une à une puis l'épreuve avec un délai (P…
Browse files Browse the repository at this point in the history
  • Loading branch information
pix-service-auto-merge authored Nov 22, 2024
2 parents f9013ba + e1bff15 commit f19ff59
Show file tree
Hide file tree
Showing 31 changed files with 290 additions and 162 deletions.
2 changes: 1 addition & 1 deletion api/src/school/application/assessment-controller.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as challengeSerializer from '../../shared/infrastructure/serializers/jsonapi/challenge-serializer.js';
import { usecases } from '../domain/usecases/index.js';
import * as activitySerializer from '../infrastructure/serializers/activity-serializer.js';
import * as assessmentSerializer from '../infrastructure/serializers/assessment-serializer.js';
import * as challengeSerializer from '../infrastructure/serializers/challenge-serializer.js';

const getNextChallengeForPix1d = async function (request, h, dependencies = { challengeSerializer }) {
const assessmentId = request.params.id;
Expand Down
2 changes: 1 addition & 1 deletion api/src/school/application/challenge-controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as challengeRepository from '../../shared/infrastructure/repositories/challenge-repository.js';
import * as challengeSerializer from '../../shared/infrastructure/serializers/jsonapi/challenge-serializer.js';
import { usecases } from '../domain/usecases/index.js';
import * as challengeSerializer from '../infrastructure/serializers/challenge-serializer.js';

const get = async function (request, h, dependencies = { challengeRepository, challengeSerializer }) {
const challenge = await usecases.getChallenge({ id: request.params.id });
Expand Down
7 changes: 1 addition & 6 deletions api/src/school/domain/services/challenge.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const challengeService = { getAlternativeVersion, mapChallenge };
export const challengeService = { getAlternativeVersion };

function getAlternativeVersion({ mission, activities, activityInfo }) {
const alreadyPlayedAlternativeVersions = activities
Expand Down Expand Up @@ -32,8 +32,3 @@ function getAlternativeVersion({ mission, activities, activityInfo }) {
function _randomIndexForChallenges(length, random = Math.random()) {
return Math.floor(random * length);
}

function mapChallenge(challenge) {
challenge.instruction = challenge.instruction?.split('***');
return challenge;
}
5 changes: 1 addition & 4 deletions api/src/school/domain/usecases/get-challenge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { challengeService } from '../services/challenge.js';

export async function getChallenge({ id, challengeRepository }) {
const challenge = await challengeRepository.get(id);
return challengeService.mapChallenge(challenge);
return challengeRepository.get(id);
}
4 changes: 1 addition & 3 deletions api/src/school/domain/usecases/get-next-challenge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Activity } from '../models/Activity.js';
import { ActivityInfo } from '../models/ActivityInfo.js';
import { challengeService } from '../services/challenge.js';

export async function getNextChallenge({
assessmentId,
Expand All @@ -26,6 +25,5 @@ export async function getNextChallenge({
});

await assessmentRepository.updateWhenNewChallengeIsAsked({ id: assessmentId, lastChallengeId: challengeId });
const challenge = await challengeRepository.get(challengeId);
return challengeService.mapChallenge(challenge);
return challengeRepository.get(challengeId);
}
32 changes: 32 additions & 0 deletions api/src/school/infrastructure/serializers/challenge-serializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import jsonapiSerializer from 'jsonapi-serializer';

const { Serializer } = jsonapiSerializer;

const serialize = function (challenges) {
return new Serializer('challenge', {
attributes: [
'type',
'instructions',
'proposals',
'illustrationUrl',
'embedUrl',
'embedTitle',
'embedHeight',
'webComponentTagName',
'webComponentProps',
'illustrationAlt',
'format',
'autoReply',
'focused',
'shuffled',
],
transform: (challenge) => {
return {
...challenge,
instructions: challenge.instruction?.split('***'),
};
},
}).serialize(challenges);
};

export { serialize };
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('Integration | Controller | challenge-controller', function () {
});

expect(response.statusCode).to.equal(200);
expect(response.result.data.attributes.instruction).to.deep.equal(expectedResult);
expect(response.result.data.attributes.instructions).to.deep.equal(expectedResult);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -102,57 +102,6 @@ describe('Integration | Usecase | get-next-challenge', function () {
expect(challenge).to.be.instanceOf(Challenge);
expect(updatedAssessment.lastChallengeId).to.equal('second_va_challenge_on_step_2_id');
});
it('should return next challenge with the instruction in an array', async function () {
const { assessmentId, missionId } = databaseBuilder.factory.buildMissionAssessment();

databaseBuilder.factory.buildActivity({
assessmentId,
level: Activity.levels.VALIDATION,
status: Activity.status.STARTED,
stepIndex: 0,
createdAt: new Date('2022-09-15'),
});

await databaseBuilder.commit();

mockLearningContent({
challenges: [
learningContentBuilder.buildChallenge({
id: 'multi_instruction_id',
skillId: 'skill_id',
instruction: 'Une première bulle.<br/>Pour tout mettre***Une deuxième bulle\n sur plusieurs lignes',
}),
],
skills: [learningContentBuilder.buildSkill({ id: 'skill_id' })],
missions: [
learningContentBuilder.buildMission({
id: missionId,
content: {
steps: [
{
validationChallenges: [['multi_instruction_id']],
},
],
},
}),
],
});

const challenge = await getNextChallenge({
assessmentId,
activityRepository,
activityAnswerRepository,
challengeRepository,
assessmentRepository,
missionAssessmentRepository,
missionRepository,
});

expect(challenge.instruction).to.deep.equal([
'Une première bulle.<br/>Pour tout mettre',
'Une deuxième bulle\n sur plusieurs lignes',
]);
});
});
});
});
18 changes: 0 additions & 18 deletions api/tests/school/unit/domain/services/challenge_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,22 +222,4 @@ describe('Unit | Service | Challenge', function () {
});
});
});

describe('#mapChallenge', function () {
context('when challenge has *** separator in instruction key', function () {
it('should split the text into an Array', async function () {
const instruction = 'Une première bulle.<br/>Pour tout mettre\n***\nUne deuxième bulle \n sur plusieurs lignes';
const challenge = domainBuilder.buildChallenge({
instruction,
});

const mappedChallenge = await challengeService.mapChallenge(challenge);

expect(mappedChallenge.instruction).to.deep.equal([
'Une première bulle.<br/>Pour tout mettre\n',
'\nUne deuxième bulle \n sur plusieurs lignes',
]);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as serializer from '../../../../../src/school/infrastructure/serializers/challenge-serializer.js';
import { Challenge } from '../../../../../src/shared/domain/models/index.js';
import { expect } from '../../../../test-helper.js';

describe('Unit | Serializer | challenge-serializer', function () {
describe('#serialize()', function () {
it('should convert a Challenge model object into JSON API data', function () {
// given
const challenge = new Challenge({
id: 'challenge_id',
instruction: 'Une première bulle.<br/>Pour tout mettre***Une deuxième bulle\n sur plusieurs lignes',
proposals:
'- Ils sont bio.\n- Ils pèsent plus de 63 grammes.\n- Ce sont des oeufs frais.\n- Ils sont destinés aux consommateurs.\n- Ils ne sont pas lavés.\n',
type: 'QCM',
illustrationUrl: 'http://illustration.url',
timer: 300,
competenceId: 'competence_id',
embedUrl: 'url',
embedTitle: 'title',
embedHeight: 12,
webComponentTagName: 'tagName',
webComponentProps: {
prop1: 'value 1',
prop2: 'value 2',
},
format: 'mots',
shuffled: false,
locales: ['fr', 'en'],
focused: false,
illustrationAlt: 'alt',
autoReply: false,
});

// when
const json = serializer.serialize(challenge);

// then
expect(json).to.deep.equal({
data: {
type: 'challenges',
id: challenge.id,
attributes: {
instructions: ['Une première bulle.<br/>Pour tout mettre', 'Une deuxième bulle\n sur plusieurs lignes'],
proposals: challenge.proposals,
type: challenge.type,
'illustration-url': challenge.illustrationUrl,
format: 'mots',
shuffled: false,
focused: false,
'embed-url': 'url',
'embed-title': 'title',
'embed-height': 12,
'web-component-tag-name': 'tagName',
'web-component-props': {
prop1: 'value 1',
prop2: 'value 2',
},
'illustration-alt': 'alt',
'auto-reply': false,
},
},
});
});
});
});
28 changes: 28 additions & 0 deletions junior/app/components/challenge/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@ import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import ENV from 'junior/config/environment';

const CHALLENGE_DISPLAY_DELAY = ENV.APP.CHALLENGE_DISPLAY_DELAY;

export default class Challenge extends Component {
@service store;
@service router;
@service intl;
@tracked answerHasBeenValidated = false;
@tracked answer = null;
@tracked answerValue = null;
@tracked displayValidationWarning = false;
validationWarning = null;

get challengeItemDisplayDelay() {
return this.args.challenge.instructions.length * CHALLENGE_DISPLAY_DELAY;
}

bubbleDisplayDelay(index) {
return (index || 0) * CHALLENGE_DISPLAY_DELAY;
}

get disableCheckButton() {
return this.answerValue === null || this.answerValue === '';
}
Expand All @@ -33,6 +45,22 @@ export default class Challenge extends Component {
return 'default';
}

get robotFeedback() {
const feedback = {};

if (this.answer?.result === 'ok') {
feedback.message = this.intl.t('pages.challenge.messages.correct-answer');
feedback.status = 'success';
} else if (this.answer?.result === 'ko') {
feedback.message = this.intl.t('pages.challenge.messages.wrong-answer');
feedback.status = 'error';
} else if (this.displayValidationWarning) {
feedback.message = this.validationWarning;
feedback.status = 'warning';
}
return feedback;
}

@action
setAnswerValue(value) {
this.answerValue = value ?? null;
Expand Down
54 changes: 24 additions & 30 deletions junior/app/components/challenge/template.hbs
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
{{page-title (t "pages.challenge.title")}}
<div class="container">
<RobotDialog @class={{this.robotMood}}>
{{#each @challenge.instruction as |instruction|}}
<Bubble @message={{instruction}} @oralization={{@oralization}} />
{{#each @challenge.instructions as |instruction index|}}
<DelayedElement @shouldDisplayIn={{this.bubbleDisplayDelay index}}>
<Bubble @message={{instruction}} @oralization={{@oralization}} />
</DelayedElement>
{{/each}}
{{#if (eq this.answer.result "ok")}}

{{#if this.robotFeedback.message}}
<Bubble
@message={{t "pages.challenge.messages.correct-answer"}}
@status="success"
@message={{this.robotFeedback.message}}
@status={{this.robotFeedback.status}}
@oralization={{@oralization}}
aria-live="polite"
/>
{{/if}}
{{#if (eq this.answer.result "ko")}}
<Bubble
@message={{t "pages.challenge.messages.wrong-answer"}}
@oralization={{@oralization}}
@status="error"
aria-live="polite"
/>
{{/if}}
{{#if this.displayValidationWarning}}
<Bubble @message={{this.validationWarning}} @status="warning" aria-live="polite" @oralization={{@oralization}} />
{{/if}}
</RobotDialog>
<Challenge::Item
@setAnswerValue={{this.setAnswerValue}}
@setValidationWarning={{this.setValidationWarning}}
@validateAnswer={{this.validateAnswer}}
@skipChallenge={{this.skipChallenge}}
@challenge={{@challenge}}
@assessment={{@assessment}}
@disableCheckButton={{this.disableCheckButton}}
@disableLessonButton={{this.disableLessonButton}}
@answerHasBeenValidated={{this.answerHasBeenValidated}}
@activity={{@activity}}
@resume={{this.resume}}
@isDisabled={{this.hasBeenAnswered}}
/>
<DelayedElement @shouldDisplayIn={{this.challengeItemDisplayDelay}}>
<Challenge::Item
@setAnswerValue={{this.setAnswerValue}}
@setValidationWarning={{this.setValidationWarning}}
@validateAnswer={{this.validateAnswer}}
@skipChallenge={{this.skipChallenge}}
@challenge={{@challenge}}
@assessment={{@assessment}}
@disableCheckButton={{this.disableCheckButton}}
@disableLessonButton={{this.disableLessonButton}}
@answerHasBeenValidated={{this.answerHasBeenValidated}}
@activity={{@activity}}
@resume={{this.resume}}
@isDisabled={{this.hasBeenAnswered}}
/>
</DelayedElement>
</div>
26 changes: 26 additions & 0 deletions junior/app/components/delayed-element.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class DelayedElement extends Component {
@tracked display = false;

constructor() {
super(...arguments);

if (this.args.shouldDisplayIn > 0) {
setTimeout(() => {
this.display = true;
}, this.args.shouldDisplayIn);
} else {
this.display = true;
}
}

<template>
<section class="element-delayed-wrapper {{if this.display 'display' ''}}">
{{#if this.display}}
{{yield}}
{{/if}}
</section>
</template>
}
Loading

0 comments on commit f19ff59

Please sign in to comment.