Merged develop into feature/abacus-export

This commit is contained in:
Christian Cueni 2024-06-20 05:21:05 +00:00
commit f7798cfa55
33 changed files with 2882 additions and 185 deletions

1
.gitignore vendored
View File

@ -46,7 +46,6 @@ coverage.xml
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:

View File

@ -3,21 +3,17 @@ import { computed, ref, watch } from "vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useTranslation } from "i18next-vue";
import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql";
import type { StatisticsFilterItem } from "@/types";
const { t } = useTranslation();
interface Item {
_id: string;
course_session_id: string;
generation: string;
circle_id: string;
}
const props = defineProps<{
items: Item[];
items: StatisticsFilterItem[];
courseSessionProperties: StatisticsCourseSessionPropertiesType;
}>();
defineExpose({ getFilteredItems });
const sessionFilter = computed(() => {
const f = props.courseSessionProperties.sessions.map((session) => ({
name: `${t("a.Durchfuehrung")}: ${session.name}`,
@ -71,6 +67,10 @@ const filteredItems = computed(() => {
return sessionMatch && generationMatch && circleMatch;
});
});
function getFilteredItems() {
return filteredItems.value;
}
</script>
<template>

View File

@ -9,6 +9,10 @@ import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"
import { getDateString } from "@/components/dueDates/dueDatesUtils";
import dayjs from "dayjs";
import ItProgress from "@/components/ui/ItProgress.vue";
import { type Ref, ref } from "vue";
import { exportDataAsXls } from "@/utils/export";
import { exportCompetenceElements } from "@/services/dashboard";
import { useUserStore } from "@/stores/user";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
@ -17,6 +21,9 @@ const props = defineProps<{
circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
const userStore = useUserStore();
const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
if (!metrics.ranking_completed) {
return {
@ -36,15 +43,33 @@ const assignmentStats = (metrics: AssignmentCompletionMetricsType) => {
const total = (metrics: AssignmentCompletionMetricsType) => {
return metrics.passed_count + metrics.failed_count + metrics.unranked_count;
};
async function exportData() {
if (!statisticFilter.value) {
return;
}
const filteredItems = statisticFilter.value.getFilteredItems();
await exportDataAsXls(filteredItems, exportCompetenceElements, userStore.language);
}
</script>
<template>
<main>
<div class="mb-10 flex items-center justify-between">
<h3>{{ $t("a.Kompetenznachweis-Elemente") }}</h3>
<button
v-if="userStore.course_session_experts.length > 0"
class="flex"
data-cy="export-button"
@click="exportData"
>
<it-icon-export></it-icon-export>
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button>
</div>
<div v-if="courseStatistics?.assignments.records" class="mt-8 bg-white">
<StatisticFilterList
ref="statisticFilter"
:course-session-properties="courseStatistics?.course_session_properties"
:items="courseStatistics.assignments.records"
>

View File

@ -8,6 +8,10 @@ import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"
import ItProgress from "@/components/ui/ItProgress.vue";
import { getDateString } from "@/components/dueDates/dueDatesUtils";
import dayjs from "dayjs";
import { ref, type Ref } from "vue";
import { exportDataAsXls } from "@/utils/export";
import { exportAttendance } from "@/services/dashboard";
import { useUserStore } from "@/stores/user";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
@ -16,6 +20,9 @@ const props = defineProps<{
circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
const userStore = useUserStore();
const attendanceStats = (present: number, total: number) => {
return {
SUCCESS: present,
@ -23,18 +30,36 @@ const attendanceStats = (present: number, total: number) => {
UNKNOWN: 0,
};
};
async function exportData() {
if (!statisticFilter.value) {
return;
}
const filteredItems = statisticFilter.value.getFilteredItems();
await exportDataAsXls(filteredItems, exportAttendance, userStore.language);
}
</script>
<template>
<main>
<div class="mb-10 flex items-center justify-between">
<h3>{{ $t("Anwesenheit") }}</h3>
<button
v-if="userStore.course_session_experts.length > 0"
class="flex"
data-cy="export-button"
@click="exportData"
>
<it-icon-export></it-icon-export>
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button>
</div>
<div
v-if="courseStatistics?.attendance_day_presences.records"
class="mt-8 bg-white"
>
<StatisticFilterList
ref="statisticFilter"
:course-session-properties="courseStatistics.course_session_properties"
:items="courseStatistics.attendance_day_presences.records"
>

View File

@ -7,6 +7,10 @@ import type {
} from "@/gql/graphql";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import { getBlendedColorForRating } from "@/utils/ratingToColor";
import { ref, type Ref } from "vue";
import { exportDataAsXls } from "@/utils/export";
import { exportFeedback } from "@/services/dashboard";
import { useUserStore } from "@/stores/user";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
@ -14,15 +18,36 @@ const props = defineProps<{
courseSessionName: (sessionId: string) => string;
circleMeta: (circleId: string) => StatisticsCircleDataType;
}>();
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
const userStore = useUserStore();
async function exportData() {
if (!statisticFilter.value) {
return;
}
const filteredItems = statisticFilter.value.getFilteredItems();
await exportDataAsXls(filteredItems, exportFeedback, userStore.language);
}
</script>
<template>
<main>
<div class="mb-10 flex items-center justify-between">
<h3>{{ $t("a.Feedback Teilnehmer") }}</h3>
<button
v-if="userStore.course_session_experts.length > 0"
class="flex"
data-cy="export-button"
@click="exportData"
>
<it-icon-export></it-icon-export>
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button>
</div>
<div v-if="courseStatistics?.feedback_responses.records" class="mt-8 bg-white">
<StatisticFilterList
ref="statisticFilter"
:course-session-properties="courseStatistics.course_session_properties"
:items="courseStatistics.feedback_responses.records"
>

View File

@ -6,14 +6,19 @@ import {
DASHBOARD_MENTOR_COMPETENCE_SUMMARY,
} from "@/graphql/queries";
import { itGetCached } from "@/fetchHelpers";
import { itGetCached, itPost } from "@/fetchHelpers";
import type {
AssignmentsStatisticsType,
CourseProgressType,
CourseStatisticsType,
DashboardConfigType,
} from "@/gql/graphql";
import type { DashboardPersonsPageMode, DueDate } from "@/types";
import type {
DashboardPersonsPageMode,
DueDate,
XlsExportRequestData,
XlsExportResponseData,
} from "@/types";
export type DashboardPersonRoleType =
| "SUPERVISOR"
@ -189,6 +194,33 @@ export async function fetchOpenTasksCount(courseId: string) {
);
}
export async function exportFeedback(
data: XlsExportRequestData,
language: string
): Promise<XlsExportResponseData> {
return await itPost("/api/dashboard/export/feedback/", data, {
headers: { "Accept-Language": language },
});
}
export async function exportAttendance(
data: XlsExportRequestData,
language: string
): Promise<XlsExportResponseData> {
return await itPost("/api/dashboard/export/attendance/", data, {
headers: { "Accept-Language": language },
});
}
export async function exportCompetenceElements(
data: XlsExportRequestData,
language: string
): Promise<XlsExportResponseData> {
return await itPost("/api/dashboard/export/competence_elements/", data, {
headers: { "Accept-Language": language },
});
}
export function courseIdForCourseSlug(
dashboardConfigs: DashboardCourseConfigType[],
courseSlug: string

View File

@ -25,22 +25,6 @@ export const useDashboardStore = defineStore("dashboard", () => {
ref(null);
const loading = ref(false);
// const loadDashboardData = async (type: DashboardType, id: string) => {
// let data;
// switch (type) {
// case "STATISTICS_DASHBOARD":
// data = await fetchStatisticData(id);
// break;
// case "PROGRESS_DASHBOARD":
// data = await fetchProgressData(id);
// break;
// default:
// return;
// }
// dashBoardDataCache[id] = data;
// currentDashBoardData.value = data;
// };
const switchAndLoadDashboardConfig = async (config: DashboardConfigType) => {
currentDashboardConfig.value = config;
await loadDashboardDetails();
@ -59,21 +43,7 @@ export const useDashboardStore = defineStore("dashboard", () => {
loading.value = true;
dashboardConfigsv2.value = await fetchDashboardConfigv2();
console.log("got dashboard config v2: ", dashboardConfigsv2.value);
try {
// if (!currentDashboardConfig.value) {
// await loadDashboardConfig();
// return;
// }
// const { id, dashboard_type } = currentDashboardConfig.value;
// if (dashBoardDataCache[id]) {
// currentDashBoardData.value = dashBoardDataCache[id];
// return;
// }
// // await loadDashboardData(dashboard_type, id);
} finally {
console.log("done loading dashboard details");
loading.value = false;
}
loading.value = false;
};
const loadStatisticsData = async (id: string) => {

View File

@ -618,3 +618,20 @@ export type User = {
};
export type DashboardPersonsPageMode = "default" | "competenceMetrics";
export interface StatisticsFilterItem {
_id: string;
course_session_id: string;
generation: string;
circle_id: string;
}
export interface XlsExportRequestData {
courseSessionIds: number[];
circleIds: number[];
}
export interface XlsExportResponseData {
encoded_data: string;
file_name: string;
}

View File

@ -0,0 +1,59 @@
import type {
StatisticsFilterItem,
XlsExportRequestData,
XlsExportResponseData,
} from "@/types";
interface exportApiCall {
(data: XlsExportRequestData, language: string): Promise<XlsExportResponseData>;
}
export async function exportDataAsXls(
items: StatisticsFilterItem[],
apiCall: exportApiCall,
language: string
) {
const itemIds = extractUniqueIds(items);
const data = await apiCall(itemIds, language);
openDataAsXls(data.encoded_data, data.file_name);
}
export async function openDataAsXls(encodedData: string, filename: string) {
// Decode base64 string to binary data
const byteCharacters = atob(encodedData);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: "application/octet-stream" });
// Create a link element and trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
// Function to extract unique circle_ids and course_session_ids
export function extractUniqueIds(data: StatisticsFilterItem[]): {
circleIds: number[];
courseSessionIds: number[];
} {
const circleIdsSet = new Set<number>();
const courseSessionIdsSet = new Set<number>();
data.forEach((item) => {
circleIdsSet.add(Number(item.circle_id));
courseSessionIdsSet.add(Number(item.course_session_id));
});
return {
circleIds: Array.from(circleIdsSet),
courseSessionIds: Array.from(courseSessionIdsSet),
};
}

View File

@ -0,0 +1,87 @@
import { login } from "../helpers";
const path = require("path");
// ignore automatic import mess-up...
// const getDashboardStatistics = (what) => {
// return cy.get(`[data-cy="dashboard.stats.${what}"]`);
// };
//
// const clickOnDetailsLink = (within) => {
// cy.get(`[data-cy="dashboard.stats.${within}"]`).within(() => {
// cy.get('[data-cy="basebox.detailsLink"]').click();
// });
// };
//
function getCurrentDate() {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function verifyExportFileExists(fileName) {
const downloadsFolder = Cypress.config("downloadsFolder");
cy.readFile(
path.join(downloadsFolder, `${fileName}_${getCurrentDate()}.xlsx`)
).should("exist");
}
function testExport(url, fileName) {
cy.visit(url);
cy.get('[data-cy="export-button"]').click();
verifyExportFileExists(fileName);
}
describe("dashboardExport.cy.js", () => {
beforeEach(() => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days"
);
});
describe("as supervisor", () => {
beforeEach(() => {
login("test-supervisor1@example.com", "test");
});
it("should download the attendance export", () => {
testExport("/statistic/test-lehrgang/attendance", "export_anwesenheit");
});
it("should download the competence elements export", () => {
testExport(
"/statistic/test-lehrgang/assignment",
"export_kompetenznachweis_elemente"
);
});
it("should download the feedback export", () => {
testExport("/statistic/test-lehrgang/feedback", "export_feedback");
});
});
describe("as trainer", () => {
beforeEach(() => {
login("test-trainer1@example.com", "test");
});
it("should download the attendance export", () => {
testExport("/statistic/test-lehrgang/attendance", "export_anwesenheit");
});
it("should download the competence elements export", () => {
testExport(
"/statistic/test-lehrgang/assignment",
"export_kompetenznachweis_elemente"
);
});
it("should download the feedback export", () => {
testExport("/statistic/test-lehrgang/feedback", "export_feedback");
});
});
});

Binary file not shown.

View File

@ -4,6 +4,7 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth.decorators import user_passes_test
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.http import HttpResponse
from django.urls import include, path, re_path, register_converter
from django.urls.converters import IntConverter
from django.views import defaults as default_views
@ -40,6 +41,9 @@ from vbv_lernwelt.course.views import (
)
from vbv_lernwelt.course_session.views import get_course_session_documents
from vbv_lernwelt.dashboard.views import (
export_attendance_as_xsl,
export_competence_elements_as_xsl,
export_feedback_as_xsl,
get_dashboard_config,
get_dashboard_due_dates,
get_dashboard_persons,
@ -135,6 +139,10 @@ urlpatterns = [
path(r"api/dashboard/course/<str:course_id>/mentees/", get_mentee_count, name="get_mentee_count"),
path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count,
name="get_mentor_open_tasks_count"),
path(r"api/dashboard/export/attendance/", export_attendance_as_xsl, name="export_attendance_as_xsl"),
path(r"api/dashboard/export/competence_elements/", export_competence_elements_as_xsl,
name="export_certificate_as_xsl"),
path(r"api/dashboard/export/feedback/", export_feedback_as_xsl, name="export_feedback_as_xsl"),
# course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),

Binary file not shown.

View File

@ -0,0 +1,286 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-18 15:24+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: vbv_lernwelt/assignment/export.py:31
msgid "export_kompetenznachweis_elemente"
msgstr ""
#: vbv_lernwelt/assignment/export.py:142
msgid "Resultat"
msgstr ""
#: vbv_lernwelt/assignment/export.py:143
msgid "bestanden"
msgstr ""
#: vbv_lernwelt/assignment/export.py:187
msgid "Bestanden"
msgstr ""
#: vbv_lernwelt/assignment/export.py:189
msgid "Nicht bestanden"
msgstr ""
#: vbv_lernwelt/assignment/export.py:203 vbv_lernwelt/assignment/export.py:206
#: vbv_lernwelt/assignment/export.py:207
msgid "Keine Daten"
msgstr ""
#: vbv_lernwelt/core/admin.py:32
msgid "Personal info"
msgstr ""
#: vbv_lernwelt/core/admin.py:34
msgid "Permissions"
msgstr ""
#: vbv_lernwelt/core/admin.py:45
msgid "Important dates"
msgstr ""
#: vbv_lernwelt/core/admin.py:47
msgid "Profile"
msgstr ""
#: vbv_lernwelt/core/admin.py:62
msgid "Organisation"
msgstr ""
#: vbv_lernwelt/core/admin.py:75
msgid "Additional data"
msgstr ""
#: vbv_lernwelt/course/models.py:24 vbv_lernwelt/course/models.py:62
msgid "Titel"
msgstr ""
#: vbv_lernwelt/course/models.py:26
msgid "Kategorie-Name"
msgstr ""
#: vbv_lernwelt/course/models.py:29
msgid "Slug"
msgstr ""
#: vbv_lernwelt/course/models.py:64
msgid "Allgemein"
msgstr ""
#: vbv_lernwelt/course/models.py:177
msgid "Lehrgang-Seite"
msgstr ""
#: vbv_lernwelt/course/models.py:272
msgid "Teilnehmer"
msgstr ""
#: vbv_lernwelt/course/models.py:273
msgid "Experte/Trainer"
msgstr ""
#: vbv_lernwelt/course/models.py:332
msgid "Dokumente im Circle ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:336
msgid "Lernmentor-Funktion ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:340
msgid "Kompetenzweise ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:343
msgid "Versicherungsvermittler-Lehrgang"
msgstr ""
#: vbv_lernwelt/course/models.py:344
msgid "ÜK-Lehrgang"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:15
msgid "export_anwesenheit"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:92
msgid "Anwesenheit"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:116
msgid "Anwesend"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:116
msgid "Nicht anwesend"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:123
msgid "Vorname"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:124
msgid "Nachname"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:125
msgid "Email"
msgstr "Email"
#: vbv_lernwelt/course_session/services/export_attendance.py:126
msgid "Lehrvertragsnummer"
msgstr ""
#: vbv_lernwelt/feedback/export.py:19
msgid "export_feedback"
msgstr ""
#: vbv_lernwelt/feedback/export.py:35
msgid "Zufriedenheit insgesamt"
msgstr ""
#: vbv_lernwelt/feedback/export.py:36
msgid "Zielerreichung insgesamt"
msgstr ""
#: vbv_lernwelt/feedback/export.py:39
msgid ""
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
msgstr ""
#: vbv_lernwelt/feedback/export.py:43
msgid "Waren die Vorbereitungsaufträge klar und verständlich?"
msgstr ""
#: vbv_lernwelt/feedback/export.py:48
msgid ""
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der "
"Kursleiterin?"
msgstr ""
#: vbv_lernwelt/feedback/export.py:54
msgid ""
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und "
"aufgegriffen?"
msgstr ""
#: vbv_lernwelt/feedback/export.py:59
msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
msgstr ""
#: vbv_lernwelt/feedback/export.py:61
msgid "Würdest du den Kurs weiterempfehlen?"
msgstr ""
#: vbv_lernwelt/feedback/export.py:62
msgid "Was hat dir besonders gut gefallen?"
msgstr ""
#: vbv_lernwelt/feedback/export.py:63
msgid "Wo siehst du Verbesserungspotential?"
msgstr ""
#: vbv_lernwelt/feedback/export.py:150
msgid "Durchführung"
msgstr ""
#: vbv_lernwelt/feedback/export.py:151
msgid "Datum"
msgstr ""
#: vbv_lernwelt/feedback/models.py:31
msgid "Internet"
msgstr ""
#: vbv_lernwelt/feedback/models.py:32
msgid "Leaflet"
msgstr ""
#: vbv_lernwelt/feedback/models.py:33
msgid "Newspaper"
msgstr ""
#: vbv_lernwelt/feedback/models.py:34
msgid "Personal recommendation"
msgstr ""
#: vbv_lernwelt/feedback/models.py:35
msgid "Public event"
msgstr ""
#: vbv_lernwelt/feedback/models.py:36
msgid "Other"
msgstr ""
#: vbv_lernwelt/media_files/models.py:27 vbv_lernwelt/media_files/models.py:54
#: vbv_lernwelt/media_files/models.py:64 vbv_lernwelt/media_files/models.py:83
msgid "file"
msgstr ""
#: vbv_lernwelt/notify/models.py:9
msgid "User Interaction"
msgstr ""
#: vbv_lernwelt/notify/models.py:10
msgid "Progress"
msgstr ""
#: vbv_lernwelt/notify/models.py:11
msgid "Information"
msgstr ""
#: vbv_lernwelt/notify/models.py:16
msgid "Attendance Course Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:18
msgid "Assignment Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:21
msgid "Casework Expert Evaluation Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:23
msgid "Casework Submitted"
msgstr ""
#: vbv_lernwelt/notify/models.py:24
msgid "Casework Evaluated"
msgstr ""
#: vbv_lernwelt/notify/models.py:25
msgid "New Feedback"
msgstr ""
#: vbv_lernwelt/notify/models.py:28
msgid "Self Evaluation Feedback Requested"
msgstr ""
#: vbv_lernwelt/notify/models.py:31
msgid "Self Evaluation Feedback Provided"
msgstr ""
#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:8
msgid "Sites menu"
msgstr ""
#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:14
msgid "Edit this page"
msgstr ""

Binary file not shown.

View File

@ -0,0 +1,295 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-18 15:24+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: vbv_lernwelt/assignment/export.py:31
msgid "export_kompetenznachweis_elemente"
msgstr "export_elements_de_controle"
#: vbv_lernwelt/assignment/export.py:142
msgid "Resultat"
msgstr "Résultats"
#: vbv_lernwelt/assignment/export.py:143
msgid "bestanden"
msgstr "réussi"
#: vbv_lernwelt/assignment/export.py:187
msgid "Bestanden"
msgstr "Réussi"
#: vbv_lernwelt/assignment/export.py:189
msgid "Nicht bestanden"
msgstr "Échoué"
#: vbv_lernwelt/assignment/export.py:203 vbv_lernwelt/assignment/export.py:206
#: vbv_lernwelt/assignment/export.py:207
msgid "Keine Daten"
msgstr "Aucune donnée"
#: vbv_lernwelt/core/admin.py:32
msgid "Personal info"
msgstr ""
#: vbv_lernwelt/core/admin.py:34
msgid "Permissions"
msgstr ""
#: vbv_lernwelt/core/admin.py:45
msgid "Important dates"
msgstr ""
#: vbv_lernwelt/core/admin.py:47
msgid "Profile"
msgstr ""
#: vbv_lernwelt/core/admin.py:62
msgid "Organisation"
msgstr ""
#: vbv_lernwelt/core/admin.py:75
msgid "Additional data"
msgstr ""
#: vbv_lernwelt/course/models.py:24 vbv_lernwelt/course/models.py:62
msgid "Titel"
msgstr ""
#: vbv_lernwelt/course/models.py:26
msgid "Kategorie-Name"
msgstr ""
#: vbv_lernwelt/course/models.py:29
msgid "Slug"
msgstr ""
#: vbv_lernwelt/course/models.py:64
msgid "Allgemein"
msgstr ""
#: vbv_lernwelt/course/models.py:177
msgid "Lehrgang-Seite"
msgstr ""
#: vbv_lernwelt/course/models.py:272
msgid "Teilnehmer"
msgstr ""
#: vbv_lernwelt/course/models.py:273
msgid "Experte/Trainer"
msgstr ""
#: vbv_lernwelt/course/models.py:332
msgid "Dokumente im Circle ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:336
msgid "Lernmentor-Funktion ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:340
msgid "Kompetenzweise ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:343
msgid "Versicherungsvermittler-Lehrgang"
msgstr ""
#: vbv_lernwelt/course/models.py:344
msgid "ÜK-Lehrgang"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:15
msgid "export_anwesenheit"
msgstr "export_presence"
#: vbv_lernwelt/course_session/services/export_attendance.py:92
#| msgid "Anwesend"
msgid "Anwesenheit"
msgstr "Présence"
#: vbv_lernwelt/course_session/services/export_attendance.py:116
msgid "Anwesend"
msgstr "Présent"
#: vbv_lernwelt/course_session/services/export_attendance.py:116
msgid "Nicht anwesend"
msgstr "Pas présent"
#: vbv_lernwelt/course_session/services/export_attendance.py:123
msgid "Vorname"
msgstr "Prénom"
#: vbv_lernwelt/course_session/services/export_attendance.py:124
msgid "Nachname"
msgstr "Nom de famille"
#: vbv_lernwelt/course_session/services/export_attendance.py:125
msgid "Email"
msgstr "Email"
#: vbv_lernwelt/course_session/services/export_attendance.py:126
msgid "Lehrvertragsnummer"
msgstr "Numéro de contrat d'apprentissage"
#: vbv_lernwelt/feedback/export.py:19
msgid "export_feedback"
msgstr "export_feedback"
#: vbv_lernwelt/feedback/export.py:35
msgid "Zufriedenheit insgesamt"
msgstr "Degré de satisfaction au global"
#: vbv_lernwelt/feedback/export.py:36
msgid "Zielerreichung insgesamt"
msgstr "Degré de réalisation des objectifs"
#: vbv_lernwelt/feedback/export.py:39
msgid ""
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
msgstr ""
"As-tu limpression de bien maîtriser les sujets qui ont été abordés pendant "
"le cours ?"
#: vbv_lernwelt/feedback/export.py:43
msgid "Waren die Vorbereitungsaufträge klar und verständlich?"
msgstr "Les travaux préparatoires étaient-ils clairs et compréhensibles ?"
#: vbv_lernwelt/feedback/export.py:48
msgid ""
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der "
"Kursleiterin?"
msgstr ""
"Que penses-tu des compétences techniques de la personne chargée du cours et "
"de sa maîtrise du sujet ?"
#: vbv_lernwelt/feedback/export.py:54
msgid ""
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und "
"aufgegriffen?"
msgstr ""
"Les questions et les suggestions des participants ont-elles été prises au "
"sérieux et traitées correctement ?"
#: vbv_lernwelt/feedback/export.py:59
msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
msgstr ""
"Souhaites-tu ajouter quelque chose à lintention de la personne chargée du "
"cours ?"
#: vbv_lernwelt/feedback/export.py:61
msgid "Würdest du den Kurs weiterempfehlen?"
msgstr "Est-ce que tu recommandes ce cours ?"
#: vbv_lernwelt/feedback/export.py:62
msgid "Was hat dir besonders gut gefallen?"
msgstr "Quest-ce qui ta particulièrement plu ?"
#: vbv_lernwelt/feedback/export.py:63
msgid "Wo siehst du Verbesserungspotential?"
msgstr "À ton avis, quels sont les points qui pourraient être améliorés ?"
#: vbv_lernwelt/feedback/export.py:150
msgid "Durchführung"
msgstr "Opérations"
#: vbv_lernwelt/feedback/export.py:151
msgid "Datum"
msgstr "Date"
#: vbv_lernwelt/feedback/models.py:31
msgid "Internet"
msgstr ""
#: vbv_lernwelt/feedback/models.py:32
msgid "Leaflet"
msgstr ""
#: vbv_lernwelt/feedback/models.py:33
msgid "Newspaper"
msgstr ""
#: vbv_lernwelt/feedback/models.py:34
msgid "Personal recommendation"
msgstr ""
#: vbv_lernwelt/feedback/models.py:35
msgid "Public event"
msgstr ""
#: vbv_lernwelt/feedback/models.py:36
msgid "Other"
msgstr ""
#: vbv_lernwelt/media_files/models.py:27 vbv_lernwelt/media_files/models.py:54
#: vbv_lernwelt/media_files/models.py:64 vbv_lernwelt/media_files/models.py:83
msgid "file"
msgstr ""
#: vbv_lernwelt/notify/models.py:9
msgid "User Interaction"
msgstr ""
#: vbv_lernwelt/notify/models.py:10
msgid "Progress"
msgstr ""
#: vbv_lernwelt/notify/models.py:11
msgid "Information"
msgstr ""
#: vbv_lernwelt/notify/models.py:16
msgid "Attendance Course Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:18
msgid "Assignment Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:21
msgid "Casework Expert Evaluation Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:23
msgid "Casework Submitted"
msgstr ""
#: vbv_lernwelt/notify/models.py:24
msgid "Casework Evaluated"
msgstr ""
#: vbv_lernwelt/notify/models.py:25
msgid "New Feedback"
msgstr ""
#: vbv_lernwelt/notify/models.py:28
msgid "Self Evaluation Feedback Requested"
msgstr ""
#: vbv_lernwelt/notify/models.py:31
msgid "Self Evaluation Feedback Provided"
msgstr ""
#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:8
msgid "Sites menu"
msgstr ""
#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:14
msgid "Edit this page"
msgstr ""

Binary file not shown.

View File

@ -0,0 +1,291 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-18 15:24+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: vbv_lernwelt/assignment/export.py:31
msgid "export_kompetenznachweis_elemente"
msgstr "esportazione_elementi_del_controllo"
#: vbv_lernwelt/assignment/export.py:142
msgid "Resultat"
msgstr "Risultato"
#: vbv_lernwelt/assignment/export.py:143
msgid "bestanden"
msgstr "superato"
#: vbv_lernwelt/assignment/export.py:187
msgid "Bestanden"
msgstr "Superato"
#: vbv_lernwelt/assignment/export.py:189
msgid "Nicht bestanden"
msgstr "Fallito"
#: vbv_lernwelt/assignment/export.py:203 vbv_lernwelt/assignment/export.py:206
#: vbv_lernwelt/assignment/export.py:207
msgid "Keine Daten"
msgstr "Nessun dato"
#: vbv_lernwelt/core/admin.py:32
msgid "Personal info"
msgstr ""
#: vbv_lernwelt/core/admin.py:34
msgid "Permissions"
msgstr ""
#: vbv_lernwelt/core/admin.py:45
msgid "Important dates"
msgstr ""
#: vbv_lernwelt/core/admin.py:47
msgid "Profile"
msgstr ""
#: vbv_lernwelt/core/admin.py:62
msgid "Organisation"
msgstr ""
#: vbv_lernwelt/core/admin.py:75
msgid "Additional data"
msgstr ""
#: vbv_lernwelt/course/models.py:24 vbv_lernwelt/course/models.py:62
msgid "Titel"
msgstr ""
#: vbv_lernwelt/course/models.py:26
msgid "Kategorie-Name"
msgstr ""
#: vbv_lernwelt/course/models.py:29
msgid "Slug"
msgstr ""
#: vbv_lernwelt/course/models.py:64
msgid "Allgemein"
msgstr ""
#: vbv_lernwelt/course/models.py:177
msgid "Lehrgang-Seite"
msgstr ""
#: vbv_lernwelt/course/models.py:272
msgid "Teilnehmer"
msgstr ""
#: vbv_lernwelt/course/models.py:273
msgid "Experte/Trainer"
msgstr ""
#: vbv_lernwelt/course/models.py:332
msgid "Dokumente im Circle ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:336
msgid "Lernmentor-Funktion ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:340
msgid "Kompetenzweise ein/aus"
msgstr ""
#: vbv_lernwelt/course/models.py:343
msgid "Versicherungsvermittler-Lehrgang"
msgstr ""
#: vbv_lernwelt/course/models.py:344
msgid "ÜK-Lehrgang"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:15
msgid "export_anwesenheit"
msgstr "esportazione_presenza"
#: vbv_lernwelt/course_session/services/export_attendance.py:92
#| msgid "Anwesend"
msgid "Anwesenheit"
msgstr "Presenza"
#: vbv_lernwelt/course_session/services/export_attendance.py:116
msgid "Anwesend"
msgstr "Presente"
#: vbv_lernwelt/course_session/services/export_attendance.py:116
msgid "Nicht anwesend"
msgstr "Non presente"
#: vbv_lernwelt/course_session/services/export_attendance.py:123
msgid "Vorname"
msgstr "Nome"
#: vbv_lernwelt/course_session/services/export_attendance.py:124
msgid "Nachname"
msgstr "Cognome"
#: vbv_lernwelt/course_session/services/export_attendance.py:125
msgid "Email"
msgstr "E-mail"
#: vbv_lernwelt/course_session/services/export_attendance.py:126
msgid "Lehrvertragsnummer"
msgstr "Numero di contratto di tirocinio"
#: vbv_lernwelt/feedback/export.py:19
msgid "export_feedback"
msgstr "esportazione_feedback"
#: vbv_lernwelt/feedback/export.py:35
msgid "Zufriedenheit insgesamt"
msgstr "Soddisfazione complessiva"
#: vbv_lernwelt/feedback/export.py:36
msgid "Zielerreichung insgesamt"
msgstr "Raggiungimento complessivo degli obiettivi"
#: vbv_lernwelt/feedback/export.py:39
msgid ""
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
msgstr "Come valuti il tuo livello di preparazione sui temi dopo il corso?"
#: vbv_lernwelt/feedback/export.py:43
msgid "Waren die Vorbereitungsaufträge klar und verständlich?"
msgstr "Gli incarichi di preparazione erano chiari e comprensibili?"
#: vbv_lernwelt/feedback/export.py:48
msgid ""
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der "
"Kursleiterin?"
msgstr ""
"Come valuti il livello di preparazione sui temi e le competenze "
"specialistiche dellistruttore/istruttrice del corso?"
#: vbv_lernwelt/feedback/export.py:54
msgid ""
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und "
"aufgegriffen?"
msgstr ""
"Le domande e i suggerimenti dei/delle partecipanti al corso sono stati "
"accolti e presi sul serio?"
#: vbv_lernwelt/feedback/export.py:59
msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
msgstr "Cosaltro vorresti ancora dire allistruttore/istruttrice del corso?"
#: vbv_lernwelt/feedback/export.py:61
msgid "Würdest du den Kurs weiterempfehlen?"
msgstr "Raccomanderesti il corso?"
#: vbv_lernwelt/feedback/export.py:62
msgid "Was hat dir besonders gut gefallen?"
msgstr "Coshai apprezzato particolarmente?"
#: vbv_lernwelt/feedback/export.py:63
msgid "Wo siehst du Verbesserungspotential?"
msgstr "Dove vedi un potenziale di miglioramento?"
#: vbv_lernwelt/feedback/export.py:150
msgid "Durchführung"
msgstr "Svolgimenti"
#: vbv_lernwelt/feedback/export.py:151
msgid "Datum"
msgstr "Data"
#: vbv_lernwelt/feedback/models.py:31
msgid "Internet"
msgstr ""
#: vbv_lernwelt/feedback/models.py:32
msgid "Leaflet"
msgstr ""
#: vbv_lernwelt/feedback/models.py:33
msgid "Newspaper"
msgstr ""
#: vbv_lernwelt/feedback/models.py:34
msgid "Personal recommendation"
msgstr ""
#: vbv_lernwelt/feedback/models.py:35
msgid "Public event"
msgstr ""
#: vbv_lernwelt/feedback/models.py:36
msgid "Other"
msgstr ""
#: vbv_lernwelt/media_files/models.py:27 vbv_lernwelt/media_files/models.py:54
#: vbv_lernwelt/media_files/models.py:64 vbv_lernwelt/media_files/models.py:83
msgid "file"
msgstr ""
#: vbv_lernwelt/notify/models.py:9
msgid "User Interaction"
msgstr ""
#: vbv_lernwelt/notify/models.py:10
msgid "Progress"
msgstr ""
#: vbv_lernwelt/notify/models.py:11
msgid "Information"
msgstr ""
#: vbv_lernwelt/notify/models.py:16
msgid "Attendance Course Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:18
msgid "Assignment Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:21
msgid "Casework Expert Evaluation Reminder"
msgstr ""
#: vbv_lernwelt/notify/models.py:23
msgid "Casework Submitted"
msgstr ""
#: vbv_lernwelt/notify/models.py:24
msgid "Casework Evaluated"
msgstr ""
#: vbv_lernwelt/notify/models.py:25
msgid "New Feedback"
msgstr ""
#: vbv_lernwelt/notify/models.py:28
msgid "Self Evaluation Feedback Requested"
msgstr ""
#: vbv_lernwelt/notify/models.py:31
msgid "Self Evaluation Feedback Provided"
msgstr ""
#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:8
msgid "Sites menu"
msgstr ""
#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:14
msgid "Edit this page"
msgstr ""

View File

@ -0,0 +1,250 @@
from dataclasses import dataclass
from io import BytesIO
import structlog
from django.utils.translation import gettext_lazy as _
from openpyxl import Workbook
from vbv_lernwelt.assignment.models import (
Assignment,
AssignmentCompletion,
AssignmentType,
)
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.course_session.services.export_attendance import (
add_user_export_data,
add_user_headers,
get_ordered_csus_by_course_session,
group_by_session_title,
make_export_filename,
sanitize_sheet_name,
)
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.learnpath.models import LearningContent
logger = structlog.get_logger(__name__)
COMPETENCE_ELEMENT_EXPORT_FILE_NAME = _("export_kompetenznachweis_elemente")
@dataclass
class CompetenceCertificateElement:
assignment: Assignment
date: DueDate
learning_content: LearningContent
course_session: CourseSession
def export_competence_elements(
course_session_ids: list[str],
circle_ids: list[int] = None,
save_as_file: bool = False,
):
if len(course_session_ids) == 0:
return
COMPETENCE_ASSIGNMENT_TYPES = [
AssignmentType.CASEWORK.value,
AssignmentType.EDONIQ_TEST.value,
]
wb = Workbook()
# remove the first sheet is just easier than keeping track of the active sheet
wb.remove(wb.active)
competence_certificate_elements = _get_competence_certificate_elements(
course_session_ids
)
assignemnt_completions = AssignmentCompletion.objects.filter(
course_session_id__in=course_session_ids,
assignment__assignment_type__in=COMPETENCE_ASSIGNMENT_TYPES,
).order_by("course_session", "assignment")
# group all by the sessions title {session_id1: [...], session_id2: [...], ...}
grouped_cs_users = get_ordered_csus_by_course_session(course_session_ids)
grouped_cce = group_by_session_title(competence_certificate_elements)
grouped_ac = group_by_session_title(assignemnt_completions)
# create a sheet for each course session
for course_session_title, cs_users in grouped_cs_users.items():
logger.debug(
"export_assignment_completion",
data={
"course_session": course_session_title,
},
label="assignment_export",
)
# handle the case where there are no competence certificate elements for the course session
try:
cces = grouped_cce[course_session_title]
except KeyError:
cces = []
try:
acs = grouped_ac[course_session_title]
except KeyError:
acs = []
_create_sheet(
wb,
course_session_title,
cs_users,
cces,
acs,
circle_ids,
)
if save_as_file:
wb.save(make_export_filename(COMPETENCE_ELEMENT_EXPORT_FILE_NAME))
else:
output = BytesIO()
wb.save(output)
output.seek(0)
return output.getvalue()
def _create_sheet(
wb: Workbook,
title: str,
users: list[CourseSessionUser],
competence_certificate_element: list[CompetenceCertificateElement],
assignment_completions: list[AssignmentCompletion],
circle_ids: list[int],
):
sheet = wb.create_sheet(title=sanitize_sheet_name(title))
if len(users) == 0 or len(competence_certificate_element) == 0:
return sheet
# headers
# common user headers, Circle <title> <learningcontenttitle> bestanden, Circle <title> <learningcontenttitle> Resultat, ...
col_idx = add_user_headers(sheet)
ordered_assignement_ids = (
[]
) # keep track of the order of the columns when adding the rows
for cse in competence_certificate_element:
circle = cse.learning_content.get_circle()
if circle_ids and circle.id not in circle_ids:
continue
col_prefix = f'Circle "{circle.title}" {cse.learning_content.title}'
# add translation strings here as they are not picked up in f-strings
result_str = str(_("Resultat"))
success_str = str(_("bestanden"))
sheet.cell(
row=1,
column=col_idx,
value=f"{col_prefix} {success_str}",
)
sheet.cell(
row=1,
column=col_idx + 1,
value=f"{col_prefix} {result_str} %",
)
ordered_assignement_ids.append(cse.assignment.id)
col_idx += 2
# add rows with user results
_add_rows(sheet, users, ordered_assignement_ids, assignment_completions)
return sheet
def _add_rows(
sheet,
users: list[CourseSessionUser],
ordered_assignement_ids,
assignment_completions,
):
for row_idx, user in enumerate(users, start=2):
col_idx = add_user_export_data(sheet, user, row_idx)
for assignment_id in ordered_assignement_ids:
# get the completion for the user and the assignment
user_acs = [
ac
for ac in assignment_completions
if ac.assignment_id == assignment_id and ac.assignment_user == user.user
]
user_ac = user_acs[0] if user_acs else None
if user_ac:
status_text = (
str(_("Bestanden"))
if user_ac.evaluation_passed
else str(_("Nicht bestanden"))
)
sheet.cell(row=row_idx, column=col_idx, value=status_text)
try:
sheet.cell(
row=row_idx,
column=col_idx + 1,
value=round(
100
* user_ac.evaluation_points
/ user_ac.evaluation_max_points
),
)
except (ZeroDivisionError, TypeError):
sheet.cell(
row=row_idx, column=col_idx + 1, value=str(_("Keine Daten"))
)
else:
sheet.cell(row=row_idx, column=col_idx, value=str(_("Keine Daten")))
sheet.cell(row=row_idx, column=col_idx + 1, value=str(_("Keine Daten")))
col_idx += 2
def _get_competence_certificate_elements(
course_session_ids: list[str],
) -> list[CompetenceCertificateElement]:
course_session_assignments = CourseSessionAssignment.objects.filter(
course_session__id__in=course_session_ids,
learning_content__content_assignment__competence_certificate__isnull=False,
).order_by("course_session", "submission_deadline__start")
course_session_edoniqtests = CourseSessionEdoniqTest.objects.filter(
course_session__id__in=course_session_ids,
learning_content__content_assignment__competence_certificate__isnull=False,
).order_by("course_session", "deadline__start")
cse = [
CompetenceCertificateElement(
assignment=csa.learning_content.content_assignment,
date=csa.submission_deadline,
learning_content=csa.learning_content,
course_session=csa.course_session,
)
for csa in course_session_assignments
]
cse += [
CompetenceCertificateElement(
assignment=cset.learning_content.content_assignment,
date=cset.deadline,
learning_content=cset.learning_content,
course_session=cset.course_session,
)
for cset in course_session_edoniqtests
]
# order by course_session and submission_deadline
cse.sort(key=lambda x: (x.course_session.title, x.date.start))
return cse

View File

@ -0,0 +1,18 @@
import djclick as click
import structlog
from vbv_lernwelt.assignment.export import export_competence_elements
logger = structlog.get_logger(__name__)
@click.command()
@click.argument("course_session_id")
@click.option(
"--save-as-file/--no-save-as-file",
default=True,
help="`save-as-file` to save the file, `no-save-as-file` returns bytes. Default is `save-as-file`.",
)
def command(course_session_id, save_as_file):
# using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function
export_competence_elements([course_session_id], save_as_file=save_as_file)

View File

@ -1,5 +1,6 @@
from copy import deepcopy
import structlog
from django.utils import timezone
from rest_framework import serializers
from wagtail.models import Page
@ -19,6 +20,8 @@ from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.notify.services import NotificationService
logger = structlog.get_logger(__name__)
def update_assignment_completion(
assignment_user: User,

View File

@ -0,0 +1,324 @@
import io
from django.utils.translation import activate
from openpyxl import load_workbook
from vbv_lernwelt.assignment.export import export_competence_elements
from vbv_lernwelt.assignment.models import Assignment
from vbv_lernwelt.assignment.services import update_assignment_completion
from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTestCase
from vbv_lernwelt.learnpath.models import LearningContentAssignment
class AssignmentCompletionExportTestCase(ExportBaseTestCase):
def setUp(self):
create_default_users()
self.course = create_test_course(include_vv=False, with_sessions=True)
self.course_session_be = CourseSession.objects.get(title="Test Bern 2022 a")
self.course_session_zh = CourseSession.objects.get(title="Test Zürich 2022 a")
self.casework = (
self.course.coursepage.get_descendants()
.exact_type(Assignment)
.filter(assignment__assignment_type="CASEWORK")
.first()
.specific
)
self.edoniq_test = (
self.course.coursepage.get_descendants()
.exact_type(Assignment)
.filter(assignment__assignment_type="EDONIQ_TEST")
.first()
.specific
)
self.trainer = User.objects.get(username="admin")
self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
self.test_student1.additional_json_data = {"Lehrvertragsnummer": 1234567890}
self.test_student1.save()
self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID)
self.test_student2.additional_json_data = {"Lehrvertragsnummer": 1987654321}
self.test_student2.save()
self.test_student3 = User.objects.get(email="test-student3@example.com")
# Bern assignments
update_assignment_completion(
assignment_user=self.test_student1,
assignment=self.casework,
course_session=self.course_session_be,
completion_data={},
evaluation_points=20,
)
update_assignment_completion(
assignment_user=self.test_student1,
assignment=self.edoniq_test,
course_session=self.course_session_be,
completion_data={},
evaluation_points=14,
evaluation_passed=False,
)
update_assignment_completion(
assignment_user=self.test_student2,
assignment=self.edoniq_test,
course_session=self.course_session_be,
completion_data={},
evaluation_points=24,
evaluation_passed=True,
)
self.expected_data_be = [
self._make_header(),
[
self.test_student1.first_name,
self.test_student1.last_name,
self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"],
"Nicht bestanden",
58,
"Bestanden",
83,
],
[
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Bestanden",
100,
"Keine Daten",
"Keine Daten",
],
[
self.test_student3.first_name,
self.test_student3.last_name,
self.test_student3.email,
None,
"Keine Daten",
"Keine Daten",
"Keine Daten",
"Keine Daten",
],
]
def _generate_workbook(self, course_session_ids):
export_data = io.BytesIO(
export_competence_elements(course_session_ids, save_as_file=False)
)
return load_workbook(export_data)
def _make_header(
self,
):
casework_assignment, edoniq_assignment = self._get_assignments()
return [
"Vorname",
"Nachname",
"Email",
"Lehrvertragsnummer",
f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} bestanden',
f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} Resultat %',
f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} bestanden',
f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} Resultat %',
]
def _get_assignments(self):
casework_assignment = CourseSessionAssignment.objects.filter(
course_session__id=self.course_session_be.id,
learning_content__content_assignment__competence_certificate__isnull=False,
).first()
edoniq_assignment = CourseSessionEdoniqTest.objects.filter(
course_session__id=self.course_session_be.id,
learning_content__content_assignment__competence_certificate__isnull=False,
).first()
return casework_assignment, edoniq_assignment
def test_export_single_cs(self):
wb = self._generate_workbook([self.course_session_be.id])
self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
self._check_export(wb, self.expected_data_be, 4, 8)
def test_export_only_kn_elements(self):
self.edoniq_test.competence_certificate = None
self.edoniq_test.save()
expected_data = []
for row in self.expected_data_be:
expected_data.append(
[cell for i, cell in enumerate(row) if (i != 4 and i != 5)]
)
wb = self._generate_workbook([self.course_session_be.id])
self._check_export(wb, expected_data, 4, 6)
def test_export_multiple_cs(self):
_csa = CourseSessionAssignment.objects.create(
course_session=self.course_session_zh,
learning_content=LearningContentAssignment.objects.get(
slug=f"{self.course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"
),
)
update_assignment_completion(
assignment_user=self.test_student2,
assignment=self.casework,
course_session=self.course_session_zh,
completion_data={},
evaluation_points=18,
)
update_assignment_completion(
assignment_user=self.test_student2,
assignment=self.edoniq_test,
course_session=self.course_session_zh,
completion_data={},
evaluation_points=22,
evaluation_passed=False,
)
expected_data = [
[cell for i, cell in enumerate(self._make_header()) if (i != 4 and i != 5)],
[
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Bestanden",
75,
],
]
wb = self._generate_workbook(
[self.course_session_be.id, self.course_session_zh.id]
)
self.assertEqual(len(wb.sheetnames), 2)
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
self.assertEqual(wb.sheetnames[1], "Test Zürich 2022 a")
self._check_export(wb, self.expected_data_be, 4, 5)
wb.active = wb["Test Zürich 2022 a"]
self._check_export(wb, expected_data, 2, 5)
def test_french_export(self):
activate("fr")
wb = self._generate_workbook([self.course_session_be.id])
casework_assignment, edoniq_assignment = self._get_assignments()
header = [
"Prénom",
"Nom de famille",
"E-mail",
"Numéro de contrat d'apprentissage",
f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} réussi',
f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} Résultats %',
f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} réussi',
f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} Résultats %',
]
expected_data_be = [
header,
[
self.test_student1.first_name,
self.test_student1.last_name,
self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"],
"Échoué",
58,
"Réussi",
83,
],
[
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Réussi",
100,
"Aucune donnée",
"Aucune donnée",
],
[
self.test_student3.first_name,
self.test_student3.last_name,
self.test_student3.email,
None,
"Aucune donnée",
"Aucune donnée",
"Aucune donnée",
"Aucune donnée",
],
]
self._check_export(wb, expected_data_be, 4, 8)
def test_italian_export(self):
activate("it")
wb = self._generate_workbook([self.course_session_be.id])
casework_assignment, edoniq_assignment = self._get_assignments()
header = [
"Nome",
"Cognome",
"Email",
"Numero di contratto di tirocinio",
f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} superato',
f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} Risultato %',
f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} superato',
f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} Risultato %',
]
expected_data_be = [
header,
[
self.test_student1.first_name,
self.test_student1.last_name,
self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"],
"Fallito",
58,
"Superato",
83,
],
[
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Superato",
100,
"Nessun dato",
"Nessun dato",
],
[
self.test_student3.first_name,
self.test_student3.last_name,
self.test_student3.email,
None,
"Nessun dato",
"Nessun dato",
"Nessun dato",
"Nessun dato",
],
]
self._check_export(wb, expected_data_be, 4, 8)

View File

@ -80,7 +80,7 @@ AVATAR_DIR = settings.APPS_DIR / "static" / "avatars"
def create_default_users(default_password="test", set_avatar=False):
admin_group, created = Group.objects.get_or_create(name="admin_group")
_content_creator_grop, _created = Group.objects.get_or_create(
_content_creator_group, _created = Group.objects.get_or_create(
name="content_creator_grop"
)
student_group, created = Group.objects.get_or_create(name="student_group")

View File

@ -0,0 +1,18 @@
import djclick as click
import structlog
from vbv_lernwelt.course_session.services.export_attendance import export_attendance
logger = structlog.get_logger(__name__)
@click.command()
@click.argument("course_session_id")
@click.option(
"--save-as-file/--no-save-as-file",
default=True,
help="`save-as-file` to save the file, `no-save-as-file` returns bytes. Default is `save-as-file`.",
)
def command(course_session_id, save_as_file):
# using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function
export_attendance([course_session_id], save_as_file=save_as_file)

View File

@ -0,0 +1,184 @@
import typing
from datetime import datetime
from io import BytesIO
from itertools import groupby
import structlog
from django.utils.translation import gettext_lazy as _
from openpyxl import Workbook
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
logger = structlog.get_logger(__name__)
ATTENDANCE_EXPORT_FILENAME = _("export_anwesenheit")
def export_attendance(
course_session_ids: list[str],
save_as_file: bool = False,
circle_ids: list[int] = None,
):
wb = Workbook()
# remove the first sheet is just easier than keeping track of the active sheet
wb.remove(wb.active)
attendance_courses = CourseSessionAttendanceCourse.objects.filter(
course_session_id__in=course_session_ids
).order_by("course_session", "due_date")
grouped_cs_users = get_ordered_csus_by_course_session(course_session_ids)
# create dict with course_session_title as key and list of attendance_courses as value. Easier to access in the loop
grouped_attendance_course = {
key: list(group)
for key, group in groupby(
sorted(attendance_courses, key=lambda x: x.course_session.title),
key=lambda x: x.course_session.title,
)
}
# create a sheet for each course_session
for course_session, cs_users in grouped_cs_users.items():
logger.debug(
"export_attendance_for_course_session",
data={
"course_session": course_session,
},
label="attendance_export",
)
_create_sheet(
wb,
course_session,
cs_users,
grouped_attendance_course[course_session],
circle_ids,
)
if save_as_file:
wb.save(make_export_filename(ATTENDANCE_EXPORT_FILENAME))
else:
output = BytesIO()
wb.save(output)
output.seek(0)
return output.getvalue()
def _create_sheet(
wb: Workbook,
title: str,
users: list[CourseSessionUser],
attendance_courses: list[CourseSessionAttendanceCourse],
circle_ids: typing.Optional[list[int]],
):
sheet = wb.create_sheet(title=sanitize_sheet_name(title))
if len(users) == 0:
return sheet
# headers
# common user headers..., <attendance_course> <date>, status <attendance_course>, ..
col_idx = add_user_headers(sheet)
attendance_data = {}
for course in attendance_courses:
circle = course.get_circle()
if circle_ids and circle.id not in circle_ids:
continue
presence_str = str(_("Anwesenheit")) # f-strings are not picked up by gettext
sheet.cell(
row=1,
column=col_idx,
value=f"{presence_str} {circle.title} {course.due_date.start.strftime('%d.%m.%Y')}",
)
user_dict_map = {d["user_id"]: d for d in course.attendance_user_list}
attendance_data[circle.title] = user_dict_map
col_idx += 1
# add rows with user data
_add_rows(sheet, users, attendance_data)
return sheet
def _add_rows(sheet, users: list[CourseSessionUser], attendance_data):
for row_idx, user in enumerate(users, start=2):
col_idx = add_user_export_data(sheet, user, row_idx)
for key, user_dict_map in attendance_data.items():
user_dict = user_dict_map.get(str(user.user.id), {})
status = user_dict.get("status", "") if user_dict else ""
status_text = (
str(_("Anwesend")) if status == "PRESENT" else str(_("Nicht anwesend"))
)
sheet.cell(row=row_idx, column=col_idx, value=status_text)
col_idx += 1
def add_user_headers(sheet):
sheet.cell(row=1, column=1, value=str(_("Vorname")))
sheet.cell(row=1, column=2, value=str(_("Nachname")))
sheet.cell(row=1, column=3, value=str(_("Email")))
sheet.cell(row=1, column=4, value=str(_("Lehrvertragsnummer")))
return 5 # return the next column index
def add_user_export_data(sheet, user: CourseSessionUser, row_idx: int) -> int:
sheet.cell(row=row_idx, column=1, value=user.user.first_name)
sheet.cell(row=row_idx, column=2, value=user.user.last_name)
sheet.cell(row=row_idx, column=3, value=user.user.email)
sheet.cell(
row=row_idx,
column=4,
value=user.user.additional_json_data.get("Lehrvertragsnummer", ""),
)
return 5 # return the next column index
def get_ordered_csus_by_course_session(course_session_ids: list[str]):
csus = CourseSessionUser.objects.filter(
course_session_id__in=course_session_ids, role=CourseSessionUser.Role.MEMBER
).order_by("course_session", "user__last_name", "user__first_name")
return group_by_session_title(
sorted(csus, key=lambda x: x.course_session.title),
)
def group_by_session_title(items):
return {
key: list(group)
for key, group in groupby(
sorted(items, key=lambda x: x.course_session.title),
key=lambda x: x.course_session.title,
)
}
def make_export_filename(name: str):
today_date = datetime.today().strftime("%Y-%m-%d")
return f"{name}_{today_date}.xlsx"
def sanitize_sheet_name(text, default_name="DefaultSheet"):
if text is None:
return default_name
prohibited_chars = ["\\", "/", "*", "?", ":", "[", "]"]
for char in prohibited_chars:
text = text.replace(char, "")
text = text.strip("'")
text = text[:31]
if len(text) == 0:
return default_name
return text

View File

@ -0,0 +1,218 @@
import io
from django.test import TestCase
from django.utils.translation import activate, deactivate
from openpyxl import load_workbook
from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.services.export_attendance import export_attendance
class ExportBaseTestCase(TestCase):
def _check_export(self, wb, expected_data, max_row, max_col):
for row in wb.active.iter_rows(max_col=max_col, max_row=max_row):
for cell in row:
self.assertEqual(
cell.value, expected_data[row[0].row - 1][row.index(cell)]
)
def tearDown(self):
# Deactivate the language after the test
deactivate()
class AttendanceExportTestCase(ExportBaseTestCase):
def setUp(self):
create_default_users()
create_test_course(include_vv=False, with_sessions=True)
self.course_session_be = CourseSession.objects.get(title="Test Bern 2022 a")
self.course_session_zh = CourseSession.objects.get(title="Test Zürich 2022 a")
self.attendance_course_be = (
self.course_session_be.coursesessionattendancecourse_set.first()
)
self.attendance_course_zh = (
self.course_session_zh.coursesessionattendancecourse_set.first()
)
self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
self.test_student1.additional_json_data = {"Lehrvertragsnummer": 1234567890}
self.test_student1.save()
self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID)
self.test_student2.additional_json_data = {"Lehrvertragsnummer": 1987654321}
self.test_student2.save()
self.test_student3 = User.objects.get(email="test-student3@example.com")
self.attendance_course_be.attendance_user_list = [
{
"email": self.test_student1.email,
"status": "PRESENT",
"user_id": str(self.test_student1.id),
"last_name": self.test_student1.last_name,
"first_name": self.test_student1.first_name,
}
]
self.expected_data_be = [
self._make_header(self.attendance_course_be),
[
self.test_student1.first_name,
self.test_student1.last_name,
self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"],
"Anwesend",
],
[
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Nicht anwesend",
],
[
self.test_student3.first_name,
self.test_student3.last_name,
self.test_student3.email,
None,
"Nicht anwesend",
],
]
self.attendance_course_be.save()
def _generate_workbook(self, course_session_ids):
export_data = io.BytesIO(
export_attendance(course_session_ids, save_as_file=False)
)
return load_workbook(export_data)
def _make_header(self, csac):
return [
"Vorname",
"Nachname",
"Email",
"Lehrvertragsnummer",
f"Anwesenheit {csac.get_circle().title} {csac.due_date.start.strftime('%d.%m.%Y')}",
]
def test_attendance_export_single_cs(self):
wb = self._generate_workbook([self.course_session_be.id])
self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
self._check_export(wb, self.expected_data_be, 4, 5)
def test_attendance_export_multiple_cs(self):
self.attendance_course_zh.attendance_user_list = [
{
"email": self.test_student2.email,
"status": "PRESENT",
"user_id": str(self.test_student2.id),
"last_name": self.test_student2.last_name,
"first_name": self.test_student2.first_name,
}
]
expected_data_zh = [
self._make_header(self.attendance_course_zh),
[
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Anwesend",
],
]
self.attendance_course_zh.save()
wb = self._generate_workbook(
[self.course_session_be.id, self.course_session_zh.id]
)
self.assertEqual(len(wb.sheetnames), 2)
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
self.assertEqual(wb.sheetnames[1], "Test Zürich 2022 a")
self._check_export(wb, self.expected_data_be, 4, 5)
wb.active = wb["Test Zürich 2022 a"]
self._check_export(wb, expected_data_zh, 2, 5)
def test_french_export(self):
activate("fr")
wb = self._generate_workbook([self.course_session_be.id])
header = [
"Prénom",
"Nom de famille",
"E-mail",
"Numéro de contrat d'apprentissage",
f"Présence {self.attendance_course_be.get_circle().title} {self.attendance_course_be.due_date.start.strftime('%d.%m.%Y')}",
]
expected_data_be = [
header,
[
self.test_student1.first_name,
self.test_student1.last_name,
self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"],
"Présent",
],
[
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Pas présent",
],
[
self.test_student3.first_name,
self.test_student3.last_name,
self.test_student3.email,
None,
"Pas présent",
],
]
self._check_export(wb, expected_data_be, 4, 5)
def test_italian_export(self):
activate("it")
wb = self._generate_workbook([self.course_session_be.id])
header = [
"Nome",
"Cognome",
"Email",
"Numero di contratto di tirocinio",
f"Presenza {self.attendance_course_be.get_circle().title} {self.attendance_course_be.due_date.start.strftime('%d.%m.%Y')}",
]
expected_data_be = [
header,
[
self.test_student1.first_name,
self.test_student1.last_name,
self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"],
"Presente",
],
[
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Non presente",
],
[
self.test_student3.first_name,
self.test_student3.last_name,
self.test_student3.email,
None,
"Non presente",
],
]
self._check_export(wb, expected_data_be, 4, 5)

View File

@ -5,7 +5,14 @@ from vbv_lernwelt.assignment.models import (
AssignmentCompletion,
AssignmentCompletionStatus,
)
from vbv_lernwelt.core.constants import (
TEST_COURSE_SESSION_BERN_ID,
TEST_COURSE_SESSION_ZURICH_ID,
TEST_STUDENT1_USER_ID,
TEST_SUPERVISOR1_USER_ID,
)
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.creators.test_utils import (
add_course_session_group_supervisor,
@ -17,13 +24,15 @@ from vbv_lernwelt.course.creators.test_utils import (
)
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
from vbv_lernwelt.dashboard.views import (
_get_course_sessions_with_roles_for_user,
_get_mentee_count,
_get_mentor_open_tasks_count,
_get_permitted_circles_ids_for_user_and_course_session,
get_course_config,
get_course_sessions_with_roles_for_user,
)
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.models import LearningUnit
from vbv_lernwelt.learnpath.models import Circle, LearningUnit
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
@ -432,3 +441,103 @@ class GetMentorOpenTasksTestCase(BaseMentorAssignmentTestCase):
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
count=0,
)
class ExportXlsTestCase(TestCase):
def setUp(self):
create_default_users()
create_test_course(include_vv=True, with_sessions=True)
self.ALLOWED_ROLES = ["EXPERT", "SUPERVISOR"]
def test_can_export_cs_dats(self):
# supervisor sees all cs in region
supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID)
requested_cs_ids = [
TEST_COURSE_SESSION_ZURICH_ID,
TEST_COURSE_SESSION_BERN_ID,
]
allowed_csrs_ids = _get_course_sessions_with_roles_for_user(
supervisor, self.ALLOWED_ROLES, requested_cs_ids
)
self.assertCountEqual(requested_cs_ids, [csr.id for csr in allowed_csrs_ids])
def test_student_cannot_export_data(self):
# student cannot export any data
student = User.objects.get(id=TEST_STUDENT1_USER_ID)
requested_cs_ids = [str(TEST_COURSE_SESSION_ZURICH_ID)]
allowed_csrs_ids = _get_course_sessions_with_roles_for_user(
student, self.ALLOWED_ROLES, requested_cs_ids
)
self.assertCountEqual([], allowed_csrs_ids)
def test_trainer_cannot_export_other_cs(self):
# trainer can only export cs where she is assigned
trainer = User.objects.get(email="test-trainer2@example.com")
requested_cs_ids = [
TEST_COURSE_SESSION_BERN_ID,
TEST_COURSE_SESSION_ZURICH_ID,
]
allowed_csrs_ids = _get_course_sessions_with_roles_for_user(
trainer, self.ALLOWED_ROLES, requested_cs_ids
)
self.assertCountEqual(
[TEST_COURSE_SESSION_ZURICH_ID], [csr.id for csr in allowed_csrs_ids]
)
def test_trainer_can_get_circles_where_expert(self):
trainer = User.objects.get(email="test-trainer2@example.com")
circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID]
allowed_csrs_ids = _get_course_sessions_with_roles_for_user(
trainer, self.ALLOWED_ROLES, requested_cs_ids
)
allowed_circles = _get_permitted_circles_ids_for_user_and_course_session(
trainer, allowed_csrs_ids, [circle.id]
)
self.assertEqual(
[(TEST_COURSE_SESSION_ZURICH_ID, [circle.id])], allowed_circles
)
def test_trainer_cannot_get_circles_where_not_expert(self):
trainer = User.objects.get(email="test-trainer2@example.com")
circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID]
allowed_csrs_ids = _get_course_sessions_with_roles_for_user(
trainer, self.ALLOWED_ROLES, requested_cs_ids
)
allowed_circles = _get_permitted_circles_ids_for_user_and_course_session(
trainer, allowed_csrs_ids, [circle.id]
)
self.assertEqual([(TEST_COURSE_SESSION_ZURICH_ID, [])], allowed_circles)
def test_supervisor_can_get_all_circles(self):
supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID)
circle_reisen = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
circle_fahrzeug = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID]
allowed_csrs_ids = _get_course_sessions_with_roles_for_user(
supervisor, self.ALLOWED_ROLES, requested_cs_ids
)
allowed_circles = _get_permitted_circles_ids_for_user_and_course_session(
supervisor,
allowed_csrs_ids,
[
circle_fahrzeug.id,
circle_reisen.id,
],
)
self.assertEqual(
[(TEST_COURSE_SESSION_ZURICH_ID, [circle_fahrzeug.id, circle_reisen.id])],
allowed_circles,
)

