Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ export default class CertificationInformationGlobalActions extends Component {
}

get displayRescoringCertificationButton() {
return Boolean(!this.args.certification.isPublished && this.args.session.finalizedAt);
return Boolean(
this.args.certification.status !== 'pending-scoring' &&
!this.args.certification.isPublished &&
this.args.session.finalizedAt,
);
}

@action
Expand Down
8 changes: 8 additions & 0 deletions admin/app/models/certification.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const assessmentResultStatus = {
CANCELLED: 'cancelled',
CANCELLED_BY_JURY: 'cancelled_by_jury',
ERROR: 'error',
PENDING_SCORING: 'pending-scoring',
VALIDATED: 'validated',
REJECTED: 'rejected',
};
Expand Down Expand Up @@ -78,6 +79,12 @@ export default class Certification extends Model {
}

get statusLabelAndValue() {
if (this.status === assessmentResultStatus.PENDING_SCORING) {
return {
value: assessmentResultStatus.PENDING_SCORING,
label: this.intl.t('pages.certifications.certification.details.v3.assessment-result-status.pending-scoring'),
};
}
return certificationStatuses.find((certificationStatus) => certificationStatus.value === this.status);
}

Expand Down Expand Up @@ -117,6 +124,7 @@ export default class Certification extends Model {
}

