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

Pix 15358 read learningcontent from pg #10679

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eadc7db
feat(api): allows logging at debug level for specific sections
nlepage Dec 3, 2024
c9d47fc
feat(api): seeds learning content in pg
nlepage Nov 26, 2024
3d4875c
feat(api): setup class to handle distributed caching and memoization
laura-bergoens Nov 29, 2024
f936280
feat(api): refacto frameworkRepository with new cache and to use PG
laura-bergoens Nov 29, 2024
4593bad
feat(api): refacto areaRepository with new cache and to use PG
laura-bergoens Nov 29, 2024
377c930
feat(api): refacto competenceRepository with new cache and to use PG
laura-bergoens Nov 29, 2024
f7b65cc
feat(api): refacto thematicRepository with new cache and to use PG
laura-bergoens Nov 29, 2024
e8d04c5
feat(api): refacto tubeRepository with new cache and to use PG
laura-bergoens Nov 29, 2024
3c9b1cb
feat(api): refacto skillRepository with new cache and to use PG
laura-bergoens Nov 29, 2024
91b20d6
feat(api): refacto challengeRepository with new cache and to use PG
laura-bergoens Nov 29, 2024
97bcb95
feat(api): refacto courseRepository with new cache and to use PG
laura-bergoens Nov 30, 2024
622db73
feat(api): refacto tutorialRepository with new cache and to use PG
laura-bergoens Nov 30, 2024
12b99c6
feat(api): refacto missionRepository with new cache and to use PG
laura-bergoens Nov 30, 2024
e0824cc
fix various test by changing how learning content is initialized
laura-bergoens Dec 1, 2024
c1067f2
delete datasource (todo old delete learning content cache ?)
laura-bergoens Dec 1, 2024
f4efde0
try to fix e2e
laura-bergoens Dec 1, 2024
a5fd170
feat(api): clear cache when saving learning content
laura-bergoens Dec 2, 2024
0d42099
refactor(api): rename LearningContentRepository.save to saveMany
nlepage Dec 2, 2024
20c7ba5
refactor(api): patch learning content clears only patched entry
nlepage Dec 2, 2024
6ef42ac
refactor(api): replaces old learning content cache by new one
nlepage Dec 2, 2024
aa5b9d2
make seeds work
laura-bergoens Dec 2, 2024
648ee72
refactor(api): clears learning content cache after writing to PG
nlepage Dec 2, 2024
adf16bd
cleanup(api): removes old cache classes
nlepage Dec 2, 2024
2084c2c
WIP documentation and logs
nlepage Dec 3, 2024
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
14 changes: 14 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,13 @@ jobs:
background: true
command: npm start

- run:
name: Refresh cache
environment:
JOBS: 1
working_directory: ~/pix/api
command: npm run cache:refresh

- run:
name: Run tests
environment:
Expand Down Expand Up @@ -900,6 +907,13 @@ jobs:
background: true
command: npm start

- run:
name: Refresh cache
environment:
JOBS: 1
working_directory: ~/pix/api
command: npm run cache:refresh