View File

@ -1,12 +1,20 @@
import base64
from dataclasses import asdict, dataclass
from datetime import date
from enum import Enum
from typing import List, Set
from typing import List, Set, Tuple
from django.db.models import Q
from django.http import HttpResponse
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from vbv_lernwelt.assignment.export import (
COMPETENCE_ELEMENT_EXPORT_FILE_NAME,
export_competence_elements,
)
from vbv_lernwelt.assignment.models import (
AssignmentCompletion,
AssignmentCompletionStatus,
@ -22,10 +30,20 @@ from vbv_lernwelt.course.models import (
CourseSessionUser,
)
from vbv_lernwelt.course.views import logger
from vbv_lernwelt.course_session.services.export_attendance import (
ATTENDANCE_EXPORT_FILENAME,
export_attendance,
make_export_filename,
)
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.duedate.serializers import DueDateSerializer
from vbv_lernwelt.feedback.export import (
export_feedback_with_circle_restriction,
FEEDBACK_EXPORT_FILE_NAME,
)
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
@ -494,7 +512,7 @@ def get_mentor_open_tasks_count(request, course_id: str):
raise e
except Exception as e:
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=404)
return Response({"error": str(e)}, status=status.HTTP_404_NOT_FOUND)
def _get_mentor_open_tasks_count(course_id: str, mentor: User) -> int:
@ -526,3 +544,112 @@ def _get_mentor_open_tasks_count(course_id: str, mentor: User) -> int:
)
return open_assigment_count + open_feedback_count
@api_view(["POST"])
def export_attendance_as_xsl(request):
circle_ids = request.data.get("circleIds", None)
requested_course_session_ids = request.data.get("courseSessionIds", [])
course_sessions_with_roles = _get_permitted_courses_sessions_for_user(
request.user, requested_course_session_ids
)
data = export_attendance(
[cs.id for cs in course_sessions_with_roles],
circle_ids=circle_ids,
)
return _make_excel_response(data, file_name=ATTENDANCE_EXPORT_FILENAME)
@api_view(["POST"])
def export_competence_elements_as_xsl(request):
circle_ids = request.data.get("circleIds", None)
requested_course_session_ids = request.data.get("courseSessionIds", [])
course_sessions_with_roles = _get_permitted_courses_sessions_for_user(
request.user, requested_course_session_ids
)
data = export_competence_elements(
[cswr.id for cswr in course_sessions_with_roles],
circle_ids=circle_ids,
)
return _make_excel_response(data, COMPETENCE_ELEMENT_EXPORT_FILE_NAME)
@api_view(["POST"])
def export_feedback_as_xsl(request):
circle_ids = request.data.get("circleIds", None)
requested_course_session_ids = request.data.get("courseSessionIds", [])
course_sessions_with_roles = _get_permitted_courses_sessions_for_user(
request.user, requested_course_session_ids
) # noqa
allowed_circles = _get_permitted_circles_ids_for_user_and_course_session(
request.user,
course_sessions_with_roles,
circle_ids,
) # noqa
data = export_feedback_with_circle_restriction(allowed_circles, False)
return _make_excel_response(data, FEEDBACK_EXPORT_FILE_NAME)
def _get_permitted_courses_sessions_for_user(
user: User, requested_coursesession_ids: List[str]
) -> List[CourseSessionWithRoles]:
ALLOWED_ROLES = ["EXPERT", "SUPERVISOR"]
user_course_sessions_with_roles = _get_course_sessions_with_roles_for_user(
user, ALLOWED_ROLES, requested_coursesession_ids
) # noqa
return user_course_sessions_with_roles
def _make_excel_response(data: bytes, file_name: str) -> HttpResponse:
encoded_data = base64.b64encode(data).decode("utf-8")
# Create the JSON response
response_data = {
"encoded_data": encoded_data,
"file_name": make_export_filename(file_name),
}
return Response(response_data, status=200)
def _get_course_sessions_with_roles_for_user(
user: User, allowed_roles: List[str], requested_cs_ids: List[str]
) -> List[CourseSessionWithRoles]:
all_cs_roles_for_user = [
csr
for csr in get_course_sessions_with_roles_for_user(user)
if any(role in allowed_roles for role in csr.roles)
and csr.id in requested_cs_ids
] # noqa
return all_cs_roles_for_user
def _get_permitted_circles_ids_for_user_and_course_session(
user: User,
user_course_sessions_with_roles: List[CourseSessionWithRoles],
requested_circle_ids: Tuple[int, int],
):
allowed_circles_for_sessions = []
for cswr in user_course_sessions_with_roles:
if "SUPERVISOR" in cswr.roles:
allowed_circles_for_sessions.append((cswr.id, requested_circle_ids))
else:
course_session_users = CourseSessionUser.objects.filter(
course_session=cswr.id,
user=user,
)
allowed_circles = (
Circle.objects.filter(
Q(expert__in=course_session_users) & Q(id__in=requested_circle_ids)
)
.distinct()
.values_list("id", flat=True)
)
allowed_circles_for_sessions.append((cswr.id, list(allowed_circles)))
return allowed_circles_for_sessions

