diff --git a/docs/yarn.lock b/docs/yarn.lock index be9a7f7c..a542cf2a 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -1344,6 +1344,11 @@ picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + pkg-types@^1.0.3, pkg-types@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.2.0.tgz#d0268e894e93acff11a6279de147e83354ebd42d" @@ -1366,7 +1371,7 @@ points-on-path@^0.2.1: path-data-parser "0.1.0" points-on-curve "0.2.0" -postcss@^8.4.40, postcss@^8.4.41: +postcss@^8.4.40: version "8.4.44" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.44.tgz#d56834ef6508610ba224bb22b2457b2169ed0480" integrity sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw== @@ -1375,6 +1380,15 @@ postcss@^8.4.40, postcss@^8.4.41: picocolors "^1.0.1" source-map-js "^1.2.0" +postcss@^8.4.43: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + preact@^10.0.0: version "10.23.2" resolved "https://registry.yarnpkg.com/preact/-/preact-10.23.2.tgz#52deec92796ae0f0cc6b034d9c66e0fbc1b837dc" @@ -1449,6 +1463,11 @@ source-map-js@^1.2.0: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + speakingurl@^14.0.1: version "14.0.1" resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" @@ -1497,12 +1516,12 @@ uuid@^9.0.1: integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== vite@^5.4.1: - version "5.4.2" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.2.tgz#8acb6ec4bfab823cdfc1cb2d6c53ed311bc4e47e" - integrity sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA== + version "5.4.11" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" + integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q== dependencies: esbuild "^0.21.3" - postcss "^8.4.41" + postcss "^8.4.43" rollup "^4.20.0" optionalDependencies: fsevents "~2.3.3" diff --git a/package.json b/package.json index f291da44..a712c9e1 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@vitejs/plugin-react-swc": "^3.3", "acorn": "^8.10", "blockly": "^11.1", + "client-zip": "^2.4", "clsx": "^2.1", "commander": "^12.0", "date-fns": "^3.0", diff --git a/src/cli/firebase/import.ts b/src/cli/firebase/import.ts index 38de0c35..237411b9 100644 --- a/src/cli/firebase/import.ts +++ b/src/cli/firebase/import.ts @@ -225,6 +225,7 @@ async function importUsers(users: User[], customClaims: object, options: ImportO let user = await auth.getUserByEmail(record.email).catch(noop); if (!user) { user = await auth.createUser({ + uid: record.id, email: record.email, emailVerified: true, password: record.password, @@ -305,6 +306,7 @@ async function importVariantMappings(db: Firestore, options: ImportOptions) { } const userSchema = z.object({ + id: z.string().optional(), name: z.string(), email: z.string().email(), password: z.string(), diff --git a/src/web/firebase/teacher-login.tsx b/src/web/firebase/teacher-login.tsx index b7f7a90b..b5fc4380 100644 --- a/src/web/firebase/teacher-login.tsx +++ b/src/web/firebase/teacher-login.tsx @@ -152,14 +152,23 @@ async function setParticipation( } } -function getPdfStatements(db: Firestore, statementVersion: number, variantIds: string[]) { +async function getPdfStatements( + db: Firestore, + statementVersion: number, + variantIds: string[], +): Promise> { const storage = getStorage(db.app); - return Promise.all( - variantIds.map((id) => - getBytes(ref(storage, `statements/${id}/statement-${statementVersion}.pdf`)), + const files = await Promise.all( + variantIds.map( + async (id) => + [ + id, + await getBytes(ref(storage, `statements/${id}/statement-${statementVersion}.pdf`)), + ] as const, ), ); + return Object.fromEntries(files); } function useStudents(participationId: string) { diff --git a/src/web/teacher/dashboard.tsx b/src/web/teacher/dashboard.tsx index 1d627e1c..0c5ed783 100644 --- a/src/web/teacher/dashboard.tsx +++ b/src/web/teacher/dashboard.tsx @@ -12,6 +12,7 @@ import { SubmitButton, WithinTimeRange, } from "@olinfo/react-components"; +import { downloadZip } from "client-zip"; import { addMinutes, addSeconds, isSameDay, roundToNearestMinutes, subMinutes } from "date-fns"; import { saveAs } from "file-saver"; import { range } from "lodash-es"; @@ -171,6 +172,7 @@ export default function TeacherDashboard() { {contest.hasPdf && ( + )} @@ -234,7 +236,7 @@ function DownloadPdfButton() { const { PDFDocument } = await import("@cantoo/pdf-lib"); const pdf = await PDFDocument.create(); - for (const statement of statements) { + for (const statement of Object.values(statements)) { const otherPdf = await PDFDocument.load(statement); const toCopy = range(otherPdf.getPages().length); const pages = await pdf.copyPages(otherPdf, toCopy); @@ -257,7 +259,28 @@ function DownloadPdfButton() { return ( + ); +} + +function DownloadZipButton() { + const { participation, getPdfStatements } = useTeacher(); + + const onClick = async () => { + const statements = await getPdfStatements(); + + const files = Object.entries(statements).map( + ([name, data]) => new File([data], `${name}.pdf`, { type: "application/pdf" }), + ); + const zip = await downloadZip(files).blob(); + + saveAs(zip, `${participation.id}.zip`); + }; + + return ( + ); } diff --git a/src/web/teacher/provider.tsx b/src/web/teacher/provider.tsx index c8bb1ffc..d35f2c91 100644 --- a/src/web/teacher/provider.tsx +++ b/src/web/teacher/provider.tsx @@ -26,7 +26,7 @@ type TeacherContextProps = { /** Funzione per effettuare il logout */ logout: () => Promise; /** Funzione per ottenere i pdf dei testi */ - getPdfStatements: () => Promise<(Uint8Array | ArrayBuffer)[]>; + getPdfStatements: () => Promise>; /** Hook per ottenere gli studenti di una scuola */ useStudents: ( participationId: string, @@ -54,7 +54,7 @@ type TeacherProviderProps = { getPdfStatements: ( statementVersion: number, variantIds: string[], - ) => Promise<(Uint8Array | ArrayBuffer)[]>; + ) => Promise>; useStudents: ( participationId: string, ) => readonly [Student[], (student: Student) => Promise]; diff --git a/src/web/teacher/table.tsx b/src/web/teacher/table.tsx index 5abe44b9..ee7e6592 100644 --- a/src/web/teacher/table.tsx +++ b/src/web/teacher/table.tsx @@ -113,13 +113,13 @@ const FinalizeModal = forwardRef(function FinalizeModal( // Generate a list of string that can uniquely identify a student. Multiple // strings are generated to prevent possible errors during data entry. function normalize(student: Student) { - const info = student.userData!; + const info = student.userData; const orderings = [ ["name", "surname", "classYear", "classSection"], ["surname", "name", "classYear", "classSection"], ]; return orderings.map((fields) => { - return deburr(fields.map((field) => info[field]).join("\n")) + return deburr(fields.map((field) => info?.[field] ?? "").join("\n")) .toLowerCase() .replaceAll(/[^\w\n]/g, ""); }); @@ -247,7 +247,7 @@ const DeleteModal = forwardRef(function DeleteModal( Seleziona tutti” come filtro.

- +
close("0")}>Annulla Continua @@ -382,7 +382,7 @@ function Table() { singleClickEdit={true} suppressClickEdit={frozen} readOnlyEdit={true} - rowSelection="single" + rowSelection={{ mode: "singleRow" }} onCellEditRequest={onCellEditRequest} enableBrowserTooltips={true} localeText={AG_GRID_LOCALE_IT} diff --git a/yarn.lock b/yarn.lock index be9f29d0..2f1e57d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1860,6 +1860,11 @@ client-only@^0.0.1: resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +client-zip@^2.4: + version "2.4.6" + resolved "https://registry.yarnpkg.com/client-zip/-/client-zip-2.4.6.tgz#c797c29d9463b17eca4b623339ccf06e620b2794" + integrity sha512-e7t1u14h/yT0A12qBwFsaus8UZZ8+MCaNAEn/z53mrukLq/LFcKX7TkbntAppGu8he2p8pz9vc5NEGE/h4ohlw== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"