diff --git a/CHANGELOG.md b/CHANGELOG.md index 06619946a..45bbc11ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ Tous les changements notables de Ara sont documentés ici avec leur date, leur catégorie (nouvelle fonctionnalité, correction de bug ou autre changement) et leur pull request (PR) associée. +## 18/10/2024 + +### Autres changements ⚙️ + +- Ajout de liens de recours dans la déclaration d’accessibilité générée ([#812](https://github.com/DISIC/Ara/pull/812)) + +## 17/10/2024 + +### Nouvelles fonctionnalités 🚀 + +- Modifie la gestion des éléments transverses : l’interrupteur "Sur toutes les pages" est remplacé par l’onglet "Éléments transverses" qui permet d’évaluer les éléments communs à toutes les pages : en-tête, pied de page... ([#758](https://github.com/DISIC/Ara/pull/758)) + +## 09/10/2024 + +### Corrections 🐛 + +- Corrige la mise à jour de l’ordre des pages quand les 2 pages ne sont pas adjacentes ([#809](https://github.com/DISIC/Ara/pull/809)) +- Corrige l’application de l'attribut `autocomplete` sur le champ "email" du formulaire de connexion ([#808](https://github.com/DISIC/Ara/pull/808)) + +## 05/09/2024 + +### Nouvelles fonctionnalités 🚀 + +- Ajoute une modale de confirmation lors de la suppression d’une pièce jointe sur un critère "Non conforme" ([#788](https://github.com/DISIC/Ara/pull/788)) +- Met automatiquement le focus sur le champs "Erreur et recommandation" lorsque qu’un critère est défini comme "Non conforme" ([#766](https://github.com/DISIC/Ara/pull/766)) + ## 24/07/2024 ### Autres changements ⚙️ diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 000000000..4a65a546b --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,46 @@ +# Documentation + +Ce fichier détaille les règles métiers utilisées au sein du projet. + +## Calcul du nombre de critères + +Le statut final d’un critère est déterminé en calculant le résultat de l’ensemble des statuts sur chaque page et sur les éléments transverses pour ce critère. + +### Critères applicables + +Un critère est applicable si : + +- il est conforme ou non-conforme sur au moins une page ou sur les éléments transverses. + +### Critères non-applicables + +Un critère est non-applicable si : + +- il est non-applicable sur l’ensemble des pages et sur les éléments transverses testés. + +### Critères conformes + +Un critère est conforme si : + +- il est conforme ou non-applicable sur l’ensemble des pages et sur les éléments transverses testés. +- il est conforme sur au moins une page ou sur les éléments transverses testés. + +### Critères non-conformes + +Un critère est non-conforme si : + +- il est non-conforme sur au moins une page ou sur les éléments transverses. + +## Taux de conformité d’un audit + +Le taux de conformité d’un audit se fait selon le calcul suivant : + +``` +Taux de conformité = (Nombre de critères conformes / Nombre de critères applicables) * 100 +``` + +Exemple avec 89 critères applicables et 27 critères conformes : + +``` +Taux de conformité = (27 / 89) * 100 = 30,34% +``` diff --git a/confiture-rest-api/prisma/migrations/20240911153613_add_transverse_elements_page/migration.sql b/confiture-rest-api/prisma/migrations/20240911153613_add_transverse_elements_page/migration.sql new file mode 100644 index 000000000..7e68796c8 --- /dev/null +++ b/confiture-rest-api/prisma/migrations/20240911153613_add_transverse_elements_page/migration.sql @@ -0,0 +1,25 @@ +-- AlterTable +ALTER TABLE "Audit" ADD COLUMN "transverseElementsPageId" INTEGER; + +-- Create one transverse elements page for each existing audit and link it +DO $$ +DECLARE temprow RECORD; +DECLARE transversePageId "AuditedPage"."id"%TYPE; +BEGIN FOR temprow IN + SELECT * FROM "Audit" WHERE "transverseElementsPageId" IS NULL + LOOP + -- Crée la page élément transverse + INSERT INTO "AuditedPage"("name", "url", "order") VALUES ('Éléments transverses (optionnel)', '', -1) RETURNING "id" INTO transversePageId; + -- Lie la page à l'audit + UPDATE "Audit" SET "transverseElementsPageId" = transversePageId WHERE "id" = "temprow"."id"; + END LOOP; +END $$; + +-- Make transverseElementsPageId not nullable +ALTER TABLE "Audit" ALTER COLUMN "transverseElementsPageId" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Audit_transverseElementsPageId_key" ON "Audit"("transverseElementsPageId"); + +-- AddForeignKey +ALTER TABLE "Audit" ADD CONSTRAINT "Audit_transverseElementsPageId_fkey" FOREIGN KEY ("transverseElementsPageId") REFERENCES "AuditedPage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/confiture-rest-api/prisma/schema.prisma b/confiture-rest-api/prisma/schema.prisma index 674f21e84..60da3ac45 100644 --- a/confiture-rest-api/prisma/schema.prisma +++ b/confiture-rest-api/prisma/schema.prisma @@ -49,7 +49,10 @@ model Audit { auditType AuditType procedureName String /// @DtoEntityHidden - pages AuditedPage[] + pages AuditedPage[] @relation("UserPages") + // single page for transverse elements + transverseElementsPage AuditedPage @relation("TransversePage", fields: [transverseElementsPageId], references: [id]) + transverseElementsPageId Int @unique auditorName String? auditorEmail String? showAuditorEmailInReport Boolean @default(false) @@ -111,9 +114,14 @@ model AuditedPage { name String url String - audit Audit? @relation(fields: [auditUniqueId], references: [editUniqueId], onDelete: Cascade) + // parent audit when the page is a user made page + audit Audit? @relation(name: "UserPages", fields: [auditUniqueId], references: [editUniqueId], onDelete: Cascade) auditUniqueId String? + // parent audit when the page is a transverse page + auditTransverse Audit? @relation(name: "TransversePage") + + results CriterionResult[] } diff --git a/confiture-rest-api/src/audits/audit-export.service.ts b/confiture-rest-api/src/audits/audit-export.service.ts index 3f8a21a01..d32589850 100644 --- a/confiture-rest-api/src/audits/audit-export.service.ts +++ b/confiture-rest-api/src/audits/audit-export.service.ts @@ -38,7 +38,11 @@ export class AuditExportService { const data = []; // Column headers - data.push(["Critères", ...audit.pages.map((p) => p.name)]); + data.push([ + "Critères", + "Éléments transverses", + ...audit.pages.map((p) => p.name) + ]); const resultsByCriteria = groupBy( results, @@ -50,6 +54,13 @@ export class AuditExportService { // Tests results criteria.forEach((c) => { const criterionKey = c.topic + "." + c.criterium; + + const transverseStatus = + CRITERIUM_STATUS[ + resultsByCriteria[criterionKey].find( + (r) => r.pageId === audit.transverseElementsPageId + ).status + ]; const criteriumStatuses = audit.pages.map( (p) => CRITERIUM_STATUS[ @@ -57,7 +68,7 @@ export class AuditExportService { .status ] ); - data.push([criterionKey, ...criteriumStatuses]); + data.push([criterionKey, transverseStatus, ...criteriumStatuses]); }); // compile data to CSV buffer diff --git a/confiture-rest-api/src/audits/audit.service.ts b/confiture-rest-api/src/audits/audit.service.ts index 4600aec54..08f5d5f8d 100644 --- a/confiture-rest-api/src/audits/audit.service.ts +++ b/confiture-rest-api/src/audits/audit.service.ts @@ -8,8 +8,7 @@ import { CriterionResultUserImpact, FileDisplay, Prisma, - StoredFile, - TestEnvironment + StoredFile } from "@prisma/client"; import { nanoid } from "nanoid"; import sharp from "sharp"; @@ -25,9 +24,10 @@ import { UpdateAuditDto } from "./dto/update-audit.dto"; import { UpdateResultsDto } from "./dto/update-results.dto"; import { PatchAuditDto } from "./dto/patch-audit.dto"; -const AUDIT_EDIT_INCLUDE: Prisma.AuditInclude = { +const AUDIT_EDIT_INCLUDE = { recipients: true, environments: true, + transverseElementsPage: true, pages: true, sourceAudit: { select: { @@ -35,7 +35,22 @@ const AUDIT_EDIT_INCLUDE: Prisma.AuditInclude = { } }, notesFiles: true -}; +} as const; + +const isCompliant = (c: CriterionResult) => + c.status === CriterionResultStatus.COMPLIANT; + +const isNotCompliant = (c: CriterionResult) => + c.status === CriterionResultStatus.NOT_COMPLIANT; + +const isNotApplicable = (c: CriterionResult) => + c.status === CriterionResultStatus.NOT_APPLICABLE; + +const isNotTested = (c: CriterionResult) => + c.status === CriterionResultStatus.NOT_TESTED; + +const isTransverse = (c: CriterionResult, transversePageId: number) => + c.pageId === transversePageId; @Injectable() export class AuditService { @@ -62,10 +77,17 @@ export class AuditService { auditorEmail: data.auditorEmail, auditorName: data.auditorName, + transverseElementsPage: { + create: { + name: "Éléments transverses", + url: "", + order: -1 + } + }, pages: { createMany: { data: data.pages.map((p, i) => { - return { ...p, order: i }; + return { ...p, order: i + 1 }; }) } }, @@ -131,6 +153,9 @@ export class AuditService { > { const [audit, pages, existingResults] = await Promise.all([ this.prisma.audit.findUnique({ + include: { + transverseElementsPage: true + }, where: { editUniqueId: uniqueId } @@ -141,9 +166,18 @@ export class AuditService { this.prisma.criterionResult.findMany({ where: { page: { - audit: { - editUniqueId: uniqueId - } + OR: [ + { + audit: { + editUniqueId: uniqueId + } + }, + { + auditTransverse: { + editUniqueId: uniqueId + } + } + ] } }, include: { @@ -154,7 +188,7 @@ export class AuditService { // We do not create every empty criterion result rows in the db when creating pages. // Instead we return the results in the database and fill missing criteria with placeholder data. - return pages.flatMap((page) => + return [audit.transverseElementsPage, ...pages].flatMap((page) => CRITERIA_BY_AUDIT_TYPE[audit.auditType].map((criterion) => { const existingResult = existingResults.find( (result) => @@ -755,14 +789,10 @@ export class AuditService { async getAuditReportData( consultUniqueId: string ): Promise { - const audit = (await this.prisma.audit.findUnique({ + const audit = await this.prisma.audit.findUnique({ where: { consultUniqueId }, include: AUDIT_EDIT_INCLUDE - })) as Audit & { - environments: TestEnvironment[]; - pages: AuditedPage[]; - notesFiles: AuditFile[]; - }; + }); if (!audit) { return; @@ -771,14 +801,21 @@ export class AuditService { const results = await this.prisma.criterionResult.findMany({ where: { page: { - auditUniqueId: audit.editUniqueId - }, - criterium: { - in: CRITERIA_BY_AUDIT_TYPE[audit.auditType].map((c) => c.criterium) + OR: [ + { + auditUniqueId: audit.editUniqueId + }, + { + auditTransverse: { + editUniqueId: audit.editUniqueId + } + } + ] }, - topic: { - in: CRITERIA_BY_AUDIT_TYPE[audit.auditType].map((c) => c.topic) - } + OR: CRITERIA_BY_AUDIT_TYPE[audit.auditType].map((c) => ({ + criterium: c.criterium, + topic: c.topic + })) }, include: { exampleImages: true @@ -799,25 +836,36 @@ export class AuditService { ); const applicableCriteria = Object.values(groupedCriteria).filter( - (criteria) => - criteria.some((c) => c.status !== CriterionResultStatus.NOT_APPLICABLE) + (criteria) => criteria.some((c) => isCompliant(c) || isNotCompliant(c)) ); - const notApplicableCriteria = Object.values(groupedCriteria).filter( - (criteria) => - criteria.every((c) => c.status === CriterionResultStatus.NOT_APPLICABLE) - ); - - const compliantCriteria = applicableCriteria.filter((criteria) => - criteria.every( + const compliantCriteria = applicableCriteria.filter((criteria) => { + // remove untested transverse criterion + const withoutUntestedTrans = criteria.filter( (c) => - c.status === CriterionResultStatus.COMPLIANT || - c.status === CriterionResultStatus.NOT_APPLICABLE - ) - ); + !(isTransverse(c, audit.transverseElementsPageId) && isNotTested(c)) + ); - const notCompliantCriteria = applicableCriteria.filter((criteria) => - criteria.some((c) => c.status === CriterionResultStatus.NOT_COMPLIANT) + 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 = @@ -884,7 +932,7 @@ export class AuditService { browser: e.browser })), referencial: "RGAA Version 4.1", - samples: audit.pages + samples: [audit.transverseElementsPage, ...audit.pages] .map((p, i) => ({ name: p.name, order: p.order, @@ -897,53 +945,56 @@ export class AuditService { technologies: audit.technologies }, - pageDistributions: audit.pages.map((p) => ({ - name: p.name, - compliant: { - raw: results.filter( - (r) => - r.pageId === p.id && r.status === CriterionResultStatus.COMPLIANT - ).length, - percentage: - (results.filter( + pageDistributions: [audit.transverseElementsPage, ...audit.pages].map( + (p) => ({ + name: p.name, + compliant: { + raw: results.filter( (r) => r.pageId === p.id && r.status === CriterionResultStatus.COMPLIANT - ).length / - totalCriteriaCount) * - 100 - }, - notApplicable: { - raw: results.filter( - (r) => - r.pageId === p.id && - r.status === CriterionResultStatus.NOT_APPLICABLE - ).length, - percentage: - (results.filter( + ).length, + percentage: + (results.filter( + (r) => + r.pageId === p.id && + r.status === CriterionResultStatus.COMPLIANT + ).length / + totalCriteriaCount) * + 100 + }, + notApplicable: { + raw: results.filter( (r) => r.pageId === p.id && r.status === CriterionResultStatus.NOT_APPLICABLE - ).length / - totalCriteriaCount) * - 100 - }, - notCompliant: { - raw: results.filter( - (r) => - r.pageId === p.id && - r.status === CriterionResultStatus.NOT_COMPLIANT - ).length, - percentage: - (results.filter( + ).length, + percentage: + (results.filter( + (r) => + r.pageId === p.id && + r.status === CriterionResultStatus.NOT_APPLICABLE + ).length / + totalCriteriaCount) * + 100 + }, + notCompliant: { + raw: results.filter( (r) => r.pageId === p.id && r.status === CriterionResultStatus.NOT_COMPLIANT - ).length / - totalCriteriaCount) * - 100 - } - })), + ).length, + percentage: + (results.filter( + (r) => + r.pageId === p.id && + r.status === CriterionResultStatus.NOT_COMPLIANT + ).length / + totalCriteriaCount) * + 100 + } + }) + ), resultDistribution: { compliant: { @@ -1057,6 +1108,15 @@ export class AuditService { where: { editUniqueId: sourceUniqueId, isHidden: false }, include: { environments: true, + transverseElementsPage: { + include: { + results: { + include: { + exampleImages: true + } + } + } + }, pages: { include: { results: { @@ -1189,7 +1249,12 @@ export class AuditService { const newAudit = await this.prisma.audit.create({ data: { - ...omit(originalAudit, ["id", "auditTraceId", "sourceAuditId"]), + ...omit(originalAudit, [ + "id", + "auditTraceId", + "sourceAuditId", + "transverseElementsPageId" + ]), // link new audit with the original sourceAudit: { @@ -1215,6 +1280,25 @@ export class AuditService { } }, + transverseElementsPage: { + create: { + ...omit(originalAudit.transverseElementsPage, ["id"]), + results: { + create: originalAudit.transverseElementsPage.results.map((r) => ({ + ...omit(r, ["id", "pageId"]), + exampleImages: { + create: r.exampleImages.map( + (e) => + imagesCreateData[originalAudit.transverseElementsPage.id][ + r.id + ][e.id] + ) + } + })) + } + } + }, + pages: { create: originalAudit.pages.map((p) => ({ name: p.name, @@ -1274,16 +1358,25 @@ export class AuditService { editUniqueId: true, consultUniqueId: true, initiator: true, + transverseElementsPageId: true, pages: { select: { results: true } + }, + transverseElementsPage: { + select: { + results: true + } } } }); return audits.map((a) => { - const results = a.pages.flatMap((p) => p.results); + const results = [ + ...a.transverseElementsPage.results, + ...a.pages.flatMap((p) => p.results) + ]; const progress = results.filter((r) => r.status !== CriterionResultStatus.NOT_TESTED) @@ -1309,21 +1402,24 @@ export class AuditService { (c) => resultsGroupedById[`${c.topic}.${c.criterium}`] ?? null ); - const applicableCriteria = results2.filter( - (criteria) => - criteria && - criteria.some( - (c) => c.status !== CriterionResultStatus.NOT_APPLICABLE - ) + const applicableCriteria = results2.filter((criteria) => + criteria.some((c) => isCompliant(c) || isNotCompliant(c)) ); - const compliantCriteria = applicableCriteria.filter((criteria) => - criteria.every( + const compliantCriteria = applicableCriteria.filter((criteria) => { + // remove untested transverse criterion + const withoutUntestedTrans = criteria.filter( (c) => - c.status === CriterionResultStatus.COMPLIANT || - c.status === CriterionResultStatus.NOT_APPLICABLE - ) - ); + !(isTransverse(c, a.transverseElementsPageId) && isNotTested(c)) + ); + + return ( + withoutUntestedTrans.some((c) => isCompliant(c)) && + withoutUntestedTrans.every( + (c) => isCompliant(c) || isNotApplicable(c) + ) + ); + }); complianceLevel = Math.round( (compliantCriteria.length / applicableCriteria.length) * 100 diff --git a/confiture-rest-api/src/audits/audits.controller.ts b/confiture-rest-api/src/audits/audits.controller.ts index c373e8574..391737ae4 100644 --- a/confiture-rest-api/src/audits/audits.controller.ts +++ b/confiture-rest-api/src/audits/audits.controller.ts @@ -89,6 +89,7 @@ export class AuditsController { async getAudit(@Param("uniqueId") uniqueId: string) { const audit = await this.auditService.findAuditWithEditUniqueId(uniqueId, { environments: true, + transverseElementsPage: true, pages: true, sourceAudit: { select: { diff --git a/confiture-web-app/src/App.vue b/confiture-web-app/src/App.vue index 2ede7f2d2..9d870bbf5 100644 --- a/confiture-web-app/src/App.vue +++ b/confiture-web-app/src/App.vue @@ -15,13 +15,13 @@ useHead({ { name: "description", content: - "Ara est l’outil qui vous permet de réaliser, simplement et rapidement, des audits d'accessibilité numérique." + "Avec Ara, vous évaluez manuellement les 106 critères du RGAA, générez un rapport d’audit et une déclaration d’accessibilité" }, { name: "og:title", content: "Audit d’accessibilité numérique" }, { name: "og:description", content: - "Ara est l’outil qui vous permet de réaliser, simplement et rapidement, des audits d'accessibilité numérique." + "Avec Ara, vous évaluez manuellement les 106 critères du RGAA, générez un rapport d’audit et une déclaration d’accessibilité" }, { name: "og:url", content: "URL" }, { name: "og:image", content: "image" }, diff --git a/confiture-web-app/src/components/audit/AraTabs.vue b/confiture-web-app/src/components/audit/AraTabs.vue index c2d0a67e6..e58468a38 100644 --- a/confiture-web-app/src/components/audit/AraTabs.vue +++ b/confiture-web-app/src/components/audit/AraTabs.vue @@ -8,9 +8,10 @@ import { ref, watch } from "vue"; import { useUniqueId } from "../../composables/useUniqueId"; +import LayoutIcon from "../icons/LayoutIcon.vue"; const props = defineProps<{ - tabs: { label: string; data: T; icon?: string }[]; + tabs: { label: string; data: T }[]; stickyTop: string; }>(); @@ -74,18 +75,13 @@ watch(currentTab, (currentTab) => { :aria-controls="panelId(i)" :aria-selected="i === currentTab ? 'true' : 'false'" :tabindex="i === currentTab ? undefined : '-1'" - :class="{ - 'fr-tabs__tab--icon-left': !!tab.icon, - ...(!!tab.icon && { - [`fr-icon-${tab.icon}`]: !!tab.icon - }) - }" @click="currentTab = i" @keydown.right.down.prevent="selectNextTab" @keydown.left.up.prevent="selectPreviousTab" @keydown.home.prevent="selectFirstTab" @keydown.end.prevent="selectLastTab" > + {{ tab.label }} diff --git a/confiture-web-app/src/components/audit/AuditGenerationCriterium.vue b/confiture-web-app/src/components/audit/AuditGenerationCriterium.vue index 8add83da1..0d167eac4 100644 --- a/confiture-web-app/src/components/audit/AuditGenerationCriterium.vue +++ b/confiture-web-app/src/components/audit/AuditGenerationCriterium.vue @@ -20,11 +20,14 @@ import { handleFileDeleteError, handleFileUploadError } from "../../utils"; -import RadioGroup, { RadioColor } from "../ui/RadioGroup.vue"; +import MarkdownRenderer from "../ui/MarkdownRenderer.vue"; +import { RadioColor } from "../ui/Radio.vue"; +import RadioGroup from "../ui/RadioGroup.vue"; import CriteriumCompliantAccordion from "./CriteriumCompliantAccordion.vue"; import CriteriumNotApplicableAccordion from "./CriteriumNotApplicableAccordion.vue"; import CriteriumNotCompliantAccordion from "./CriteriumNotCompliantAccordion.vue"; import CriteriumTestsAccordion from "./CriteriumTestsAccordion.vue"; +import DeleteFileModal from "./DeleteFileModal.vue"; const store = useResultsStore(); const auditStore = useAuditStore(); @@ -40,23 +43,26 @@ const props = defineProps<{ const statuses: Array<{ label: string; + extraLabel?: string; value: CriteriumResultStatus; color?: RadioColor; }> = [ { label: formatStatus(CriteriumResultStatus.COMPLIANT), value: CriteriumResultStatus.COMPLIANT, - color: "green" + color: RadioColor.GREEN }, { label: formatStatus(CriteriumResultStatus.NOT_COMPLIANT), + extraLabel: + "Le focus se déplacera dans le champ « Erreur et recommandation »", value: CriteriumResultStatus.NOT_COMPLIANT, - color: "red" + color: RadioColor.RED }, { label: formatStatus(CriteriumResultStatus.NOT_APPLICABLE), value: CriteriumResultStatus.NOT_APPLICABLE, - color: "grey" + color: RadioColor.GREY } ]; @@ -69,6 +75,48 @@ const result = computed( )! ); +const transversePageId = computed(() => { + return auditStore.currentAudit?.transverseElementsPage.id; +}); + +const transverseStatus = computed((): CriteriumResultStatus | null => { + if (store.data && transversePageId.value) { + return store.data?.[transversePageId.value][props.topicNumber][ + props.criterium.number + ].status; + } + + return null; +}); + +const transverseComment = computed((): string | null => { + if (store.data && transversePageId.value) { + const result = + store.data?.[transversePageId.value][props.topicNumber][ + props.criterium.number + ]; + + switch (transverseStatus.value) { + case CriteriumResultStatus.COMPLIANT: + return result.compliantComment; + case CriteriumResultStatus.NOT_COMPLIANT: + return result.notCompliantComment; + case CriteriumResultStatus.NOT_APPLICABLE: + return result.notApplicableComment; + default: + return null; + } + } + + return null; +}); + +const showTransverseComment = ref(false); + +function toggleTransverseComment() { + showTransverseComment.value = !showTransverseComment.value; +} + const notify = useNotifications(); const errorMessage: Ref = ref(null); @@ -95,14 +143,24 @@ function handleUploadExample(file: File) { }); } -function handleDeleteExample(image: AuditFile) { +const deleteFileModalRef = ref>(); +const fileToDelete = ref(); + +function openDeleteFileModal(image: AuditFile) { + deleteFileModalRef.value?.show(); + fileToDelete.value = image; +} + +function handleDeleteExample() { + if (!fileToDelete.value) return; + store .deleteExampleImage( props.auditUniqueId, props.page.id, props.topicNumber, props.criterium.number, - image.id + fileToDelete.value.id ) .then(() => { errorMessage.value = null; @@ -112,6 +170,7 @@ function handleDeleteExample(image: AuditFile) { }) .finally(() => { criteriumNotCompliantAccordion.value?.onFileRequestFinished(); + deleteFileModalRef.value?.hide(); }); } @@ -128,6 +187,10 @@ function updateResultStatus(status: CriteriumResultStatus) { store .updateResults(props.auditUniqueId, [{ ...result.value, status }]) .then(() => { + if (status === CriteriumResultStatus.NOT_COMPLIANT) { + criteriumNotCompliantAccordion.value?.disclose(); + } + if ( store.everyCriteriumAreTested && !auditStore.currentAudit?.publicationDate @@ -166,13 +229,6 @@ function updateResultImpact(userImpact: CriterionResultUserImpact | null) { .catch(handleUpdateResultError); } -function updateTransverseStatus(e: Event) { - const transverse = (e.target as HTMLInputElement).checked; - store - .updateResults(props.auditUniqueId, [{ ...result.value, transverse }]) - .catch(handleUpdateResultError); -} - function updateQuickWin(quickWin: boolean) { store .updateResults(props.auditUniqueId, [{ ...result.value, quickWin }]) @@ -200,14 +256,7 @@ const isOffline = useIsOffline(); -
+
+
+ + +
+
+
@@ -269,10 +347,17 @@ const isOffline = useIsOffline(); @update:comment="updateResultComment($event, 'notCompliantComment')" @update:user-impact="updateResultImpact($event)" @upload-file="handleUploadExample" - @delete-file="handleDeleteExample" + @delete-file="openDeleteFileModal" @update:quick-win="updateQuickWin" /> + + -import { computed } from "vue"; +import { computed, ref } from "vue"; -import { useFiltersStore } from "../../store"; +import { useAuditStore, useFiltersStore } from "../../store"; import { AuditPage } from "../../types"; import TopLink from "../ui/TopLink.vue"; import AuditGenerationCriterium from "./AuditGenerationCriterium.vue"; @@ -13,6 +13,11 @@ defineProps<{ }>(); const store = useFiltersStore(); +const auditStore = useAuditStore(); + +const transversePageId = computed(() => { + return auditStore.currentAudit?.transverseElementsPage.id; +}); const noResults = computed(() => { if (store.hasNoResultsFromEvaluated) { @@ -41,11 +46,42 @@ const noResults = computed(() => { }; } }); + +// TODO: remove this alert in 3 months (16/10/2024) +const topicNameRefs = ref(); + +async function hideTransverseAlert() { + showTransverseAlert.value = false; + localStorage.setItem("ara:hide-transverse-alert", "true"); + topicNameRefs.value?.[0].focus(); +} + +const showTransverseAlert = ref( + localStorage.getItem("ara:hide-transverse-alert") !== "true" +); diff --git a/confiture-web-app/src/components/report/getReportErrors.ts b/confiture-web-app/src/components/report/getReportErrors.ts index fcdd2a71f..31cc50f73 100644 --- a/confiture-web-app/src/components/report/getReportErrors.ts +++ b/confiture-web-app/src/components/report/getReportErrors.ts @@ -1,4 +1,4 @@ -import { groupBy, mapValues, sortBy, uniqWith } from "lodash-es"; +import { groupBy, mapValues, sortBy } from "lodash-es"; import rgaa from "../../criteres.json"; import { ReportStoreState } from "../../store"; @@ -9,7 +9,7 @@ import { ReportCriteriumResult } from "../../types"; -export type ReportErrors = { +export type ReportError = { id: number; name?: string; order: number; @@ -25,7 +25,7 @@ export function getReportErrors( report: ReportStoreState, quickWinFilter: boolean, userImpactFilters: Array -): ReportErrors[] { +): ReportError[] { const resultsGroupedByPage = { // include pages with no errors ...report.data?.context.samples.reduce>((acc, val) => { @@ -38,7 +38,6 @@ export function getReportErrors( .filter((r) => { return ( r.status === CriteriumResultStatus.NOT_COMPLIANT && - !r.transverse && userImpactFilters.includes(r.userImpact) ); }) @@ -62,10 +61,7 @@ export function getReportErrors( return { topic: Number(topicNumber), name: getTopicName(Number(topicNumber)), - errors: sortBy( - results.filter((r) => !r.transverse), - "criterium" - ) + errors: sortBy(results, "criterium") }; }) ), @@ -81,42 +77,6 @@ function getPage(report: ReportStoreState, pageId: number | string) { return report.data!.context.samples.find((p) => p.id === Number(pageId))!; } -export type ReportTransverseError = { - topic: number; - name?: string; - errors: ReportCriteriumResult[]; -}; - -export function getReportTransverseErrors( - report: ReportStoreState, - userImpactFilters: Array -): ReportTransverseError[] { - return Object.values( - mapValues( - groupBy( - uniqWith( - report.data?.results.filter((r) => { - return ( - r.transverse && - r.status === CriteriumResultStatus.NOT_COMPLIANT && - userImpactFilters.includes(r.userImpact) - ); - }), - (a, b) => a.criterium === b.criterium && a.topic === b.topic - ), - "topic" - ), - (results, topicNumber) => { - return { - topic: Number(topicNumber), - name: getTopicName(Number(topicNumber)), - errors: results - }; - } - ) - ); -} - function getTopicName(topicNumber: number) { return rgaa.topics.find((t) => t.number === topicNumber)?.topic; } diff --git a/confiture-web-app/src/components/report/getReportImprovements.ts b/confiture-web-app/src/components/report/getReportImprovements.ts index 7c38eb427..93717096d 100644 --- a/confiture-web-app/src/components/report/getReportImprovements.ts +++ b/confiture-web-app/src/components/report/getReportImprovements.ts @@ -1,8 +1,12 @@ -import { groupBy, mapValues, uniqWith } from "lodash-es"; +import { groupBy, mapValues, sortBy } from "lodash-es"; import rgaa from "../../criteres.json"; import { ReportStoreState } from "../../store"; -import { CriteriumResultStatus, ReportCriteriumResult } from "../../types"; +import { + AuditReport, + CriteriumResultStatus, + ReportCriteriumResult +} from "../../types"; export type ReportImprovement = { id: number; @@ -21,45 +25,67 @@ export type ReportImprovement = { }; export function getReportImprovements( - reportData: ReportStoreState + report: ReportStoreState ): ReportImprovement[] { - return ( - reportData.data?.context.samples - .map((pageSample) => { - return { - ...pageSample, - topics: Object.entries( - groupBy( - reportData.data?.results.filter(resultIsFromPage(pageSample.id)), - "topic" - ) - ) - .map(([topic, results]) => { + const resultsGroupedByPage = { + // include pages with no errors + ...report.data?.context.samples.reduce>((acc, val) => { + acc[val.id] = []; + return acc; + }, {}), + + ...groupBy( + report.data?.results.filter((r) => { + return ( + (r.status === CriteriumResultStatus.COMPLIANT && + r.compliantComment) || + (r.status === CriteriumResultStatus.NOT_APPLICABLE && + r.notApplicableComment) + ); + }), + "pageId" + ) + } as Record; + + return sortBy( + Object.entries(resultsGroupedByPage).map(([pageId, results]) => { + return { + id: Number(pageId), + order: getPage(report, pageId).order, + name: getPage(report, pageId).name, + url: getPage(report, pageId).url, + topics: sortBy( + Object.values( + mapValues(groupBy(results, "topic"), (results, topicNumber) => { return { - number: Number(topic), - name: getTopicName(Number(topic)), - improvements: results - .filter(hasImprovement) - .map(getImprovementObject) + number: Number(topicNumber), + name: getTopicName(Number(topicNumber)), + improvements: sortBy( + results.filter(hasImprovement).map(getImprovementObject), + "criterium" + ) }; }) - .filter(hasOneOrMoreImprovements) - }; - }) - .filter(hasOneOrMoreTopics) || [] + ), + "topic" + ) + }; + }), + (el) => el.order ); } -const hasOneOrMoreTopics = (p: { topics: unknown[] }) => p.topics.length > 0; +function getPage(report: ReportStoreState, pageId: number | string) { + return report.data!.context.samples.find((p) => p.id === Number(pageId))!; +} -const hasOneOrMoreImprovements = (t: { improvements: unknown[] }) => - t.improvements.length > 0; +function getTopicName(topicNumber: number) { + return rgaa.topics.find((t) => t.number === topicNumber)?.topic; +} const hasImprovement = (r: ReportCriteriumResult) => - !r.transverse && - ((r.status === CriteriumResultStatus.COMPLIANT && r.compliantComment) || - (r.status === CriteriumResultStatus.NOT_APPLICABLE && - r.notApplicableComment)); + (r.status === CriteriumResultStatus.COMPLIANT && r.compliantComment) || + (r.status === CriteriumResultStatus.NOT_APPLICABLE && r.notApplicableComment); const getImprovementObject = (r: ReportCriteriumResult) => { return { @@ -68,62 +94,3 @@ const getImprovementObject = (r: ReportCriteriumResult) => { comment: r.compliantComment || r.notApplicableComment }; }; - -const resultIsFromPage = (pageId: number) => (result: ReportCriteriumResult) => - result.pageId === pageId; - -function getTopicName(topicNumber: number) { - return rgaa.topics.find((t) => t.number === topicNumber)?.topic; -} - -export type ReportTransverseImprovement = { - number: number; - name?: string; - improvements: { - topic: number; - criterium: number; - status: CriteriumResultStatus; - comment: string | null; - }[]; -}; - -export function getReportTransverseImprovements( - reportData: ReportStoreState -): ReportTransverseImprovement[] { - return Object.values( - mapValues( - groupBy( - uniqWith( - reportData.data?.results.filter((r) => { - return ( - r.transverse && - [ - CriteriumResultStatus.COMPLIANT, - CriteriumResultStatus.NOT_APPLICABLE - ].includes(r.status) - ); - }), - (a, b) => a.criterium === b.criterium && a.topic === b.topic - ), - "topic" - ), - (results, topicNumber) => { - return { - number: Number(topicNumber), - name: getTopicName(Number(topicNumber)), - improvements: results.map((r) => { - return { - topic: r.topic, - criterium: r.criterium, - status: r.status, - comment: - r.status === CriteriumResultStatus.COMPLIANT - ? r.compliantComment - : r.notApplicableComment - }; - }) - }; - } - ) - ); -} diff --git a/confiture-web-app/src/components/ui/DsfrField.vue b/confiture-web-app/src/components/ui/DsfrField.vue index 755fe0f01..53e0da3ef 100644 --- a/confiture-web-app/src/components/ui/DsfrField.vue +++ b/confiture-web-app/src/components/ui/DsfrField.vue @@ -11,6 +11,7 @@ const props = defineProps<{ title?: string; error?: string; id: string; + autocomplete?: string; }>(); defineEmits<{ @@ -43,6 +44,7 @@ defineExpose({ inputRef }); :required="required" :pattern="pattern ? pattern.toString().slice(1, -1) : undefined" :title="title" + :autocomplete="autocomplete" :value="modelValue" @input=" $emit('update:modelValue', ($event.target as HTMLInputElement).value) diff --git a/confiture-web-app/src/components/ui/Radio.vue b/confiture-web-app/src/components/ui/Radio.vue index a166ee64e..675025715 100644 --- a/confiture-web-app/src/components/ui/Radio.vue +++ b/confiture-web-app/src/components/ui/Radio.vue @@ -1,5 +1,10 @@ - diff --git a/confiture-web-app/src/pages/report/ContextPage.vue b/confiture-web-app/src/pages/report/ContextPage.vue index 478b17c19..d1287b98c 100644 --- a/confiture-web-app/src/pages/report/ContextPage.vue +++ b/confiture-web-app/src/pages/report/ContextPage.vue @@ -139,7 +139,7 @@ useWrappedFetch(() => report.fetchReport(uniqueId));

L’audit a porté sur un échantillon de - {{ report.data.context.samples.length }} pages : + {{ report.data.context.samples.length - 1 }} pages :

@@ -158,7 +158,10 @@ useWrappedFetch(() => report.fetchReport(uniqueId)); - + {{ i + 1 }} {{ page.name }} diff --git a/confiture-web-app/src/pages/report/ReportPage.vue b/confiture-web-app/src/pages/report/ReportPage.vue index dd7a4255c..493b75dfc 100644 --- a/confiture-web-app/src/pages/report/ReportPage.vue +++ b/confiture-web-app/src/pages/report/ReportPage.vue @@ -41,7 +41,7 @@ const tabs = computed(() => [ ...(hasNotes.value ? [{ title: "Notes", component: ReportNotes }] : []), { title: "Détails des non-conformités", component: ReportErrors }, ...(hasCompliantOrNotApplicableComments.value - ? [{ title: "Points d’améliorations", component: ReportImprovements }] + ? [{ title: "Points d’amélioration", component: ReportImprovements }] : []) ]); @@ -88,7 +88,7 @@ const targetTabIndex = computed(() => { }); const router = useRouter(); -function handleTabChange(tab: { title: string }) { +function handleTabChange(tabTitle: string) { // change the URL in the browser adress bar without triggering vue-router navigation history.pushState( {}, @@ -97,12 +97,12 @@ function handleTabChange(tab: { title: string }) { name: "report", params: { uniqueId, - tab: slugify(tab.title) + tab: slugify(tabTitle) } }).fullPath ); - targetTab.value = slugify(tab.title); + targetTab.value = slugify(tabTitle); } const csvExportUrl = computed(() => `/api/reports/${uniqueId}/exports/csv`); @@ -122,7 +122,7 @@ const siteUrl = computed(() => { if (report.data) { return ( report.data.procedureUrl || - new URL(report.data.context.samples[0].url).origin + new URL(report.data.context.samples[1].url).origin ); } @@ -286,9 +286,10 @@ const siteUrl = computed(() => { role="tabpanel" :aria-labelledby="`tabpanel-${slugify(tab.title)}`" tabindex="0" - v-on="{ 'dsfr.disclose': () => handleTabChange(tab) }" + v-on="{ 'dsfr.disclose': () => handleTabChange(tab.title) }" > - + +
diff --git a/confiture-web-app/src/store/results.ts b/confiture-web-app/src/store/results.ts index 9f747580d..b52a4e7d9 100644 --- a/confiture-web-app/src/store/results.ts +++ b/confiture-web-app/src/store/results.ts @@ -116,13 +116,17 @@ export const useResultsStore = defineStore("results", { }, /** - * @returns True when every criterium in the audit have been tested (status is different from NOT_TESTED) + * @returns True when every criterium (transverse excluded) in the audit have been tested (status is different from NOT_TESTED) */ everyCriteriumAreTested(): boolean { + const auditStore = useAuditStore(); + const transversePageId = + auditStore.currentAudit?.transverseElementsPage.id; + return ( - !this.allResults?.some( - (r) => r.status === CriteriumResultStatus.NOT_TESTED - ) ?? false + !this.allResults + ?.filter((r) => r.pageId !== transversePageId) + .some((r) => r.status === CriteriumResultStatus.NOT_TESTED) ?? false ); }, @@ -145,6 +149,7 @@ export const useResultsStore = defineStore("results", { /** * Ratio of tested criteria over total number of criteria. + * Transverse criteria are excluded. * * `0.5` means half of the audit criteria have been tested. */ @@ -152,9 +157,15 @@ export const useResultsStore = defineStore("results", { if (!this.data) { return 0; } + + const auditStore = useAuditStore(); + const transversePageId = + auditStore.currentAudit?.transverseElementsPage.id; + const r = Object.values(this.data) .flatMap(Object.values) - .flatMap(Object.values) as CriteriumResult[]; + .flatMap(Object.values) + .filter((cr) => cr.pageId !== transversePageId) as CriteriumResult[]; const total = r.length; @@ -212,37 +223,6 @@ export const useResultsStore = defineStore("results", { // Update UI immediately, rollbacks later if update fails. this.data[update.pageId][update.topic][update.criterium] = update; - - // Apply `transverse` result update to every pages - if (update.transverse) { - Object.keys(this.data) - .map(Number) // this.data requires a number index - .filter((pageId) => pageId !== update.pageId) // Ignore current page - .forEach((pageId) => { - if (!this.data) { - return; - } - - const target = this.data[pageId][update.topic][update.criterium]; - - target.status = update.status; - target.transverse = true; - - if (update.status === CriteriumResultStatus.COMPLIANT) { - target.compliantComment = update.compliantComment; - } - - if (update.status === CriteriumResultStatus.NOT_COMPLIANT) { - target.notCompliantComment = update.notCompliantComment; - target.userImpact = update.userImpact; - target.quickWin = update.quickWin; - } - - if (update.status === CriteriumResultStatus.NOT_APPLICABLE) { - target.notApplicableComment = update.notApplicableComment; - } - }); - } }); // update the edition date of the local audit. It will not be the same diff --git a/confiture-web-app/src/types/types.ts b/confiture-web-app/src/types/types.ts index a3ce735dd..003e711d9 100644 --- a/confiture-web-app/src/types/types.ts +++ b/confiture-web-app/src/types/types.ts @@ -44,6 +44,7 @@ export interface Audit { // Audit creation auditType: AuditType; procedureName: string; + transverseElementsPage: AuditPage; pages: AuditPage[]; auditorEmail: string; auditorName: string | null; diff --git a/confiture-web-app/src/utils.ts b/confiture-web-app/src/utils.ts index 00c4f7e67..31c3bdcbc 100644 --- a/confiture-web-app/src/utils.ts +++ b/confiture-web-app/src/utils.ts @@ -79,15 +79,20 @@ export function getCriteriaCount(auditType: AuditType): number { /** * Return the audit status based on: - * - the number of results (criteria count * number of pages) + * - the number of results excluding transverse (criteria count * number of pages) * - the status of each criteria * - the completion of a11y statement */ export function getAuditStatus(report: AuditReport): string { + const transversePageId = report.context.samples[0].id; + if ( - report.results.length !== - getCriteriaCount(report.auditType) * report.pageDistributions.length || - report?.results.some((r) => r.status === CriteriumResultStatus.NOT_TESTED) + report.results.filter((r) => r.pageId !== transversePageId).length !== + getCriteriaCount(report.auditType) * + (report.context.samples.length - 1) || + report?.results + .filter((r) => r.pageId !== transversePageId) + .some((r) => r.status === CriteriumResultStatus.NOT_TESTED) ) { return AuditStatus.IN_PROGRESS; } diff --git a/publiccode.yml b/publiccode.yml new file mode 100644 index 000000000..db271eb7f --- /dev/null +++ b/publiccode.yml @@ -0,0 +1,30 @@ +publiccodeYmlVersion: "0.2" +name: Ara +url: https://github.com/DISIC/Ara +landingURL: https://ara.numerique.gouv.fr/ +creationDate: 2022-07-11 +latestRelease: + date: "" + version: "" +logo: https://github.com/DISIC.png +usedBy: [] +roadmap: "https://ara.numerique.gouv.fr/feuille-de-route" +softwareType: "" +description: + fr: + shortDescription: Faire des audits RGAA, les rendre lisibles et suivre + l’amélioration de leur taux de conformité + documentation: "" +legal: + license: MIT + authorsFile: "" +maintenance: + type: community + contacts: + - name: "" + email: "" +fundedBy: + - name: Direction interministérielle du numérique + url: https://www.numerique.gouv.fr/dinum/ + - name: FIPHFP + url: https://www.fiphfp.fr/