diff --git a/confiture-rest-api/scripts/yearly-stats.ts b/confiture-rest-api/scripts/yearly-stats.ts new file mode 100644 index 000000000..e4928f1cb --- /dev/null +++ b/confiture-rest-api/scripts/yearly-stats.ts @@ -0,0 +1,129 @@ +/** Calculate some yearly stats. */ +import { Audit, AuditType, PrismaClient } from "@prisma/client"; +import { AuditService } from "../src/audits/audit.service"; + +const EMAIL_BLACKLIST = [ + "etienne-dupont@example.com", + "test@test.fr", + "test@test.com", + "test@gmail.com", + "toto@toto.fr", + "benoit.dequick@modernisation.gouv.fr", + "adrien.muzyczka@modernisation.gouv.fr", + "adrien.muzyczka@gmail.com" +]; + +// regexes to filter out emails with aliases +const EMAIL_BLACKLIST_REGEXES = EMAIL_BLACKLIST.map((email) => { + const [username, domain] = email.split("@"); + return new RegExp(`${username}(\\+.+)?${domain}`); +}); + +const filterTestAudits = (audit: Pick): boolean => !EMAIL_BLACKLIST_REGEXES.some((regex) => regex.test(audit.auditorEmail)); + +async function getStatsOnDateInterval(prisma: PrismaClient, startDate: Date, endDate: Date) { + const createdAudits2024 = (await prisma.audit.findMany({ + where: { + auditorEmail: { + notIn: EMAIL_BLACKLIST + }, + creationDate: { + gte: startDate, + lt: endDate + } + }, + select: { + auditorEmail: true + } + })).filter(filterTestAudits).length; + + const createdFullAudits2024 = (await prisma.audit.findMany({ + where: { + auditType: AuditType.FULL, + auditorEmail: { + notIn: EMAIL_BLACKLIST + }, + creationDate: { + gte: startDate, + lt: endDate + } + }, + select: { + auditorEmail: true + } + })).filter(filterTestAudits).length; + + const finishedFullAudits2024 = (await prisma.audit.findMany({ + where: { + auditType: AuditType.FULL, + auditorEmail: { + notIn: EMAIL_BLACKLIST + }, + publicationDate: { + gte: startDate, + lt: endDate + } + }, + select: { + auditorEmail: true + } + })).filter(filterTestAudits).length; + + const auditsWithStatements2024 = (await prisma.audit.findMany({ + where: { + auditorEmail: { + notIn: EMAIL_BLACKLIST + }, + publicationDate: { + gte: startDate, + lt: endDate + }, + initiator: { + not: null + } + }, + include: { + pages: { + include: { + results: true + } + }, + transverseElementsPage: { + include: { results: true } + } + } + })).filter(filterTestAudits); + + const filledStatements2024 = auditsWithStatements2024.length; + + const auditRates = auditsWithStatements2024.map(audit => { + const results = [audit.pages.map(p => p.results), audit.transverseElementsPage.results].flat(2); + const { accessibilityRate } = AuditService.groupResultsByStatus(results, audit.transverseElementsPageId); + return accessibilityRate; + }); + + const compliants2024 = auditRates.filter(r => r >= 100).length; + const partiallyCompliants2024 = auditRates.filter(r => r >= 50 && r < 100).length; + const notCompliant2024 = auditRates.filter(r => r < 50).length; + + return { + createdAudits2024, + createdFullAudits2024, + finishedFullAudits2024, + filledStatements2024, + compliants2024, + partiallyCompliants2024, + notCompliant2024 + }; +} + +async function main() { + const prisma = new PrismaClient(); + + const stats2024 = await getStatsOnDateInterval(prisma, new Date("2024-01-01"), new Date("2025-01-01")); + const stats2025 = await getStatsOnDateInterval(prisma, new Date("2025-01-01"), new Date("2026-01-01")); + + console.log(JSON.stringify({ stats2024, stats2025 }, null, 4)); +} + +main(); diff --git a/confiture-rest-api/src/audits/audit.service.ts b/confiture-rest-api/src/audits/audit.service.ts index d8d76b5e8..0430baec8 100644 --- a/confiture-rest-api/src/audits/audit.service.ts +++ b/confiture-rest-api/src/audits/audit.service.ts @@ -944,59 +944,16 @@ export class AuditService { }) ]).then(results => results.flat()); - const groupedCriteria = results.reduce>( - (acc, c) => { - const key = `${c.topic}.${c.criterium}`; - if (acc[key]) { - acc[key].push(c); - } else { - acc[key] = [c]; - } - return acc; - }, - {} - ); - - const applicableCriteria = Object.values(groupedCriteria).filter( - (criteria) => criteria.some((c) => isCompliant(c) || isNotCompliant(c)) - ); - - const compliantCriteria = applicableCriteria.filter((criteria) => { - // remove untested transverse criterion - const withoutUntestedTrans = criteria.filter( - (c) => - !isTransverse(c, audit.transverseElementsPageId) || !isNotTested(c) - ); - - return ( - withoutUntestedTrans.some((c) => isCompliant(c)) && - withoutUntestedTrans.every((c) => isCompliant(c) || isNotApplicable(c)) - ); - }); - - const notCompliantCriteria = applicableCriteria.filter((criteria) => { - return criteria.some((c) => isNotCompliant(c)); - }); - - const notApplicableCriteria = Object.values(groupedCriteria).filter( - (criteria) => { - // remove untested transverse criterion - const withoutUntestedTrans = criteria.filter( - (c) => - !isTransverse(c, audit.transverseElementsPageId) || !isNotTested(c) - ); - - return withoutUntestedTrans.every((c) => isNotApplicable(c)); - } - ); - - const accessibilityRate = - Math.round( - (compliantCriteria.length / applicableCriteria.length) * 100 - ) || 0; - const totalCriteriaCount = CRITERIA_BY_AUDIT_TYPE[audit.auditType].length; + const { + compliantCriteria, + notCompliantCriteria, + applicableCriteria, + notApplicableCriteria, + accessibilityRate + } = AuditService.groupResultsByStatus(results, audit.transverseElementsPageId); + const report: AuditReportDto = { consultUniqueId: audit.consultUniqueId, @@ -1196,6 +1153,68 @@ export class AuditService { return report; } + public static groupResultsByStatus(results: CriterionResult[], transversePageId: number) { + const groupedCriteria = results.reduce>( + (acc, c) => { + const key = `${c.topic}.${c.criterium}`; + if (acc[key]) { + acc[key].push(c); + } else { + acc[key] = [c]; + } + return acc; + }, + {} + ); + + const applicableCriteria = Object.values(groupedCriteria).filter( + (criteria) => criteria.some((c) => isCompliant(c) || isNotCompliant(c)) + ); + + const compliantCriteria = applicableCriteria.filter((criteria) => { + // remove untested transverse criterion + const withoutUntestedTrans = criteria.filter( + (c) => + !isTransverse(c, transversePageId) || !isNotTested(c) + ); + + return ( + withoutUntestedTrans.some((c) => isCompliant(c)) && + withoutUntestedTrans.every((c) => isCompliant(c) || isNotApplicable(c)) + ); + }); + + const notCompliantCriteria = applicableCriteria.filter((criteria) => { + return criteria.some((c) => isNotCompliant(c)); + }); + + const notApplicableCriteria = Object.values(groupedCriteria).filter( + (criteria) => { + // remove untested transverse criterion + const withoutUntestedTrans = criteria.filter( + (c) => + !isTransverse(c, transversePageId) || !isNotTested(c) + ); + + return withoutUntestedTrans.every((c) => isNotApplicable(c)); + } + ); + + const accessibilityRate = + Math.round( + (compliantCriteria.length / applicableCriteria.length) * 100 + ) || 0; + + return { + groupedCriteria, + applicableCriteria, + compliantCriteria, + notCompliantCriteria, + notApplicableCriteria, + accessibilityRate + }; + } + async isAuditComplete(uniqueId: string): Promise { const audit = await this.findAuditWithEditUniqueId(uniqueId, { pages: true