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] exporte les champs supplémentaires fourni lors de l'import (PIX-13695) #10145

Merged
merged 9 commits into from
Sep 26, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const buildOrganizationLearnerWithUser = function ({
user,
updatedAt = new Date('2021-01-01'), // for BEGINNING_OF_THE_2020_SCHOOL_YEAR, can outdate very fast! ;)
group = 'LB1',
attributes = null,
} = {}) {
organizationId = _.isUndefined(organizationId) ? buildOrganization().id : organizationId;
const { id: userId } = buildUser(user);
Expand All @@ -50,6 +51,7 @@ const buildOrganizationLearnerWithUser = function ({
userId,
updatedAt,
group,
attributes,
};

return databaseBuffer.pushInsertable({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const organizationLearnerImportFormat = async function ({ databaseBuilder
headers: [
{ key: 1, name: 'Nom apprenant', property: 'lastName', required: true },
{ key: 2, name: 'Prénom apprenant', property: 'firstName', required: true },
{ key: 3, name: 'Classe', required: true },
{ key: 3, name: 'Classe', required: true, config: { exportable: true } },
{ key: 4, name: 'Date de naissance', required: true },
],
},
Expand Down
3 changes: 2 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@
"start:job:fast": "node worker.js fast",
"start:job:watch": "nodemon worker.js",
"start:job:fast:watch": "nodemon worker.js fast",
"test": "NODE_ENV=test npm run db:prepare && npm run test:api",
"test": "npm run test:db:reset && npm run test:api",
"test:db:reset": "NODE_ENV=test npm run db:prepare",
xav-car marked this conversation as resolved.
Show resolved Hide resolved
"test:api": "for testType in 'unit' 'integration' 'acceptance'; do npm run test:api:$testType || status=1 ; done ; exit $status",
"test:api:path": "NODE_ENV=test mocha --exit --recursive --reporter=${MOCHA_REPORTER:-dot}",
"test:api:scripts": "npm run test:api:path -- tests/integration/scripts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const findProfilesCollectionResultDataByCampaignId = async function (campaignId)
'view-active-organization-learners.group',
'view-active-organization-learners.firstName',
'view-active-organization-learners.lastName',
'view-active-organization-learners.attributes',
])
.join(
'view-active-organization-learners',
Expand Down Expand Up @@ -152,6 +153,7 @@ function _rowToResult(row) {
participantFirstName: row.firstName,
participantLastName: row.lastName,
division: row.division,
additionalInfos: row.attributes,
pixScore: row.pixScore,
group: row.group,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import BaseJoi from 'joi';
const Joi = BaseJoi.extend(JoiDate);
import _ from 'lodash';

import { validateEntity } from '../validators/entity-validator.js';
import { validateEntity } from '../../../../shared/domain/validators/entity-validator.js';

const validationSchema = Joi.object({
participantFirstName: Joi.string().required().allow(''),
participantLastName: Joi.string().required().allow(''),
participantExternalId: Joi.string().optional().allow(null),
additionalInfos: Joi.object().allow(null),
studentNumber: Joi.string().optional().allow(null),
userId: Joi.number().integer().required(),
campaignParticipationId: Joi.number().integer().required(),
Expand All @@ -28,6 +29,7 @@ class CampaignParticipationInfo {
participantExternalId = null,
studentNumber = null,
userId,
additionalInfos,
campaignParticipationId,
isCompleted,
createdAt,
Expand All @@ -42,6 +44,7 @@ class CampaignParticipationInfo {
this.participantExternalId = participantExternalId;
this.studentNumber = studentNumber;
this.userId = userId;
this.additionalInfos = additionalInfos;
this.campaignParticipationId = campaignParticipationId;
this.isCompleted = isCompleted;
this.createdAt = createdAt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { createOneCsvLine };
function createOneCsvLine({
organization,
campaign,
additionalHeaders,
campaignParticipationInfo,
targetProfile,
learningContent,
Expand All @@ -20,6 +21,7 @@ function createOneCsvLine({
campaign,
campaignParticipationInfo,
targetProfile,
additionalHeaders,
learningContent,
stageCollection,
participantKnowledgeElementsByCompetenceId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
const getCampaignByCode = async function ({ code, campaignToJoinRepository, organizationLearnerImportFormat }) {
const getCampaignByCode = async function ({
code,
campaignToJoinRepository,
organizationLearnerImportFormatRepository,
}) {
const campaignToJoin = await campaignToJoinRepository.getByCode({ code });

if (campaignToJoin.isRestricted) {
const config = await organizationLearnerImportFormat.get(campaignToJoin.organizationId);
const config = await organizationLearnerImportFormatRepository.get(campaignToJoin.organizationId);

if (config) campaignToJoin.setReconciliationFields(config.reconciliationFields);
}
Expand Down
67 changes: 53 additions & 14 deletions api/src/prescription/campaign/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { fileURLToPath } from 'node:url';

import * as badgeAcquisitionRepository from '../../../../../lib/infrastructure/repositories/badge-acquisition-repository.js';
import * as campaignRepository from '../../../../../lib/infrastructure/repositories/campaign-repository.js';
import { repositories as libRepositories } from '../../../../../lib/infrastructure/repositories/index.js';
import * as knowledgeElementRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-repository.js';
import * as knowledgeElementSnapshotRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-snapshot-repository.js';
import * as learningContentRepository from '../../../../../lib/infrastructure/repositories/learning-content-repository.js';
import * as membershipRepository from '../../../../../lib/infrastructure/repositories/membership-repository.js';
import * as stageCollectionRepository from '../../../../../lib/infrastructure/repositories/user-campaign-results/stage-collection-repository.js';
import * as tutorialRepository from '../../../../devcomp/infrastructure/repositories/tutorial-repository.js';
import * as badgeRepository from '../../../../evaluation/infrastructure/repositories/badge-repository.js';
import * as userRepository from '../../../../identity-access-management/infrastructure/repositories/user.repository.js';
import * as organizationFeatureApi from '../../../../organizational-entities/application/api/organization-features-api.js';
import * as codeGenerator from '../../../../shared/domain/services/code-generator.js';
import * as placementProfileService from '../../../../shared/domain/services/placement-profile-service.js';
import * as competenceRepository from '../../../../shared/infrastructure/repositories/competence-repository.js';
Expand All @@ -19,7 +20,7 @@ import { injectDependencies } from '../../../../shared/infrastructure/utils/depe
import { importNamedExportsFromDirectory } from '../../../../shared/infrastructure/utils/import-named-exports-from-directory.js';
import * as campaignAnalysisRepository from '../../../campaign-participation/infrastructure/repositories/campaign-analysis-repository.js';
import * as campaignParticipationRepository from '../../../campaign-participation/infrastructure/repositories/campaign-participation-repository.js';
import * as organizationLearnerImportFormat from '../../../learner-management/infrastructure/repositories/organization-learner-import-format-repository.js';
import * as organizationLearnerImportFormatRepository from '../../../learner-management/infrastructure/repositories/organization-learner-import-format-repository.js';
import * as campaignAdministrationRepository from '../../infrastructure/repositories/campaign-administration-repository.js';
import * as campaignAssessmentParticipationResultListRepository from '../../infrastructure/repositories/campaign-assessment-participation-result-list-repository.js';
import * as campaignCollectiveResultRepository from '../../infrastructure/repositories/campaign-collective-result-repository.js';
Expand All @@ -38,41 +39,79 @@ import * as targetProfileRepository from '../../infrastructure/repositories/targ
import * as campaignCsvExportService from '../services/campaign-csv-export-service.js';
import * as campaignUpdateValidator from '../validators/campaign-update-validator.js';

/**
* @typedef { import ('../../../../../lib/infrastructure/repositories/badge-acquisition-repository.js')} BadgeAcquisitionRepository
* @typedef { import ('../../../../../lib/infrastructure/repositories/campaign-repository.js')} CampaignRepository
* @typedef { import ('../../../../devcomp/infrastructure/repositories/tutorial-repository.js')} TutorialRepository
* @typedef { import ('../../../../../lib/infrastructure/repositories/knowledge-element-repository.js')} KnowledgeElementRepository
* @typedef { import ('../../../../../lib/infrastructure/repositories/knowledge-element-snapshot-repository.js')} KnowledgeElementSnapshotRepository
* @typedef { import ('../../../../../lib/infrastructure/repositories/learning-content-repository.js')} LearningContentRepository
* @typedef { import ('../../../../../lib/infrastructure/repositories/membership-repository.js')} MembershipRepository
* @typedef { import ('../../../../../lib/infrastructure/repositories/user-campaign-results/stage-collection-repository.js')} StageCollectionRepository
* @typedef { import ('../../../../evaluation/infrastructure/repositories/badge-repository.js')} BadgeRepository
* @typedef { import ('../../../../identity-access-management/infrastructure/repositories/user.repository.js')} UserRepository
* @typedef { import ('../../../../organizational-entities/application/api/organization-features-api.js')} OrganizationFeatureApi
xav-car marked this conversation as resolved.
Show resolved Hide resolved
* @typedef { import ('../../../../shared/domain/services/code-generator.js')} CodeGenerator
* @typedef { import ('../../../../shared/domain/services/placement-profile-service.js')} PlacementProfileService
* @typedef { import ('../../../../shared/infrastructure/repositories/competence-repository.js')} CompetenceRepository
* @typedef { import ('../../../../shared/infrastructure/repositories/organization-repository.js')} OrganizationRepository
* @typedef { import ('../../../campaign-participation/infrastructure/repositories/campaign-analysis-repository.js')} CampaignAnalysisRepository
* @typedef { import ('../../../campaign-participation/infrastructure/repositories/campaign-participation-repository.js')} CampaignParticipationRepository
* @typedef { import ('../../../learner-management/infrastructure/repositories/organization-learner-import-format-repository.js')} OrganizationLearnerImportFormat
* @typedef { import ('../../infrastructure/repositories/campaign-administration-repository.js')} CampaignAdministrationRepository
* @typedef { import ('../../infrastructure/repositories/campaign-assessment-participation-result-list-repository.js')} CampaignAssessmentParticipationResultListRepository
* @typedef { import ('../../infrastructure/repositories/campaign-collective-result-repository.js')} CampaignCollectiveResultRepository
* @typedef { import ('../../infrastructure/repositories/campaign-creator-repository.js')} CampaignCreatorRepository
* @typedef { import ('../../infrastructure/repositories/campaign-management-repository.js')} CampaignManagementRepository
* @typedef { import ('../../infrastructure/repositories/campaign-participant-activity-repository.js')} CampaignParticipantActivityRepository
* @typedef { import ('../../infrastructure/repositories/campaign-participation-info-repository.js')} CampaignParticipationInfoRepository
* @typedef { import ('../../infrastructure/repositories/campaign-participations-stats-repository.js')} CampaignParticipationsStatsRepository
* @typedef { import ('../../infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js')} CampaignProfilesCollectionParticipationSummaryRepository
* @typedef { import ('../../infrastructure/repositories/campaign-report-repository.js')} CampaignReportRepository
* @typedef { import ('../../infrastructure/repositories/division-repository.js')} DivisionRepository
* @typedef { import ('../../infrastructure/repositories/group-repository.js')} GroupRepository
* @typedef { import ('../../infrastructure/repositories/index.js').CampaignToJoinRepository} CampaignToJoinRepository
* @typedef { import ('../../infrastructure/repositories/index.js').OrganizationMembershipRepository} OrganizationMembershipRepository
* @typedef { import ('../../infrastructure/repositories/target-profile-repository.js')} TargetProfileRepository
* @typedef { import ('../services/campaign-csv-export-service.js')} CampaignCsvExportService
* @typedef { import ('../validators/campaign-update-validator.js')} CampaignUpdateValidator
*/
const dependencies = {
badgeAcquisitionRepository,
badgeRepository,
campaignRepository,
campaignAdministrationRepository,
campaignAnalysisRepository,
campaignManagementRepository,
campaignAssessmentParticipationResultListRepository,
campaignCollectiveResultRepository,
campaignCreatorRepository,
campaignCsvExportService,
campaignManagementRepository,
campaignParticipantActivityRepository,
campaignParticipationInfoRepository,
campaignParticipationRepository,
campaignParticipationsStatsRepository,
campaignProfilesCollectionParticipationSummaryRepository,
campaignParticipationInfoRepository,
campaignReportRepository,
organizationMembershipRepository: campaignRepositories.organizationMembershipRepository,
campaignAssessmentParticipationResultListRepository,
codeGenerator,
campaignRepository,
campaignToJoinRepository: campaignRepositories.campaignToJoinRepository,
campaignUpdateValidator,
codeGenerator,
competenceRepository,
divisionRepository,
groupRepository,
knowledgeElementSnapshotRepository,
knowledgeElementRepository,
knowledgeElementSnapshotRepository,
learningContentRepository,
membershipRepository,
organizationFeatureApi,
organizationLearnerImportFormatRepository,
organizationMembershipRepository: campaignRepositories.organizationMembershipRepository,
organizationRepository,
placementProfileService,
stageCollectionRepository,
targetProfileRepository, // TODO
tutorialRepository,
userRepository,
campaignCollectiveResultRepository,
campaignToJoinRepository: campaignRepositories.campaignToJoinRepository,
campaignParticipationsStatsRepository,
tutorialRepository: libRepositories.tutorialRepository,
organizationLearnerImportFormat,
};

const path = dirname(fileURLToPath(import.meta.url));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,35 @@ import {
import * as csvSerializer from '../../../../shared/infrastructure/serializers/csv/csv-serializer.js';
import { PromiseUtils } from '../../../../shared/infrastructure/utils/promise-utils.js';

/**
* @typedef {import ('./index.js').CampaignRepository} CampaignRepository
* @typedef {import ('./index.js').CampaignParticipationInfoRepository} CampaignParticipationInfoRepository
* @typedef {import ('./index.js').OrganizationRepository} OrganizationRepository
* @typedef {import ('./index.js').KnowledgeElementSnapshotRepository} KnowledgeElementSnapshotRepository
* @typedef {import ('./index.js').CampaignCsvExportService} CampaignCsvExportService
* @typedef {import ('./index.js').TargetProfileRepository} TargetProfileRepository
* @typedef {import ('./index.js').LearningContentRepository} LearningContentRepository
* @typedef {import ('./index.js').StageCollectionRepository} StageCollectionRepository
* @typedef {import ('./index.js').OrganizationFeatureApi} OrganizationFeatureApi
* @typedef {import ('./index.js').OrganizationLearnerImportFormat} OrganizationLearnerImportFormat
*/

/**
* @param {Object} params
* @param {Number} params.campaignId
* @param {Object} params.writableStream
* @param {Object} params.i18n
* @param {CampaignRepository} params.campaignRepository
* @param {CampaignParticipationInfoRepository} params.campaignParticipationInfoRepository
* @param {OrganizationRepository} params.organizationRepository
* @param {KnowledgeElementSnapshotRepository} params.knowledgeElementSnapshotRepository
* @param {CampaignCsvExportService} params.campaignCsvExportService
* @param {TargetProfileRepository} params.targetProfileRepository
* @param {LearningContentRepository} params.learningContentRepository
* @param {StageCollectionRepository} params.stageCollectionRepository
* @param {OrganizationFeatureApi} params.organizationFeatureApi
* @param {OrganizationLearnerImportFormat} params.organizationLearnerImportFormatRepository
*/
const startWritingCampaignAssessmentResultsToStream = async function ({
campaignId,
writableStream,
Expand All @@ -28,7 +57,10 @@ const startWritingCampaignAssessmentResultsToStream = async function ({
targetProfileRepository,
learningContentRepository,
stageCollectionRepository,
organizationFeatureApi,
organizationLearnerImportFormatRepository,
}) {
let additionalHeaders = [];
const campaign = await campaignRepository.get(campaignId);
const translate = i18n.__;

Expand All @@ -44,15 +76,22 @@ const startWritingCampaignAssessmentResultsToStream = async function ({
const organization = await organizationRepository.get(campaign.organizationId);
const campaignParticipationInfos = await campaignParticipationInfoRepository.findByCampaignId(campaign.id);

const organizationFeatures = await organizationFeatureApi.getAllFeaturesFromOrganization(campaign.organizationId);
if (organizationFeatures.hasLearnersImportFeature) {
const importFormat = await organizationLearnerImportFormatRepository.get(campaign.organizationId);
additionalHeaders = importFormat.exportableColumns;
}

// Create HEADER of CSV
const headers = _createHeaderOfCSV(
const headers = _createHeaderOfCSV({
targetProfile,
campaign.idPixLabel,
idPixLabel: campaign.idPixLabel,
organization,
translate,
campaignLearningContent,
learningContent: campaignLearningContent,
stageCollection,
);
additionalHeaders,
});

// WHY: add \uFEFF the UTF-8 BOM at the start of the text, see:
// - https://en.wikipedia.org/wiki/Byte_order_mark
Expand Down Expand Up @@ -134,6 +173,7 @@ const startWritingCampaignAssessmentResultsToStream = async function ({
organization,
campaign,
campaignParticipationInfo,
additionalHeaders,
targetProfile,
learningContent: campaignLearningContent,
stageCollection,
Expand Down Expand Up @@ -165,10 +205,20 @@ const startWritingCampaignAssessmentResultsToStream = async function ({

export { startWritingCampaignAssessmentResultsToStream };

function _createHeaderOfCSV(targetProfile, idPixLabel, organization, translate, learningContent, stageCollection) {
function _createHeaderOfCSV({
targetProfile,
idPixLabel,
organization,
translate,
learningContent,
stageCollection,
additionalHeaders,
}) {
const forSupStudents = organization.isSup && organization.isManagingStudents;
const displayDivision = organization.isSco && organization.isManagingStudents;

const extraHeaders = additionalHeaders.map((header) => header.columnName);

return [
translate('campaign-export.common.organization-name'),
translate('campaign-export.common.campaign-id'),
Expand All @@ -177,6 +227,7 @@ function _createHeaderOfCSV(targetProfile, idPixLabel, organization, translate,
translate('campaign-export.assessment.target-profile-name'),
translate('campaign-export.common.participant-lastname'),
translate('campaign-export.common.participant-firstname'),
...extraHeaders,
...(displayDivision ? [translate('campaign-export.common.participant-division')] : []),
...(forSupStudents ? [translate('campaign-export.common.participant-group')] : []),
...(forSupStudents ? [translate('campaign-export.common.participant-student-number')] : []),
Expand Down
Loading
Loading