get result() {
if (!this.reachedResultKey) return null;
return this.intl.t(`common.certification.meshLevels.${this.reachedResultKey}`, {
pixScore: this.pixScore,
});
Expand Down
1 change: 1 addition & 0 deletions admin/app/models/jury-certification-summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default class JuryCertificationSummary extends Model {
}

get result() {
if (!this.reachedResultKey) return null;
return this.intl.t(`common.certification.meshLevels.${this.reachedResultKey}`, {
pixScore: this.pixScore,
});
Expand Down
1 change: 1 addition & 0 deletions admin/tests/unit/models/certification-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ module('Unit | Model | certification', function (hooks) {
module('#statusLabelAndValue', function () {
[
{ value: assessmentStates.STARTED, label: 'Démarrée' },
{ value: assessmentResultStatus.PENDING_SCORING, label: 'En attente de scoring' },
{ value: assessmentResultStatus.ERROR, label: 'En erreur' },
{ value: assessmentResultStatus.VALIDATED, label: 'Validée' },
{ value: assessmentResultStatus.REJECTED, label: 'Rejetée' },
Expand Down
9 changes: 5 additions & 4 deletions admin/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -657,20 +657,20 @@
"save-error": "An error occurred while saving the scoring.",
"save-scoring-button": "Save scoring",
"save-scoring-modal": {
"content": "This action will save the scoring configuration on the active version.",
"content": "This action will save the scoring configuration to the active version and will trigger the scoring that has already been applied to that version. This action is irreversible, the scoring cannot be modified.",
"title": "Confirm scoring save"
},
"success-notification": "The scoring has been successfully updated",
"success-notification": "The scoring has been successfully activated. Certifications already completed for this version will be scored.",
"title": "Bounds of capacity by mesh"
},
"title": "Creating a Certification Release"
}
},
"deletion-modal": {
"action-button": "Confirm deletion",
"content": "You are about to delete a certification version !!!!",
"content": "You are about to delete a certification version.",
"error-message": "An error has occured",
"success-message": "That version has indeed been removed",
"success-message": "That version has indeed been removed.",
"title": "Confirmation of version deletion"
},
"labels": {
Expand Down Expand Up @@ -1691,6 +1691,7 @@
"cancelled_by_jury": "Cancelled",
"error": "Error",
"fraud": "Rejected for fraud",
"pending-scoring": "Pending scoring",
"rejected": "Rejected",
"validated": "Validated"
},
Expand Down
7 changes: 4 additions & 3 deletions admin/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -663,10 +663,10 @@
"save-error": "Une erreur s'est produite lors de l'enregistrement du scoring.",
"save-scoring-button": "Enregistrer le scoring",
"save-scoring-modal": {
"content": "Cette action va enregistrer la configuration de scoring sur la version active.",
"content": "Cette action va enregistrer la configuration de scoring sur la version active et va déclencher le scoring déjà passée sur cette version. Cette action est irréversible, le scoring ne pourra pas être modifié.",
"title": "Confirmation de l'enregistrement du scoring"
},
"success-notification": "Le scoring a bien été mis à jour",
"success-notification": "Le scoring a bien été activé. Les certifications déjà passée sur la version seront scorées.",
"title": "Bornes de capacités par mailles"
},
"title": "Création d'une version de certification"
Expand All @@ -676,7 +676,7 @@
"action-button": "Confirmer la suppression",
"content": "Vous vous apprêtez à supprimer une version.",
"error-message": "Une erreur s'est produite",
"success-message": "La version à bien été supprimée",
"success-message": "La version à bien été supprimée.",
"title": "Confirmation de suppression d'une version"
},
"labels": {
Expand Down Expand Up @@ -1692,6 +1692,7 @@
"cancelled_by_jury": "Annulée",
"error": "Erreur",
"fraud": "Rejetée pour fraude",
"pending-scoring": "En attente de scoring",
"rejected": "Rejetée",
"validated": "Validée"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class ScoreCertificationJob {
constructor({ certificationCourseId }) {
this.certificationCourseId = certificationCourseId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import * as attachableTargetProfileRepository from '../../infrastructure/reposit
import * as calibratedChallengesRepository from '../../infrastructure/repositories/calibrated-challenges-repository.js';
import * as calibrationRepository from '../../infrastructure/repositories/calibration-repository.js';
import * as centerRepository from '../../infrastructure/repositories/center-repository.js';
import * as certificationCoursesToScoreRepository from '../../infrastructure/repositories/certification-courses-to-score-repository.js';
import * as certificationInfoRepository from '../../infrastructure/repositories/certification-info-repository.js';
import * as complementaryCertificationBadgesRepository from '../../infrastructure/repositories/complementary-certification-badge-repository.js';
import * as complementaryCertificationForTargetProfileAttachmentRepository from '../../infrastructure/repositories/complementary-certification-for-target-profile-attachment-repository.js';
import * as complementaryCertificationRepository from '../../infrastructure/repositories/complementary-certification-repository.js';
import { scoreCertificationJobRepository } from '../../infrastructure/repositories/jobs/score-certification-job-repository.js';
import * as organizationRepository from '../../infrastructure/repositories/organization-repository.js';
import * as ScoBlockedAccessDatesRepository from '../../infrastructure/repositories/sco-blocked-access-dates-repository.js';
import * as versionDetailsRepository from '../../infrastructure/repositories/version-details-repository.js';
Expand Down Expand Up @@ -56,6 +58,8 @@ import { updateVersionComment } from './update-version-comment.js';
const dependencies = {
calibratedChallengesRepository,
attachableTargetProfileRepository,
certificationCoursesToScoreRepository,
scoreCertificationJobRepository,
centerRepository,
ScoBlockedAccessDatesRepository,
certificationInfoRepository,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { NotFoundError } from '../../../../shared/domain/errors.js';
import { ScoreCertificationJob } from '../models/ScoreCertificationJob.js';

/**
* @param {object} params
* @param {number} params.id
* @param {Array} params.globalScoringConfiguration
* @param {Array|null} params.competencesScoringConfiguration
* @param {object} params.versionRepository
* @param {object} params.certificationCoursesToScoreRepository
* @param {object} params.scoreCertificationJobRepository
*/
export async function saveScoringConfiguration({
id,
globalScoringConfiguration,
competencesScoringConfiguration,
versionRepository,
certificationCoursesToScoreRepository,
scoreCertificationJobRepository,
}) {
const version = await versionRepository.getById({ id });

Expand All @@ -20,4 +25,9 @@ export async function saveScoringConfiguration({
}

await versionRepository.updateScoring({ id, globalScoringConfiguration, competencesScoringConfiguration });

const certificationCourseIds = await certificationCoursesToScoreRepository.findIdsByVersionId({ versionId: id });
await scoreCertificationJobRepository.performAsync(
...certificationCourseIds.map((certificationCourseId) => new ScoreCertificationJob({ certificationCourseId })),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js';

export async function findIdsByVersionId({ versionId }) {
const knexConn = DomainTransaction.getConnection();
const rows = await knexConn('certification-courses')
.select('certification-courses.id')
.join('sessions', 'sessions.id', 'certification-courses.sessionId')
.whereNotNull('sessions.finalizedAt')
.where('certification-courses.versionId', versionId);
return rows.map((row) => row.id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { JobRepository } from '../../../../../shared/infrastructure/repositories/jobs/job-repository.js';
import { ScoreCertificationJob } from '../../../domain/models/ScoreCertificationJob.js';

class ScoreCertificationJobRepository extends JobRepository {
constructor() {
super({ name: ScoreCertificationJob.name });
}
}

export const scoreCertificationJobRepository = new ScoreCertificationJobRepository();
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { JobController } from '../../../../shared/application/jobs/job-controller.js';
import { ScoreCertificationJob } from '../../../configuration/domain/models/ScoreCertificationJob.js';
import { usecases } from '../../domain/usecases/index.js';

export class ScoreCertificationJobController extends JobController {
constructor() {
super(ScoreCertificationJob.name);
}

async handle({ data }) {
const { certificationCourseId } = data;
await usecases.scoreV3Certification({ certificationCourseId });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export class JuryCertification {
return `${this.certificationFramework}.NONE`;
}

if (this.status === 'pending-scoring') {
return null;
}

const resultKey = this.eduV3ExternalJuryResult || (this.reachedMeshIndex ?? 'BELOW_MINIMUM');

return `${this.certificationFramework}.${resultKey}`;
Expand Down Expand Up @@ -154,7 +158,9 @@ export class JuryCertification {
birthCountry: juryCertificationDTO.birthCountry,
birthPostalCode: juryCertificationDTO.birthPostalCode,
createdAt: juryCertificationDTO.createdAt,
status: juryCertificationDTO.assessmentResultStatus,
status:
juryCertificationDTO.assessmentResultStatus ??
(juryCertificationDTO.hasScoringConfiguration ? null : 'pending-scoring'),
isPublished: juryCertificationDTO.isPublished,
isRejectedForFraud: juryCertificationDTO.isRejectedForFraud,
juryId: juryCertificationDTO.juryId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export class JuryCertificationSummary {
return `${this.certificationFramework}.NONE`;
}

if (this.status === STARTED) {
return null;
}

const resultKey = this.eduV3ExternalJuryResult || (this.reachedMeshIndex ?? 'BELOW_MINIMUM');

return `${this.certificationFramework}.${resultKey}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ function _selectJuryCertifications(knexConn) {
commentByAutoJury: 'assessment-results.commentByAutoJury',
certificationFramework: 'certification-courses.framework',
lastAnswerAt: 'certification-courses.lastAnswerAt',
hasScoringConfiguration: knexConn.raw(
`(certification_versions."globalScoringConfiguration" IS NOT NULL AND jsonb_array_length(certification_versions."globalScoringConfiguration") > 0)`,
),
})
.from('certification-courses')
.join('assessments', 'assessments.certificationCourseId', 'certification-courses.id')
Expand All @@ -133,7 +136,8 @@ function _selectJuryCertifications(knexConn) {
'assessment-results.id',
'certification-courses-last-assessment-results.lastAssessmentResultId',
)
.groupBy('certification-courses.id', 'assessments.id', 'assessment-results.id');
.leftJoin('certification_versions', 'certification_versions.id', 'certification-courses.versionId')
.groupBy('certification-courses.id', 'assessments.id', 'assessment-results.id', 'certification_versions.id');
}

async function _toDomainWithComplementaryCertifications({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ describe('Acceptance | Certification | Configuration | API | certification-versi
});

describe('PATCH /api/admin/certification-versions/{certificationVersionId}/scoring', function () {
it('updates the scoring configuration of a version', async function () {
it('updates the scoring configuration of a version and enqueues scoring jobs for existing certifications', async function () {
// given
domainBuilder.certification.configuration
.versionBuilder()
Expand All @@ -944,6 +944,13 @@ describe('Acceptance | Certification | Configuration | API | certification-versi
competencesScoringConfiguration: null,
})
.insertToDB({ databaseBuilder });

const finalizedSession = databaseBuilder.factory.buildSession({ finalizedAt: new Date('2025-01-01') });
const course = databaseBuilder.factory.buildCertificationCourse({
sessionId: finalizedSession.id,
versionId: 42,
});

await databaseBuilder.commit();

const globalScoringConfiguration = [{ meshLevel: 1, bounds: { min: -2, max: 3 } }];
Expand All @@ -970,6 +977,9 @@ describe('Acceptance | Certification | Configuration | API | certification-versi
expect(response.statusCode).to.equal(204);
const updatedVersion = await knex('certification_versions').where({ id: 42 }).first();
expect(updatedVersion.globalScoringConfiguration).to.deep.equal(globalScoringConfiguration);
await expect('ScoreCertificationJob').to.have.been.performed.withJobPayload(
sinon.match({ certificationCourseId: course.id }),
);
});

it('returns 403 when the user is not a super admin', async function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { expect } from 'chai';

import { ScoreCertificationJob } from '../../../../../../src/certification/configuration/domain/models/ScoreCertificationJob.js';
import { usecases } from '../../../../../../src/certification/configuration/domain/usecases/index.js';
import * as versionRepository from '../../../../../../src/certification/configuration/infrastructure/repositories/version-repository.js';
import { SCOPES } from '../../../../../../src/certification/shared/domain/models/Scopes.js';
import { EMPTY_CORRELATION_INFO } from '../../../../../../src/shared/infrastructure/execution-context-manager.js';
import { JobPriority } from '../../../../../../src/shared/infrastructure/repositories/jobs/job-repository.js';
import { databaseBuilder } from '../../../../../tooling/databases.js';
import { domainBuilder } from '../../../../../tooling/domain-builder/domain-builder.js';

Expand Down Expand Up @@ -35,4 +38,37 @@ describe('Certification | Configuration | Integration | Domain | UseCase | save-
expect(updatedVersion.globalScoringConfiguration).to.deep.equal(globalScoringConfiguration);
expect(updatedVersion.competencesScoringConfiguration).to.be.null;
});

it('enqueues a ScoreCertificationJob for each certification course on a finalized session', async function () {
// given
const version = domainBuilder.certification.configuration
.versionBuilder()
.asActive()
.withParameters({ id: 456, scope: SCOPES.PIX_PLUS_DROIT, globalScoringConfiguration: [] })
.insertToDB({ databaseBuilder });

const finalizedSession = databaseBuilder.factory.buildSession({ finalizedAt: new Date('2025-01-01') });
const course = databaseBuilder.factory.buildCertificationCourse({
sessionId: finalizedSession.id,
versionId: version.id,
});

await databaseBuilder.commit();

// when
await usecases.saveScoringConfiguration({
id: 456,
globalScoringConfiguration: [{ meshLevel: 1, bounds: { min: 0, max: 100 } }],
competencesScoringConfiguration: null,
});

// then
await expect(ScoreCertificationJob.name).to.have.been.performed.withJob({
data: {
certificationCourseId: course.id,
correlationContext: EMPTY_CORRELATION_INFO,
},
priority: JobPriority.DEFAULT,
});
});
});
Loading
Loading