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 <[email protected]>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user