View File

@ -0,0 +1,168 @@
from io import BytesIO
from itertools import groupby
from operator import attrgetter
from typing import List, Tuple
import structlog
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from openpyxl import Workbook
from vbv_lernwelt.course_session.services.export_attendance import (
make_export_filename,
sanitize_sheet_name,
)
from vbv_lernwelt.feedback.models import FeedbackResponse
logger = structlog.get_logger(__name__)
FEEDBACK_EXPORT_FILE_NAME = _("export_feedback")
VV_FEEDBACK_QUESTIONS = [
("satisfaction", "Zufriedenheit insgesamt"),
("goal_attainment", "Zielerreichung insgesamt"),
(
"proficiency",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
),
("preparation_task_clarity", "Waren die Praxisaufträge klar und verständlich?"),
("would_recommend", "Würdest du den Circle weiterempfehlen?"),
("course_positive_feedback", "Was hat dir besonders gut gefallen?"),
("course_negative_feedback", "Wo siehst du Verbesserungspotential?"),
]
UK_FEEDBACK_QUESTIONS = [
("satisfaction", _("Zufriedenheit insgesamt")),
("goal_attainment", _("Zielerreichung insgesamt")),
(
"proficiency",
_("Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"),
),
(
"preparation_task_clarity",
_("Waren die Vorbereitungsaufträge klar und verständlich?"),
),
(
"instructor_competence",
_(
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
),
),
(
"instructor_respect",
_(
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
),
),
(
"instructor_open_feedback",
_("Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"),
),
("would_recommend", _("Würdest du den Kurs weiterempfehlen?")),
("course_positive_feedback", _("Was hat dir besonders gut gefallen?")),
("course_negative_feedback", _("Wo siehst du Verbesserungspotential?")),
]
def export_feedback(course_session_ids: list[str], save_as_file: bool, circles=None):
"""
Export for django view, all circles are allowed
"""
if circles:
feedback_unordered = FeedbackResponse.objects.filter(
course_session_id__in=course_session_ids,
circle__in=circles,
submitted=True,
)
else:
feedback_unordered = FeedbackResponse.objects.filter(
course_session_id__in=course_session_ids,
submitted=True,
)
return _generate_feedback_export(feedback_unordered, save_as_file)
def export_feedback_with_circle_restriction(
course_sessions_with_circles: List[Tuple[int, List[int]]], save_as_file: bool
):
"""
Export for user export, only circles in specified course sessions are allowed
"""
feedback_unordered = FeedbackResponse.objects.none()
for course_session_with_circles in course_sessions_with_circles:
feedback_unordered = feedback_unordered | FeedbackResponse.objects.filter(
course_session_id=course_session_with_circles[0],
circle_id__in=course_session_with_circles[1],
submitted=True,
)
return _generate_feedback_export(feedback_unordered, save_as_file)
def _generate_feedback_export(feedback_unordered: QuerySet, save_as_file: bool):
wb = Workbook()
# remove the first sheet is just easier than keeping track of the active sheet
wb.remove(wb.active)
feedbacks = feedback_unordered.order_by("circle", "course_session", "updated_at")
grouped_feedbacks = groupby(feedbacks, key=attrgetter("circle"))
for circle, group_feedbacks in grouped_feedbacks:
group_feedbacks = list(group_feedbacks)
logger.debug(
"export_feedback_for_circle",
data={
"circle": circle.id,
"count": len(group_feedbacks),
},
label="feedback_export",
)
_create_sheet(wb, circle.title, group_feedbacks)
if save_as_file:
wb.save(make_export_filename(FEEDBACK_EXPORT_FILE_NAME))
else:
# todo handle IndexError
output = BytesIO()
wb.save(output)
output.seek(0)
return output.getvalue()
def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]):
sheet = wb.create_sheet(title=sanitize_sheet_name(title))
if len(data) == 0:
return sheet
# we instruct the users not to mix exports of different courses, so we can assume the questions are the same and of the first type
question_data = (
UK_FEEDBACK_QUESTIONS
if data[0].data["feedback_type"] == "uk"
else VV_FEEDBACK_QUESTIONS
)
# add header
sheet.cell(row=1, column=1, value=str(_("Durchführung")))
sheet.cell(row=1, column=2, value=str(_("Datum")))
questions = [q[1] for q in question_data]
for col_idx, title in enumerate(questions, start=3):
sheet.cell(row=1, column=col_idx, value=str(title))
_add_rows(sheet, data, question_data)
return sheet
def _add_rows(sheet, data, question_data):
for row_idx, feedback in enumerate(data, start=2):
sheet.cell(row=row_idx, column=1, value=feedback.course_session.title)
sheet.cell(
row=row_idx, column=2, value=feedback.updated_at.date().strftime("%d.%m.%Y")
)
for col_idx, question in enumerate(question_data, start=3):
response = feedback.data.get(question[0], "")
sheet.cell(row=row_idx, column=col_idx, value=response)

