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:
@@ -46,11 +46,13 @@
|
|||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"hotkeys-js": "^3.11.2",
|
"hotkeys-js": "^3.11.2",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
|
"html2pdf.js": "^0.10.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"openai-edge": "^1.2.2",
|
"openai-edge": "^1.2.2",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"posthog-js": "^1.141.4",
|
"posthog-js": "^1.155.0",
|
||||||
"shared": "workspace:*",
|
"shared": "workspace:*",
|
||||||
"sirv": "^2.0.3",
|
"sirv": "^2.0.3",
|
||||||
"stripe": "^14.22.0",
|
"stripe": "^14.22.0",
|
||||||
|
|||||||
@@ -475,7 +475,11 @@
|
|||||||
"title": "Markierungen",
|
"title": "Markierungen",
|
||||||
"student": "Student",
|
"student": "Student",
|
||||||
"total": "Gesamt",
|
"total": "Gesamt",
|
||||||
"no_student": "Kein Student hinzugefügt"
|
"no_student": "Kein Student hinzugefügt",
|
||||||
|
"export": {
|
||||||
|
"csv": "Als CSV exportieren",
|
||||||
|
"pdf": "Als PDF exportieren"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"submissions": {
|
"submissions": {
|
||||||
"title": "Eingereichte Übungen",
|
"title": "Eingereichte Übungen",
|
||||||
|
|||||||
@@ -801,6 +801,10 @@
|
|||||||
"title": "Marks",
|
"title": "Marks",
|
||||||
"student": "Student",
|
"student": "Student",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"export": {
|
||||||
|
"csv": "Export as CSV",
|
||||||
|
"pdf": "Export as PDF"
|
||||||
|
},
|
||||||
"no_student": "No Student Added"
|
"no_student": "No Student Added"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -475,7 +475,11 @@
|
|||||||
"title": "Marcas",
|
"title": "Marcas",
|
||||||
"student": "Alumno",
|
"student": "Alumno",
|
||||||
"total": "Total",
|
"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": {
|
"submissions": {
|
||||||
"title": "Ejercicios enviados",
|
"title": "Ejercicios enviados",
|
||||||
|
|||||||
@@ -475,7 +475,11 @@
|
|||||||
"title": "Des marques",
|
"title": "Des marques",
|
||||||
"student": "Étudiant",
|
"student": "Étudiant",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"no_student": "Aucun étudiant ajouté"
|
"no_student": "Aucun étudiant ajouté",
|
||||||
|
"export": {
|
||||||
|
"csv": "Exporter au format CSV",
|
||||||
|
"pdf": "Exporter au format PDF"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"submissions": {
|
"submissions": {
|
||||||
"title": "Exercices soumis",
|
"title": "Exercices soumis",
|
||||||
|
|||||||
@@ -475,7 +475,11 @@
|
|||||||
"title": "निशान",
|
"title": "निशान",
|
||||||
"student": "विद्यार्थी",
|
"student": "विद्यार्थी",
|
||||||
"total": "कुल",
|
"total": "कुल",
|
||||||
"no_student": "कोई छात्र नहीं जोड़ा गया"
|
"no_student": "कोई छात्र नहीं जोड़ा गया",
|
||||||
|
"export": {
|
||||||
|
"csv": "सीएसवी के रूप में निर्यात करें",
|
||||||
|
"pdf": "पीडीएफ के रूप में निर्यात करें"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"submissions": {
|
"submissions": {
|
||||||
"title": "प्रस्तुत अभ्यास",
|
"title": "प्रस्तुत अभ्यास",
|
||||||
|
|||||||
@@ -475,7 +475,11 @@
|
|||||||
"title": "Marcas",
|
"title": "Marcas",
|
||||||
"student": "Estudante",
|
"student": "Estudante",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"no_student": "Nenhum aluno adicionado"
|
"no_student": "Nenhum aluno adicionado",
|
||||||
|
"export": {
|
||||||
|
"csv": "Exportar como CSV",
|
||||||
|
"pdf": "Exportar como PDF"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"submissions": {
|
"submissions": {
|
||||||
"title": "Exercícios enviados",
|
"title": "Exercícios enviados",
|
||||||
|
|||||||
@@ -475,7 +475,11 @@
|
|||||||
"title": "Метки",
|
"title": "Метки",
|
||||||
"student": "Студент",
|
"student": "Студент",
|
||||||
"total": "Общий",
|
"total": "Общий",
|
||||||
"no_student": "Студент не добавлен"
|
"no_student": "Студент не добавлен",
|
||||||
|
"export": {
|
||||||
|
"csv": "Экспортировать в формате CSV",
|
||||||
|
"pdf": "Экспортировать в PDF"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"submissions": {
|
"submissions": {
|
||||||
"title": "Отправленные упражнения",
|
"title": "Отправленные упражнения",
|
||||||
|
|||||||
@@ -475,7 +475,11 @@
|
|||||||
"title": "Điểm",
|
"title": "Điểm",
|
||||||
"student": "Học sinh",
|
"student": "Học sinh",
|
||||||
"total": "Tổng cộng",
|
"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": {
|
"submissions": {
|
||||||
"title": "Bài tập đã gửi",
|
"title": "Bài tập đã gửi",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte';
|
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 CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||||
import PageBody from '$lib/components/PageBody/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 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 borderleftGrey = 'border-r-0 border-t-0 border-b-0 border-l border-gray-300';
|
||||||
let students: GroupPerson[] = [];
|
let students: GroupPerson[] = [];
|
||||||
|
|
||||||
let lessonMapping = {}; // { lessonId: { exerciseId: exerciseTitle, ... }, ... }
|
let lessonMapping = {}; // { lessonId: { exerciseId: exerciseTitle, ... }, ... }
|
||||||
let studentMarksByExerciseId = {}; // { groupMemberId: { exerciseId: `total_gotten/points`, ... }, ... }
|
let studentMarksByExerciseId = {}; // { groupMemberId: { exerciseId: `total_gotten/points`, ... }, ... }
|
||||||
|
let isDownloading = false;
|
||||||
|
|
||||||
function calculateStudentTotal(studentExerciseData) {
|
function calculateStudentTotal(studentExerciseData) {
|
||||||
if (!studentExerciseData) return 0;
|
if (!studentExerciseData) return 0;
|
||||||
@@ -95,6 +99,73 @@
|
|||||||
return roles;
|
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
|
$: students = $globalStore.isStudent
|
||||||
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
|
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
|
||||||
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
|
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
|
||||||
@@ -109,10 +180,27 @@
|
|||||||
goto(`/courses/${data.courseId}/lessons?next=true`);
|
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">
|
<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="flex items-center {borderBottomGrey}">
|
||||||
<div class="box flex items-center p-3">
|
<div class="box flex items-center p-3">
|
||||||
<p class="dark:text-white w-40">{$t('course.navItem.marks.student')}</p>
|
<p class="dark:text-white w-40">{$t('course.navItem.marks.student')}</p>
|
||||||
|
|||||||
Generated
+453
-185
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user