From fd2cbb96bcb5057f82ba478d8829f25c6d12e4c9 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 15 Jul 2024 15:10:33 +0200 Subject: [PATCH 01/10] wip: Add functions for person export [skip ci] --- .../vbv_lernwelt/dashboard/person_export.py | 101 +++++++++ server/vbv_lernwelt/dashboard/utils.py | 170 +++++++++++++++ server/vbv_lernwelt/dashboard/views.py | 204 ++---------------- 3 files changed, 288 insertions(+), 187 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/person_export.py create mode 100644 server/vbv_lernwelt/dashboard/utils.py diff --git a/server/vbv_lernwelt/dashboard/person_export.py b/server/vbv_lernwelt/dashboard/person_export.py new file mode 100644 index 00000000..e2167ca9 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/person_export.py @@ -0,0 +1,101 @@ +from io import BytesIO + +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_export_data, + 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, +): + if len(course_session_ids) == 0: + 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, +): + 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 cs.get("course_id") == cs_id), None) + + col_idx = add_user_export_data(sheet, user, row_idx) + sheet.cell( + row=row_idx, + column=col_idx, + value=user.user.additional_json_data.get("phone", ""), + ) + sheet.cell( + row=row_idx, + column=col_idx + 1, + value=get_user_cs_by_id(user.course_sessions, course_session_id).get( + "user_role" + ), + ) + + col_idx += 2 diff --git a/server/vbv_lernwelt/dashboard/utils.py b/server/vbv_lernwelt/dashboard/utils.py new file mode 100644 index 00000000..0002004c --- /dev/null +++ b/server/vbv_lernwelt/dashboard/utils.py @@ -0,0 +1,170 @@ +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): + 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() diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 2e64c804..8616a2bf 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,23 @@ 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 +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 +63,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 +77,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 +119,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 +157,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) @@ -561,20 +396,15 @@ def export_competence_elements_as_xsl(request): @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) + data = export_persons( + [cswr.id for cswr in course_sessions_with_roles], + ) + return _make_excel_response(data, COMPETENCE_ELEMENT_EXPORT_FILE_NAME) def _get_permitted_courses_sessions_for_user( @@ -608,7 +438,7 @@ def _get_course_sessions_with_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 + and csr.id in requested_cs_ids ] # noqa return all_cs_roles_for_user From 9f880baffd47f676de05e390b845642c25f217ae Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 16 Jul 2024 09:56:19 +0200 Subject: [PATCH 02/10] wip: Fix export, add test --- .../vbv_lernwelt/dashboard/person_export.py | 34 +-- .../dashboard/tests/test_export.py | 225 ++++++++++++++++++ server/vbv_lernwelt/dashboard/utils.py | 13 +- 3 files changed, 256 insertions(+), 16 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/tests/test_export.py diff --git a/server/vbv_lernwelt/dashboard/person_export.py b/server/vbv_lernwelt/dashboard/person_export.py index e2167ca9..0c7e5eae 100644 --- a/server/vbv_lernwelt/dashboard/person_export.py +++ b/server/vbv_lernwelt/dashboard/person_export.py @@ -7,7 +7,6 @@ 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_export_data, add_user_headers, make_export_filename, sanitize_sheet_name, @@ -82,20 +81,27 @@ def _add_rows( 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 cs.get("course_id") == cs_id), None) + return next((cs for cs in user_cs if int(cs.get("id")) == cs_id), None) - col_idx = add_user_export_data(sheet, user, row_idx) + 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, + ) + continue + + user_role = (user_cs.get("user_role"),) + + sheet.cell(row=row_idx, column=1, value=user["first_name"]) + sheet.cell(row=row_idx, column=2, value=user["last_name"]) + sheet.cell(row=row_idx, column=3, value=user["email"]) + sheet.cell(row=row_idx, column=4, value=user.get("Lehrvertragsnummer", "")) sheet.cell( row=row_idx, - column=col_idx, - value=user.user.additional_json_data.get("phone", ""), + column=5, + value=user.get("phone", ""), ) - sheet.cell( - row=row_idx, - column=col_idx + 1, - value=get_user_cs_by_id(user.course_sessions, course_session_id).get( - "user_role" - ), - ) - - col_idx += 2 + sheet.cell(row=row_idx, column=6, value=user_role[0]) 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..083e7f60 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/test_export.py @@ -0,0 +1,225 @@ +import io + +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, + "MEMBER", + ] + self.test_student2_row = [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + None, + None, + "MEMBER", + ] + self.test_student3_row = [ + self.test_student3.first_name, + self.test_student3.last_name, + self.test_student3.email, + None, + None, + "MEMBER", + ] + self.test_trainer1_row = [ + self.test_trainer1.first_name, + self.test_trainer1.last_name, + self.test_trainer1.email, + None, + None, + "EXPERT", + ] + self.test_trainer2_row = [ + self.test_trainer2.first_name, + self.test_trainer2.last_name, + self.test_trainer2.email, + None, + None, + "EXPERT", + ] + + 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_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) diff --git a/server/vbv_lernwelt/dashboard/utils.py b/server/vbv_lernwelt/dashboard/utils.py index 0002004c..8967eb90 100644 --- a/server/vbv_lernwelt/dashboard/utils.py +++ b/server/vbv_lernwelt/dashboard/utils.py @@ -92,9 +92,11 @@ def create_course_session_dict(course_session_object, my_role, user_role): } -def create_person_list_with_roles(user, course_session_ids=None): +def create_person_list_with_roles( + user, course_session_ids=None, include_private_data=False +): def create_user_dict(user_object): - return { + user_data = { "user_id": user_object.id, "first_name": user_object.first_name, "last_name": user_object.last_name, @@ -103,6 +105,13 @@ def create_person_list_with_roles(user, course_session_ids=None): "avatar_url": user_object.avatar_url, "course_sessions": [], } + if include_private_data: + user_data["phone"] = user_object.additional_json_data.get("phone", "") + user_data["Lehrvertragsnummer"] = user_object.additional_json_data.get( + "Lehrvertragsnummer", "" + ) + + return user_data course_sessions = get_course_sessions_with_roles_for_user(user) From b8c4125b37a9605b1eea3ae57ec821be895a74ef Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 16 Jul 2024 10:57:37 +0200 Subject: [PATCH 03/10] Add tests --- .../dashboard/tests/test_export.py | 96 ++----------------- server/vbv_lernwelt/dashboard/views.py | 26 ++++- 2 files changed, 30 insertions(+), 92 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/tests/test_export.py b/server/vbv_lernwelt/dashboard/tests/test_export.py index 083e7f60..ed48761e 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_export.py +++ b/server/vbv_lernwelt/dashboard/tests/test_export.py @@ -132,94 +132,10 @@ class PersonsExportTestCase(ExportBaseTestCase): ) self._check_export(wb, data, 3, 6) - # 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_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"] - # 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) + self._check_export(wb, [[None] * 6], 1, 6) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 8616a2bf..290397fd 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -31,7 +31,7 @@ from vbv_lernwelt.course_session.services.export_attendance import ( export_attendance, make_export_filename, ) -from vbv_lernwelt.dashboard.person_export import export_persons +from vbv_lernwelt.dashboard.person_export import export_persons, PERSONS_EXPORT_FILENAME from vbv_lernwelt.dashboard.utils import ( CourseSessionWithRoles, create_course_session_dict, @@ -41,6 +41,10 @@ from vbv_lernwelt.dashboard.utils import ( ) 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.learnpath.models import Circle from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback @@ -396,6 +400,24 @@ def export_competence_elements_as_xsl(request): @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) + + +@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 @@ -404,7 +426,7 @@ def export_feedback_as_xsl(request): data = export_persons( [cswr.id for cswr in course_sessions_with_roles], ) - return _make_excel_response(data, COMPETENCE_ELEMENT_EXPORT_FILE_NAME) + return _make_excel_response(data, PERSONS_EXPORT_FILENAME) def _get_permitted_courses_sessions_for_user( From f69b607ca8bd5c571cdf1f8e6c4ac68ad31ae5f9 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 16 Jul 2024 14:37:15 +0200 Subject: [PATCH 04/10] Add frontend code --- .../pages/dashboard/DashboardPersonsPage.vue | 49 +++++++++++++++++-- client/src/services/dashboard.ts | 9 ++++ server/config/urls.py | 2 + .../vbv_lernwelt/dashboard/person_export.py | 16 +++--- server/vbv_lernwelt/dashboard/views.py | 1 + 5 files changed, 68 insertions(+), 9 deletions(-) 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<string>(); + // 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, () => { <it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left> <span class="inline">{{ $t("general.back") }}</span> </router-link> - <h2 class="my-4">{{ $t("a.Personen") }}</h2> + <div class="mb-10 flex items-center justify-between"> + <h2 class="my-4">{{ $t("a.Personen") }}</h2> + <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 class="bg-white px-4 py-2"> <section v-if="filtersVisible" diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index 422bf2dc..f8ee1a37 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -221,6 +221,15 @@ export async function exportCompetenceElements( }); } +export async function exportPersons( + data: XlsExportRequestData, + language: string +): Promise<XlsExportResponseData> { + return await itPost("/api/dashboard/export/persons/", data, { + headers: { "Accept-Language": language }, + }); +} + export function courseIdForCourseSlug( dashboardConfigs: DashboardCourseConfigType[], courseSlug: string 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/vbv_lernwelt/dashboard/person_export.py b/server/vbv_lernwelt/dashboard/person_export.py index 0c7e5eae..e9a0495a 100644 --- a/server/vbv_lernwelt/dashboard/person_export.py +++ b/server/vbv_lernwelt/dashboard/person_export.py @@ -78,6 +78,8 @@ def _add_rows( 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): @@ -91,17 +93,19 @@ def _add_rows( user_id=user["user_id"], course_session_id=course_session_id, ) + idx_offset += 1 continue user_role = (user_cs.get("user_role"),) + idx = row_idx - idx_offset - sheet.cell(row=row_idx, column=1, value=user["first_name"]) - sheet.cell(row=row_idx, column=2, value=user["last_name"]) - sheet.cell(row=row_idx, column=3, value=user["email"]) - sheet.cell(row=row_idx, column=4, value=user.get("Lehrvertragsnummer", "")) + 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=row_idx, + row=idx, column=5, value=user.get("phone", ""), ) - sheet.cell(row=row_idx, column=6, value=user_role[0]) + sheet.cell(row=idx, column=6, value=user_role[0]) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 290397fd..11f58563 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -424,6 +424,7 @@ def export_persons_as_xsl(request): ) # noqa data = export_persons( + request.user, [cswr.id for cswr in course_sessions_with_roles], ) return _make_excel_response(data, PERSONS_EXPORT_FILENAME) From dc4af21e0020c2dc4a6f5ca7c73fd8c751e04d48 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 16 Jul 2024 16:07:32 +0200 Subject: [PATCH 05/10] Add translations in backend --- server/locale/de/LC_MESSAGES/django.po | 26 ++++++++- server/locale/fr/LC_MESSAGES/django.mo | Bin 2743 -> 2855 bytes server/locale/fr/LC_MESSAGES/django.po | 25 ++++++++- server/locale/it/LC_MESSAGES/django.mo | Bin 2646 -> 2746 bytes server/locale/it/LC_MESSAGES/django.po | 25 ++++++++- .../vbv_lernwelt/dashboard/person_export.py | 15 ++++- .../dashboard/tests/test_export.py | 53 ++++++++++++++++-- 7 files changed, 132 insertions(+), 12 deletions(-) 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 57810233a1dc8e67621a4022cb8761f6c74a6e9b..31cad5f5592e86e242942c10a9703903a3f25e9f 100644 GIT binary patch delta 917 zcmZY7KWGzC9Ki9PN$o|eQDYmct@T>#P*96omk#kCw1bow)E1XByqeSN-Ep}}2O+3~ zn?p|*!9|1;!KsRFqH%F_6>+eOMMV%N3+ngRT&Movm(P2-m-pW9mwVZ>RBU`MbUYAZ zknAUiw}_15(F`BNSIprmcHuAFhJSG@=CUF?F^@X$M_o6B1w4-W`x54H5_jQrd)^Q? zU9f<<!7bd5chSPd)Q72$ag6Vu;$d9DlW28_OyDHW;C&p%pLia-+58CBuov&)K52+7 zG0=)$qu%&E>J2}kcI7*k=t{n`UC9);()H7*7p(HZ8FC%9@(@`nH&H9TgG0EO&R^gT z=I=1a`SKxs@Da80Rpd<hjk@8V)K2<XWL`kNk`X+OC-4~Fzzg^UFXI~4@DjH@gKscI zi$k<C3)s-XBL+6UMcsG>wFBQ$*RYRy7q_7iGC=A&jlCr22(JU}6xl-_BKMOTHmTKa zgmy|p_am;YSEy~*iU&z;nZ8in<ZiNu<oxx@VN>WuwcQ%}w`{~g2KquDAbCZkH+hyB zPkv-ZlD_P~SlL{41K;?babpun+0^H~*ombac%G4paXsJE=6F&uo|z4NsYH(Jn`rrB z_DePyw@S%vt9Xu|g@Ippu6V{iH6i6zv%O87;dAzxa-!I+x}oF8QfW24Rx_+|s+<j? VIVU#rkv(d+$B|n#%df0|ng5h$b5sBT delta 822 zcmXxiv1=1i9Ki9Pxtc_qnl^3Prq+0=sTd)PI9MS=N*r3mv?+*)f=4*XLAV6(Vh6!M zH|Zc4M8&CvQo59u4x->75eFwHM+d1OLjM3K{r;}Wk9+U)?(*)J_j~WQ$KNH}KN15k zg*Z=}rj4Bt$zd!+N9<t~_i+$E;3@oqC-EB&;rHJCA#%wdI-P%n3H*ziCuWEYU=q)W zv}KAxoCi5f;vDM4GDh$YUc(1?0pH*}e#8aTY<FQ1v$%yT_!Va{%_=9cgcEolwSX2T z_<w0L&;`7}3~pkPUHFW;kT9#z7viW3O3{&9GN=_^#4uh#)+BRCWm3QyyxF^N^nQPe zF`lnsl>e9K47Bpscp7(+OWxC!@Dt`S%xZ4qRlJAmSi|3V1D9E46JKE#qa;)FXkrC5 zuZLRj6Kv~Xoxxdrixe(XG!60wk)qc#)?i9dy(a31=HPgP??=yCtJi!Q=Z*=!FD+7c zh>Z=7Vwg<}wnFFVR_Zy8(ne^y<^33Apj(`#jdy;93ZX@-lQAwEw&Qjd3^QGB`cA{O z>b6;QS8dOBYqsM{#r9ft-|`POjGu;4ZaJN`$o%zN4Of;d&ul*G?l9Ar?z3vG(dzEV b((#k3Z9cTB!JE!wTPjQoZoGp_(SM=;x;0%5 diff --git a/server/locale/fr/LC_MESSAGES/django.po b/server/locale/fr/LC_MESSAGES/django.po index f18ecf09..3498fd8e 100644 --- a/server/locale/fr/LC_MESSAGES/django.po +++ b/server/locale/fr/LC_MESSAGES/django.po @@ -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" @@ -160,6 +161,28 @@ msgstr "E-mail" msgid "Lehrvertragsnummer" msgstr "Numéro de contrat d'apprentissage" +#: vbv_lernwelt/dashboard/person_export.py:16 +#, fuzzy +#| msgid "export_anwesenheit" +msgid "export_personen" +msgstr "export_presence" + +#: 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 a046bd9415926ec35a412e4d2314c0f5562689d1..2ec499c4d39874cedc1aaaddc627f7324c823099 100644 GIT binary patch delta 915 zcmZwFPi#z46vy#nrc5cLYD(#-Dea;nEH#lXRDw1ZH6{_=J<V<Vn(Mvs<`o+mVqr&O zu&@#f5{aaVjj*t?u&}klf&~(d#L}Yi{dHcgoXmNjd*7e?=gj?3dez(f+|zO2h(WSU z4y`jgj0baEi0{~mKd=jb<3?P;_1Kv=+k!<@zl?fsANJrO)c41*h?BS#&$s(c3$qul zq6WBuoA4GE@NVYA%*Qy&{b#rzzv4I+I?N_;5~uMV4&g66izNmh#(C_++ql!3W>0Bo zM{iIoeurA&M^sgQ;y6_)ZfNi146D+PrZZ=dIn1MWehK-+u4Vn3SfPIh`PmYe&G=G% z=C`*rG~s*HLO!7e_=b9M88zV_<Y#$0ny?FxVmA)sX*`7qsv@uP3a;P?US{wBAK@;{ zQRe0W8f6-~aT0mg0_??w%<HI9KR^m$1EdzB*iPyjjsxX1+eYpsHJ+kEY7=Xrg)92o zEUcDHW!H`eNgWa$s1n&tY9Y*PJIMcqD#t&w8mit}?4h9py_@8SSYPXTZmhMO8)^0D z2S%%IE{r1=c^9TGv8rn<`ZP$b8hh`o=0YF2`HMWMIqzoU$ZAOtMlN}AF8?Lp8Y>)` ziYoQQHC&WBI}vyvrm;q;$5A7=5PDZRK4I0cJt|KYV;|d8kfd%VtOxoi8#w+q@{__+ F?k}kuZR-F4 delta 814 zcmX}q&r1|x9LMo*JL;NkYU{4Q(sisv)}vjN^pJ?O9y0V`1VM8cXkwXJbQZzOf=7=o z9uy1;qKgN^O9-Bd4xT!B>`)0I_y=?ddVgjYzsxhQXNG5<=lgu0Imw^rYCp0qyGD%A zhG@APW<|`TxDiL#j-T)rp5RUVhOPJ=Z{vCM`8RUeB{z+~!Yuwp&69D=S}>2DW;Gk6 z)5VJ-=5ZP|a0%1+9H;OV-p4(h!&5AwW_tmP_y9lP3ZCH@7Ra(6=dll8qBc;)EbCj1 zjuP0xQGAbcRN*Tsku+Jz?xq9C+;$JO!*OJ&Jw$S{>E`=cba^fzm%ZUe5o`@JSk-&h zw=Fs<(Kafx1JnW^@ev;482-U0*hx03z$$LwJ{B>{#~#k$0KUZu{M>y0g$%WSs7kkW zuz7Wc=%}=l$i{7urqE!8Ces-!7?LDMF4v*zX=nO>v=PqL?$Q*)4U+#$YpFu2mX4yE zC6iLnPfDqC>Y?4Ck>qvc=(W)bwBGtfYBsg#)kmEP#}C4K$#Dxyl{kn(uk5>z!wrAk z4`2I1Y|H+7wH$l#@jK_I<E(r+tZ${KChchyx|KDJihWz~%H<%A>{;?+({rQBW@9Q_ N4jP|F#~tlQssGU9R_Oo$ diff --git a/server/locale/it/LC_MESSAGES/django.po b/server/locale/it/LC_MESSAGES/django.po index 13e9a24a..8aa12dea 100644 --- a/server/locale/it/LC_MESSAGES/django.po +++ b/server/locale/it/LC_MESSAGES/django.po @@ -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" @@ -160,6 +161,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_presenza" + +#: 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 index e9a0495a..67dfb1b0 100644 --- a/server/vbv_lernwelt/dashboard/person_export.py +++ b/server/vbv_lernwelt/dashboard/person_export.py @@ -96,7 +96,7 @@ def _add_rows( idx_offset += 1 continue - user_role = (user_cs.get("user_role"),) + 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"]) @@ -108,4 +108,15 @@ def _add_rows( column=5, value=user.get("phone", ""), ) - sheet.cell(row=idx, column=6, value=user_role[0]) + 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 index ed48761e..c00d1fef 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_export.py +++ b/server/vbv_lernwelt/dashboard/tests/test_export.py @@ -1,5 +1,6 @@ import io +from django.utils.translation import activate from openpyxl import load_workbook from vbv_lernwelt.core.constants import ( @@ -45,7 +46,7 @@ class PersonsExportTestCase(ExportBaseTestCase): self.test_student1.email, None, None, - "MEMBER", + "Teilnehmer", ] self.test_student2_row = [ self.test_student2.first_name, @@ -53,7 +54,7 @@ class PersonsExportTestCase(ExportBaseTestCase): self.test_student2.email, None, None, - "MEMBER", + "Teilnehmer", ] self.test_student3_row = [ self.test_student3.first_name, @@ -61,7 +62,7 @@ class PersonsExportTestCase(ExportBaseTestCase): self.test_student3.email, None, None, - "MEMBER", + "Teilnehmer", ] self.test_trainer1_row = [ self.test_trainer1.first_name, @@ -69,7 +70,7 @@ class PersonsExportTestCase(ExportBaseTestCase): self.test_trainer1.email, None, None, - "EXPERT", + "Trainer", ] self.test_trainer2_row = [ self.test_trainer2.first_name, @@ -77,7 +78,7 @@ class PersonsExportTestCase(ExportBaseTestCase): self.test_trainer2.email, None, None, - "EXPERT", + "Trainer", ] def _generate_expected_data(self, rows): @@ -139,3 +140,45 @@ class PersonsExportTestCase(ExportBaseTestCase): wb.active = wb["Test Zürich 2022 a"] self._check_export(wb, [[None] * 6], 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") From 35e5067331d0d23223bf1e1c52221a45161eac93 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 16 Jul 2024 16:17:12 +0200 Subject: [PATCH 06/10] Add cypress tests --- cypress/e2e/dashboard/dashboardExport.cy.js | 16 ++++++++++++---- server/locale/fr/LC_MESSAGES/django.mo | Bin 2855 -> 2904 bytes server/locale/fr/LC_MESSAGES/django.po | 3 +-- server/locale/it/LC_MESSAGES/django.po | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/dashboard/dashboardExport.cy.js b/cypress/e2e/dashboard/dashboardExport.cy.js index 565955db..de0be194 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("/statistic/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("/statistic/dashboard/persons", "export_personen"); + }); }); }); diff --git a/server/locale/fr/LC_MESSAGES/django.mo b/server/locale/fr/LC_MESSAGES/django.mo index 31cad5f5592e86e242942c10a9703903a3f25e9f..bc305be390a05cf0208370290b7a2d2789bf66f5 100644 GIT binary patch delta 628 zcmY+=F-SsD6oBF5Q!6vIBvdq{%!<egoEj=9if{@E8YE~EYSD)_=;Y86v=m`AL_-BZ zqYgpP*4`A<5)_Rs(NYoAf9NhfxbM5?zWdI(_ne3BL$$}Ct1dhVMh_$95E;iwix<xa zw&5pw(Q=A-(2s76pbz6FPND8gV-TlN|IeZya~Qz&zqlq<vtbK0;V!o00k-0a@xs`^ z3D&Q$AK!5n!!D5w<}in6n8Y7k!X(KDu!0dh#<0{xE}7_!Zc$JCfO^7L?89#p$6G{Z z=*}4GjciV*|5Z^By2*=LvW?n-9b~H<q2Bxy6WB2E9d=M(UYKaYH}l~OwF4d&sV&{8 z38ThA3=yZ1t&+hx%;G5S<1*gh3dT6)8dh)~pU}o3icD#X4w<Oe*o)7oiJer{PI!$m tj1iAuCob#_TZ@iFvuf3yfqca-mNr)He5qV4<O@w-OVr)<Z?TXs%P;5SK-&NS delta 578 zcmXZYF-QVY7{Kw*Q}3><3y!#w9Lj76)M%^Gs38}FATruoj25SG2##oo76;2EXo)~* zmF6f~8rveG#U&{SqA4NzfA|jW{oeQP-Fx5nJqN8IeDh;X;hEx1@n#|-bC|Ulc;3;) zE++6B2k;N$=tf0`(L;?>sQc3BV;=SY0(w})5iIxPP<G9R8tMUuIEcsSV8eKByu=08 zuW=eXxP(qjWEG3Jg=d(-FI>k_4xhmaj$<9iBot{e(TZ+SGk!$P@D;TypID$Pp53ox zjjVKk88yKj2JVnO)XJ;KRyjbe_z2V3F!2oz5kH{I{qkf!yr5RzMedXz)B}HwHhm0; zePl^yaT(_^hx@pJSGbKmtl%cuuHYS3(cu#9Obv57xnMGh_oxSVP&@Ep?BN7);&jm} PMWlUVHKXmkos9njRMRz? diff --git a/server/locale/fr/LC_MESSAGES/django.po b/server/locale/fr/LC_MESSAGES/django.po index 3498fd8e..89cf1fae 100644 --- a/server/locale/fr/LC_MESSAGES/django.po +++ b/server/locale/fr/LC_MESSAGES/django.po @@ -162,10 +162,9 @@ msgid "Lehrvertragsnummer" msgstr "Numéro de contrat d'apprentissage" #: vbv_lernwelt/dashboard/person_export.py:16 -#, fuzzy #| msgid "export_anwesenheit" msgid "export_personen" -msgstr "export_presence" +msgstr "export_personnes" #: vbv_lernwelt/dashboard/person_export.py:68 msgid "Telefon" diff --git a/server/locale/it/LC_MESSAGES/django.po b/server/locale/it/LC_MESSAGES/django.po index 8aa12dea..4e1712ac 100644 --- a/server/locale/it/LC_MESSAGES/django.po +++ b/server/locale/it/LC_MESSAGES/django.po @@ -165,7 +165,7 @@ msgstr "Numero di contratto di tirocinio" #, fuzzy #| msgid "export_anwesenheit" msgid "export_personen" -msgstr "esportazione_presenza" +msgstr "esportazione_persone" #: vbv_lernwelt/dashboard/person_export.py:68 msgid "Telefon" From 086f0b7fcb555b7bae5d87909bcc339c8205d983 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 16 Jul 2024 16:24:34 +0200 Subject: [PATCH 07/10] Use phone_number field --- server/vbv_lernwelt/dashboard/person_export.py | 2 +- server/vbv_lernwelt/dashboard/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/person_export.py b/server/vbv_lernwelt/dashboard/person_export.py index 67dfb1b0..dacd4272 100644 --- a/server/vbv_lernwelt/dashboard/person_export.py +++ b/server/vbv_lernwelt/dashboard/person_export.py @@ -106,7 +106,7 @@ def _add_rows( sheet.cell( row=idx, column=5, - value=user.get("phone", ""), + value=user.get("phone_number", ""), ) sheet.cell(row=idx, column=6, value=user_role) diff --git a/server/vbv_lernwelt/dashboard/utils.py b/server/vbv_lernwelt/dashboard/utils.py index 8967eb90..d453ae91 100644 --- a/server/vbv_lernwelt/dashboard/utils.py +++ b/server/vbv_lernwelt/dashboard/utils.py @@ -106,7 +106,7 @@ def create_person_list_with_roles( "course_sessions": [], } if include_private_data: - user_data["phone"] = user_object.additional_json_data.get("phone", "") + user_data["phone_number"] = user_object.phone_number user_data["Lehrvertragsnummer"] = user_object.additional_json_data.get( "Lehrvertragsnummer", "" ) From efdfb0bf03b086f35b3c3e2169c160627449c763 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 16 Jul 2024 16:42:51 +0200 Subject: [PATCH 08/10] Use correct url --- cypress/e2e/dashboard/dashboardExport.cy.js | 4 ++-- server/vbv_lernwelt/dashboard/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/dashboard/dashboardExport.cy.js b/cypress/e2e/dashboard/dashboardExport.cy.js index de0be194..6db7447c 100644 --- a/cypress/e2e/dashboard/dashboardExport.cy.js +++ b/cypress/e2e/dashboard/dashboardExport.cy.js @@ -64,7 +64,7 @@ describe("dashboardExport.cy.js", () => { }); it("should download the person export", () => { - testExport("/statistic/dashboard/persons", "export_personen"); + testExport("/dashboard/persons", "export_personen"); }); }); @@ -89,7 +89,7 @@ describe("dashboardExport.cy.js", () => { }); it("should download the person export", () => { - testExport("/statistic/dashboard/persons", "export_personen"); + testExport("/dashboard/persons", "export_personen"); }); }); }); diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 11f58563..d1f57e25 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -461,7 +461,7 @@ def _get_course_sessions_with_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 + and csr.id in requested_cs_ids ] # noqa return all_cs_roles_for_user From 46760cf8aecda74bcefa568083da3beaccad52f5 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 30 Jul 2024 11:36:08 +0200 Subject: [PATCH 09/10] Fix tests --- server/locale/fr/LC_MESSAGES/django.mo | Bin 2904 -> 3036 bytes server/locale/fr/LC_MESSAGES/django.po | 4 +--- server/locale/it/LC_MESSAGES/django.mo | Bin 2746 -> 2874 bytes server/locale/it/LC_MESSAGES/django.po | 3 +-- .../dashboard/tests/test_export.py | 4 +++- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/server/locale/fr/LC_MESSAGES/django.mo b/server/locale/fr/LC_MESSAGES/django.mo index bc305be390a05cf0208370290b7a2d2789bf66f5..5eac9c775859eace60203a0b7dbd0027adce5ab9 100644 GIT binary patch delta 947 zcmXxjJ!lj`7{>9}-P22=(HPB#@uKTT8b!n+U?cHj1Wl2I2!V(Y*JF=cIQ9;+ry|5_ z1c@LPr(hul1dGJNA|TiaSBY3E7LwF9f`y8p;Q!fOhs^wDCbKi|yu0_g|I0vgqc`{1 zC?nJYbz+OzC{`UB%30T}1B<vFC$SUfa4TNLZY*cx81?9F)cPgt#bwk!PcertaHm<* z*68eF;tlrUyYwHniF)80>Y!iPg?}-R9qb}|G6(TAV-JtuJdWcMUc|R}8N1l*C<b_m z^X)#J5hgZp5Odqi25=a4(^1q7#!!LI;vu}2jqhTStUW>n{E4Xa+$Ji(uQZ%(-%%O) zi7YjT#V+StCmm8@-FO%a*?3F~7*F92oJIa@fkv6Cpq{&f%FF}g4)z3f(6h`n>}UKM z70?Hqz>nBGL1&0?=kNkv!4<6FA3To%yVvn0Ds=^RmqqljjN14fDnk!5S8zY$SJ;E^ zkwhig1_i78o9(0SO?qiVV78me`ROZ3r;}THSzkBOrbASvQ<>6hQAYMq4^oGzDx8}H zm<(u?J$<xh=gba(()+(CV%<(R(Jw+pAC$fc6@3dTdQ~b;>xXm3ne<zy++%JKMy+|* z>zl5}VJ-5jfme)f28%&d3Bq{&mU}$sPB+5V^ZfY9saj++{-RgE-d<%vZ0+SzrPgS# Y+DtlA3cRvkYE)xC4sQhOzw-<3KYRvXt^fc4 delta 802 zcmZY7IV?m`6vpwx8;pIQ!7#=id!kZ^WQ2$kiDDvZIwb|K&`31o#Zrg@6&-~{qSLD+ zBtnQ%BN{}){~K53Bxip2zPax%=iR&LO~k)13vC+FNY;{F8D_mWkjW46j$!=3e9ZLB za<K@runG$??)qJ*aeY{ZgQ)k%u?SOGjB~$z-xl3~Rn&wVn1|b#gZs`S=Lz=n{2ZI` z4TmrqG8@AbrtlD(@f#<xnaNvl0jqEiqvo5P(a?#mP&>Xw?eGN~@XPh%*=9+q(}OzE zJiF6-i>QsR@WWcRhN{3iGSqfaCqKYOJaPSNEMR?mrlAR6-5;N*3gq&TwXFg*VU4pD zBlP=_p*DuYIF8-8g;RKm(->oyv$%jG_=xk^#v;B-v_nH)U;>{|6ML+xO5{6Z7^B~b zB{+%{#NwpJE6PdMHLeTgH7h0S$!b!OAPdy^4L@BnwKcKI!<^~Qpp!R{DxEG^1*t_V zN%ohn0Q?JWSY=n}^ZZ2}4PEdWk}H$G1C|Viy#8RxYY&dS`00b!5ef>!$zUQJIb9CV Fct5kxJRtx8 diff --git a/server/locale/fr/LC_MESSAGES/django.po b/server/locale/fr/LC_MESSAGES/django.po index 89cf1fae..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 <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -121,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" @@ -162,7 +161,6 @@ msgid "Lehrvertragsnummer" msgstr "Numéro de contrat d'apprentissage" #: vbv_lernwelt/dashboard/person_export.py:16 -#| msgid "export_anwesenheit" msgid "export_personen" msgstr "export_personnes" diff --git a/server/locale/it/LC_MESSAGES/django.mo b/server/locale/it/LC_MESSAGES/django.mo index 2ec499c4d39874cedc1aaaddc627f7324c823099..5bde8f5a931fff1a90c4e72de25d612347c1e944 100644 GIT binary patch delta 924 zcmX}rziSh57{~Facd@C}*4mn=tv@bp1KOqjpio*J%utM=7K;v|!+}Q|NiIPTiWv;l zQ3oM(DgFb3L!m>lli(miQE+i9h=?fEK}6Ka_vh~PA>Vs_@0;B7eeSu-Ms6eD+Un0N z8!^f_#5mJoHi0)B4#Wx9tP4-#E}X)hID<QI7JKkodVULa>21{SYuJzXP<b9<2A8qd ztYs@q_OS32bND>@!rr1T*g|de8M|>Cv-m4j*>v88lh}tNSjH08u#BsC9sl4cUM1-q z`&*Mqk%c#S0Ds_q?CDJIw1~Qcaa5rtEZ`L^(X9wo=sH!%7pbq2z3d&T6Yr7K_7SPw zzG$8OZJUXQzmbm(l0_{X#@$#%t)D^_GKDJSBI;4xL?xa_{l12LtjR&wJwO%m5U24m z7I70>=b3zGqE=0lZ4vL|ar}usdi4DuF5wtHNzY#)m)Zx^qx+hw+tnk;BMtf=DpH|b z`{J4UPbkS=2K&X2K-DV97k5VUJfn{>$e`nK3k%3gWCM(&jKd6t+O0eP7b;FKsp1Gj zT}tA#*$^|m5NfIZ0SZ;GcR-<*D~1!yX@7FgIv4zQ!5wphO4u&DUhZ-us?<Zj8hGcz z#o%rbE(DcmZNVMNxN|R`wU@G!$Lvx)^cr{cWfa(qU#(W6y3NK5P2a0Gnn{MW&FnYl EFT2-Tk^lez delta 796 zcmZwFy>C)c7{~FaTof#dh=7QQP+`FMGTOvA$WSd|F;21(LsAH(6-nErxf%yIO}y%$ zOXFhVWWr)_=;YvF{0FRygPMpYjvYw9Ke*j|!gD|8+?;z}9?mc|OuFCV!RJO4$Shf$ zF}sB413ZYI7{*^1!GCxN$9Mq4j@c26qWW3XxH*jDMb!7#Fp5<?inphI*INF74b%h= za26k72%q}C^nH!<ynl;l@jF&96f|4JD%SBC7V$6M!UU6-a0%1+7*CjM_J)Q|w2j*F zC)5tVqN?&6D^w+VaC(xP>`Eu9``$s;uny|vcacwQ)9*jTJpCug&))GkjPKQFefvm5 z3w}mzWCu0D57fXBYQbIPXAT`L7{MzT!xGl;2KG=D`GD&<#xkxkc>}lbBnBw6dyYny zhF)Ap###fD*!I1TD)kGb5SAmg5yf#*-*6o$r`a*`45|4Pc~Xbi4{cm=YD#-jGL>B? zE|9t;x=;x+MrtFhYbo-7p~~^kOhVP$kJB`Cp)({`#M0gc=c>2tl)YC@Zt%s)1iew{ ba=e>g?KST;yZxpuG&-GDe`Qb${|W2?GlfD5 diff --git a/server/locale/it/LC_MESSAGES/django.po b/server/locale/it/LC_MESSAGES/django.po index 4e1712ac..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 <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -121,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" diff --git a/server/vbv_lernwelt/dashboard/tests/test_export.py b/server/vbv_lernwelt/dashboard/tests/test_export.py index c00d1fef..565d0e48 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_export.py +++ b/server/vbv_lernwelt/dashboard/tests/test_export.py @@ -139,7 +139,9 @@ class PersonsExportTestCase(ExportBaseTestCase): self.assertEqual(wb.sheetnames[0], "Test Zürich 2022 a") wb.active = wb["Test Zürich 2022 a"] - self._check_export(wb, [[None] * 6], 1, 6) + data = self._generate_expected_data([[None] * 6]) + + self._check_export(wb, data, 1, 6) def test_export_in_fr(self): activate("fr") From af84a0ee01bfdef599508ad9a29cb3b269992bdf Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Wed, 31 Jul 2024 11:14:19 +0200 Subject: [PATCH 10/10] Add return type --- server/vbv_lernwelt/dashboard/person_export.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/person_export.py b/server/vbv_lernwelt/dashboard/person_export.py index dacd4272..0a0d76aa 100644 --- a/server/vbv_lernwelt/dashboard/person_export.py +++ b/server/vbv_lernwelt/dashboard/person_export.py @@ -1,4 +1,5 @@ from io import BytesIO +from typing import Optional import structlog from django.utils.translation import gettext_lazy as _ @@ -22,8 +23,8 @@ def export_persons( user: User, course_session_ids: list[str], save_as_file: bool = False, -): - if len(course_session_ids) == 0: +) -> Optional[bytes]: + if not course_session_ids: return wb = Workbook()