diff --git a/client/src/pages/dashboard/DashboardPersonsPage.vue b/client/src/pages/dashboard/DashboardPersonsPage.vue index 59557836..77256adb 100644 --- a/client/src/pages/dashboard/DashboardPersonsPage.vue +++ b/client/src/pages/dashboard/DashboardPersonsPage.vue @@ -6,9 +6,14 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import { computed, ref, watch } from "vue"; import { useTranslation } from "i18next-vue"; import _ from "lodash"; -import type { DashboardPersonCourseSessionType } from "@/services/dashboard"; +import { + type DashboardPersonCourseSessionType, + exportPersons, +} from "@/services/dashboard"; import { useRouteQuery } from "@vueuse/router"; -import type { DashboardPersonsPageMode } from "@/types"; +import type { DashboardPersonsPageMode, StatisticsFilterItem } from "@/types"; +import { useUserStore } from "@/stores/user"; +import { exportDataAsXls } from "@/utils/export"; log.debug("DashboardPersonsPage created"); @@ -28,6 +33,7 @@ type MenuItem = { }; const { t } = useTranslation(); +const userStore = useUserStore(); const { loading, dashboardPersons } = useDashboardPersonsDueDates(props.mode); @@ -227,6 +233,32 @@ function personRoleDisplayValue(personCourseSession: DashboardPersonCourseSessio return ""; } +function exportData() { + const courseSessionIdsSet = new Set(); + // get all course session ids from users + if (selectedSession.value.id === UNFILTERED) { + for (const person of filteredPersons.value) { + for (const courseSession of person.course_sessions) { + courseSessionIdsSet.add(courseSession.id); + } + } + } else { + courseSessionIdsSet.add(selectedSession.value.id); + } + + // construct StatisticsFilterItems for export call + const items: StatisticsFilterItem[] = []; + for (const csId of courseSessionIdsSet) { + items.push({ + _id: "", + course_session_id: csId, + generation: "", + circle_id: "", + }); + } + exportDataAsXls(items, exportPersons, userStore.language); +} + watch(selectedCourse, () => { selectedRegion.value = regions.value[0]; }); @@ -253,7 +285,18 @@ watch(selectedRegion, () => { {{ $t("general.back") }} -

{{ $t("a.Personen") }}

+
+

{{ $t("a.Personen") }}

+ +
{ + return await itPost("/api/dashboard/export/persons/", data, { + headers: { "Accept-Language": language }, + }); +} + export function courseIdForCourseSlug( dashboardConfigs: DashboardCourseConfigType[], courseSlug: string diff --git a/cypress/e2e/dashboard/dashboardExport.cy.js b/cypress/e2e/dashboard/dashboardExport.cy.js index 565955db..6db7447c 100644 --- a/cypress/e2e/dashboard/dashboardExport.cy.js +++ b/cypress/e2e/dashboard/dashboardExport.cy.js @@ -26,7 +26,7 @@ function getCurrentDate() { function verifyExportFileExists(fileName) { const downloadsFolder = Cypress.config("downloadsFolder"); cy.readFile( - path.join(downloadsFolder, `${fileName}_${getCurrentDate()}.xlsx`) + path.join(downloadsFolder, `${fileName}_${getCurrentDate()}.xlsx`), ).should("exist"); } @@ -39,7 +39,7 @@ function testExport(url, fileName) { describe("dashboardExport.cy.js", () => { beforeEach(() => { cy.manageCommand( - "cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days" + "cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days", ); }); @@ -55,13 +55,17 @@ describe("dashboardExport.cy.js", () => { it("should download the competence elements export", () => { testExport( "/statistic/test-lehrgang/assignment", - "export_kompetenznachweis_elemente" + "export_kompetenznachweis_elemente", ); }); it("should download the feedback export", () => { testExport("/statistic/test-lehrgang/feedback", "export_feedback"); }); + + it("should download the person export", () => { + testExport("/dashboard/persons", "export_personen"); + }); }); describe("as trainer", () => { @@ -76,12 +80,16 @@ describe("dashboardExport.cy.js", () => { it("should download the competence elements export", () => { testExport( "/statistic/test-lehrgang/assignment", - "export_kompetenznachweis_elemente" + "export_kompetenznachweis_elemente", ); }); it("should download the feedback export", () => { testExport("/statistic/test-lehrgang/feedback", "export_feedback"); }); + + it("should download the person export", () => { + testExport("/dashboard/persons", "export_personen"); + }); }); }); diff --git a/server/config/urls.py b/server/config/urls.py index eeaaa517..79f0bcd7 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -44,6 +44,7 @@ from vbv_lernwelt.dashboard.views import ( export_attendance_as_xsl, export_competence_elements_as_xsl, export_feedback_as_xsl, + export_persons_as_xsl, get_dashboard_config, get_dashboard_due_dates, get_dashboard_persons, @@ -143,6 +144,7 @@ urlpatterns = [ 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"), + path(r"api/dashboard/export/persons/", export_persons_as_xsl, name="export_persons_as_xsl"), # course path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), diff --git a/server/locale/de/LC_MESSAGES/django.po b/server/locale/de/LC_MESSAGES/django.po index d377dfff..b0106577 100644 --- a/server/locale/de/LC_MESSAGES/django.po +++ b/server/locale/de/LC_MESSAGES/django.po @@ -87,7 +87,7 @@ msgstr "" msgid "Lehrgang-Seite" msgstr "" -#: vbv_lernwelt/course/models.py:278 +#: vbv_lernwelt/dashboard/person_export.py:116 msgid "Teilnehmer" msgstr "" @@ -111,7 +111,7 @@ msgstr "" msgid "Versicherungsvermittler-Lehrgang" msgstr "" -#: vbv_lernwelt/course/models.py:351 +#: vbv_lernwelt/course/models.py:350 msgid "ÜK-Lehrgang" msgstr "" @@ -155,10 +155,30 @@ msgstr "" msgid "Email" msgstr "Email" -#: vbv_lernwelt/course_session/services/export_attendance.py:138 +#: vbv_lernwelt/course_session/services/export_attendance.py:127 msgid "Lehrvertragsnummer" msgstr "" +#: vbv_lernwelt/dashboard/person_export.py:16 +msgid "export_personen" +msgstr "" + +#: vbv_lernwelt/dashboard/person_export.py:68 +msgid "Telefon" +msgstr "" + +#: vbv_lernwelt/dashboard/person_export.py:69 +msgid "Rolle" +msgstr "" + +#: vbv_lernwelt/dashboard/person_export.py:118 +msgid "Trainer" +msgstr "" + +#: vbv_lernwelt/dashboard/person_export.py:120 +msgid "Regionenleiter" +msgstr "" + #: vbv_lernwelt/feedback/export.py:19 msgid "export_feedback" msgstr "" diff --git a/server/locale/fr/LC_MESSAGES/django.mo b/server/locale/fr/LC_MESSAGES/django.mo index 57810233..5eac9c77 100644 Binary files a/server/locale/fr/LC_MESSAGES/django.mo and b/server/locale/fr/LC_MESSAGES/django.mo differ diff --git a/server/locale/fr/LC_MESSAGES/django.po b/server/locale/fr/LC_MESSAGES/django.po index f18ecf09..1351894f 100644 --- a/server/locale/fr/LC_MESSAGES/django.po +++ b/server/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-27 20:59+0200\n" +"POT-Creation-Date: 2024-07-30 11:16+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -88,8 +88,9 @@ msgid "Lehrgang-Seite" msgstr "" #: vbv_lernwelt/course/models.py:278 +#: vbv_lernwelt/dashboard/person_export.py:116 msgid "Teilnehmer" -msgstr "" +msgstr "Participant" #: vbv_lernwelt/course/models.py:279 msgid "Experte/Trainer" @@ -120,7 +121,6 @@ msgid "export_anwesenheit" msgstr "export_presence" #: vbv_lernwelt/course_session/services/export_attendance.py:86 -#| msgid "Anwesenheit" msgid "Optionale Anwesenheit" msgstr "Présence facultative" @@ -160,6 +160,26 @@ msgstr "E-mail" msgid "Lehrvertragsnummer" msgstr "Numéro de contrat d'apprentissage" +#: vbv_lernwelt/dashboard/person_export.py:16 +msgid "export_personen" +msgstr "export_personnes" + +#: vbv_lernwelt/dashboard/person_export.py:68 +msgid "Telefon" +msgstr "Téléphone" + +#: vbv_lernwelt/dashboard/person_export.py:69 +msgid "Rolle" +msgstr "Rôle" + +#: vbv_lernwelt/dashboard/person_export.py:118 +msgid "Trainer" +msgstr "Formateur / Formatrice" + +#: vbv_lernwelt/dashboard/person_export.py:120 +msgid "Regionenleiter" +msgstr "Responsable CI" + #: vbv_lernwelt/feedback/export.py:19 msgid "export_feedback" msgstr "export_feedback" diff --git a/server/locale/it/LC_MESSAGES/django.mo b/server/locale/it/LC_MESSAGES/django.mo index a046bd94..5bde8f5a 100644 Binary files a/server/locale/it/LC_MESSAGES/django.mo and b/server/locale/it/LC_MESSAGES/django.mo differ diff --git a/server/locale/it/LC_MESSAGES/django.po b/server/locale/it/LC_MESSAGES/django.po index 13e9a24a..7b9dd192 100644 --- a/server/locale/it/LC_MESSAGES/django.po +++ b/server/locale/it/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-27 20:59+0200\n" +"POT-Creation-Date: 2024-07-30 11:16+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -88,8 +88,9 @@ msgid "Lehrgang-Seite" msgstr "" #: vbv_lernwelt/course/models.py:278 +#: vbv_lernwelt/dashboard/person_export.py:116 msgid "Teilnehmer" -msgstr "" +msgstr "Partecipante" #: vbv_lernwelt/course/models.py:279 msgid "Experte/Trainer" @@ -120,7 +121,6 @@ msgid "export_anwesenheit" msgstr "esportazione_presenza" #: vbv_lernwelt/course_session/services/export_attendance.py:86 -#| msgid "Anwesenheit" msgid "Optionale Anwesenheit" msgstr "Presenza opzionale" @@ -160,6 +160,28 @@ msgstr "Email" msgid "Lehrvertragsnummer" msgstr "Numero di contratto di tirocinio" +#: vbv_lernwelt/dashboard/person_export.py:16 +#, fuzzy +#| msgid "export_anwesenheit" +msgid "export_personen" +msgstr "esportazione_persone" + +#: vbv_lernwelt/dashboard/person_export.py:68 +msgid "Telefon" +msgstr "Telefono" + +#: vbv_lernwelt/dashboard/person_export.py:69 +msgid "Rolle" +msgstr "Ruolo" + +#: vbv_lernwelt/dashboard/person_export.py:118 +msgid "Trainer" +msgstr "Trainer" + +#: vbv_lernwelt/dashboard/person_export.py:120 +msgid "Regionenleiter" +msgstr "Responsabile CI" + #: vbv_lernwelt/feedback/export.py:19 msgid "export_feedback" msgstr "esportazione_feedback" diff --git a/server/vbv_lernwelt/dashboard/person_export.py b/server/vbv_lernwelt/dashboard/person_export.py new file mode 100644 index 00000000..0a0d76aa --- /dev/null +++ b/server/vbv_lernwelt/dashboard/person_export.py @@ -0,0 +1,123 @@ +from io import BytesIO +from typing import Optional + +import structlog +from django.utils.translation import gettext_lazy as _ +from openpyxl import Workbook + +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course_session.services.export_attendance import ( + add_user_headers, + make_export_filename, + sanitize_sheet_name, +) +from vbv_lernwelt.dashboard.utils import create_person_list_with_roles + +PERSONS_EXPORT_FILENAME = _("export_personen") + +logger = structlog.get_logger(__name__) + + +def export_persons( + user: User, + course_session_ids: list[str], + save_as_file: bool = False, +) -> Optional[bytes]: + if not course_session_ids: + return + + wb = Workbook() + # remove the first sheet is just easier than keeping track of the active sheet + wb.remove(wb.active) + + user_with_roles = create_person_list_with_roles(user, course_session_ids) + course_sessions = CourseSession.objects.filter(id__in=course_session_ids) + + for cs in course_sessions: + _create_sheet( + wb, + cs.title, + cs.id, + user_with_roles, + ) + + if save_as_file: + wb.save(make_export_filename(PERSONS_EXPORT_FILENAME)) + else: + output = BytesIO() + wb.save(output) + + output.seek(0) + return output.getvalue() + + +def _create_sheet( + wb: Workbook, + title: str, + cs_id: int, + user_with_roles, +): + sheet = wb.create_sheet(title=sanitize_sheet_name(title)) + + if len(user_with_roles) == 0: + return sheet + + # headers + # common user headers, Circle <learningcontenttitle> bestanden, Circle <title> <learningcontenttitle> Resultat, ... + col_idx = add_user_headers(sheet) + sheet.cell(row=1, column=col_idx, value=str(_("Telefon"))) + sheet.cell(row=1, column=col_idx + 1, value=str(_("Rolle"))) + + _add_rows(sheet, user_with_roles, cs_id) + + return sheet + + +def _add_rows( + sheet, + users, + course_session_id, +): + idx_offset = 0 + + for row_idx, user in enumerate(users, start=2): + + def get_user_cs_by_id(user_cs, cs_id): + return next((cs for cs in user_cs if int(cs.get("id")) == cs_id), None) + + user_cs = get_user_cs_by_id(user["course_sessions"], course_session_id) + + if not user_cs: + logger.warning( + "User not found in course session", + user_id=user["user_id"], + course_session_id=course_session_id, + ) + idx_offset += 1 + continue + + user_role = _role_as_string(user_cs.get("user_role")) + idx = row_idx - idx_offset + + sheet.cell(row=idx, column=1, value=user["first_name"]) + sheet.cell(row=idx, column=2, value=user["last_name"]) + sheet.cell(row=idx, column=3, value=user["email"]) + sheet.cell(row=idx, column=4, value=user.get("Lehrvertragsnummer", "")) + sheet.cell( + row=idx, + column=5, + value=user.get("phone_number", ""), + ) + sheet.cell(row=idx, column=6, value=user_role) + + +def _role_as_string(role): + if role == "MEMBER": + return str(_("Teilnehmer")) + elif role == "EXPERT": + return str(_("Trainer")) + elif role == "SUPERVISOR": + return str(_("Regionenleiter")) + else: + return role diff --git a/server/vbv_lernwelt/dashboard/tests/test_export.py b/server/vbv_lernwelt/dashboard/tests/test_export.py new file mode 100644 index 00000000..565d0e48 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/test_export.py @@ -0,0 +1,186 @@ +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, + TEST_STUDENT3_USER_ID, + TEST_TRAINER1_USER_ID, + TEST_TRAINER2_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.dashboard.person_export import export_persons +from vbv_lernwelt.learnpath.models import Circle + + +class PersonsExportTestCase(ExportBaseTestCase): + def setUp(self): + super().setUp() + + 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.circle_fahrzeug = Circle.objects.get( + slug="test-lehrgang-lp-circle-fahrzeug" + ) + self.circle_reisen = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug") + + self.test_trainer1 = User.objects.get(id=TEST_TRAINER1_USER_ID) + self.test_trainer2 = User.objects.get(id=TEST_TRAINER2_USER_ID) + self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID) + self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID) + self.test_student3 = User.objects.get(id=TEST_STUDENT3_USER_ID) + + self.test_student1_row = [ + self.test_student1.first_name, + self.test_student1.last_name, + self.test_student1.email, + None, + None, + "Teilnehmer", + ] + self.test_student2_row = [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + None, + None, + "Teilnehmer", + ] + self.test_student3_row = [ + self.test_student3.first_name, + self.test_student3.last_name, + self.test_student3.email, + None, + None, + "Teilnehmer", + ] + self.test_trainer1_row = [ + self.test_trainer1.first_name, + self.test_trainer1.last_name, + self.test_trainer1.email, + None, + None, + "Trainer", + ] + self.test_trainer2_row = [ + self.test_trainer2.first_name, + self.test_trainer2.last_name, + self.test_trainer2.email, + None, + None, + "Trainer", + ] + + def _generate_expected_data(self, rows): + expected_data = [ + self._make_header(), + ] + for r in rows: + expected_data.append(r) + + return expected_data + + def _generate_workbook(self, user, course_session_ids): + export_data = io.BytesIO( + export_persons(user, course_session_ids, save_as_file=False) + ) + return load_workbook(export_data) + + def _make_header(self): + return [ + "Vorname", + "Nachname", + "Email", + "Lehrvertragsnummer", + "Telefon", + "Rolle", + ] + + def test_export_persons(self): + wb = self._generate_workbook(self.test_trainer1, [self.course_session_be.id]) + self.assertEqual(len(wb.sheetnames), 1) + self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") + wb.active = wb["Test Bern 2022 a"] + + data = self._generate_expected_data( + [ + self.test_student1_row, + self.test_student2_row, + self.test_student3_row, + self.test_trainer1_row, + ] + ) + + self._check_export(wb, data, 4, 6) + + wb = self._generate_workbook(self.test_trainer2, [self.course_session_zh.id]) + self.assertEqual(len(wb.sheetnames), 1) + self.assertEqual(wb.sheetnames[0], "Test Zürich 2022 a") + wb.active = wb["Test Zürich 2022 a"] + + data = self._generate_expected_data( + [self.test_student2_row, self.test_trainer2_row] + ) + self._check_export(wb, data, 3, 6) + + def test_cannot_export_other_session(self): + wb = self._generate_workbook(self.test_trainer1, [self.course_session_zh.id]) + self.assertEqual(len(wb.sheetnames), 1) + self.assertEqual(wb.sheetnames[0], "Test Zürich 2022 a") + wb.active = wb["Test Zürich 2022 a"] + + data = self._generate_expected_data([[None] * 6]) + + self._check_export(wb, data, 1, 6) + + def test_export_in_fr(self): + activate("fr") + wb = self._generate_workbook(self.test_trainer1, [self.course_session_be.id]) + self.assertEqual(len(wb.sheetnames), 1) + self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") + wb.active = wb["Test Bern 2022 a"] + + header = [ + "Prénom", + "Nom de famille", + "E-mail", + "Numéro de contrat d'apprentissage", + "Téléphone", + "Rôle", + ] + + self.assertEqual([cell.value for cell in wb.active[1]], header) + self.assertEqual(wb.active.cell(row=2, column=6).value, "Participant") + self.assertEqual( + wb.active.cell(row=5, column=6).value, "Formateur / Formatrice" + ) + + def test_export_in_it(self): + activate("it") + wb = self._generate_workbook(self.test_trainer1, [self.course_session_be.id]) + self.assertEqual(len(wb.sheetnames), 1) + self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") + wb.active = wb["Test Bern 2022 a"] + + header = [ + "Nome", + "Cognome", + "Email", + "Numero di contratto di tirocinio", + "Telefono", + "Ruolo", + ] + + self.assertEqual([cell.value for cell in wb.active[1]], header) + self.assertEqual(wb.active.cell(row=2, column=6).value, "Partecipante") + self.assertEqual(wb.active.cell(row=5, column=6).value, "Trainer") diff --git a/server/vbv_lernwelt/dashboard/utils.py b/server/vbv_lernwelt/dashboard/utils.py new file mode 100644 index 00000000..d453ae91 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/utils.py @@ -0,0 +1,179 @@ +from dataclasses import dataclass +from typing import List, Set + +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course_session_group.models import CourseSessionGroup +from vbv_lernwelt.learning_mentor.models import LearningMentor + + +@dataclass(frozen=True) +class CourseSessionWithRoles: + _original: CourseSession + roles: Set[str] + + def __getattr__(self, name: str): + # Delegate attribute access to the _original CourseSession object + return getattr(self._original, name) + + def save(self, *args, **kwargs): + raise NotImplementedError("This proxy object cannot be saved.") + + +def get_course_sessions_with_roles_for_user(user: User) -> List[CourseSessionWithRoles]: + result_course_sessions = {} + + # participant/member/expert course sessions + csu_qs = CourseSessionUser.objects.filter(user=user).prefetch_related( + "course_session", "course_session__course" + ) + for csu in csu_qs: + cs = csu.course_session + # member/expert is mutually exclusive... + cs.roles = {csu.role} + result_course_sessions[cs.id] = cs + + # enrich with supervisor course sessions + csg_qs = CourseSessionGroup.objects.filter(supervisor=user).prefetch_related( + "course_session", "course_session__course" + ) + for csg in csg_qs: + for cs in csg.course_session.all(): + cs.roles = set() + cs = result_course_sessions.get(cs.id, cs) + + cs.roles.add("SUPERVISOR") + result_course_sessions[cs.id] = cs + + # enrich with mentor course sessions + lm_qs = LearningMentor.objects.filter(mentor=user).prefetch_related( + "course_session", "course_session__course" + ) + for lm in lm_qs: + cs = lm.course_session + cs.roles = set() + cs = result_course_sessions.get(cs.id, cs) + + cs.roles.add("LEARNING_MENTOR") + result_course_sessions[cs.id] = cs + + return [ + CourseSessionWithRoles(cs, cs.roles) for cs in result_course_sessions.values() + ] + + +def has_cs_role(roles: Set[str]) -> bool: + return bool(roles & {"SUPERVISOR", "EXPERT", "MEMBER"}) + + +def user_role(roles: Set[str]) -> str: + if "SUPERVISOR" in roles: + return "SUPERVISOR" + if "EXPERT" in roles: + return "EXPERT" + if "MEMBER" in roles: + return "MEMBER" + return "LEARNING_MENTOR" + + +def create_course_session_dict(course_session_object, my_role, user_role): + return { + "id": str(course_session_object.id), + "session_title": course_session_object.title, + "course_id": str(course_session_object.course.id), + "course_title": course_session_object.course.title, + "course_slug": course_session_object.course.slug, + "region": course_session_object.region, + "generation": course_session_object.generation, + "my_role": my_role, + "user_role": user_role, + "is_uk": course_session_object.course.configuration.is_uk, + "is_vv": course_session_object.course.configuration.is_vv, + } + + +def create_person_list_with_roles( + user, course_session_ids=None, include_private_data=False +): + def create_user_dict(user_object): + user_data = { + "user_id": user_object.id, + "first_name": user_object.first_name, + "last_name": user_object.last_name, + "email": user_object.email, + "avatar_url_small": user_object.avatar_url_small, + "avatar_url": user_object.avatar_url, + "course_sessions": [], + } + if include_private_data: + user_data["phone_number"] = user_object.phone_number + user_data["Lehrvertragsnummer"] = user_object.additional_json_data.get( + "Lehrvertragsnummer", "" + ) + + return user_data + + course_sessions = get_course_sessions_with_roles_for_user(user) + + result_persons = {} + for cs in course_sessions: + if has_cs_role(cs.roles) and cs.course.configuration.is_uk: + course_session_users = CourseSessionUser.objects.filter( + course_session=cs.id + ).select_related("user") + my_role = user_role(cs.roles) + for csu in course_session_users: + person_data = result_persons.get( + csu.user.id, create_user_dict(csu.user) + ) + person_data["course_sessions"].append( + create_course_session_dict(cs, my_role, csu.role) + ) + result_persons[csu.user.id] = person_data + + # add persons where request.user is mentor + for cs in course_sessions: + if "LEARNING_MENTOR" in cs.roles: + lm = LearningMentor.objects.filter( + mentor=user, course_session=cs.id + ).first() + + for participant in lm.participants.all(): + course_session_entry = create_course_session_dict( + cs, + "LEARNING_MENTOR", + "LEARNING_MENTEE", + ) + + if participant.user.id not in result_persons: + person_data = create_user_dict(participant.user) + person_data["course_sessions"] = [course_session_entry] + result_persons[participant.user.id] = person_data + else: + # user is already in result_persons + result_persons[participant.user.id]["course_sessions"].append( + course_session_entry + ) + + # add persons where request.user is mentee + mentor_relation_qs = LearningMentor.objects.filter( + participants__user=user + ).prefetch_related("mentor", "course_session") + for mentor_relation in mentor_relation_qs: + cs = mentor_relation.course_session + course_session_entry = create_course_session_dict( + cs, + "LEARNING_MENTEE", + "LEARNING_MENTOR", + ) + + if mentor_relation.mentor.id not in result_persons: + person_data = create_user_dict(mentor_relation.mentor) + person_data["course_sessions"] = [course_session_entry] + result_persons[mentor_relation.mentor.id] = person_data + else: + # user is already in result_persons + result_persons[mentor_relation.mentor.id]["course_sessions"].append( + course_session_entry + ) + return result_persons.values() diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 2e64c804..d1f57e25 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -2,7 +2,7 @@ import base64 from dataclasses import asdict, dataclass from datetime import date from enum import Enum -from typing import List, Set, Tuple +from typing import List, Tuple from django.db.models import Q from django.http import HttpResponse @@ -24,25 +24,27 @@ from vbv_lernwelt.competence.services import ( query_competence_course_session_edoniq_tests, ) from vbv_lernwelt.core.models import User -from vbv_lernwelt.course.models import ( - CourseConfiguration, - CourseSession, - CourseSessionUser, -) +from vbv_lernwelt.course.models import CourseConfiguration, 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.dashboard.person_export import export_persons, PERSONS_EXPORT_FILENAME +from vbv_lernwelt.dashboard.utils import ( + CourseSessionWithRoles, + create_course_session_dict, + create_person_list_with_roles, + get_course_sessions_with_roles_for_user, + user_role, +) 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 @@ -65,19 +67,6 @@ class RoleKeyType(Enum): TRAINER = "Trainer" -@dataclass(frozen=True) -class CourseSessionWithRoles: - _original: CourseSession - roles: Set[str] - - def __getattr__(self, name: str): - # Delegate attribute access to the _original CourseSession object - return getattr(self._original, name) - - def save(self, *args, **kwargs): - raise NotImplementedError("This proxy object cannot be saved.") - - @dataclass(frozen=True) class CourseConfig: course_id: str @@ -92,156 +81,6 @@ class CourseConfig: session_to_continue_id: str | None -def get_course_sessions_with_roles_for_user(user: User) -> List[CourseSessionWithRoles]: - result_course_sessions = {} - - # participant/member/expert course sessions - csu_qs = CourseSessionUser.objects.filter(user=user).prefetch_related( - "course_session", "course_session__course" - ) - for csu in csu_qs: - cs = csu.course_session - # member/expert is mutually exclusive... - cs.roles = {csu.role} - result_course_sessions[cs.id] = cs - - # enrich with supervisor course sessions - csg_qs = CourseSessionGroup.objects.filter(supervisor=user).prefetch_related( - "course_session", "course_session__course" - ) - for csg in csg_qs: - for cs in csg.course_session.all(): - cs.roles = set() - cs = result_course_sessions.get(cs.id, cs) - - cs.roles.add("SUPERVISOR") - result_course_sessions[cs.id] = cs - - # enrich with mentor course sessions - lm_qs = LearningMentor.objects.filter(mentor=user).prefetch_related( - "course_session", "course_session__course" - ) - for lm in lm_qs: - cs = lm.course_session - cs.roles = set() - cs = result_course_sessions.get(cs.id, cs) - - cs.roles.add("LEARNING_MENTOR") - result_course_sessions[cs.id] = cs - - return [ - CourseSessionWithRoles(cs, cs.roles) for cs in result_course_sessions.values() - ] - - -def has_cs_role(roles: Set[str]) -> bool: - return bool(roles & {"SUPERVISOR", "EXPERT", "MEMBER"}) - - -def user_role(roles: Set[str]) -> str: - if "SUPERVISOR" in roles: - return "SUPERVISOR" - if "EXPERT" in roles: - return "EXPERT" - if "MEMBER" in roles: - return "MEMBER" - return "LEARNING_MENTOR" - - -def _create_course_session_dict(course_session_object, my_role, user_role): - return { - "id": str(course_session_object.id), - "session_title": course_session_object.title, - "course_id": str(course_session_object.course.id), - "course_title": course_session_object.course.title, - "course_slug": course_session_object.course.slug, - "region": course_session_object.region, - "generation": course_session_object.generation, - "my_role": my_role, - "user_role": user_role, - "is_uk": course_session_object.course.configuration.is_uk, - "is_vv": course_session_object.course.configuration.is_vv, - } - - -def _create_person_list_with_roles(user): - def create_user_dict(user_object): - return { - "user_id": user_object.id, - "first_name": user_object.first_name, - "last_name": user_object.last_name, - "email": user_object.email, - "avatar_url_small": user_object.avatar_url_small, - "avatar_url": user_object.avatar_url, - "course_sessions": [], - } - - course_sessions = get_course_sessions_with_roles_for_user(user) - - result_persons = {} - for cs in course_sessions: - if has_cs_role(cs.roles) and cs.course.configuration.is_uk: - course_session_users = CourseSessionUser.objects.filter( - course_session=cs.id - ).select_related("user") - my_role = user_role(cs.roles) - for csu in course_session_users: - person_data = result_persons.get( - csu.user.id, create_user_dict(csu.user) - ) - person_data["course_sessions"].append( - _create_course_session_dict(cs, my_role, csu.role) - ) - result_persons[csu.user.id] = person_data - - # add persons where request.user is mentor - for cs in course_sessions: - if "LEARNING_MENTOR" in cs.roles: - lm = LearningMentor.objects.filter( - mentor=user, course_session=cs.id - ).first() - - for participant in lm.participants.all(): - course_session_entry = _create_course_session_dict( - cs, - "LEARNING_MENTOR", - "LEARNING_MENTEE", - ) - - if participant.user.id not in result_persons: - person_data = create_user_dict(participant.user) - person_data["course_sessions"] = [course_session_entry] - result_persons[participant.user.id] = person_data - else: - # user is already in result_persons - result_persons[participant.user.id]["course_sessions"].append( - course_session_entry - ) - - # add persons where request.user is mentee - mentor_relation_qs = LearningMentor.objects.filter( - participants__user=user - ).prefetch_related("mentor", "course_session") - for mentor_relation in mentor_relation_qs: - cs = mentor_relation.course_session - course_session_entry = _create_course_session_dict( - cs, - "LEARNING_MENTEE", - "LEARNING_MENTOR", - ) - - if mentor_relation.mentor.id not in result_persons: - person_data = create_user_dict(mentor_relation.mentor) - person_data["course_sessions"] = [course_session_entry] - result_persons[mentor_relation.mentor.id] = person_data - else: - # user is already in result_persons - result_persons[mentor_relation.mentor.id]["course_sessions"].append( - course_session_entry - ) - return result_persons.values() - - def _persons_list_add_competence_metrics(persons): course_session_ids = {cs["id"] for p in persons for cs in p["course_sessions"]} competence_assignments = query_competence_course_session_assignments( @@ -284,7 +123,7 @@ def _persons_list_add_competence_metrics(persons): @api_view(["GET"]) def get_dashboard_persons(request): try: - persons = list(_create_person_list_with_roles(request.user)) + persons = list(create_person_list_with_roles(request.user)) if request.GET.get("with_competence_metrics", "") == "true": persons = _persons_list_add_competence_metrics(persons) @@ -322,7 +161,7 @@ def get_dashboard_due_dates(request): cs = course_session_map.get(due_date.course_session_id) if cs: - data["course_session"] = _create_course_session_dict( + data["course_session"] = create_course_session_dict( cs, my_role=user_role(cs.roles), user_role="" ) result_due_dates.append(data) @@ -577,6 +416,20 @@ def export_feedback_as_xsl(request): return _make_excel_response(data, FEEDBACK_EXPORT_FILE_NAME) +@api_view(["POST"]) +def export_persons_as_xsl(request): + 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 + + data = export_persons( + request.user, + [cswr.id for cswr in course_sessions_with_roles], + ) + return _make_excel_response(data, PERSONS_EXPORT_FILENAME) + + def _get_permitted_courses_sessions_for_user( user: User, requested_coursesession_ids: List[str] ) -> List[CourseSessionWithRoles]: