Merged develop into feature/abacus-export
This commit is contained in:
commit
f7798cfa55
|
|
@ -46,7 +46,6 @@ coverage.xml
|
|||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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 l’impression 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 à l’intention 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 "Qu’est-ce qui t’a 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.
|
|
@ -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 dell’istruttore/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 "Cos’altro vorresti ancora dire all’istruttore/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 "Cos’hai 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 ""
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 l’impression 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 à l’intention de la personne chargée du cours ?",
|
||||
"Est-ce que tu recommandes ce cours ?",
|
||||
"Qu’est-ce qui t’a 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 dell’istruttore/istruttrice del corso?",
|
||||
"Le domande e i suggerimenti dei/delle partecipanti al corso sono stati accolti e presi sul serio?",
|
||||
"Cos’altro vorresti ancora dire all’istruttore/istruttrice del corso?",
|
||||
"Raccomanderesti il corso?",
|
||||
"Cos’hai apprezzato particolarmente?",
|
||||
"Dove vedi un potenziale di miglioramento?",
|
||||
]
|
||||
|
||||
self.expected_data_fahrzeug[0] = header
|
||||
|
||||
self._check_export(wb, self.expected_data_fahrzeug, 3, 12)
|
||||
|
|
@ -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 |
Loading…
Reference in New Issue