Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] S'assurer que les images de Modules de prod respectent les contraintes tech (PIX-17215) #11842

Merged
merged 4 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions api/src/devcomp/domain/models/element/Image.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { DomainError } from '../../../../shared/domain/errors.js';
import { assertNotNullOrUndefined } from '../../../../shared/domain/models/asserts.js';
import { Element } from './Element.js';

class Image extends Element {
static #VALID_PRODUCTION_HOSTNAME = 'assets.pix.org';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion : variabiliser le hostname pour éviter que ce soit trop en clair dans le repo ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discuté ensemble, ça reste hautement expérimental et temporaire, on ne s'embête pas à le variabiliser pour le moment


/**
* @param{object} params
* @param{string} params.id
* @param{string} url
* @param{string} alt
* @param{string} alternativeText
* @param{string} legend
* @param{string} licence
* @param{string} params.url
* @param{string} params.alt
* @param{string} params.alternativeText
* @param{string} params.legend
* @param{string} params.licence
* @param{boolean} params.isBeta
*/
constructor({ id, url, alt, alternativeText, legend, licence }) {
constructor({ id, url, alt, alternativeText, legend, licence, isBeta = true }) {
super({ id, type: 'image' });

assertNotNullOrUndefined(url, 'The URL is required for an image');
if (!URL.canParse(url)) {
throw new DomainError('The URL must be a valid URL for an image');
}

assertNotNullOrUndefined(alt, 'The alt text is required for an image');
assertNotNullOrUndefined(alternativeText, 'The alternative text is required for an image');

Expand All @@ -23,6 +31,12 @@ class Image extends Element {
this.alternativeText = alternativeText;
this.legend = legend;
this.licence = licence;

if (!isBeta) {
if (URL.parse(url).hostname !== Image.#VALID_PRODUCTION_HOSTNAME) {
throw new DomainError('The image URL must be from "assets.pix.org" when module is production ready');
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "654c44dc-0560-4acc-9860-4a67c923577f",
"slug": "bases-clavier-1",
"title": "Les bases du clavier sur ordinateur 1/2",
"isBeta": false,
"isBeta": true,
"details": {
"image": "https://images.pix.fr/modulix/placeholder-details.svg",
"description": "<p>Dans ce module, découvrez ce qu’est un clavier et à quoi il sert. <span aria-hidden=\"true\">⌨️</span><br>Vous apprendrez à taper des mots simples et à modifier du texte.<br>Pour ce module, vous devez déjà savoir utiliser une souris d’ordinateur.</p>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "bb0a4ed3-1b49-4782-b867-05ade0868c4f",
"slug": "bases-clavier-2",
"title": "Les bases du clavier sur ordinateur 2/2",
"isBeta": false,
"isBeta": true,
"details": {
"image": "https://images.pix.fr/modulix/placeholder-details.svg",
"description": "<p>Pour taper certains caractères, il suffit d’appuyer une fois sur une touche. Pour d’autres c’est un peu plus compliqué ! <br>Dans ce module, vous apprendrez à taper des caractères en appuyant sur deux touches en même temps : les majuscules ou la ponctuation par exemple.</p>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "12cd102f-9831-4264-8d5e-15a8f177594a",
"slug": "ports-connexions-essentiels",
"title": "Les ports de connexion d’un ordinateur",
"isBeta": false,
"isBeta": true,
"details": {
"image": "https://images.pix.fr/modulix/placeholder-details.svg",
"description": "<p>Pour savoir ce qu'on peut brancher ou non à un ordinateur, il faut bien connaître les principaux ports de connexion !</p>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "08ef1a47-b691-4138-b899-39f3512fa152",
"slug": "tmp08ef",
"title": "Derrière le prompt : comment fonctionnent les IA génératives ?",
"isBeta": false,
"isBeta": true,
"details": {
"image": "https://images.pix.fr/modulix/placeholder-details.svg",
"description": "<p>ChatGPT, Mistral, Dall-E : les systèmes d’Intelligence Artificielle (IA) générative sont de plus en plus utilisés par le grand public. Ce succès s’explique notamment par les prompts, ces commandes que l’on peut écrire simplement afin d’obtenir des résultats parfois bluffants. <span aria-hidden=\"true\">✨ </span> Mais ça n’a rien de magique… </p><br><p>Dans ce module, vous allez découvrir les coulisses du fonctionnement des prompts et comprendre comment les systèmes d’IA générative produisent leurs réponses en testant avec des exemples concrets.</p>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "19468565-a56b-4aa5-9bf0-369e94bc85ea",
"slug": "tri-multicritere-tableau",
"title": "Trier un tableau selon plusieurs critères",
"isBeta": false,
"isBeta": true,
"details": {
"image": "https://images.pix.fr/modulix/placeholder-details.svg",
"description": "<p>Trier des données dans une feuille de calcul peut permettre de trouver efficacement des informations. Parfois, un tri sur une seule colonne ne suffit pas. <br>Dans ce module, vous allez apprendre à trier vos données en combinant plusieurs critères.<br></p>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "e8cee13e-1d4d-47eb-bd26-d7ea6a10b1e6",
"slug": "utiliser-souris-ordinateur-1",
"title": "Utiliser une souris d'ordinateur - 1",
"isBeta": false,
"isBeta": true,
"details": {
"image": "https://images.pix.fr/modulix/placeholder-details.svg",
"description": "<p>Dans ce module, vous découvrirez ce qu’est une souris et à quoi elle sert.&nbsp;</p><p>Vous apprendrez à la déplacer et à cliquer pour interagir avec votre ordinateur.</p><p>Si vous n'avez jamais utilisé de souris, nous vous conseillons d'être accompagné pour ce module.</p><br>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "1f425bc6-7a35-4ceb-9634-a25da1e36233",
"slug": "utiliser-souris-ordinateur-2",
"title": "Utiliser une souris d'ordinateur - 2",
"isBeta": false,
"isBeta": true,
"details": {
"image": "https://images.pix.fr/modulix/placeholder-details.svg",
"description": "<p>La souris est composée de trois boutons : le bouton gauche, la molette et le bouton droit. Le bouton gauche permet de faire un clic gauche, c’est le plus utilisé. Mais il existe d’autres façons d’utiliser la souris.<br><br> Dans ce module, vous apprendrez à faire d’autres clics et à utiliser les deux autres boutons de la souris. 🖱️</p>",
Expand Down
11 changes: 6 additions & 5 deletions api/src/devcomp/infrastructure/factories/module-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class ModuleFactory {
.map((component) => {
switch (component.type) {
case 'element': {
const element = ModuleFactory.#buildElement(component.element);
const element = ModuleFactory.#buildElement(component.element, moduleData.isBeta);
if (element) {
return new ComponentElement({ element });
} else {
Expand All @@ -60,7 +60,7 @@ export class ModuleFactory {
return new Step({
elements: step.elements
.map((element) => {
const domainElement = ModuleFactory.#buildElement(element);
const domainElement = ModuleFactory.#buildElement(element, moduleData.isBeta);
if (domainElement) {
return domainElement;
} else {
Expand Down Expand Up @@ -88,7 +88,7 @@ export class ModuleFactory {
}
}

static #buildElement(element) {
static #buildElement(element, isBeta) {
switch (element.type) {
case 'custom':
return ModuleFactory.#buildCustom(element);
Expand All @@ -99,7 +99,7 @@ export class ModuleFactory {
case 'expand':
return ModuleFactory.#buildExpand(element);
case 'image':
return ModuleFactory.#buildImage(element);
return ModuleFactory.#buildImage(element, isBeta);
case 'separator':
return ModuleFactory.#buildSeparator(element);
case 'text':
Expand Down Expand Up @@ -157,14 +157,15 @@ export class ModuleFactory {
});
}

static #buildImage(element) {
static #buildImage(element, isBeta) {
return new Image({
id: element.id,
url: element.url,
alt: element.alt,
alternativeText: element.alternativeText,
legend: element.legend,
licence: element.licence,
isBeta,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ describe('Unit | Devcomp | Domain | Models | Element', function () {
it('should instanciate non answerable elements', function () {
// Given
const text = new Text({ id: 'id', content: 'content' });
const image = new Image({ id: 'id', url: 'url', alt: 'alt', alternativeText: 'alternativeText' });
const image = new Image({
id: 'id',
url: 'https://assets.pix.org/modules/placeholder-details.svg',
alt: 'alt',
alternativeText: 'alternativeText',
});

const nonAnswerableElements = [text, image];

Expand Down
44 changes: 40 additions & 4 deletions api/tests/devcomp/unit/domain/models/element/Image_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ describe('Unit | Devcomp | Domain | Models | Element | Image', function () {
// when
const image = new Image({
id: 'id',
url: 'url',
url: 'https://assets.pix.org/modules/placeholder-details.svg',
alt: 'alt',
alternativeText: 'alternativeText',
legend: 'legend',
licence: 'licence',
isBeta: false,
});

// then
expect(image.id).to.equal('id');
expect(image.url).to.equal('url');
expect(image.url).to.equal('https://assets.pix.org/modules/placeholder-details.svg');
expect(image.alt).to.equal('alt');
expect(image.alternativeText).to.equal('alternativeText');
expect(image.legend).to.equal('legend');
Expand Down Expand Up @@ -48,11 +49,22 @@ describe('Unit | Devcomp | Domain | Models | Element | Image', function () {
});
});

describe('An image without alt', function () {
describe('An image with invalid url', function () {
it('should throw an error', function () {
// when
const error = catchErrSync(() => new Image({ id: 'id', url: 'url' }))();

// then
expect(error).to.be.instanceOf(DomainError);
expect(error.message).to.equal('The URL must be a valid URL for an image');
});
});

describe('An image without alt', function () {
it('should throw an error', function () {
// when
const error = catchErrSync(() => new Image({ id: 'id', url: 'https://images.pix.fr/coolcat.jpg' }))();

// then
expect(error).to.be.instanceOf(DomainError);
expect(error.message).to.equal('The alt text is required for an image');
Expand All @@ -62,11 +74,35 @@ describe('Unit | Devcomp | Domain | Models | Element | Image', function () {
describe('An image without an alternative text', function () {
it('should throw an error', function () {
// when
const error = catchErrSync(() => new Image({ id: 'id', url: 'url', alt: 'alt' }))();
const error = catchErrSync(() => new Image({ id: 'id', url: 'https://images.pix.fr/coolcat.jpg', alt: 'alt' }))();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coolman qui choisit l'image Coolcat .. La Brigade du cool a encore frappé


// then
expect(error).to.be.instanceOf(DomainError);
expect(error.message).to.equal('The alternative text is required for an image');
});
});

describe('When isBeta is false', function () {
describe('and image URL is not from assets.pix.org', function () {
it('should throw an error', function () {
// given & when
const error = catchErrSync(
() =>
new Image({
id: 'id',
url: 'https://images.pix.fr/coolcat.jpg',
alt: 'alt',
alternativeText: 'alternativeText',
legend: 'legend',
licence: 'licence',
isBeta: false,
}),
)();

// then
expect(error).to.be.instanceOf(DomainError);
expect(error.message).to.equal('The image URL must be from "assets.pix.org" when module is production ready');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Grain } from '../../../../../src/devcomp/domain/models/Grain.js';
import { Module } from '../../../../../src/devcomp/domain/models/module/Module.js';
import { TransitionText } from '../../../../../src/devcomp/domain/models/TransitionText.js';
import { ModuleFactory } from '../../../../../src/devcomp/infrastructure/factories/module-factory.js';
import { DomainError } from '../../../../../src/shared/domain/errors.js';
import { logger } from '../../../../../src/shared/infrastructure/utils/logger.js';
import { catchErrSync, expect, sinon } from '../../../../test-helper.js';
import { validateFlashcards } from '../../../shared/validateFlashcards.js';
Expand Down Expand Up @@ -336,6 +337,54 @@ describe('Unit | Devcomp | Infrastructure | Factories | Module ', function () {
});

describe('With ComponentElement', function () {
describe('When isBeta is false', function () {
it('should throw a DomainError when Image element does not have a valid url', function () {
// given
const moduleData = {
id: '6282925d-4775-4bca-b513-4c3009ec5886',
slug: 'title',
title: 'title',
isBeta: false,
details: {
image: 'https://images.pix.fr/modulix/placeholder-details.svg',
description: 'Description',
duration: 5,
level: 'Débutant',
tabletSupport: 'comfortable',
objectives: ['Objective 1'],
},
grains: [
{
id: 'f312c33d-e7c9-4a69-9ba0-913957b8f7dd',
type: 'lesson',
title: 'title',
components: [
{
type: 'element',
element: {
id: '8d7687c8-4a02-4d7e-bf6c-693a6d481c78',
type: 'image',
url: 'https://images.pix.fr/modulix/didacticiel/ordi-spatial.svg',
alt: 'Alternative',
alternativeText: 'Alternative textuelle',
legend: 'legend',
licence: 'licence',
},
},
],
},
],
};

// when
const error = catchErrSync(() => ModuleFactory.build(moduleData))();

// then
expect(error).to.be.an.instanceOf(DomainError);
expect(error.message).to.equal('The image URL must be from "assets.pix.org" when module is production ready');
});
});

it('should instantiate a Module with a ComponentElement which contains an Image Element', function () {
// given
const moduleData = {
Expand Down Expand Up @@ -927,6 +976,58 @@ describe('Unit | Devcomp | Infrastructure | Factories | Module ', function () {
});

describe('With ComponentStepper', function () {
describe('When isBeta is false', function () {
it('should throw a DomainError when Image element does not have a valid url', function () {
// given
const moduleData = {
id: '6282925d-4775-4bca-b513-4c3009ec5886',
slug: 'title',
title: 'title',
isBeta: false,
details: {
image: 'https://images.pix.fr/modulix/placeholder-details.svg',
description: 'Description',
duration: 5,
level: 'Débutant',
tabletSupport: 'comfortable',
objectives: ['Objective 1'],
},
grains: [
{
id: 'f312c33d-e7c9-4a69-9ba0-913957b8f7dd',
type: 'lesson',
title: 'title',
components: [
{
type: 'stepper',
steps: [
{
elements: [
{
id: '8d7687c8-4a02-4d7e-bf6c-693a6d481c78',
type: 'image',
url: 'https://images.pix.fr/modulix/didacticiel/ordi-spatial.svg',
alt: "Dessin détaillé dans l'alternative textuelle",
alternativeText: "Dessin d'un ordinateur dans un univers spatial.",
},
],
},
],
},
],
},
],
};

// when
const error = catchErrSync(() => ModuleFactory.build(moduleData))();

// then
expect(error).to.be.an.instanceOf(DomainError);
expect(error.message).to.equal('The image URL must be from "assets.pix.org" when module is production ready');
});
});

it('should instantiate a Module with a ComponentStepper which contains an Image Element', function () {
// given
const moduleData = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ function getComponents() {
new ComponentElement({
element: new Image({
id: '3',
url: 'url',
url: 'https://assets.pix.org/modules/placeholder-details.svg',
alt: 'alt',
alternativeText: 'alternativeText',
licence: 'mon copyright',
Expand Down Expand Up @@ -399,7 +399,7 @@ function getAttributesComponents() {
id: '3',
isAnswerable: false,
type: 'image',
url: 'url',
url: 'https://assets.pix.org/modules/placeholder-details.svg',
legend: 'ma légende',
licence: 'mon copyright',
},
Expand Down