feat: export marks as csv or pdf (#427)

* worked on the marks csv export

* added translations

* added pdf export option and translations

* cleanup

* cleanup

* improve download UX

* capitalize title

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Em_Dev
2024-08-10 08:49:42 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 1c41450b79
commit d2fe33c6ab
11 changed files with 586 additions and 196 deletions
+3 -1
View File
@@ -46,11 +46,13 @@
"form-data": "^4.0.0",
"hotkeys-js": "^3.11.2",
"html-to-image": "^1.11.11",
"html2pdf.js": "^0.10.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"openai-edge": "^1.2.2",
"papaparse": "^5.4.1",
"pluralize": "^8.0.0",
"posthog-js": "^1.141.4",
"posthog-js": "^1.155.0",
"shared": "workspace:*",
"sirv": "^2.0.3",
"stripe": "^14.22.0",
@@ -475,7 +475,11 @@
"title": "Markierungen",
"student": "Student",
"total": "Gesamt",
"no_student": "Kein Student hinzugefügt"
"no_student": "Kein Student hinzugefügt",
"export": {
"csv": "Als CSV exportieren",
"pdf": "Als PDF exportieren"
}
},
"submissions": {
"title": "Eingereichte Übungen",
@@ -801,6 +801,10 @@
"title": "Marks",
"student": "Student",
"total": "Total",
"export": {
"csv": "Export as CSV",
"pdf": "Export as PDF"
},
"no_student": "No Student Added"
},
@@ -475,7 +475,11 @@
"title": "Marcas",
"student": "Alumno",
"total": "Total",
"no_student": "No se agregó ningún estudiante"
"no_student": "No se agregó ningún estudiante",
"export": {
"csv": "Exportar como CSV",
"pdf": "Exportar como PDF"
}
},
"submissions": {
"title": "Ejercicios enviados",
@@ -475,7 +475,11 @@
"title": "Des marques",
"student": "Étudiant",
"total": "Total",
"no_student": "Aucun étudiant ajouté"
"no_student": "Aucun étudiant ajouté",
"export": {
"csv": "Exporter au format CSV",
"pdf": "Exporter au format PDF"
}
},
"submissions": {
"title": "Exercices soumis",
@@ -475,7 +475,11 @@
"title": "निशान",
"student": "विद्यार्थी",
"total": "कुल",
"no_student": "कोई छात्र नहीं जोड़ा गया"
"no_student": "कोई छात्र नहीं जोड़ा गया",
"export": {
"csv": "सीएसवी के रूप में निर्यात करें",
"pdf": "पीडीएफ के रूप में निर्यात करें"
}
},
"submissions": {
"title": "प्रस्तुत अभ्यास",
@@ -475,7 +475,11 @@
"title": "Marcas",
"student": "Estudante",
"total": "Total",
"no_student": "Nenhum aluno adicionado"
"no_student": "Nenhum aluno adicionado",
"export": {
"csv": "Exportar como CSV",
"pdf": "Exportar como PDF"
}
},
"submissions": {
"title": "Exercícios enviados",
@@ -475,7 +475,11 @@
"title": "Метки",
"student": "Студент",
"total": "Общий",
"no_student": "Студент не добавлен"
"no_student": "Студент не добавлен",
"export": {
"csv": "Экспортировать в формате CSV",
"pdf": "Экспортировать в PDF"
}
},
"submissions": {
"title": "Отправленные упражнения",
@@ -475,7 +475,11 @@
"title": "Điểm",
"student": "Học sinh",
"total": "Tổng cộng",
"no_student": "Không có sinh viên nào được thêm vào"
"no_student": "Không có sinh viên nào được thêm vào",
"export": {
"csv": "Xuất dưới dạng CSV",
"pdf": "Xuất dưới dạng PDF"
}
},
"submissions": {
"title": "Bài tập đã gửi",
@@ -1,6 +1,10 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte';
import Papa from 'papaparse';
import html2pdf from 'html2pdf.js';
import { OverflowMenu, OverflowMenuItem, Loading } from 'carbon-components-svelte';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
@@ -27,9 +31,9 @@
let borderBottomGrey = 'border-r-0 border-t-0 border-b border-l-0 border-gray-300';
let borderleftGrey = 'border-r-0 border-t-0 border-b-0 border-l border-gray-300';
let students: GroupPerson[] = [];
let lessonMapping = {}; // { lessonId: { exerciseId: exerciseTitle, ... }, ... }
let studentMarksByExerciseId = {}; // { groupMemberId: { exerciseId: `total_gotten/points`, ... }, ... }
let isDownloading = false;
function calculateStudentTotal(studentExerciseData) {
if (!studentExerciseData) return 0;
@@ -95,6 +99,73 @@
return roles;
}
const downloadCSV = () => {
isDownloading = true;
const exportData = students.map((student) => {
const rowData = { name: student.profile.fullname, Total: 0 };
const totalPoints = calculateStudentTotal(studentMarksByExerciseId[student.id]);
$lessons.forEach((lesson, lessonIndex) => {
const quizzes = lessonMapping[lesson.id] || {};
const quizMark = studentMarksByExerciseId[student.id] || {};
Object.keys(quizzes).forEach((quizId) => {
const quiz = quizzes[quizId];
const key = `L${lessonIndex + 1} - ${quiz.title} (${lesson.title})`;
rowData[key] = quizMark[quizId] || '-';
});
});
rowData.total = totalPoints;
return rowData;
});
const csv = Papa.unparse(exportData);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `${$course?.title}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
isDownloading = false;
};
const downloadPDF = () => {
isDownloading = true;
const element = document.getElementById('tableContainer');
if (!element) {
console.error('Table container not found');
return;
}
const options = {
margin: 0.5,
filename: `${$course?.title}-marks.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' }
};
// Start the PDF generation
html2pdf()
.from(element)
.set(options)
.save()
.then(() => {
isDownloading = false;
})
.catch((error) => {
console.error('Error generating PDF:', error);
isDownloading = false;
});
};
$: students = $globalStore.isStudent
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
@@ -109,10 +180,27 @@
goto(`/courses/${data.courseId}/lessons?next=true`);
}}
>
<PageNav title={$t('course.navItem.marks.title')} />
<PageNav title={$t('course.navItem.marks.title')}>
<slot:fragment slot="widget">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<OverflowMenu flipped style="background: #e5e5e5; border-radius: 50px">
<div slot="menu" style="">
{#if isDownloading}
<Loading withOverlay={false} small />
{:else}
<Download />
{/if}
</div>
<OverflowMenuItem text={$t('course.navItem.marks.export.csv')} on:click={downloadCSV} />
<OverflowMenuItem text={$t('course.navItem.marks.export.pdf')} on:click={downloadPDF} />
</OverflowMenu>
</RoleBasedSecurity>
</slot:fragment>
</PageNav>
<PageBody width="w-full max-w-6xl md:w-11/12">
<div class="table rounded-md border border-gray-300 w-full">
<div id="tableContainer" class="table rounded-md border border-gray-300 w-full">
<div class="flex items-center {borderBottomGrey}">
<div class="box flex items-center p-3">
<p class="dark:text-white w-40">{$t('course.navItem.marks.student')}</p>
+453 -185
View File
File diff suppressed because it is too large Load Diff