View File

@ -0,0 +1,38 @@
import itertools
import graphene
from graphene import ObjectType
from vbv_lernwelt.feedback.models import FeedbackResponse
class FeedbackSummary(ObjectType):
circle_id = graphene.Int()
count = graphene.Int()
class FeedbackForCourseQuery(object):
course_feedback_summary = graphene.List(
FeedbackSummary, course_id=graphene.Int(required=True)
)
def resolve_course_feedback_summary(self, info, **kwargs):
course_id = kwargs.get("course_id")
user = info.context.user
feedbacks = FeedbackResponse.objects.filter(
course_session__course_id=course_id, circle__expert__user=user
).order_by("circle_id")
summary = []
grouped_feedbacks = itertools.groupby(feedbacks, lambda x: x.circle_id)
for key, feedbacks in grouped_feedbacks:
summary.append(
{
"circle_id": key,
"count": len(list(feedbacks)),
}
)
return summary

View File

@ -1,16 +1,13 @@
from datetime import datetime
from io import BytesIO
from itertools import groupby
from operator import attrgetter
from typing import Union
import structlog
from django.http import HttpResponse
from openpyxl import Workbook
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.course_session.services.export_attendance import make_export_filename
from vbv_lernwelt.feedback.export import export_feedback
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import (
LearningContentFeedbackUK,
@ -19,47 +16,6 @@ from vbv_lernwelt.learnpath.models import (
logger = structlog.get_logger(__name__)
VV_FEEDBACK_QUESTIONS = [
("satisfaction", "Zufriedenheit insgesamt"),
("goal_attainment", "Zielerreichung insgesamt"),
(
"proficiency",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
),
("preparation_task_clarity", "Waren die Praxisaufträge klar und verständlich?"),
("would_recommend", "Würdest du den Circle weiterempfehlen?"),
("course_positive_feedback", "Was hat dir besonders gut gefallen?"),
("course_negative_feedback", "Wo siehst du Verbesserungspotential?"),
]
UK_FEEDBACK_QUESTIONS = [
("satisfaction", "Zufriedenheit insgesamt"),
("goal_attainment", "Zielerreichung insgesamt"),
(
"proficiency",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?",
),
(
"preparation_task_clarity",
"Waren die Vorbereitungsaufträge klar und verständlich?",
),
(
"instructor_competence",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?",
),
(
"instructor_respect",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?",
),
(
"instructor_open_feedback",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?",
),
("would_recommend", "Würdest du den Kurs weiterempfehlen?"),
("course_positive_feedback", "Was hat dir besonders gut gefallen?"),
("course_negative_feedback", "Wo siehst du Verbesserungspotential?"),
]
def update_feedback_response(
feedback_user: User,
@ -151,99 +107,6 @@ def initial_data_for_feedback_page(
return {}
def export_feedback(course_session_ids: list[str], save_as_file: bool):
wb = Workbook()
# remove the first sheet is just easier than keeping track of the active sheet
wb.remove_sheet(wb.active)
feedbacks = FeedbackResponse.objects.filter(
course_session_id__in=course_session_ids,
submitted=True,
).order_by("circle", "course_session", "updated_at")
grouped_feedbacks = groupby(feedbacks, key=attrgetter("circle"))
for circle, group_feedbacks in grouped_feedbacks:
group_feedbacks = list(group_feedbacks)
logger.debug(
"export_feedback_for_circle",
data={
"circle": circle.id,
"course_session_ids": course_session_ids,
"count": len(group_feedbacks),
},
label="feedback_export",
)
_create_sheet(wb, circle.title, group_feedbacks)
if save_as_file:
wb.save(make_export_filename())
else:
output = BytesIO()
wb.save(output)
output.seek(0)
return output.getvalue()
def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]):
sheet = wb.create_sheet(title=_sanitize_sheet_name(title))
if len(data) == 0:
return sheet
# we instruct the users not to mix exports of different courses, so we can assume the questions are the same and of the first type
question_data = (
UK_FEEDBACK_QUESTIONS
if data[0].data["feedback_type"] == "uk"
else VV_FEEDBACK_QUESTIONS
)
# add header
sheet.cell(row=1, column=1, value="Durchführung")
sheet.cell(row=1, column=2, value="Datum")
questions = [q[1] for q in question_data]
for col_idx, title in enumerate(questions, start=3):
sheet.cell(row=1, column=col_idx, value=title)
_add_rows(sheet, data, question_data)
return sheet
def _sanitize_sheet_name(text, default_name="DefaultSheet"):
if text is None:
return default_name
prohibited_chars = ["\\", "/", "*", "?", ":", "[", "]"]
for char in prohibited_chars:
text = text.replace(char, "")
text = text.strip("'")
text = text[:31]
if len(text) == 0:
return default_name
return text
def _add_rows(sheet, data, question_data):
for row_idx, feedback in enumerate(data, start=2):
sheet.cell(row=row_idx, column=1, value=feedback.course_session.title)
sheet.cell(
row=row_idx, column=2, value=feedback.updated_at.date().strftime("%d.%m.%Y")
)
for col_idx, question in enumerate(question_data, start=3):
response = feedback.data.get(question[0], "")
sheet.cell(row=row_idx, column=col_idx, value=response)
def make_export_filename(name: str = "feedback_export"):
today_date = datetime.today().strftime("%Y-%m-%d")
return f"{name}_{today_date}.xlsx"
# used as admin action, that's why it's not in the views.py
def get_feedbacks_for_course_sessions(_modeladmin, _request, queryset):
file_name = "feedback_export_durchfuehrungen"

View File

@ -0,0 +1,254 @@
import datetime
import io
from django.utils.translation import activate
from openpyxl import load_workbook
from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTestCase
from vbv_lernwelt.feedback.export import export_feedback_with_circle_restriction
from vbv_lernwelt.feedback.factories import FeedbackResponseFactory
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.feedback.services import export_feedback
from vbv_lernwelt.learnpath.models import Circle
class FeedbackExportTestCase(ExportBaseTestCase):
def setUp(self):
super().setUp()
create_default_users()
create_test_course(include_vv=True, with_sessions=True)
self.course_session_be = CourseSession.objects.get(title="Test Bern 2022 a")
self.course_session_zh = CourseSession.objects.get(title="Test Zürich 2022 a")
self.circle_fahrzeug = Circle.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug"
)
self.circle_reisen = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID)
self.course_sessions = [
self.course_session_be,
self.course_session_be,
self.course_session_zh,
]
self.feedback_data = {
"satisfaction": [1, 4, 2],
"goal_attainment": [2, 4, 3],
"proficiency": [20, 60, 80],
"preparation_task_clarity": [True, False, True],
"instructor_competence": [1, 2, 3],
"instructor_respect": [40, 80, 100],
"instructor_open_feedback": ["super", "ok", "naja"],
"would_recommend": [False, True, False],
"course_positive_feedback": ["Bla", "Katze", "Hund"],
"course_negative_feedback": ["Maus", "Hase", "Fuchs"],
}
self.users = [self.test_student1, self.test_student2, self.test_student2]
self.circles = [self.circle_fahrzeug, self.circle_fahrzeug, self.circle_reisen]
for i in range(3):
FeedbackResponseFactory(
circle=self.circles[i],
course_session=self.course_sessions[i],
data={
"satisfaction": self.feedback_data["satisfaction"][i],
"goal_attainment": self.feedback_data["goal_attainment"][i],
"proficiency": self.feedback_data["proficiency"][i],
"preparation_task_clarity": self.feedback_data[
"preparation_task_clarity"
][i],
"instructor_competence": self.feedback_data[
"instructor_competence"
][i],
"instructor_open_feedback": self.feedback_data[
"instructor_open_feedback"
][i],
"would_recommend": self.feedback_data["would_recommend"][i],
"instructor_respect": self.feedback_data["instructor_respect"][i],
"course_positive_feedback": self.feedback_data[
"course_positive_feedback"
][i],
"course_negative_feedback": self.feedback_data[
"course_negative_feedback"
][i],
"feedback_type": "uk",
},
feedback_user=self.users[i],
submitted=True,
)
(
self.expected_data_fahrzeug,
self.expected_data_reisen,
) = self._generate_expected_data()
def _generate_expected_data(self):
feedback_data = []
for i in range(3):
feedback_data.append(
[
self.course_sessions[i].title,
datetime.datetime.now().strftime("%d.%m.%Y"),
self.feedback_data["satisfaction"][i],
self.feedback_data["goal_attainment"][i],
self.feedback_data["proficiency"][i],
self.feedback_data["preparation_task_clarity"][i],
self.feedback_data["instructor_competence"][i],
self.feedback_data["instructor_respect"][i],
self.feedback_data["instructor_open_feedback"][i],
self.feedback_data["would_recommend"][i],
self.feedback_data["course_positive_feedback"][i],
self.feedback_data["course_negative_feedback"][i],
]
)
expected_data_fahrzeug = [
self._make_header(),
feedback_data[0],
feedback_data[1],
]
expected_data_reisen = [
self._make_header(),
feedback_data[2],
]
return expected_data_fahrzeug, expected_data_reisen
def _generate_workbook(self, course_session_ids):
export_data = io.BytesIO(
export_feedback(course_session_ids, save_as_file=False)
)
return load_workbook(export_data)
def _make_header(self):
return [
"Durchführung",
"Datum",
"Zufriedenheit insgesamt",
"Zielerreichung insgesamt",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?",
"Waren die Vorbereitungsaufträge klar und verständlich?",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?",
"Würdest du den Kurs weiterempfehlen?",
"Was hat dir besonders gut gefallen?",
"Wo siehst du Verbesserungspotential?",
]
def test_export_feedback(self):
wb = self._generate_workbook(
[self.course_session_be.id, self.course_session_zh.id]
)
self.assertEqual(len(wb.sheetnames), 2)
self.assertEqual(wb.sheetnames[0], "Fahrzeug")
self.assertEqual(wb.sheetnames[1], "Reisen")
self._check_export(wb, self.expected_data_fahrzeug, 3, 12)
wb.active = wb["Reisen"]
self._check_export(wb, self.expected_data_reisen, 2, 12)
def test_export_feedback_with_cs_circle_pairs(self):
cs_circle_pairs = [
(
self.course_session_be.id,
[self.circle_fahrzeug.id, self.circle_reisen.id],
),
(
self.course_session_zh.id,
[self.circle_reisen.id, self.circle_fahrzeug.id],
),
]
export_data = io.BytesIO(
export_feedback_with_circle_restriction(cs_circle_pairs, save_as_file=False)
)
wb = load_workbook(export_data)
self.assertEqual(len(wb.sheetnames), 2)
self.assertEqual(wb.sheetnames[0], "Fahrzeug")
self.assertEqual(wb.sheetnames[1], "Reisen")
self._check_export(wb, self.expected_data_fahrzeug, 3, 12)
wb.active = wb["Reisen"]
self._check_export(wb, self.expected_data_reisen, 2, 12)
def test_does_not_include_unsubmitted_feedback(self):
feedback = FeedbackResponse.objects.get(
circle=self.circle_reisen,
course_session=self.course_session_zh,
feedback_user=self.test_student2,
)
feedback.submitted = False
feedback.save()
wb = self._generate_workbook(
[self.course_session_be.id, self.course_session_zh.id]
)
self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Fahrzeug")
self._check_export(wb, self.expected_data_fahrzeug, 3, 12)
def test_french_export(self):
activate("fr")
wb = self._generate_workbook(
[self.course_session_be.id, self.course_session_zh.id]
)
header = [
"Opérations",
"Date",
"Degré de satisfaction au global",
"Degré de réalisation des objectifs",
"As-tu limpression de bien maîtriser les sujets qui ont été abordés pendant le cours ?",
"Les travaux préparatoires étaient-ils clairs et compréhensibles ?",
"Que penses-tu des compétences techniques de la personne chargée du cours et de sa maîtrise du sujet ?",
"Les questions et les suggestions des participants ont-elles été prises au sérieux et traitées correctement ?",
"Souhaites-tu ajouter quelque chose à lintention de la personne chargée du cours ?",
"Est-ce que tu recommandes ce cours ?",
"Quest-ce qui ta particulièrement plu ?",
"À ton avis, quels sont les points qui pourraient être améliorés ?",
]
self.expected_data_fahrzeug[0] = header
self._check_export(wb, self.expected_data_fahrzeug, 3, 12)
def test_italian_export(self):
activate("it")
wb = self._generate_workbook(
[self.course_session_be.id, self.course_session_zh.id]
)
header = [
"Svolgimenti",
"Data",
"Soddisfazione complessiva",
"Raggiungimento complessivo degli obiettivi",
"Come valuti il tuo livello di preparazione sui temi dopo il corso?",
"Gli incarichi di preparazione erano chiari e comprensibili?",
"Come valuti il livello di preparazione sui temi e le competenze specialistiche dellistruttore/istruttrice del corso?",
"Le domande e i suggerimenti dei/delle partecipanti al corso sono stati accolti e presi sul serio?",
"Cosaltro vorresti ancora dire allistruttore/istruttrice del corso?",
"Raccomanderesti il corso?",
"Coshai apprezzato particolarmente?",
"Dove vedi un potenziale di miglioramento?",
]
self.expected_data_fahrzeug[0] = header
self._check_export(wb, self.expected_data_fahrzeug, 3, 12)

View File

@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0002 3.57463C13.8229 3.57463 13.6456 3.63996 13.5056 3.77996L6.55224 10.7333L7.54158 11.7226L13.3002 5.96396L13.3002 20.8226L14.7002 20.8226L14.7002 5.96396L20.4589 11.7226L21.4482 10.7333L14.4949 3.77996C14.3549 3.63996 14.1776 3.57463 14.0002 3.57463V3.57463Z" fill="#00224D"/>
<path d="M23.3802 23.184H4.61084V24.584H23.3802V23.184Z" fill="#00224D"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B