- run:
name: Run tests
environment:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export function buildArea({
name = 'name Domaine A',
title_i18n = { fr: 'title FR Domaine A', en: 'title EN Domaine A' },
color = 'color Domaine A',
frameworkId = 'frameworkPix',
competenceIds = ['competenceIdA'],
frameworkId = null,
competenceIds = [],
} = {}) {
const values = {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function buildChallenge({
embedUrl = 'embedUrl Epreuve A',
embedTitle = 'embedTitle Epreuve A',
locales = ['fr'],
competenceId = 'competenceIdA',
skillId = 'skillIdA',
competenceId = null,
skillId = null,
} = {}) {
const values = {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export function buildCompetence({
description_i18n = { fr: 'description FR Compétence A', en: 'description EN Compétence A' },
index = 'index Compétence A',
origin = 'origin Compétence A',
areaId = 'areaIdA',
skillIds = ['skillIdA'],
thematicIds = ['thematicIdA'],
areaId = null,
skillIds = [],
thematicIds = [],
} = {}) {
const values = {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export function buildCourse({
name = 'instruction Test Statique A',
description = 'description Test Statique A',
isActive = true,
competences = ['competenceIdA'],
challenges = ['challengeIdA'],
competences = [],
challenges = [],
} = {}) {
const values = {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function buildMission({
introductionMediaAlt_i18n = { fr: 'introductionMediaAlt FR Mission A', en: 'introductionMediaAlt EN Mission A' },
documentationUrl = 'documentationUrl Mission A',
cardImageUrl = 'cardImageUrl Mission A',
competenceId = 'competenceIdA',
competenceId = null,
} = {}) {
const values = {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export function buildSkill({
version = 5,
level = 2,
hintStatus = 'hintStatus Acquis A',
competenceId = 'competenceIdA',
tubeId = 'tubeIdA',
tutorialIds = ['tutorialIdA'],
competenceId = null,
tubeId = null,
tutorialIds = [],
learningMoreTutorialIds = [],
hint_i18n = { fr: 'Un indice' },
} = {}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export function buildThematic({
id = 'thematicIdA',
name_i18n = { fr: 'name FR Thématique A', en: 'name EN Thématique A' },
index = 8,
competenceId = 'competenceIdA',
tubeIds = ['tubeIdA'],
competenceId = null,
tubeIds = [],
} = {}) {
const values = {
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ export function buildTube({
description = 'description Tube A',
practicalTitle_i18n = { fr: 'practicalTitle FR Tube A', en: 'practicalTitle EN Tube A' },
practicalDescription_i18n = { fr: 'practicalDescription FR Tube A', en: 'practicalDescription EN Tube A' },
competenceId = 'competenceIdA',
thematicId = 'thematicIdA',
skillIds = ['skillIdA'],
competenceId = null,
thematicId = null,
skillIds = [],
isMobileCompliant = true,
isTabletCompliant = true,
} = {}) {
Expand Down
44 changes: 44 additions & 0 deletions api/db/seeds/data/common/learningcontent-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { lcmsClient } from '../../../../src/shared/infrastructure/lcms-client.js';
import { logger } from '../../../../src/shared/infrastructure/utils/logger.js';

const MAX_SKILL_ALTERNATIVES_COUNT = 1;
const SKILL_STATUSES = ['actif', 'archivé'];
const CHALLENGE_STATUSES = ['validé', 'archivé'];
const GENEALOGY_PROTOTYPE = 'Prototype 1';

export async function learningContentBuilder({ databaseBuilder }) {
const learningContent = await lcmsClient.getLatestRelease();

const totalSkillsCount = learningContent.skills.length;
const totalChallengesCount = learningContent.challenges.length;
const totalTutorialsCount = learningContent.tutorials.length;

learningContent.skills = learningContent.skills.filter((skill) => SKILL_STATUSES.includes(skill.status));

const skillIds = new Set(learningContent.skills.map((skill) => skill.id));
const skillAlternativesCount = new Map();

learningContent.challenges = learningContent.challenges.filter((challenge) => {
const { skillId } = challenge;
if (!skillIds.has(skillId)) return false;
if (!CHALLENGE_STATUSES.includes(challenge.status)) return false;
if (challenge.genealogy === GENEALOGY_PROTOTYPE) return true;
const alternativeKey = `${skillId}:${challenge.locales[0]}`;
if (skillAlternativesCount.get(alternativeKey) ?? 0 >= MAX_SKILL_ALTERNATIVES_COUNT) return false;
skillAlternativesCount.set(alternativeKey, skillAlternativesCount.get(alternativeKey) + 1);
return true;
});

const tutorialIds = new Set(
learningContent.skills.flatMap((skill) => [...skill.tutorialIds, skill.learningMoreTutorialIds]),
);

learningContent.tutorials = learningContent.tutorials.filter((tutorial) => tutorialIds.has(tutorial.id));

logger.debug(`inserting ${learningContent.skills.length} skills out of ${totalSkillsCount}`);
logger.debug(`inserting ${learningContent.challenges.length} challenges out of ${totalChallengesCount}`);
logger.debug(`inserting ${learningContent.tutorials.length} tutorials out of ${totalTutorialsCount}`);

databaseBuilder.factory.learningContent.build(learningContent);
await databaseBuilder.commit();
}
2 changes: 1 addition & 1 deletion api/db/seeds/data/common/tooling/campaign-tooling.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ async function _buildCampaignSkills({ databaseBuilder, campaignId, targetProfile

for (const cappedTube of cappedTubes) {
const skillsForTube = await learningContent.findActiveSkillsByTubeId(cappedTube.tubeId);
const skillsCapped = skillsForTube.filter((skill) => skill.level <= parseInt(cappedTube.level));
const skillsCapped = skillsForTube.filter((skill) => skill.difficulty <= parseInt(cappedTube.level));
skillsCapped.forEach((skill) => {
skills.push(skill);
databaseBuilder.factory.buildCampaignSkill({ campaignId, skillId: skill.id });
Expand Down
4 changes: 2 additions & 2 deletions api/db/seeds/data/common/tooling/learning-content.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import _ from 'lodash';

import { skillDatasource } from '../../../../../src/shared/infrastructure/datasources/learning-content/index.js';
import * as challengeRepository from '../../../../../src/shared/infrastructure/repositories/challenge-repository.js';
import * as competenceRepository from '../../../../../src/shared/infrastructure/repositories/competence-repository.js';
import * as skillRepository from '../../../../../src/shared/infrastructure/repositories/skill-repository.js';
import { logger } from '../../../../../src/shared/infrastructure/utils/logger.js';

let ALL_COMPETENCES, ALL_ACTIVE_SKILLS, ALL_CHALLENGES, ACTIVE_SKILLS_BY_COMPETENCE, ACTIVE_SKILLS_BY_TUBE;
Expand Down Expand Up @@ -35,7 +35,7 @@ async function getCoreCompetences() {

async function getAllActiveSkills() {
if (!ALL_ACTIVE_SKILLS) {
ALL_ACTIVE_SKILLS = await skillDatasource.findActive();
ALL_ACTIVE_SKILLS = (await skillRepository.list()).filter((skill) => skill.status === 'actif');
}
return ALL_ACTIVE_SKILLS;
}
Expand Down
2 changes: 1 addition & 1 deletion api/db/seeds/data/common/tooling/session-tooling.js
Original file line number Diff line number Diff line change
Expand Up @@ -1076,7 +1076,7 @@ async function _makeCandidatesComplementaryCertificationCertifiable(
for (const [areaId, { fourMostDifficultSkillsAndChallenges }] of Object.entries(complementaryProfileData)) {
complementaryProfileData[areaId].fourMostDifficultSkillsAndChallenges = _.orderBy(
fourMostDifficultSkillsAndChallenges,
({ skill }) => skill.level,
({ skill }) => skill.difficulty,
);
complementaryProfileData[areaId].fourMostDifficultSkillsAndChallenges = _.takeRight(
complementaryProfileData[areaId].fourMostDifficultSkillsAndChallenges,
Expand Down
5 changes: 5 additions & 0 deletions api/db/seeds/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DatabaseBuilder } from '../database-builder/database-builder.js';
import { commonBuilder } from './data/common/common-builder.js';
import { complementaryCertificationBuilder } from './data/common/complementary-certification-builder.js';
import { featuresBuilder } from './data/common/feature-builder.js';
import { learningContentBuilder } from './data/common/learningcontent-builder.js';
import { organizationBuilder } from './data/common/organization-builder.js';
import { organizationLearnerImportFormat } from './data/common/organization-learner-import-formats.js';
import { tagsBuilder } from './data/common/tag-builder.js';
Expand All @@ -23,6 +24,10 @@ const seed = async function (knex) {

const databaseBuilder = new DatabaseBuilder({ knex });

// Learning content
logger.info('Seeding: Learning content');
await learningContentBuilder({ databaseBuilder });

// Common
await commonBuilder({ databaseBuilder });
await tagsBuilder({ databaseBuilder });
Expand Down
62 changes: 26 additions & 36 deletions api/lib/infrastructure/repositories/correction-repository.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import _ from 'lodash';

// TODO modifier les dépendances avec datasource en meme temps que la PR de lecture, pour des considérations de perf
import { Answer } from '../../../src/evaluation/domain/models/Answer.js';
import { Challenge } from '../../../src/shared/domain/models/Challenge.js';
import { Correction } from '../../../src/shared/domain/models/Correction.js';
import { Hint } from '../../../src/shared/domain/models/Hint.js';
import {
challengeDatasource,
skillDatasource,
} from '../../../src/shared/infrastructure/datasources/learning-content/index.js';
import { Challenge } from '../../../src/shared/domain/models/index.js';
import { Correction } from '../../../src/shared/domain/models/index.js';
import * as challengeRepository from '../../../src/shared/infrastructure/repositories/challenge-repository.js';
import * as skillRepository from '../../../src/shared/infrastructure/repositories/skill-repository.js';

const VALIDATED_HINT_STATUSES = ['Validé', 'pré-validé'];

Expand All @@ -21,10 +18,10 @@ const getByChallengeId = async function ({
fromDatasourceObject,
getCorrection,
} = {}) {
const challenge = await challengeDatasource.get(challengeId);
const skill = await _getSkill(challenge);
const hint = await _getHint({ skill, locale });
const solution = fromDatasourceObject(challenge);
const challengeForCorrection = await challengeRepository.get(challengeId, { forCorrection: true });
const skill = await _getSkill(challengeForCorrection, locale);
const hint = await _getHint(skill);
const solution = fromDatasourceObject(challengeForCorrection);
let correctionDetails;

const tutorials = await _getTutorials({
Expand All @@ -42,14 +39,17 @@ const getByChallengeId = async function ({
tutorialRepository,
});

if (challenge.type === Challenge.Type.QROCM_DEP && answerValue !== Answer.FAKE_VALUE_FOR_SKIPPED_QUESTIONS) {
if (
challengeForCorrection.type === Challenge.Type.QROCM_DEP &&
answerValue !== Answer.FAKE_VALUE_FOR_SKIPPED_QUESTIONS
) {
correctionDetails = getCorrection({ solution, answerValue });
}

return new Correction({
id: challenge.id,
solution: challenge.solution,
solutionToDisplay: challenge.solutionToDisplay,
id: challengeForCorrection.id,
solution: challengeForCorrection.solution,
solutionToDisplay: challengeForCorrection.solutionToDisplay,
hint,
tutorials,
learningMoreTutorials: learningMoreTutorials,
Expand All @@ -59,32 +59,22 @@ const getByChallengeId = async function ({
};
export { getByChallengeId };

async function _getHint({ skill, locale }) {
if (_hasValidatedHint(skill)) {
return _convertSkillToHint({ skill, locale });
async function _getHint(skill) {
if (_hasValidatedHint(skill) && skill.hint) {
return new Hint({
skillName: skill.name,
value: skill.hint,
});
}
return null;
}

function _getSkill(challengeDataObject) {
return skillDatasource.get(challengeDataObject.skillId);
function _getSkill(challenge, locale) {
return skillRepository.get(challenge.skillId, { locale: locale?.slice(0, 2), useFallback: false });
}

function _hasValidatedHint(skillDataObject) {
return VALIDATED_HINT_STATUSES.includes(skillDataObject.hintStatus);
}

function _convertSkillToHint({ skill, locale }) {
const matches = locale.match(/^([a-z]{2,3})-?[a-z]{0,3}$/);
const language = matches?.[1];
const translation = skill.hint_i18n?.[language];
if (!translation) {
return null;
}

return new Hint({
skillName: skill.name,
value: translation,
});
function _hasValidatedHint(skill) {
return VALIDATED_HINT_STATUSES.includes(skill.hintStatus);
}

async function _getTutorials({ userId, skill, tutorialIdsProperty, locale, tutorialRepository }) {
Expand Down
61 changes: 41 additions & 20 deletions api/lib/infrastructure/repositories/framework-repository.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,56 @@
import _ from 'lodash';

import { NotFoundError } from '../../../src/shared/domain/errors.js';
import { Framework } from '../../../src/shared/domain/models/Framework.js';
import { frameworkDatasource } from '../../../src/shared/infrastructure/datasources/learning-content/framework-datasource.js';
import { Framework } from '../../../src/shared/domain/models/index.js';
import { LearningContentRepository } from '../../../src/shared/infrastructure/repositories/learning-content-repository.js';

const TABLE_NAME = 'learningcontent.frameworks';

export async function list() {
const cacheKey = 'list';
const listCallback = (knex) => knex.orderBy('name');
const frameworkDtos = await getInstance().find(cacheKey, listCallback);
return frameworkDtos.map(toDomain);
}

export async function getByName(name) {
const cacheKey = `getByName(${name})`;
const findByNameCallback = (knex) => knex.where('name', name).limit(1);
const [frameworkDto] = await getInstance().find(cacheKey, findByNameCallback);
if (!frameworkDto) {
throw new NotFoundError(`Framework not found for name ${name}`);
}
return toDomain(frameworkDto);
}

async function list() {
const frameworkDataObjects = await frameworkDatasource.list();
return frameworkDataObjects.map(_toDomain);
export async function findByRecordIds(ids) {
const frameworkDtos = await getInstance().loadMany(ids);
return frameworkDtos
.filter((frameworkDto) => frameworkDto)
.sort(byId)
.map(toDomain);
}

function _toDomain(frameworkData) {
function toDomain(frameworkData) {
return new Framework({
id: frameworkData.id,
name: frameworkData.name,
areas: [],
});
}

async function getByName(name) {
const framework = await frameworkDatasource.getByName(name);

if (framework === undefined) {
throw new NotFoundError(`Framework not found for name ${name}`);
}
return _toDomain(framework);
export function clearCache() {
return getInstance().clearCache();
}

async function findByRecordIds(frameworkIds) {
const frameworkDatas = await frameworkDatasource.findByRecordIds(frameworkIds);
const frameworks = _.map(frameworkDatas, (frameworkData) => _toDomain(frameworkData));
return _.orderBy(frameworks, (framework) => framework.name.toLowerCase());
function byId(entityA, entityB) {
return entityA.id < entityB.id ? -1 : 1;
}

export { findByRecordIds, getByName, list };
/** @type {LearningContentRepository} */
let instance;

function getInstance() {
if (!instance) {
instance = new LearningContentRepository({ tableName: TABLE_NAME });
}
return instance;
}
Loading
Loading