From 296644ff22faca08bb1f143e80d8df12881ae2f4 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 15 May 2024 14:25:42 +0200 Subject: [PATCH 01/24] wip: Export attendance data (no circle data) --- .../management/commands/export_attendance.py | 18 +++ .../course_session/services/export.py | 126 ++++++++++++++++++ server/vbv_lernwelt/feedback/services.py | 50 +++---- 3 files changed, 162 insertions(+), 32 deletions(-) create mode 100644 server/vbv_lernwelt/course_session/management/commands/export_attendance.py create mode 100644 server/vbv_lernwelt/course_session/services/export.py diff --git a/server/vbv_lernwelt/course_session/management/commands/export_attendance.py b/server/vbv_lernwelt/course_session/management/commands/export_attendance.py new file mode 100644 index 00000000..7d914120 --- /dev/null +++ b/server/vbv_lernwelt/course_session/management/commands/export_attendance.py @@ -0,0 +1,18 @@ +import djclick as click +import structlog + +from vbv_lernwelt.course_session.services.export import export_attendance + +logger = structlog.get_logger(__name__) + + +@click.command() +@click.argument("course_session_id") +@click.option( + "--save-as-file/--no-save-as-file", + default=True, + help="`save-as-file` to save the file, `no-save-as-file` returns bytes. Default is `save-as-file`.", +) +def command(course_session_id, save_as_file): + # using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function + export_attendance([course_session_id, 14], save_as_file) diff --git a/server/vbv_lernwelt/course_session/services/export.py b/server/vbv_lernwelt/course_session/services/export.py new file mode 100644 index 00000000..1078541e --- /dev/null +++ b/server/vbv_lernwelt/course_session/services/export.py @@ -0,0 +1,126 @@ +from datetime import datetime +from io import BytesIO +from itertools import groupby + +import structlog +from openpyxl import Workbook + +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse + +logger = structlog.get_logger(__name__) + + +def export_attendance(course_session_ids: list[str], save_as_file: bool, circles=None): + wb = Workbook() + + # remove the first sheet is just easier than keeping track of the active sheet + wb.remove(wb.active) + + # get attencdance courses for course sessions and circles + # get users for each course session + # set headers + # sheets group, cs or generation? + + cs_users = CourseSessionUser.objects.filter(course_session_id__in=course_session_ids, + role=CourseSessionUser.Role.MEMBER).order_by("course_session", + "user__last_name", + "user__first_name") + attendance_courses = CourseSessionAttendanceCourse.objects.filter( + course_session_id__in=course_session_ids).order_by("course_session", "due_date") + + grouped_cs_users = {key: list(group) for key, group in + groupby(sorted(cs_users, key=lambda x: x.course_session.title), + key=lambda x: x.course_session.title)} + grouped_attendance_course = {key: list(group) for key, group in + groupby(sorted(attendance_courses, key=lambda x: x.course_session.title), + key=lambda x: x.course_session.title)} + + for course_session, cs_users in grouped_cs_users.items(): + logger.debug( + "export_attendance_for_course_session", + data={ + "course_session": course_session, + }, + label="attendance_export", + ) + _create_sheet(wb, course_session, cs_users, grouped_attendance_course[course_session]) + + if save_as_file: + wb.save(make_export_filename()) + else: + output = BytesIO() + wb.save(output) + + output.seek(0) + return output.getvalue() + + +def _create_sheet(wb: Workbook, title: str, users: list[CourseSessionUser], + attendance_courses: list[CourseSessionAttendanceCourse]): + sheet = wb.create_sheet(title=sanitize_sheet_name(title)) + + if len(users) == 0: + return sheet + + # headers + # firstname, lastname, email, lehrvertragsnummer, , status , .. + # todo: translate headers + sheet.cell(row=1, column=1, value="Vorname") + sheet.cell(row=1, column=2, value="Nachname") + sheet.cell(row=1, column=3, value="Email") + sheet.cell(row=1, column=4, value="Lehrvertragsnummer") + + col_idx = 5 + attendance_data = {} + for course in attendance_courses: + course_title = course.get_circle().title + sheet.cell(row=1, column=col_idx, + value=f"Anwesenheit {course_title} {course.due_date.start.strftime('%d.%m.%Y')}") + user_dict_map = {d['user_id']: d for d in course.attendance_user_list} + attendance_data[course_title] = user_dict_map + + col_idx += 1 + + _add_rows(sheet, users, attendance_data) + + return sheet + + +def _add_rows(sheet, users: list[CourseSessionUser], attendance_data): + for row_idx, user in enumerate(users, start=2): + sheet.cell(row=row_idx, column=1, value=user.user.first_name) + sheet.cell(row=row_idx, column=2, value=user.user.last_name) + sheet.cell(row=row_idx, column=3, value=user.user.email) + sheet.cell(row=row_idx, column=4, value=user.user.additional_json_data.get("Lehrvertragsnummer", "")) + + col_idx = 5 + for key, user_dict_map in attendance_data.items(): + user_dict = user_dict_map.get(str(user.user.id), {}) + status = user_dict.get("status", "") if user_dict else "" + status_text = "Anwesend" if status == "PRESENT" else "Nicht anwesend" + sheet.cell(row=row_idx, column=col_idx, value=status_text) + col_idx += 1 + + +def make_export_filename(name: str = "attendance_export"): + today_date = datetime.today().strftime("%Y-%m-%d") + return f"{name}_{today_date}.xlsx" + + +def sanitize_sheet_name(text, default_name="DefaultSheet"): + if text is None: + return default_name + + prohibited_chars = ["\\", "/", "*", "?", ":", "[", "]"] + for char in prohibited_chars: + text = text.replace(char, "") + + text = text.strip("'") + + text = text[:31] + + if len(text) == 0: + return default_name + + return text diff --git a/server/vbv_lernwelt/feedback/services.py b/server/vbv_lernwelt/feedback/services.py index b593992a..b3ada7aa 100644 --- a/server/vbv_lernwelt/feedback/services.py +++ b/server/vbv_lernwelt/feedback/services.py @@ -1,4 +1,3 @@ -from datetime import datetime from io import BytesIO from itertools import groupby from operator import attrgetter @@ -11,6 +10,7 @@ from openpyxl import Workbook from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession from vbv_lernwelt.course.services import mark_course_completion +from vbv_lernwelt.course_session.services.export import make_export_filename, sanitize_sheet_name from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.learnpath.models import ( LearningContentFeedbackUK, @@ -149,16 +149,25 @@ def initial_data_for_feedback_page( return {} -def export_feedback(course_session_ids: list[str], save_as_file: bool): +def export_feedback(course_session_ids: list[str], save_as_file: bool, circles=None): wb = Workbook() # remove the first sheet is just easier than keeping track of the active sheet - wb.remove_sheet(wb.active) + wb.remove(wb.active) - feedbacks = FeedbackResponse.objects.filter( - course_session_id__in=course_session_ids, - submitted=True, - ).order_by("circle", "course_session", "updated_at") + if circles: + feedback_unordered = FeedbackResponse.objects.filter( + course_session_id__in=course_session_ids, + circle__in=circles, + submitted=True, + ) + else: + feedback_unordered = FeedbackResponse.objects.filter( + course_session_id__in=course_session_ids, + submitted=True, + ) + + feedbacks = feedback_unordered.order_by("circle", "course_session", "updated_at") grouped_feedbacks = groupby(feedbacks, key=attrgetter("circle")) for circle, group_feedbacks in grouped_feedbacks: @@ -175,7 +184,7 @@ def export_feedback(course_session_ids: list[str], save_as_file: bool): _create_sheet(wb, circle.title, group_feedbacks) if save_as_file: - wb.save(make_export_filename()) + wb.save(make_export_filename(name="feedback_export")) else: output = BytesIO() wb.save(output) @@ -185,7 +194,7 @@ def export_feedback(course_session_ids: list[str], save_as_file: bool): def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]): - sheet = wb.create_sheet(title=_sanitize_sheet_name(title)) + sheet = wb.create_sheet(title=sanitize_sheet_name(title)) if len(data) == 0: return sheet @@ -208,24 +217,6 @@ def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]): return sheet -def _sanitize_sheet_name(text, default_name="DefaultSheet"): - if text is None: - return default_name - - prohibited_chars = ["\\", "/", "*", "?", ":", "[", "]"] - for char in prohibited_chars: - text = text.replace(char, "") - - text = text.strip("'") - - text = text[:31] - - if len(text) == 0: - return default_name - - return text - - def _add_rows(sheet, data, question_data): for row_idx, feedback in enumerate(data, start=2): sheet.cell(row=row_idx, column=1, value=feedback.course_session.title) @@ -237,11 +228,6 @@ def _add_rows(sheet, data, question_data): sheet.cell(row=row_idx, column=col_idx, value=response) -def make_export_filename(name: str = "feedback_export"): - today_date = datetime.today().strftime("%Y-%m-%d") - return f"{name}_{today_date}.xlsx" - - # used as admin action, that's why it's not in the views.py def get_feedbacks_for_course_sessions(_modeladmin, _request, queryset): file_name = "feedback_export_durchfuehrungen" From 984513b3a2d7eb1750248e12d6f3db2a7a449955 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 15 May 2024 14:50:53 +0200 Subject: [PATCH 02/24] Filter export by circles if present --- .../management/commands/export_attendance.py | 2 +- .../course_session/services/export.py | 79 +++++++++++++------ server/vbv_lernwelt/feedback/services.py | 5 +- 3 files changed, 58 insertions(+), 28 deletions(-) diff --git a/server/vbv_lernwelt/course_session/management/commands/export_attendance.py b/server/vbv_lernwelt/course_session/management/commands/export_attendance.py index 7d914120..064d16e6 100644 --- a/server/vbv_lernwelt/course_session/management/commands/export_attendance.py +++ b/server/vbv_lernwelt/course_session/management/commands/export_attendance.py @@ -15,4 +15,4 @@ logger = structlog.get_logger(__name__) ) def command(course_session_id, save_as_file): # using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function - export_attendance([course_session_id, 14], save_as_file) + export_attendance([course_session_id], save_as_file) diff --git a/server/vbv_lernwelt/course_session/services/export.py b/server/vbv_lernwelt/course_session/services/export.py index 1078541e..c6703f4d 100644 --- a/server/vbv_lernwelt/course_session/services/export.py +++ b/server/vbv_lernwelt/course_session/services/export.py @@ -1,3 +1,4 @@ +import typing from datetime import datetime from io import BytesIO from itertools import groupby @@ -11,30 +12,35 @@ from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse logger = structlog.get_logger(__name__) -def export_attendance(course_session_ids: list[str], save_as_file: bool, circles=None): +def export_attendance( + course_session_ids: list[str], save_as_file: bool, circle_ids: list[int] = None +): wb = Workbook() # remove the first sheet is just easier than keeping track of the active sheet wb.remove(wb.active) - # get attencdance courses for course sessions and circles - # get users for each course session - # set headers - # sheets group, cs or generation? - - cs_users = CourseSessionUser.objects.filter(course_session_id__in=course_session_ids, - role=CourseSessionUser.Role.MEMBER).order_by("course_session", - "user__last_name", - "user__first_name") + cs_users = CourseSessionUser.objects.filter( + course_session_id__in=course_session_ids, role=CourseSessionUser.Role.MEMBER + ).order_by("course_session", "user__last_name", "user__first_name") attendance_courses = CourseSessionAttendanceCourse.objects.filter( - course_session_id__in=course_session_ids).order_by("course_session", "due_date") + course_session_id__in=course_session_ids + ).order_by("course_session", "due_date") - grouped_cs_users = {key: list(group) for key, group in - groupby(sorted(cs_users, key=lambda x: x.course_session.title), - key=lambda x: x.course_session.title)} - grouped_attendance_course = {key: list(group) for key, group in - groupby(sorted(attendance_courses, key=lambda x: x.course_session.title), - key=lambda x: x.course_session.title)} + grouped_cs_users = { + key: list(group) + for key, group in groupby( + sorted(cs_users, key=lambda x: x.course_session.title), + key=lambda x: x.course_session.title, + ) + } + grouped_attendance_course = { + key: list(group) + for key, group in groupby( + sorted(attendance_courses, key=lambda x: x.course_session.title), + key=lambda x: x.course_session.title, + ) + } for course_session, cs_users in grouped_cs_users.items(): logger.debug( @@ -44,7 +50,13 @@ def export_attendance(course_session_ids: list[str], save_as_file: bool, circles }, label="attendance_export", ) - _create_sheet(wb, course_session, cs_users, grouped_attendance_course[course_session]) + _create_sheet( + wb, + course_session, + cs_users, + grouped_attendance_course[course_session], + circle_ids, + ) if save_as_file: wb.save(make_export_filename()) @@ -56,8 +68,13 @@ def export_attendance(course_session_ids: list[str], save_as_file: bool, circles return output.getvalue() -def _create_sheet(wb: Workbook, title: str, users: list[CourseSessionUser], - attendance_courses: list[CourseSessionAttendanceCourse]): +def _create_sheet( + wb: Workbook, + title: str, + users: list[CourseSessionUser], + attendance_courses: list[CourseSessionAttendanceCourse], + circle_ids: typing.Optional[list[int]], +): sheet = wb.create_sheet(title=sanitize_sheet_name(title)) if len(users) == 0: @@ -74,11 +91,17 @@ def _create_sheet(wb: Workbook, title: str, users: list[CourseSessionUser], col_idx = 5 attendance_data = {} for course in attendance_courses: - course_title = course.get_circle().title - sheet.cell(row=1, column=col_idx, - value=f"Anwesenheit {course_title} {course.due_date.start.strftime('%d.%m.%Y')}") - user_dict_map = {d['user_id']: d for d in course.attendance_user_list} - attendance_data[course_title] = user_dict_map + circle = course.get_circle() + if circle_ids and circle.id not in circle_ids: + continue + + sheet.cell( + row=1, + column=col_idx, + value=f"Anwesenheit {circle.title} {course.due_date.start.strftime('%d.%m.%Y')}", + ) + user_dict_map = {d["user_id"]: d for d in course.attendance_user_list} + attendance_data[circle.title] = user_dict_map col_idx += 1 @@ -92,7 +115,11 @@ def _add_rows(sheet, users: list[CourseSessionUser], attendance_data): sheet.cell(row=row_idx, column=1, value=user.user.first_name) sheet.cell(row=row_idx, column=2, value=user.user.last_name) sheet.cell(row=row_idx, column=3, value=user.user.email) - sheet.cell(row=row_idx, column=4, value=user.user.additional_json_data.get("Lehrvertragsnummer", "")) + sheet.cell( + row=row_idx, + column=4, + value=user.user.additional_json_data.get("Lehrvertragsnummer", ""), + ) col_idx = 5 for key, user_dict_map in attendance_data.items(): diff --git a/server/vbv_lernwelt/feedback/services.py b/server/vbv_lernwelt/feedback/services.py index b3ada7aa..e528ad96 100644 --- a/server/vbv_lernwelt/feedback/services.py +++ b/server/vbv_lernwelt/feedback/services.py @@ -10,7 +10,10 @@ from openpyxl import Workbook from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession from vbv_lernwelt.course.services import mark_course_completion -from vbv_lernwelt.course_session.services.export import make_export_filename, sanitize_sheet_name +from vbv_lernwelt.course_session.services.export import ( + make_export_filename, + sanitize_sheet_name, +) from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.learnpath.models import ( LearningContentFeedbackUK, From b16016b34c749ea9c3edb34feabe9a8244cc5826 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 23 May 2024 09:51:15 +0200 Subject: [PATCH 03/24] wip: Add competence certificate export --- .../commands/export_assignment_completions.py | 18 ++ server/vbv_lernwelt/assignment/services.py | 229 +++++++++++++++++- .../course_session/services/export.py | 78 +++--- .../course_session_group/admin.py | 3 +- server/vbv_lernwelt/dashboard/views.py | 6 +- server/vbv_lernwelt/edoniq_test/views.py | 6 +- server/vbv_lernwelt/feedback/services.py | 14 +- .../content/self_evaluation_feedback.py | 8 +- .../vbv_lernwelt/notify/tests/test_service.py | 18 +- .../self_evaluation_feedback/serializers.py | 6 +- server/vbv_lernwelt/shop/invoice/abacus.py | 18 +- 11 files changed, 334 insertions(+), 70 deletions(-) create mode 100644 server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py diff --git a/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py b/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py new file mode 100644 index 00000000..fc4ed97a --- /dev/null +++ b/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py @@ -0,0 +1,18 @@ +import djclick as click +import structlog + +from vbv_lernwelt.assignment.services import export_competence_certificates + +logger = structlog.get_logger(__name__) + + +@click.command() +@click.argument("course_session_id") +@click.option( + "--save-as-file/--no-save-as-file", + default=True, + help="`save-as-file` to save the file, `no-save-as-file` returns bytes. Default is `save-as-file`.", +) +def command(course_session_id, save_as_file): + # using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function + export_competence_certificates([course_session_id], save_as_file) diff --git a/server/vbv_lernwelt/assignment/services.py b/server/vbv_lernwelt/assignment/services.py index 94a00b49..8bd82860 100644 --- a/server/vbv_lernwelt/assignment/services.py +++ b/server/vbv_lernwelt/assignment/services.py @@ -1,6 +1,10 @@ from copy import deepcopy +from dataclasses import dataclass +from io import BytesIO +import structlog from django.utils import timezone +from openpyxl import Workbook from rest_framework import serializers from wagtail.models import Page @@ -14,10 +18,38 @@ from vbv_lernwelt.assignment.models import ( ) from vbv_lernwelt.core.models import User from vbv_lernwelt.core.utils import find_first -from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession +from vbv_lernwelt.course.models import ( + CourseCompletionStatus, + CourseSession, + CourseSessionUser, +) from vbv_lernwelt.course.services import mark_course_completion +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) +from vbv_lernwelt.course_session.services.export import ( + add_user_export_data, + add_user_headers, + get_ordered_csus_by_course_session, + group_by_session_title, + make_export_filename, + sanitize_sheet_name, +) +from vbv_lernwelt.duedate.models import DueDate +from vbv_lernwelt.learnpath.models import LearningContent from vbv_lernwelt.notify.services import NotificationService +logger = structlog.get_logger(__name__) + + +@dataclass +class CompetenceCertificateElement: + assignment: Assignment + date: DueDate + learning_content: LearningContent + course_session: CourseSession + def update_assignment_completion( assignment_user: User, @@ -67,9 +99,9 @@ def update_assignment_completion( assignment_user_id=assignment_user.id, assignment_id=assignment.id, course_session_id=course_session.id, - learning_content_page_id=learning_content_page.id - if learning_content_page - else None, + learning_content_page_id=( + learning_content_page.id if learning_content_page else None + ), ) if initialize_completion: @@ -271,3 +303,192 @@ def _remove_unknown_entries(assignment, completion_data): key: value for key, value in completion_data.items() if key in input_task_ids } return filtered_completion_data + + +def export_competence_certificates( + course_session_ids: list[str], save_as_file: bool, circle_ids: list[int] = None +): + if len(course_session_ids) == 0: + return + + COMPETENCE_ASSIGNMENT_TYPES = [ + AssignmentType.CASEWORK.value, + AssignmentType.EDONIQ_TEST.value, + ] + + wb = Workbook() + # remove the first sheet is just easier than keeping track of the active sheet + wb.remove(wb.active) + + competence_certificate_elements = _get_competence_certificate_elements( + course_session_ids + ) + + assignemnt_completions = AssignmentCompletion.objects.filter( + course_session_id__in=course_session_ids, + assignment__assignment_type__in=COMPETENCE_ASSIGNMENT_TYPES, + ).order_by("course_session", "assignment") + + # group all by the sessions title {session_id1: [...], session_id2: [...], ...} + grouped_cs_users = get_ordered_csus_by_course_session(course_session_ids) + grouped_cce = group_by_session_title(competence_certificate_elements) + grouped_ac = group_by_session_title(assignemnt_completions) + + # create a sheet for each course session + for course_session, cs_users in grouped_cs_users.items(): + logger.debug( + "export_assignment_completion", + data={ + "course_session": course_session, + }, + label="assignment_export", + ) + _create_sheet( + wb, + course_session, + cs_users, + grouped_cce[course_session], + grouped_ac[course_session], + circle_ids, + ) + + if save_as_file: + wb.save(make_export_filename(name="competence_certificate_export")) + else: + output = BytesIO() + wb.save(output) + + output.seek(0) + return output.getvalue() + + +def _create_sheet( + wb: Workbook, + title: str, + users: list[CourseSessionUser], + competence_certificate_element: list[CompetenceCertificateElement], + assignment_completions: list[AssignmentCompletion], + circle_ids: list[int], +): + sheet = wb.create_sheet(title=sanitize_sheet_name(title)) + + if len(users) == 0: + return sheet + + # headers + # common user headers, Circle <learningcontenttitle> bestanden, Circle <title> <learningcontenttitle> Resultat, ... + col_idx = add_user_headers(sheet) + + ordered_assignement_ids = ( + [] + ) # keep track of the order of the columns when adding the rows + for cse in competence_certificate_element: + circle = cse.learning_content.get_circle() + + if circle_ids and circle.id not in circle_ids: + continue + + col_prefix = f'Circle "{circle.title}" {cse.learning_content.title} ' + + sheet.cell( + row=1, + column=col_idx, + value=f"{col_prefix} bestanden", + ) + + sheet.cell( + row=1, + column=col_idx + 1, + value=f"{col_prefix} Resultat %", + ) + + ordered_assignement_ids.append(cse.assignment.id) + + col_idx += 2 + + # add rows with user results + _add_rows(sheet, users, ordered_assignement_ids, assignment_completions) + + return sheet + + +def _add_rows( + sheet, + users: list[CourseSessionUser], + ordered_assignement_ids, + assignment_completions, +): + for row_idx, user in enumerate(users, start=2): + col_idx = add_user_export_data(sheet, user, row_idx) + + for assignment_id in ordered_assignement_ids: + # get the completion for the user and the assignment + user_acs = [ + ac + for ac in assignment_completions + if ac.assignment_id == assignment_id and ac.assignment_user == user.user + ] + user_ac = user_acs[0] if user_acs else None + + if user_ac: + status_text = ( + "Bestanden" if user_ac.evaluation_passed else "Nicht bestanden" + ) + sheet.cell(row=row_idx, column=col_idx, value=status_text) + try: + sheet.cell( + row=row_idx, + column=col_idx + 1, + value=round( + 100 + * user_ac.evaluation_points + / user_ac.evaluation_max_points + ), + ) + except (ZeroDivisionError, TypeError): + sheet.cell(row=row_idx, column=col_idx + 1, value="Keine Daten") + + else: + sheet.cell(row=row_idx, column=col_idx, value="Keine Daten") + sheet.cell(row=row_idx, column=col_idx + 1, value="Keine Daten") + + col_idx += 2 + + +def _get_competence_certificate_elements( + course_session_ids: list[str], +) -> list[CompetenceCertificateElement]: + course_session_assignments = CourseSessionAssignment.objects.filter( + course_session__id__in=course_session_ids, + learning_content__content_assignment__competence_certificate__isnull=False, + ).order_by("course_session", "submission_deadline__start") + + course_session_edoniqtests = CourseSessionEdoniqTest.objects.filter( + course_session__id__in=course_session_ids, + learning_content__content_assignment__competence_certificate__isnull=False, + ).order_by("course_session", "deadline__start") + + cse = [ + CompetenceCertificateElement( + assignment=csa.learning_content.content_assignment, + date=csa.submission_deadline, + learning_content=csa.learning_content, + course_session=csa.course_session, + ) + for csa in course_session_assignments + ] + + cse += [ + CompetenceCertificateElement( + assignment=cset.learning_content.content_assignment, + date=cset.deadline, + learning_content=cset.learning_content, + course_session=cset.course_session, + ) + for cset in course_session_edoniqtests + ] + + # order by course_session and submission_deadline + cse.sort(key=lambda x: (x.course_session.title, x.date.start)) + + return cse diff --git a/server/vbv_lernwelt/course_session/services/export.py b/server/vbv_lernwelt/course_session/services/export.py index c6703f4d..f5e04466 100644 --- a/server/vbv_lernwelt/course_session/services/export.py +++ b/server/vbv_lernwelt/course_session/services/export.py @@ -20,20 +20,13 @@ def export_attendance( # remove the first sheet is just easier than keeping track of the active sheet wb.remove(wb.active) - cs_users = CourseSessionUser.objects.filter( - course_session_id__in=course_session_ids, role=CourseSessionUser.Role.MEMBER - ).order_by("course_session", "user__last_name", "user__first_name") attendance_courses = CourseSessionAttendanceCourse.objects.filter( course_session_id__in=course_session_ids ).order_by("course_session", "due_date") - grouped_cs_users = { - key: list(group) - for key, group in groupby( - sorted(cs_users, key=lambda x: x.course_session.title), - key=lambda x: x.course_session.title, - ) - } + grouped_cs_users = get_ordered_csus_by_course_session(course_session_ids) + + # create dict with course_session as key and list of attendance_courses as value. Easier to access in the loop grouped_attendance_course = { key: list(group) for key, group in groupby( @@ -42,6 +35,7 @@ def export_attendance( ) } + # create a sheet for each course_session for course_session, cs_users in grouped_cs_users.items(): logger.debug( "export_attendance_for_course_session", @@ -81,14 +75,8 @@ def _create_sheet( return sheet # headers - # firstname, lastname, email, lehrvertragsnummer, <attendance_course> <date>, status <attendance_course>, .. - # todo: translate headers - sheet.cell(row=1, column=1, value="Vorname") - sheet.cell(row=1, column=2, value="Nachname") - sheet.cell(row=1, column=3, value="Email") - sheet.cell(row=1, column=4, value="Lehrvertragsnummer") - - col_idx = 5 + # common user headers..., <attendance_course> <date>, status <attendance_course>, .. + col_idx = add_user_headers(sheet) attendance_data = {} for course in attendance_courses: circle = course.get_circle() @@ -105,6 +93,7 @@ def _create_sheet( col_idx += 1 + # add rows with user data _add_rows(sheet, users, attendance_data) return sheet @@ -112,16 +101,7 @@ def _create_sheet( def _add_rows(sheet, users: list[CourseSessionUser], attendance_data): for row_idx, user in enumerate(users, start=2): - sheet.cell(row=row_idx, column=1, value=user.user.first_name) - sheet.cell(row=row_idx, column=2, value=user.user.last_name) - sheet.cell(row=row_idx, column=3, value=user.user.email) - sheet.cell( - row=row_idx, - column=4, - value=user.user.additional_json_data.get("Lehrvertragsnummer", ""), - ) - - col_idx = 5 + col_idx = add_user_export_data(sheet, user, row_idx) for key, user_dict_map in attendance_data.items(): user_dict = user_dict_map.get(str(user.user.id), {}) status = user_dict.get("status", "") if user_dict else "" @@ -130,6 +110,48 @@ def _add_rows(sheet, users: list[CourseSessionUser], attendance_data): col_idx += 1 +def add_user_headers(sheet): + # todo: translate headers + sheet.cell(row=1, column=1, value="Vorname") + sheet.cell(row=1, column=2, value="Nachname") + sheet.cell(row=1, column=3, value="Email") + sheet.cell(row=1, column=4, value="Lehrvertragsnummer") + + return 5 # return the next column index + + +def add_user_export_data(sheet, user: CourseSessionUser, row_idx: int) -> int: + sheet.cell(row=row_idx, column=1, value=user.user.first_name) + sheet.cell(row=row_idx, column=2, value=user.user.last_name) + sheet.cell(row=row_idx, column=3, value=user.user.email) + sheet.cell( + row=row_idx, + column=4, + value=user.user.additional_json_data.get("Lehrvertragsnummer", ""), + ) + + return 5 # return the next column index + + +def get_ordered_csus_by_course_session(course_session_ids: list[str]): + csus = CourseSessionUser.objects.filter( + course_session_id__in=course_session_ids, role=CourseSessionUser.Role.MEMBER + ).order_by("course_session", "user__last_name", "user__first_name") + return group_by_session_title( + sorted(csus, key=lambda x: x.course_session.title), + ) + + +def group_by_session_title(items): + return { + key: list(group) + for key, group in groupby( + sorted(items, key=lambda x: x.course_session.title), + key=lambda x: x.course_session.title, + ) + } + + def make_export_filename(name: str = "attendance_export"): today_date = datetime.today().strftime("%Y-%m-%d") return f"{name}_{today_date}.xlsx" diff --git a/server/vbv_lernwelt/course_session_group/admin.py b/server/vbv_lernwelt/course_session_group/admin.py index 5bda3780..f880cfa3 100644 --- a/server/vbv_lernwelt/course_session_group/admin.py +++ b/server/vbv_lernwelt/course_session_group/admin.py @@ -4,5 +4,4 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup @admin.register(CourseSessionGroup) -class CourseSessionAssignmentAdmin(admin.ModelAdmin): - ... +class CourseSessionAssignmentAdmin(admin.ModelAdmin): ... diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 2e1eb012..8ee1a51d 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -433,9 +433,9 @@ def get_course_config( is_mentor=is_mentor, widgets=get_widgets_for_course(role_key, is_uk, is_vv, is_mentor), has_preview=has_preview(role_key), - session_to_continue_id=str(session_to_continue.id) - if session_to_continue - else None, + session_to_continue_id=( + str(session_to_continue.id) if session_to_continue else None + ), ) ) diff --git a/server/vbv_lernwelt/edoniq_test/views.py b/server/vbv_lernwelt/edoniq_test/views.py index 00d8ff7b..e3a71ec2 100644 --- a/server/vbv_lernwelt/edoniq_test/views.py +++ b/server/vbv_lernwelt/edoniq_test/views.py @@ -153,9 +153,9 @@ def fetch_course_session_all_users(courses: List[int], excluded_domains=None): def generate_export_response(cs_users: List[User]) -> HttpResponse: response = HttpResponse(content_type="text/csv; charset=utf-8") - response[ - "Content-Disposition" - ] = f"attachment; filename=edoniq_user_export_{date.today().strftime('%Y%m%d')}.csv" + response["Content-Disposition"] = ( + f"attachment; filename=edoniq_user_export_{date.today().strftime('%Y%m%d')}.csv" + ) response.write("\ufeff".encode("utf8")) # UTF-8 BOM diff --git a/server/vbv_lernwelt/feedback/services.py b/server/vbv_lernwelt/feedback/services.py index e528ad96..e240d9f4 100644 --- a/server/vbv_lernwelt/feedback/services.py +++ b/server/vbv_lernwelt/feedback/services.py @@ -86,9 +86,11 @@ def update_feedback_response( initial_data = initial_data_for_feedback_page(learning_content_feedback_page) merged_data = initial_data | { - key: updated_data[key] - if updated_data.get(key, "") != "" - else original_data.get(key) + key: ( + updated_data[key] + if updated_data.get(key, "") != "" + else original_data.get(key) + ) for key in initial_data.keys() } @@ -249,9 +251,9 @@ def _handle_feedback_export_action(course_seesions, file_name): response = HttpResponse( content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) - response[ - "Content-Disposition" - ] = f"attachment; filename={make_export_filename(file_name)}" + response["Content-Disposition"] = ( + f"attachment; filename={make_export_filename(file_name)}" + ) response.write(excel_bytes) return response diff --git a/server/vbv_lernwelt/learning_mentor/content/self_evaluation_feedback.py b/server/vbv_lernwelt/learning_mentor/content/self_evaluation_feedback.py index 959f6f41..da451959 100644 --- a/server/vbv_lernwelt/learning_mentor/content/self_evaluation_feedback.py +++ b/server/vbv_lernwelt/learning_mentor/content/self_evaluation_feedback.py @@ -70,9 +70,11 @@ def get_self_feedback_evaluation( MentorAssignmentCompletion( # feedback_submitted as seen from the perspective of the evaluation user (feedback provider) # means that the feedback has been evaluated by the feedback provider, hence the status is EVALUATED - status=MentorCompletionStatus.EVALUATED - if f.feedback_submitted - else MentorCompletionStatus.SUBMITTED, + status=( + MentorCompletionStatus.EVALUATED + if f.feedback_submitted + else MentorCompletionStatus.SUBMITTED + ), user_id=f.feedback_requester_user.id, last_name=f.feedback_requester_user.last_name, url=f"/course/{course.slug}/cockpit/mentor/self-evaluation-feedback/{f.learning_unit.id}", diff --git a/server/vbv_lernwelt/notify/tests/test_service.py b/server/vbv_lernwelt/notify/tests/test_service.py index 6d8af677..1feb4bad 100644 --- a/server/vbv_lernwelt/notify/tests/test_service.py +++ b/server/vbv_lernwelt/notify/tests/test_service.py @@ -65,9 +65,9 @@ class TestNotificationService(TestCase): self.assertFalse(notification.emailed) def test_send_notification_with_email(self): - self.recipient.additional_json_data[ - "email_notification_categories" - ] = json.dumps(["USER_INTERACTION"]) + self.recipient.additional_json_data["email_notification_categories"] = ( + json.dumps(["USER_INTERACTION"]) + ) self.recipient.save() verb = "Anne hat deinen Auftrag bewertet" @@ -146,9 +146,9 @@ class TestNotificationService(TestCase): self.assertFalse(notification.emailed) # when the email was not sent, yet it will still send it afterwards... - self.recipient.additional_json_data[ - "email_notification_categories" - ] = json.dumps(["USER_INTERACTION"]) + self.recipient.additional_json_data["email_notification_categories"] = ( + json.dumps(["USER_INTERACTION"]) + ) self.recipient.save() result = self.notification_service._send_notification( @@ -188,9 +188,9 @@ class TestNotificationService(TestCase): self.assertFalse(self._has_sent_emails()) # Assert mail is sent if corresponding email notification type is enabled - self.recipient.additional_json_data[ - "email_notification_categories" - ] = json.dumps(["USER_INTERACTION"]) + self.recipient.additional_json_data["email_notification_categories"] = ( + json.dumps(["USER_INTERACTION"]) + ) self.recipient.save() self.notification_service._send_notification( sender=self.sender, diff --git a/server/vbv_lernwelt/self_evaluation_feedback/serializers.py b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py index fd24d363..0c73c3dc 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/serializers.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py @@ -39,9 +39,9 @@ class SelfEvaluationFeedbackSerializer(serializers.ModelSerializer): return obj.learning_unit.get_circle().title def get_criteria(self, obj): - performance_criteria: List[ - PerformanceCriteria - ] = obj.learning_unit.performancecriteria_set.all() + performance_criteria: List[PerformanceCriteria] = ( + obj.learning_unit.performancecriteria_set.all() + ) criteria = [] diff --git a/server/vbv_lernwelt/shop/invoice/abacus.py b/server/vbv_lernwelt/shop/invoice/abacus.py index a92b1056..3ab2143b 100644 --- a/server/vbv_lernwelt/shop/invoice/abacus.py +++ b/server/vbv_lernwelt/shop/invoice/abacus.py @@ -67,15 +67,15 @@ class AbacusInvoiceCreator(InvoiceCreator): ) SubElement(sales_order_header_fields, "CustomerNumber").text = customer_number - SubElement( - sales_order_header_fields, "PurchaseOrderDate" - ).text = order_date.isoformat() - SubElement( - sales_order_header_fields, "DeliveryDate" - ).text = order_date.isoformat() - SubElement( - sales_order_header_fields, "ReferencePurchaseOrder" - ).text = reference_purchase_order + SubElement(sales_order_header_fields, "PurchaseOrderDate").text = ( + order_date.isoformat() + ) + SubElement(sales_order_header_fields, "DeliveryDate").text = ( + order_date.isoformat() + ) + SubElement(sales_order_header_fields, "ReferencePurchaseOrder").text = ( + reference_purchase_order + ) SubElement(sales_order_header_fields, "UnicId").text = unic_id for index, item in enumerate(items, start=1): From 6244e02489d1b75c8c3c901af218ccd225c2f569 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 28 May 2024 14:14:17 +0200 Subject: [PATCH 04/24] wip: Add views --- server/config/urls.py | 3 ++ .../commands/export_assignment_completions.py | 2 +- server/vbv_lernwelt/assignment/services.py | 4 +- .../management/commands/export_attendance.py | 2 +- .../course_session/services/export.py | 4 +- .../course_session_group/admin.py | 3 +- server/vbv_lernwelt/dashboard/views.py | 54 ++++++++++++++++++- server/vbv_lernwelt/edoniq_test/views.py | 6 +-- server/vbv_lernwelt/feedback/services.py | 6 +-- .../vbv_lernwelt/notify/tests/test_service.py | 18 +++---- .../self_evaluation_feedback/serializers.py | 6 +-- server/vbv_lernwelt/shop/invoice/abacus.py | 18 +++---- 12 files changed, 93 insertions(+), 33 deletions(-) diff --git a/server/config/urls.py b/server/config/urls.py index 1bd57c20..c65d0665 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -4,6 +4,7 @@ from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth.decorators import user_passes_test from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.http import HttpResponse from django.urls import include, path, re_path, register_converter from django.urls.converters import IntConverter from django.views import defaults as default_views @@ -40,6 +41,7 @@ from vbv_lernwelt.course.views import ( ) from vbv_lernwelt.course_session.views import get_course_session_documents from vbv_lernwelt.dashboard.views import ( + export_attendance_as_xsl, get_dashboard_config, get_dashboard_due_dates, get_dashboard_persons, @@ -130,6 +132,7 @@ urlpatterns = [ path(r"api/dashboard/course/<str:course_id>/mentees/", get_mentee_count, name="get_mentee_count"), path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count, name="get_mentor_open_tasks_count"), + path(r"api/dashboard/export/attendance", export_attendance_as_xsl, name="export_attendance_as_xsl"), # course path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), diff --git a/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py b/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py index fc4ed97a..e7e5b139 100644 --- a/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py +++ b/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py @@ -15,4 +15,4 @@ logger = structlog.get_logger(__name__) ) def command(course_session_id, save_as_file): # using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function - export_competence_certificates([course_session_id], save_as_file) + export_competence_certificates([course_session_id], save_as_file=save_as_file) diff --git a/server/vbv_lernwelt/assignment/services.py b/server/vbv_lernwelt/assignment/services.py index 8bd82860..076a6373 100644 --- a/server/vbv_lernwelt/assignment/services.py +++ b/server/vbv_lernwelt/assignment/services.py @@ -306,7 +306,9 @@ def _remove_unknown_entries(assignment, completion_data): def export_competence_certificates( - course_session_ids: list[str], save_as_file: bool, circle_ids: list[int] = None + course_session_ids: list[str], + circle_ids: list[int] = None, + save_as_file: bool = False, ): if len(course_session_ids) == 0: return diff --git a/server/vbv_lernwelt/course_session/management/commands/export_attendance.py b/server/vbv_lernwelt/course_session/management/commands/export_attendance.py index 064d16e6..4ab8560a 100644 --- a/server/vbv_lernwelt/course_session/management/commands/export_attendance.py +++ b/server/vbv_lernwelt/course_session/management/commands/export_attendance.py @@ -15,4 +15,4 @@ logger = structlog.get_logger(__name__) ) def command(course_session_id, save_as_file): # using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function - export_attendance([course_session_id], save_as_file) + export_attendance([course_session_id], save_as_file=save_as_file) diff --git a/server/vbv_lernwelt/course_session/services/export.py b/server/vbv_lernwelt/course_session/services/export.py index f5e04466..5c6d9e26 100644 --- a/server/vbv_lernwelt/course_session/services/export.py +++ b/server/vbv_lernwelt/course_session/services/export.py @@ -13,7 +13,9 @@ logger = structlog.get_logger(__name__) def export_attendance( - course_session_ids: list[str], save_as_file: bool, circle_ids: list[int] = None + course_session_ids: list[str], + save_as_file: bool = False, + circle_ids: list[int] = None, ): wb = Workbook() diff --git a/server/vbv_lernwelt/course_session_group/admin.py b/server/vbv_lernwelt/course_session_group/admin.py index f880cfa3..5bda3780 100644 --- a/server/vbv_lernwelt/course_session_group/admin.py +++ b/server/vbv_lernwelt/course_session_group/admin.py @@ -4,4 +4,5 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup @admin.register(CourseSessionGroup) -class CourseSessionAssignmentAdmin(admin.ModelAdmin): ... +class CourseSessionAssignmentAdmin(admin.ModelAdmin): + ... diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 8ee1a51d..f9efbd4e 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -3,6 +3,8 @@ from datetime import date from enum import Enum from typing import List, Set +from django.http import HttpResponse +from rest_framework import status from rest_framework.decorators import api_view from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response @@ -11,6 +13,7 @@ from vbv_lernwelt.assignment.models import ( AssignmentCompletion, AssignmentCompletionStatus, ) +from vbv_lernwelt.assignment.services import export_competence_certificates from vbv_lernwelt.competence.services import ( query_competence_course_session_assignments, query_competence_course_session_edoniq_tests, @@ -22,6 +25,10 @@ from vbv_lernwelt.course.models import ( CourseSessionUser, ) from vbv_lernwelt.course.views import logger +from vbv_lernwelt.course_session.services.export import ( + export_attendance, + make_export_filename, +) from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.serializers import DueDateSerializer @@ -494,7 +501,7 @@ def get_mentor_open_tasks_count(request, course_id: str): raise e except Exception as e: logger.error(e, exc_info=True) - return Response({"error": str(e)}, status=404) + return Response({"error": str(e)}, status=status.HTTP_404_NOT_FOUND) def _get_mentor_open_tasks_count(course_id: str, mentor: User) -> int: @@ -526,3 +533,48 @@ def _get_mentor_open_tasks_count(course_id: str, mentor: User) -> int: ) return open_assigment_count + open_feedback_count + + +@api_view(["POST"]) +def export_attendance_as_xsl(request): + return _generate_xls_export(request, export_attendance) + + +@api_view(["POST"]) +def export_competence_certificate_as_xsl(request): + return _generate_xls_export(request, export_competence_certificates) + + +def _generate_xls_export(request, export_fn) -> HttpResponse: + requested_course_session_ids = request.data.get("courseSessionIds", []) + circle_ids = request.data.get("circleIds", None) + + if not requested_course_session_ids: + return Response({"error": "no_cs_ids"}, status=status.HTTP_400_BAD_REQUEST) + + course_session_ids = _get_allowed_course_session_ids_for_user( + request.user, requested_course_session_ids + ) # noqa + + data = export_fn(course_session_ids, circle_ids=circle_ids) + response = HttpResponse( + data, + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + response["Content-Disposition"] = f'attachment; filename="{make_export_filename()}"' + return response + + +def _get_allowed_course_session_ids_for_user( + user: User, requested_cs_ids: List[str] +) -> List[str]: + ALLOWED_ROLES = ["TRAINER", "SUPERVISOR"] + # 1. get course sessions for user with allowed roles + # 2. get overlapping course sessions with given course_session_ids + # Note: We don't care about the circle_ids as it's ok-ish that trainers could export other data + all_cs_ids_for_user = [ + csr._original.id + for csr in get_course_sessions_with_roles_for_user(user) + if any(allowed_role in ALLOWED_ROLES for role in csr.roles) + ] # noqa + return list(set(requested_cs_ids) & set(all_cs_ids_for_user)) diff --git a/server/vbv_lernwelt/edoniq_test/views.py b/server/vbv_lernwelt/edoniq_test/views.py index e3a71ec2..00d8ff7b 100644 --- a/server/vbv_lernwelt/edoniq_test/views.py +++ b/server/vbv_lernwelt/edoniq_test/views.py @@ -153,9 +153,9 @@ def fetch_course_session_all_users(courses: List[int], excluded_domains=None): def generate_export_response(cs_users: List[User]) -> HttpResponse: response = HttpResponse(content_type="text/csv; charset=utf-8") - response["Content-Disposition"] = ( - f"attachment; filename=edoniq_user_export_{date.today().strftime('%Y%m%d')}.csv" - ) + response[ + "Content-Disposition" + ] = f"attachment; filename=edoniq_user_export_{date.today().strftime('%Y%m%d')}.csv" response.write("\ufeff".encode("utf8")) # UTF-8 BOM diff --git a/server/vbv_lernwelt/feedback/services.py b/server/vbv_lernwelt/feedback/services.py index e240d9f4..57422043 100644 --- a/server/vbv_lernwelt/feedback/services.py +++ b/server/vbv_lernwelt/feedback/services.py @@ -251,9 +251,9 @@ def _handle_feedback_export_action(course_seesions, file_name): response = HttpResponse( content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) - response["Content-Disposition"] = ( - f"attachment; filename={make_export_filename(file_name)}" - ) + response[ + "Content-Disposition" + ] = f"attachment; filename={make_export_filename(file_name)}" response.write(excel_bytes) return response diff --git a/server/vbv_lernwelt/notify/tests/test_service.py b/server/vbv_lernwelt/notify/tests/test_service.py index 1feb4bad..6d8af677 100644 --- a/server/vbv_lernwelt/notify/tests/test_service.py +++ b/server/vbv_lernwelt/notify/tests/test_service.py @@ -65,9 +65,9 @@ class TestNotificationService(TestCase): self.assertFalse(notification.emailed) def test_send_notification_with_email(self): - self.recipient.additional_json_data["email_notification_categories"] = ( - json.dumps(["USER_INTERACTION"]) - ) + self.recipient.additional_json_data[ + "email_notification_categories" + ] = json.dumps(["USER_INTERACTION"]) self.recipient.save() verb = "Anne hat deinen Auftrag bewertet" @@ -146,9 +146,9 @@ class TestNotificationService(TestCase): self.assertFalse(notification.emailed) # when the email was not sent, yet it will still send it afterwards... - self.recipient.additional_json_data["email_notification_categories"] = ( - json.dumps(["USER_INTERACTION"]) - ) + self.recipient.additional_json_data[ + "email_notification_categories" + ] = json.dumps(["USER_INTERACTION"]) self.recipient.save() result = self.notification_service._send_notification( @@ -188,9 +188,9 @@ class TestNotificationService(TestCase): self.assertFalse(self._has_sent_emails()) # Assert mail is sent if corresponding email notification type is enabled - self.recipient.additional_json_data["email_notification_categories"] = ( - json.dumps(["USER_INTERACTION"]) - ) + self.recipient.additional_json_data[ + "email_notification_categories" + ] = json.dumps(["USER_INTERACTION"]) self.recipient.save() self.notification_service._send_notification( sender=self.sender, diff --git a/server/vbv_lernwelt/self_evaluation_feedback/serializers.py b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py index 0c73c3dc..fd24d363 100644 --- a/server/vbv_lernwelt/self_evaluation_feedback/serializers.py +++ b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py @@ -39,9 +39,9 @@ class SelfEvaluationFeedbackSerializer(serializers.ModelSerializer): return obj.learning_unit.get_circle().title def get_criteria(self, obj): - performance_criteria: List[PerformanceCriteria] = ( - obj.learning_unit.performancecriteria_set.all() - ) + performance_criteria: List[ + PerformanceCriteria + ] = obj.learning_unit.performancecriteria_set.all() criteria = [] diff --git a/server/vbv_lernwelt/shop/invoice/abacus.py b/server/vbv_lernwelt/shop/invoice/abacus.py index 3ab2143b..a92b1056 100644 --- a/server/vbv_lernwelt/shop/invoice/abacus.py +++ b/server/vbv_lernwelt/shop/invoice/abacus.py @@ -67,15 +67,15 @@ class AbacusInvoiceCreator(InvoiceCreator): ) SubElement(sales_order_header_fields, "CustomerNumber").text = customer_number - SubElement(sales_order_header_fields, "PurchaseOrderDate").text = ( - order_date.isoformat() - ) - SubElement(sales_order_header_fields, "DeliveryDate").text = ( - order_date.isoformat() - ) - SubElement(sales_order_header_fields, "ReferencePurchaseOrder").text = ( - reference_purchase_order - ) + SubElement( + sales_order_header_fields, "PurchaseOrderDate" + ).text = order_date.isoformat() + SubElement( + sales_order_header_fields, "DeliveryDate" + ).text = order_date.isoformat() + SubElement( + sales_order_header_fields, "ReferencePurchaseOrder" + ).text = reference_purchase_order SubElement(sales_order_header_fields, "UnicId").text = unic_id for index, item in enumerate(items, start=1): From bcf5676afdc7992d31706d9b6157d14c5774387b Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Wed, 29 May 2024 11:40:17 +0200 Subject: [PATCH 05/24] wip: Add access tests --- .../vbv_lernwelt/core/create_default_users.py | 2 +- .../dashboard/tests/test_views.py | 44 +++++++++++++++++++ server/vbv_lernwelt/dashboard/views.py | 4 +- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index e9b8aba8..971d15b6 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -79,7 +79,7 @@ AVATAR_DIR = settings.APPS_DIR / "static" / "avatars" def create_default_users(default_password="test", set_avatar=False): admin_group, created = Group.objects.get_or_create(name="admin_group") - _content_creator_grop, _created = Group.objects.get_or_create( + _content_creator_group, _created = Group.objects.get_or_create( name="content_creator_grop" ) student_group, created = Group.objects.get_or_create(name="student_group") diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index ddd0d248..015360d2 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -5,7 +5,14 @@ from vbv_lernwelt.assignment.models import ( AssignmentCompletion, AssignmentCompletionStatus, ) +from vbv_lernwelt.core.constants import ( + TEST_COURSE_SESSION_BERN_ID, + TEST_COURSE_SESSION_ZURICH_ID, + TEST_STUDENT1_USER_ID, + TEST_SUPERVISOR1_USER_ID, +) from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.models import User from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.creators.test_utils import ( add_course_session_group_supervisor, @@ -17,6 +24,7 @@ from vbv_lernwelt.course.creators.test_utils import ( ) from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser from vbv_lernwelt.dashboard.views import ( + _get_allowed_course_session_ids_for_user, _get_mentee_count, _get_mentor_open_tasks_count, get_course_config, @@ -432,3 +440,39 @@ class GetMentorOpenTasksTestCase(BaseMentorAssignmentTestCase): completion_status=AssignmentCompletionStatus.SUBMITTED.value, count=0, ) + + +class ExportXlsTestCase(TestCase): + def setUp(self): + create_default_users() + create_test_course(include_vv=False, with_sessions=True) + + def test_can_export_cs_dats(self): + # supervisor sees all cs in region + supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) + requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID, TEST_COURSE_SESSION_BERN_ID] + + allowed_cs_id = _get_allowed_course_session_ids_for_user( + supervisor, requested_cs_ids + ) + self.assertCountEqual(requested_cs_ids, allowed_cs_id) + + def test_student_cannot_export_data(self): + # student cannot export any data + student = User.objects.get(id=TEST_STUDENT1_USER_ID) + requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] + + allowed_cs_id = _get_allowed_course_session_ids_for_user( + student, requested_cs_ids + ) + self.assertCountEqual([], allowed_cs_id) + + def test_trainer_cannot_export_other_cs(self): + # trainer can only export cs where she is assigned + student = User.objects.get(email="test-trainer2@example.com") + requested_cs_ids = [TEST_COURSE_SESSION_BERN_ID, TEST_COURSE_SESSION_ZURICH_ID] + + allowed_cs_id = _get_allowed_course_session_ids_for_user( + student, requested_cs_ids + ) + self.assertCountEqual([TEST_COURSE_SESSION_ZURICH_ID], allowed_cs_id) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index f9efbd4e..e28d474a 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -568,13 +568,13 @@ def _generate_xls_export(request, export_fn) -> HttpResponse: def _get_allowed_course_session_ids_for_user( user: User, requested_cs_ids: List[str] ) -> List[str]: - ALLOWED_ROLES = ["TRAINER", "SUPERVISOR"] + ALLOWED_ROLES = ["EXPERT", "SUPERVISOR"] # 1. get course sessions for user with allowed roles # 2. get overlapping course sessions with given course_session_ids # Note: We don't care about the circle_ids as it's ok-ish that trainers could export other data all_cs_ids_for_user = [ csr._original.id for csr in get_course_sessions_with_roles_for_user(user) - if any(allowed_role in ALLOWED_ROLES for role in csr.roles) + if any(role in ALLOWED_ROLES for role in csr.roles) ] # noqa return list(set(requested_cs_ids) & set(all_cs_ids_for_user)) From 54d77264cbd033f63b92c0fda17dc23bfa0e3155 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Mon, 3 Jun 2024 15:52:09 +0200 Subject: [PATCH 06/24] wip: Split up code, add attendance tests [skip ci] --- server/vbv_lernwelt/assignment/export.py | 227 ++++++++++++++++++ .../commands/export_assignment_completions.py | 2 +- server/vbv_lernwelt/assignment/services.py | 222 +---------------- .../test_assignment_completions_export.py | 56 +++++ .../management/commands/export_attendance.py | 2 +- .../{export.py => export_attendance.py} | 2 +- .../tests/test_attendance_export.py | 135 +++++++++++ server/vbv_lernwelt/dashboard/views.py | 4 +- server/vbv_lernwelt/feedback/services.py | 2 +- 9 files changed, 425 insertions(+), 227 deletions(-) create mode 100644 server/vbv_lernwelt/assignment/export.py create mode 100644 server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py rename server/vbv_lernwelt/course_session/services/{export.py => export_attendance.py} (97%) create mode 100644 server/vbv_lernwelt/course_session/tests/test_attendance_export.py diff --git a/server/vbv_lernwelt/assignment/export.py b/server/vbv_lernwelt/assignment/export.py new file mode 100644 index 00000000..fe845274 --- /dev/null +++ b/server/vbv_lernwelt/assignment/export.py @@ -0,0 +1,227 @@ +from dataclasses import dataclass +from io import BytesIO + +import structlog +from openpyxl import Workbook + +from vbv_lernwelt.assignment.models import ( + Assignment, + AssignmentCompletion, + AssignmentType, +) +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) +from vbv_lernwelt.course_session.services.export_attendance import ( + add_user_export_data, + add_user_headers, + get_ordered_csus_by_course_session, + group_by_session_title, + make_export_filename, + sanitize_sheet_name, +) +from vbv_lernwelt.duedate.models import DueDate +from vbv_lernwelt.learnpath.models import LearningContent + +logger = structlog.get_logger(__name__) + + +@dataclass +class CompetenceCertificateElement: + assignment: Assignment + date: DueDate + learning_content: LearningContent + course_session: CourseSession + + +def export_competence_certificates( + course_session_ids: list[str], + circle_ids: list[int] = None, + save_as_file: bool = False, +): + if len(course_session_ids) == 0: + return + + COMPETENCE_ASSIGNMENT_TYPES = [ + AssignmentType.CASEWORK.value, + AssignmentType.EDONIQ_TEST.value, + ] + + wb = Workbook() + # remove the first sheet is just easier than keeping track of the active sheet + wb.remove(wb.active) + + competence_certificate_elements = _get_competence_certificate_elements( + course_session_ids + ) + + assignemnt_completions = AssignmentCompletion.objects.filter( + course_session_id__in=course_session_ids, + assignment__assignment_type__in=COMPETENCE_ASSIGNMENT_TYPES, + ).order_by("course_session", "assignment") + + # group all by the sessions title {session_id1: [...], session_id2: [...], ...} + grouped_cs_users = get_ordered_csus_by_course_session(course_session_ids) + grouped_cce = group_by_session_title(competence_certificate_elements) + grouped_ac = group_by_session_title(assignemnt_completions) + + # create a sheet for each course session + for course_session_title, cs_users in grouped_cs_users.items(): + logger.debug( + "export_assignment_completion", + data={ + "course_session": course_session_title, + }, + label="assignment_export", + ) + _create_sheet( + wb, + course_session_title, + cs_users, + grouped_cce[course_session_title], + grouped_ac[course_session_title], + circle_ids, + ) + + if save_as_file: + wb.save(make_export_filename(name="competence_certificate_export")) + else: + output = BytesIO() + wb.save(output) + + output.seek(0) + return output.getvalue() + + +def _create_sheet( + wb: Workbook, + title: str, + users: list[CourseSessionUser], + competence_certificate_element: list[CompetenceCertificateElement], + assignment_completions: list[AssignmentCompletion], + circle_ids: list[int], +): + sheet = wb.create_sheet(title=sanitize_sheet_name(title)) + + if len(users) == 0: + return sheet + + # headers + # common user headers, Circle <title> <learningcontenttitle> bestanden, Circle <title> <learningcontenttitle> Resultat, ... + col_idx = add_user_headers(sheet) + + ordered_assignement_ids = ( + [] + ) # keep track of the order of the columns when adding the rows + for cse in competence_certificate_element: + circle = cse.learning_content.get_circle() + + if circle_ids and circle.id not in circle_ids: + continue + + col_prefix = f'Circle "{circle.title}" {cse.learning_content.title} ' + + sheet.cell( + row=1, + column=col_idx, + value=f"{col_prefix} bestanden", + ) + + sheet.cell( + row=1, + column=col_idx + 1, + value=f"{col_prefix} Resultat %", + ) + + ordered_assignement_ids.append(cse.assignment.id) + + col_idx += 2 + + # add rows with user results + _add_rows(sheet, users, ordered_assignement_ids, assignment_completions) + + return sheet + + +def _add_rows( + sheet, + users: list[CourseSessionUser], + ordered_assignement_ids, + assignment_completions, +): + for row_idx, user in enumerate(users, start=2): + col_idx = add_user_export_data(sheet, user, row_idx) + + for assignment_id in ordered_assignement_ids: + # get the completion for the user and the assignment + user_acs = [ + ac + for ac in assignment_completions + if ac.assignment_id == assignment_id and ac.assignment_user == user.user + ] + user_ac = user_acs[0] if user_acs else None + + if user_ac: + status_text = ( + "Bestanden" if user_ac.evaluation_passed else "Nicht bestanden" + ) + sheet.cell(row=row_idx, column=col_idx, value=status_text) + try: + sheet.cell( + row=row_idx, + column=col_idx + 1, + value=round( + 100 + * user_ac.evaluation_points + / user_ac.evaluation_max_points + ), + ) + except (ZeroDivisionError, TypeError): + sheet.cell(row=row_idx, column=col_idx + 1, value="Keine Daten") + + else: + sheet.cell(row=row_idx, column=col_idx, value="Keine Daten") + sheet.cell(row=row_idx, column=col_idx + 1, value="Keine Daten") + + col_idx += 2 + + +def _get_competence_certificate_elements( + course_session_ids: list[str], +) -> list[CompetenceCertificateElement]: + course_session_assignments = CourseSessionAssignment.objects.filter( + course_session__id__in=course_session_ids, + learning_content__content_assignment__competence_certificate__isnull=False, + ).order_by("course_session", "submission_deadline__start") + + course_session_edoniqtests = CourseSessionEdoniqTest.objects.filter( + course_session__id__in=course_session_ids, + learning_content__content_assignment__competence_certificate__isnull=False, + ).order_by("course_session", "deadline__start") + + cse = [ + CompetenceCertificateElement( + assignment=csa.learning_content.content_assignment, + date=csa.submission_deadline, + learning_content=csa.learning_content, + course_session=csa.course_session, + ) + for csa in course_session_assignments + ] + + cse += [ + CompetenceCertificateElement( + assignment=cset.learning_content.content_assignment, + date=cset.deadline, + learning_content=cset.learning_content, + course_session=cset.course_session, + ) + for cset in course_session_edoniqtests + ] + + # order by course_session and submission_deadline + cse.sort(key=lambda x: (x.course_session.title, x.date.start)) + + return cse diff --git a/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py b/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py index e7e5b139..0cd3e291 100644 --- a/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py +++ b/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py @@ -1,7 +1,7 @@ import djclick as click import structlog -from vbv_lernwelt.assignment.services import export_competence_certificates +from vbv_lernwelt.assignment.export import export_competence_certificates logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/assignment/services.py b/server/vbv_lernwelt/assignment/services.py index bba73612..82cf5110 100644 --- a/server/vbv_lernwelt/assignment/services.py +++ b/server/vbv_lernwelt/assignment/services.py @@ -1,10 +1,7 @@ from copy import deepcopy -from dataclasses import dataclass -from io import BytesIO import structlog from django.utils import timezone -from openpyxl import Workbook from rest_framework import serializers from wagtail.models import Page @@ -19,39 +16,13 @@ from vbv_lernwelt.assignment.models import ( ) from vbv_lernwelt.core.models import User from vbv_lernwelt.core.utils import find_first -from vbv_lernwelt.course.models import ( - CourseCompletionStatus, - CourseSession, - CourseSessionUser, -) +from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession from vbv_lernwelt.course.services import mark_course_completion -from vbv_lernwelt.course_session.models import ( - CourseSessionAssignment, - CourseSessionEdoniqTest, -) -from vbv_lernwelt.course_session.services.export import ( - add_user_export_data, - add_user_headers, - get_ordered_csus_by_course_session, - group_by_session_title, - make_export_filename, - sanitize_sheet_name, -) -from vbv_lernwelt.duedate.models import DueDate -from vbv_lernwelt.learnpath.models import LearningContent from vbv_lernwelt.notify.services import NotificationService logger = structlog.get_logger(__name__) -@dataclass -class CompetenceCertificateElement: - assignment: Assignment - date: DueDate - learning_content: LearningContent - course_session: CourseSession - - def update_assignment_completion( assignment_user: User, assignment: Assignment, @@ -300,194 +271,3 @@ def _remove_unknown_entries(assignment, completion_data): key: value for key, value in completion_data.items() if key in input_task_ids } return filtered_completion_data - - -def export_competence_certificates( - course_session_ids: list[str], - circle_ids: list[int] = None, - save_as_file: bool = False, -): - if len(course_session_ids) == 0: - return - - COMPETENCE_ASSIGNMENT_TYPES = [ - AssignmentType.CASEWORK.value, - AssignmentType.EDONIQ_TEST.value, - ] - - wb = Workbook() - # remove the first sheet is just easier than keeping track of the active sheet - wb.remove(wb.active) - - competence_certificate_elements = _get_competence_certificate_elements( - course_session_ids - ) - - assignemnt_completions = AssignmentCompletion.objects.filter( - course_session_id__in=course_session_ids, - assignment__assignment_type__in=COMPETENCE_ASSIGNMENT_TYPES, - ).order_by("course_session", "assignment") - - # group all by the sessions title {session_id1: [...], session_id2: [...], ...} - grouped_cs_users = get_ordered_csus_by_course_session(course_session_ids) - grouped_cce = group_by_session_title(competence_certificate_elements) - grouped_ac = group_by_session_title(assignemnt_completions) - - # create a sheet for each course session - for course_session, cs_users in grouped_cs_users.items(): - logger.debug( - "export_assignment_completion", - data={ - "course_session": course_session, - }, - label="assignment_export", - ) - _create_sheet( - wb, - course_session, - cs_users, - grouped_cce[course_session], - grouped_ac[course_session], - circle_ids, - ) - - if save_as_file: - wb.save(make_export_filename(name="competence_certificate_export")) - else: - output = BytesIO() - wb.save(output) - - output.seek(0) - return output.getvalue() - - -def _create_sheet( - wb: Workbook, - title: str, - users: list[CourseSessionUser], - competence_certificate_element: list[CompetenceCertificateElement], - assignment_completions: list[AssignmentCompletion], - circle_ids: list[int], -): - sheet = wb.create_sheet(title=sanitize_sheet_name(title)) - - if len(users) == 0: - return sheet - - # headers - # common user headers, Circle <title> <learningcontenttitle> bestanden, Circle <title> <learningcontenttitle> Resultat, ... - col_idx = add_user_headers(sheet) - - ordered_assignement_ids = ( - [] - ) # keep track of the order of the columns when adding the rows - for cse in competence_certificate_element: - circle = cse.learning_content.get_circle() - - if circle_ids and circle.id not in circle_ids: - continue - - col_prefix = f'Circle "{circle.title}" {cse.learning_content.title} ' - - sheet.cell( - row=1, - column=col_idx, - value=f"{col_prefix} bestanden", - ) - - sheet.cell( - row=1, - column=col_idx + 1, - value=f"{col_prefix} Resultat %", - ) - - ordered_assignement_ids.append(cse.assignment.id) - - col_idx += 2 - - # add rows with user results - _add_rows(sheet, users, ordered_assignement_ids, assignment_completions) - - return sheet - - -def _add_rows( - sheet, - users: list[CourseSessionUser], - ordered_assignement_ids, - assignment_completions, -): - for row_idx, user in enumerate(users, start=2): - col_idx = add_user_export_data(sheet, user, row_idx) - - for assignment_id in ordered_assignement_ids: - # get the completion for the user and the assignment - user_acs = [ - ac - for ac in assignment_completions - if ac.assignment_id == assignment_id and ac.assignment_user == user.user - ] - user_ac = user_acs[0] if user_acs else None - - if user_ac: - status_text = ( - "Bestanden" if user_ac.evaluation_passed else "Nicht bestanden" - ) - sheet.cell(row=row_idx, column=col_idx, value=status_text) - try: - sheet.cell( - row=row_idx, - column=col_idx + 1, - value=round( - 100 - * user_ac.evaluation_points - / user_ac.evaluation_max_points - ), - ) - except (ZeroDivisionError, TypeError): - sheet.cell(row=row_idx, column=col_idx + 1, value="Keine Daten") - - else: - sheet.cell(row=row_idx, column=col_idx, value="Keine Daten") - sheet.cell(row=row_idx, column=col_idx + 1, value="Keine Daten") - - col_idx += 2 - - -def _get_competence_certificate_elements( - course_session_ids: list[str], -) -> list[CompetenceCertificateElement]: - course_session_assignments = CourseSessionAssignment.objects.filter( - course_session__id__in=course_session_ids, - learning_content__content_assignment__competence_certificate__isnull=False, - ).order_by("course_session", "submission_deadline__start") - - course_session_edoniqtests = CourseSessionEdoniqTest.objects.filter( - course_session__id__in=course_session_ids, - learning_content__content_assignment__competence_certificate__isnull=False, - ).order_by("course_session", "deadline__start") - - cse = [ - CompetenceCertificateElement( - assignment=csa.learning_content.content_assignment, - date=csa.submission_deadline, - learning_content=csa.learning_content, - course_session=csa.course_session, - ) - for csa in course_session_assignments - ] - - cse += [ - CompetenceCertificateElement( - assignment=cset.learning_content.content_assignment, - date=cset.deadline, - learning_content=cset.learning_content, - course_session=cset.course_session, - ) - for cset in course_session_edoniqtests - ] - - # order by course_session and submission_deadline - cse.sort(key=lambda x: (x.course_session.title, x.date.start)) - - return cse diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py new file mode 100644 index 00000000..10f6da0f --- /dev/null +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py @@ -0,0 +1,56 @@ +from django.test import TestCase + +from vbv_lernwelt.assignment.models import Assignment +from vbv_lernwelt.assignment.services import update_assignment_completion +from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession + + +class AttendanceExportTestCase(TestCase): + def setUp(self): + create_default_users() + self.course = create_test_course(include_vv=False, with_sessions=True) + self.course_session_be = CourseSession.objects.get(title="Test Bern 2022 a") + self.course_session_zh = CourseSession.objects.get(title="Test Zürich 2022 a") + self.attendance_course_be = ( + self.course_session_be.coursesessionattendancecourse_set.first() + ) + + some = ( + self.course.coursepage.get_descendants() + .exact_type(Assignment) + .filter(assignment__assignment_type="CASEWORK") + ) + + self.assignment = ( + self.course.coursepage.get_descendants() + .exact_type(Assignment) + .filter(assignment__assignment_type="CASEWORK") + .first() + .specific + ) + + self.trainer = User.objects.get(username="admin") + + self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID) + self.test_student1.additional_json_data = {"Lehrvertragsnummer": 1234567890} + self.test_student1.save() + self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID) + self.test_student2.additional_json_data = {"Lehrvertragsnummer": 1987654321} + self.test_student2.save() + + test_student3 = User.objects.get(email="test-student3@example.com") + + update_assignment_completion( + assignment_user=self.test_student1, + assignment=self.assignment, + course_session=self.course_session_be, + completion_data={}, + evaluation_points=20, + ) + + def test_attendance_export_single_cs(self): + self.assertTrue(True) diff --git a/server/vbv_lernwelt/course_session/management/commands/export_attendance.py b/server/vbv_lernwelt/course_session/management/commands/export_attendance.py index 4ab8560a..4854e783 100644 --- a/server/vbv_lernwelt/course_session/management/commands/export_attendance.py +++ b/server/vbv_lernwelt/course_session/management/commands/export_attendance.py @@ -1,7 +1,7 @@ import djclick as click import structlog -from vbv_lernwelt.course_session.services.export import export_attendance +from vbv_lernwelt.course_session.services.export_attendance import export_attendance logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/course_session/services/export.py b/server/vbv_lernwelt/course_session/services/export_attendance.py similarity index 97% rename from server/vbv_lernwelt/course_session/services/export.py rename to server/vbv_lernwelt/course_session/services/export_attendance.py index 5c6d9e26..92ec69f2 100644 --- a/server/vbv_lernwelt/course_session/services/export.py +++ b/server/vbv_lernwelt/course_session/services/export_attendance.py @@ -28,7 +28,7 @@ def export_attendance( grouped_cs_users = get_ordered_csus_by_course_session(course_session_ids) - # create dict with course_session as key and list of attendance_courses as value. Easier to access in the loop + # create dict with course_session_title as key and list of attendance_courses as value. Easier to access in the loop grouped_attendance_course = { key: list(group) for key, group in groupby( diff --git a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py new file mode 100644 index 00000000..49f1c085 --- /dev/null +++ b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py @@ -0,0 +1,135 @@ +import io + +from django.test import TestCase +from openpyxl import load_workbook + +from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course_session.services.export_attendance import export_attendance + + +class AttendanceExportTestCase(TestCase): + def setUp(self): + create_default_users() + create_test_course(include_vv=False, with_sessions=True) + self.course_session_be = CourseSession.objects.get(title="Test Bern 2022 a") + self.course_session_zh = CourseSession.objects.get(title="Test Zürich 2022 a") + self.attendance_course_be = ( + self.course_session_be.coursesessionattendancecourse_set.first() + ) + + self.attendance_course_zh = ( + self.course_session_zh.coursesessionattendancecourse_set.first() + ) + + self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID) + self.test_student1.additional_json_data = {"Lehrvertragsnummer": 1234567890} + self.test_student1.save() + self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID) + self.test_student2.additional_json_data = {"Lehrvertragsnummer": 1987654321} + self.test_student2.save() + + test_student3 = User.objects.get(email="test-student3@example.com") + self.attendance_course_be.attendance_user_list = [ + { + "email": self.test_student1.email, + "status": "PRESENT", + "user_id": str(self.test_student1.id), + "last_name": self.test_student1.last_name, + "first_name": self.test_student1.first_name, + } + ] + + self.expected_data_be = [ + self._make_header(self.attendance_course_be), + [ + self.test_student1.first_name, + self.test_student1.last_name, + self.test_student1.email, + self.test_student1.additional_json_data["Lehrvertragsnummer"], + "Anwesend", + ], + [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + self.test_student2.additional_json_data["Lehrvertragsnummer"], + "Nicht anwesend", + ], + [ + test_student3.first_name, + test_student3.last_name, + test_student3.email, + None, + "Nicht anwesend", + ], + ] + self.attendance_course_be.save() + + def _generate_workbook(self, course_session_ids): + export_data = io.BytesIO( + export_attendance(course_session_ids, save_as_file=False) + ) + return load_workbook(export_data) + + def _make_header(self, csac): + return ( + [ + "Vorname", + "Nachname", + "Email", + "Lehrvertragsnummer", + f"Anwesenheit {csac.get_circle().title} {csac.attendance_course_zh.due_date.start.strftime('%d.%m.%Y')}", + ], + ) + + def _check_attendance_export(self, wb, expected_data, max_row, max_col): + for row in wb.active.iter_rows(max_col=max_col, max_row=max_row): + for cell in row: + self.assertEqual( + cell.value, expected_data[row[0].row - 1][row.index(cell)] + ) + + def test_attendance_export_single_cs(self): + wb = self._generate_workbook([self.course_session_be.id]) + self.assertEqual(len(wb.sheetnames), 1) + self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") + + self._check_attendance_export(wb, self.expected_data_be, 4, 5) + + def test_attendance_export_multiple_cs(self): + self.attendance_course_zh.attendance_user_list = [ + { + "email": self.test_student2.email, + "status": "PRESENT", + "user_id": str(self.test_student2.id), + "last_name": self.test_student2.last_name, + "first_name": self.test_student2.first_name, + } + ] + + expected_data_zh = [ + self._make_header(self.attendance_course_zh), + [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + self.test_student2.additional_json_data["Lehrvertragsnummer"], + "Anwesend", + ], + ] + + self.attendance_course_zh.save() + + wb = self._generate_workbook( + [self.course_session_be.id, self.course_session_zh.id] + ) + self.assertEqual(len(wb.sheetnames), 2) + self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") + self.assertEqual(wb.sheetnames[1], "Test Zürich 2022 a") + + wb.active = wb["Test Zürich 2022 a"] + self._check_attendance_export(wb, expected_data_zh, 2, 5) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index e28d474a..5f660a0e 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -9,11 +9,11 @@ from rest_framework.decorators import api_view from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response +from vbv_lernwelt.assignment.export import export_competence_certificates from vbv_lernwelt.assignment.models import ( AssignmentCompletion, AssignmentCompletionStatus, ) -from vbv_lernwelt.assignment.services import export_competence_certificates from vbv_lernwelt.competence.services import ( query_competence_course_session_assignments, query_competence_course_session_edoniq_tests, @@ -25,7 +25,7 @@ from vbv_lernwelt.course.models import ( CourseSessionUser, ) from vbv_lernwelt.course.views import logger -from vbv_lernwelt.course_session.services.export import ( +from vbv_lernwelt.course_session.services.export_attendance import ( export_attendance, make_export_filename, ) diff --git a/server/vbv_lernwelt/feedback/services.py b/server/vbv_lernwelt/feedback/services.py index 57422043..e4b49cfd 100644 --- a/server/vbv_lernwelt/feedback/services.py +++ b/server/vbv_lernwelt/feedback/services.py @@ -10,7 +10,7 @@ from openpyxl import Workbook from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession from vbv_lernwelt.course.services import mark_course_completion -from vbv_lernwelt.course_session.services.export import ( +from vbv_lernwelt.course_session.services.export_attendance import ( make_export_filename, sanitize_sheet_name, ) From 7a7caec2194b2ca48bd2c233d8ba1ed73178b834 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 4 Jun 2024 13:02:26 +0200 Subject: [PATCH 07/24] wip: Add assignment completion tests --- env_secrets/local_chrigu.env | Bin 2593 -> 2746 bytes server/vbv_lernwelt/assignment/export.py | 20 +- .../test_assignment_completions_export.py | 172 ++++++++++++++++-- .../course/creators/test_course.py | 7 + .../tests/test_attendance_export.py | 40 ++-- 5 files changed, 205 insertions(+), 34 deletions(-) diff --git a/env_secrets/local_chrigu.env b/env_secrets/local_chrigu.env index 431043ffe4399d1f0a764a1b986c6d2f9f682c76..23475a25faa8084c1204de1ebf541da650ad45f5 100644 GIT binary patch literal 2746 zcmV;r3Ptq*M@dveQdv+`05A$#V^2!X$+C1f>HOb(zq<9s>e5Do5u&Mm1EUIKWU$Mg zoXZr<t?c%|GhV<5*y5wB&S6-S0p|{XloBD8t8Q8w03nv~fN;3Ut@0K=#z?u!YI)l( z2v;*^edF^-+R9y7{vV!ki0{1U7U}KW@W3@2(wbL#uFt`{93EQ>ITHtR=5}y5Jvu@m ziXX!(x}JASJxE5!Vtf#{i?Fc<Vx_{lHvIj%<{+yL=1~HP6P1>S_?|4Vfir)JJ<yI{ zC&j+FU@ABM|3Q&EA%{S|_`M{1^(REAo@VPvKm)$1(}yU%(~!NVd#e-!cz|P+gFFP| z=?RJs>LQP<<0#NY;bL3&V@&|*JDIb>ghs7m0n$HM{b*y68C)A7mfKxQAg1G+8KGM@ z;kAOlPn`o)u&n(+g`O@nSsdf#ud9fuLHO^YW2}Ff+o~lGmHmL2BG`gU__A!uyr>TJ zY^dYW7mEQ)k`}dYz-}d!R6NtgqfZMFYJf{3mzW~+ad@T~n9b)EUfFqDey!_EVFSx` z$&i%yd@j@PyqV1q4uVS0odzCZe_DeCwKY58jOhAKpwuTmStS;IfA3@=#^A0&2h&n^ z7`7aShvMCNM%(_E(A1dc?vbXhrRoVf9bd>=S)2e5YZTe+gCwN8gWukU>_PGZXEFlm zxhOAKWT#w<LBIf_PG03cd1@#(?|r>h4AN{FFJ!y#)Zc{*;kn<;Ix=Qll&c39w%-h> zTYzzt16c-D^RxM_JmXV-t>nP#iU|-gooI0deuMd<*c!@w!2+>?E=(0yl}Cz1L*YA2 zFGK8z8v_SxYNnHVr+#nmWakoZ##p3B-L_8}srxr^y&1_|ixUwD)!u->3M!+?+he00 zjyM*#({&#!3IN*x`rx;TON{;jA$sPrYvn(5U!gyF(w}huUaWS^@MwH0UARDR5%WEg zV<`hdJG83`hSGakcu1muz8|d_+I`t{=j7&M=2SoUmAU=LGPl@UFr+<6<aMpPDC=V# z%6bQ+iQ#B$-k|3Diiemkm&0gL%+wiicB@Rjpu{*ui$ccRHfek6_^1?xEL)X)CB;u; z>s`%^)Px?klEn&I6-6@@g<vMwUzl`=74Rc94vfr*;pJ>gEAs6t;kgbRg;_&%<vRC+ zbtd)@jf~q!DZ-WBlMcJ@4SmzVNuhVyau`2mm&q5b?`%D@$d)!wt<)t-eYdGIIytUL zPnf0Vs)u>Msf2Zv8hn>V+Q$s+X-AnEQVqU!BGO}!$FhORs>U;Vl0#TV<%(+}KV)yi zE#YqxK;iA$$pavmX{@}BAx$?3i25x{r2e}JQo>2*y^8luc7&OKAKnIjLp+*NZl-#Z zTD8Lb7_Wbf?b+RYhc=1K2Pd6PKx9SVlIP+aB3%r7%FW^IoJW{)C;vR1U_h%?_VHj< zC$@&@4_?mQ8I+v!|Gm#?YvOOoKYaS{h%BNac>(&Vaayy#Xs~tf069%0Q5oCcc<v#d z$S3c%ND@!=k$$!<;L;?V5Wo<Qhq^xVBD=Hk;Hyvt*@9}3zRycXHkW9Mjq0|fJ-U>y z8Q^6qrR2j6d%k~3b*Bw3=imk-c+C*Mdsbn0lB12ypY+M<)E2*^)#ZNnSO#Ja?xkH& zD1>3;(c3(syT>9%B<*C!uE4;aM=Wju_B1?<Q-)a(ZrV4$aJ_Q@$oJ#z%@zg6*n(Kq zKei`3mGy0HUqWGSAcaofh-t)e>;oY$mc<OhX#^Dfrp8ra3#a=kRA#kx&ypms_JT<K z0ilN-5b$vA0G-^ZU>8v0K0iaL;AtTUgOT0avGXS^P%T;+9Pb9hdp_ybY9vY2ft{;S zb8Lm?W!O7q=XK6DMrHwW=FO%XHSAH5m$%}pYdHATMn7Wt8F~|8JfM^7luzN1dSIkQ zBUMOVp$0sEV-_ouPWgJXv5pY$eMd&Z8_vRQFssCJQMX49Xvk0R+Wq;7cUUzgI}L|t zH*dw&h*)6#OxAPeQJFO=dUKLO%E|!a$BG(A2yXjuw|}@Od2`GTFW)0uJrPBWyse2m zTM;S239?j)A%Ng$Y*l&to6+MwKPc7Cz%l}x=nM}4QReSBf>JtQ1qXIX0;og0GK6|v zCv(yfjQG&|nl2%i9Ey7ohTmi)da1h%l@j<g3Jl0=N}HD!J)yBqfIzR5x;K<&y2&9y zci}j{3vv_x$4Oie)z!8BB_u4or@K&jA7x9|R`kSM(c2efz<rcI!THZAuNKeLBB9mg zwc$m)e{Wh)$uL-$>B%?ow+~FUYkCf@+IzN{DjN!r1y&Nx>i&@qL;ug6DbHPyOofT@ zLx0!|(0Q5~=uiKgRq*^rmiu`sqvkLT_LXw{OCZuRMRmqXbwOSNT;ysttRh|ckl4&A zuAn81@Vdx<GMy)Z#ruRSzrt-zavyFMq_YtTv>ITo!CBS;E>M>K*q4QXGXZ{pynpB> zaX0=rYA6jDEmSQ}B0%#eAle9v_t4(X56=Y1^;#cF7dy6=jt7TS6jf<`9x9SbLbd)0 z#Lf{>7B0=t{l=g&pYBLoCIoDmSKGWEQz%X^a(^#xAK9}I?h(4-7#Oj=1dE4_k2~pr zlO`2Pp%sn|!-$+Vr<NCBYstVvsYyLfIxSpC47oSO1WRBRuERXY)b3ExxA>!aDpk3w zcZ}<A&LI_;0Y%<ZzQ<Z}OzF~>Z{D2LlBmr3QiFEC{r7|Ci1I9Xi)>!zD!3E)WU%5P z+lDl&^hVl51KW^UKM%l|Ob=nFzClOK1$V-WLRiUd7}hd0&B2|!Hs-{z&he&15W#Z% zFDS8_edjM}EQQc^$tdgm?a|6Oy!E6j>ev_9%1}K`c<^V@Ymyd(vJK}eRpxlvr7-;Z zist&bq>!<ukvO1T5*FgT=?|p~<eI?$^*5ZuO8VdCl-A|>`Fw~}d_5T<C8vEb4{m%{ zfWuKg=6a54cDRT%g$U8!Ye4r#*aEA_7fz4$ElP7<&~~ORQxn4QLo5v)Dydmc?b~Lc z3uG#rM!a8g)jB*Q(*gfFJiCT^TbCljk?}i@t2elq{cN#gQ1%?33`9pm;Yi4MDE5kH zn~25$(lG~*c+inK7x#Z-oE9c`hJeIR2vL4J%fh%klZGC3uvphSF7nQA3M?>xs}ITw z;DRxVUKt(e@UdM1hORy7Y|tVE17JZQ(xEXs?Xiik6}yO#@jMYdJoywhHW=QLg>zsd z+cCx^?E$!6YV|Bwkme7=x$ufcXcz~qae1Mssb+L@x!O}lHBjWXa1tL_>`|d`w6>u8 zNeso*rVf|Z!*)@apZ%!(dOT4T?IBoh|8RbAsg<DN0v*Z~G~CNlzLU9*Jv0K65Q|E3 z_iQk%Z|g2T7w1Km6uTAk-kQ@|7i<HREAfGflRK9Q-_uUPHii??3LhouAC}x|y?W^s z3whh|xF0Uq)+3B~ytYk#y1v)mWlrpGg_1(3<BPG1rZvO`|F`BG-szoeiYfbKcurH= z3lx8Szl$=Qd7h0(ZIK#=2DQ874*JrHq&PPIWftbRRjE=R*k((32au{>nGIlYXuOuT z`Q{8(aEgKEwR$B6ZJ-4+=!_eM>@@phZA*3f(R87JVd+c#N0mTZnAcPYEI?3ocXMh{ AzyJUM literal 2593 zcmV++3f}bqM@dveQdv+`0H0->OwvTk4dEw=c}FbrHNE+g_ePsDSP)es!F`K{<HdCv zm1*&Tlp1~vn8V<oqpfE_88H7%MU|`1OR8J%c*K@1q)P%HPQYe}uU@EXrFqsrB<UxX zY_(h)5;Mf+iLfRS?oYTk7WOj}#FTG9L!{%pg+|JC3Pyd|3SUX63#GHr|MAeb!qFmH zsp%<6miPGZVk~EmlDEIHIgssU^;o5Oioo?k^TCN#=9E1Gl=xH@dF`Wz?jR@Nt&CN> z&&lS!HejwJyry5D+-F6+PLo~x-*>&8yB|p9)8Q*yLkdP(U^5DMz!y(7YZtO1r@FA1 z?}#)Ce1~i;jdev>lpnAM1lr(>H$~sD=Fgwq4FhxCJr_dEUx!VsRKJ{sgU3sQEAZYS z^Zz0gmt)=bh{t)sL5GHo9clZmCVW$Nl_d>f`gbO1gX2obw7kZR*s<+h+}~q-xb-Se zt&K2knykhSM;_%b{s(TIipYC9MoueMMPlsGkx9gN(o)l@?bf@A%Tt<O8hfEgs;&zM z&)6Cx!<{Gh@BOb*X`_%+p0=m9)ZWhHM0(KaeeW|Vt53%WiV&DU9;^pz)X3e#Vvuk# zo|b|PB{`<xhLKJX$B7wPS&9AiNj6wz80Uyhxddq(Cd(lvRrU2}u?}}rUsVnpRRlrt zHefvXyL__$f2GbHjdcTJ3i%Z(;M;oJztRa7nK+35wfZ-Pm9aP5i(qm<5j|?vM0sHz zWITk>T$-^HIYcx}-?C-o4QvhG!Q)}FoxazTr5_2<d(|8Q#lD#4BK!Pct6*!VRF~=e zgj)>{9E6Xq3MHw%emTsP=_UYo?`QJ$kjvUDsv1Bp2reK|`G0#lhu^FpfJM|3fu9_f zFrQ!C#{bsw!_8H2(M4BlR{N9Sx>!0$(Cx=))(6<53W!a^-E7=%M^lxDM>#SVoQo^l zYZ+EL19pYlxLLXG3P~WQ<CiayjKRtUF~~g=EF{tMnxN-hvLCVT$rJuFQ+C}tu@ZdN zfrT4MX+u_z|26a|<k*@nlgVT-ozR)B$v9v>R48ey)xj)?oEf&1ME6MGqESP3Ef~vW z#zDn1Sw|ipDR}85h3{Cu{5pqxi`v3w)$tv&zKWhI)XP^X(5n|4G2BsyN+EuWt&=5K z@BxSTz~Mcfur?ZwIKltl*e|5!aF)4<JKUN=wq6WH8aQPyUcm>ImNi|r*Jk(P3_9Sp zX2M-9z=+9%6BBynsz(wYIh~M>qyWWILxb1~jjO^^YFTu{J`(AY-<^!eDBP{{9%v9} zL~QwhHVDI!(JE%;d~_;P(<tuuO$%RYpYZrQvy9;!11!MhkrhH?u2r%Zby$sRtU;1k zyoIpF83!3@q040xw_!R3b4}@~i1ekwZ0K@37(L3vUg^eK*uE0B=~oaVx*6wR;;EID zE6dlcJ==#M%SmP;md)xlOtLIX;vS2-OgBU{84&^%Ml)DHWChn22VIbnK+GN&hk)J- zqKQigig0v)7avjmP$u~v>QZPcaM|DHt5=|!Z=te|shgVcNFDr|z}L3eRF^rZfCSx8 z!Le<1e|G`hjeSiCE2t&vTl*2YQen#>Riugy_1z#@95JKUzOG36ShqxDct>p<5|<*a zMm%JlN!cmyOjt-1wCLsJcClaq1y-;Gavi%Pbrz`PczXT*B^2z!*8tTCtd8w&97hay zHp`0a$zn?YGippz>**ryUgAs*+sc#3x$2dQj{K}KL_+*EIRQ~SAg}`$YCwjwNLw3Y z1GLezE7Yjvj=k@q54Yvwjy)vr5Lv0@YPj2-eVX-@;gKu1UTV)YX>S;IRFRzMw;%6Q zuy-NiVoL#Iq1E7qR2<NR>E-k|=4U-%C8CSA4|mAeb-jJ5;E}H*12cK*^{=N|>{@o% z+7(;$BRlnDcUvQsz!-(>B*5tgs{e!K^FEiNgVtG-pftp*sBSe&RA}m#Ai|atmA(9U zMV&okBm>I%ph%t}@>b4*(5ESNKrWNJwh`xNS6XWy{*z_F`i5`{AgrvR^^65ppWUJ; zbn|?N@xW~fm4!4=SX%^L@_0J|!~^dK@9*h0OYt$@+!@}7`1S!S%?xX{szMWut8hDe zibPjDAOD6;n&1uC@1gLt@jhS%Tx!jJ$rv#})9o9gV3A=R!&XEhEK{$cZ6XpI;6`9} z{^rRh8-Q>Md*3ekY+YByw1vFq2(-0oJhNliSIa5!sDFTv!q}$kV*VL+U$3;MGs#P7 z!pMB=@k{|8a7}v|RPaF9eaog^UQ}3L2Vz^BS&+j67I_yHLl{N#G-5v^Esz!>!p;b) z!;0&&xouG>Re-DDDKq}{H@}0!M7e)0UOi4gZw@0tvQbd@TUbiX`G7}Tcz#RjhDr(M zA}DP#%WA#5R-0#>&sIf;z9dT}n@PnxY@`dNitZNoJfxz?t0=vZtfA!;IVzov;kfZr zx(0koiiAqE>2T<iah+7iAjHEF0E%Vw1<kz^g~m0iT7~hE;9)wv<vx2iWpUr+?TUU| zReSlE0gsS{nn%Z1&rO0*VG2Md*)z~m%|z)+*$#~lW{(T>l|B5hlc=MVx@QsR$BfiB z{)<+UYF*MwqEqL&_@ue)weypp7k5o=E*je@j?dwga$zHcpzW#0`hSMOZk%UlJBGA7 z<aG3@m$O}tmzT^|26u5t(~BY%ow~n7sZF#X2F7~7BWBS0u5`&M6>@gAj2rG_)8%~W z3y7&Oe>|SnTx%TF-nwE^>|Qdarr#OXuycW?w&_v*LDUwt9r21+meT3U$Z%-M=w$WV z`ovlmSUIz2;{)H?TKdpc;}k?eZ*R?@!uv*paEZez6_L#rAVBjm<jrutGbo4StNrqT zlVME4DtEiCKHMtA9q-0wQ*>;plK^zNwyk+*au#T;yh*u`_WinosgHb>^^TGH?ZAE! z<Na!FpP%Y@GnHFyYBk|IiESZ2JZ&bXqgkN{OXJjZ@<yXuWEzshywe1EEZH4ZF0ip} zNPAEZ2U3AFj+b1<&;HK6g{BispXu7Xr~R(@E*0Yym~{?Pa)3uYVTxB<m>I5{LS$yO zY9d)plI^`BLFmE*5HAZnaX<#Ks^$t34*BU}D5|LoORlBV8Xs7x5IoJ+{bv6PAotzG z(4_J!)ZE}u(f%Uf)1-jL-kr8d6wdDfXL+f&WnBv+$FQcf#4Ov|oqD%1LEvz(X^kjB zCC6)I15qwLL3l*{%ux*EuL2$fd{ovM^A-TEb%5c2Q0w7IdQk-3n61Z+tO6b9;(lJQ zsr!8%ZXMk-oB)X?uP2(_&b8NhkBGdRV(Seu+l@pvIn4egB%T1q(uW$iceXEz2zcry zApWZgThoGO#)}U~8~+&@Yzm_#SECx^37Z>_A!<7uw}NxZsoi83OqbqX0PU&Bnv9{I DnjH~# diff --git a/server/vbv_lernwelt/assignment/export.py b/server/vbv_lernwelt/assignment/export.py index fe845274..39ff9247 100644 --- a/server/vbv_lernwelt/assignment/export.py +++ b/server/vbv_lernwelt/assignment/export.py @@ -76,12 +76,24 @@ def export_competence_certificates( }, label="assignment_export", ) + + # handle the case where there are no competence certificate elements for the course session + try: + cces = grouped_cce[course_session_title] + except KeyError: + cces = [] + + try: + acs = grouped_ac[course_session_title] + except KeyError: + acs = [] + _create_sheet( wb, course_session_title, cs_users, - grouped_cce[course_session_title], - grouped_ac[course_session_title], + cces, + acs, circle_ids, ) @@ -105,7 +117,7 @@ def _create_sheet( ): sheet = wb.create_sheet(title=sanitize_sheet_name(title)) - if len(users) == 0: + if len(users) == 0 or len(competence_certificate_element) == 0: return sheet # headers @@ -121,7 +133,7 @@ def _create_sheet( if circle_ids and circle.id not in circle_ids: continue - col_prefix = f'Circle "{circle.title}" {cse.learning_content.title} ' + col_prefix = f'Circle "{circle.title}" {cse.learning_content.title}' sheet.cell( row=1, diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py index 10f6da0f..ee21e875 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py @@ -1,5 +1,8 @@ -from django.test import TestCase +import io +from openpyxl import load_workbook + +from vbv_lernwelt.assignment.export import export_competence_certificates from vbv_lernwelt.assignment.models import Assignment from vbv_lernwelt.assignment.services import update_assignment_completion from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID @@ -7,28 +10,32 @@ from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.models import User from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) +from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTextCase -class AttendanceExportTestCase(TestCase): +class AssignmentCompletionExportTestCase(ExportBaseTextCase): def setUp(self): create_default_users() self.course = create_test_course(include_vv=False, with_sessions=True) self.course_session_be = CourseSession.objects.get(title="Test Bern 2022 a") self.course_session_zh = CourseSession.objects.get(title="Test Zürich 2022 a") - self.attendance_course_be = ( - self.course_session_be.coursesessionattendancecourse_set.first() - ) - some = ( + self.casework = ( self.course.coursepage.get_descendants() .exact_type(Assignment) .filter(assignment__assignment_type="CASEWORK") + .first() + .specific ) - self.assignment = ( + self.edoniq_test = ( self.course.coursepage.get_descendants() .exact_type(Assignment) - .filter(assignment__assignment_type="CASEWORK") + .filter(assignment__assignment_type="EDONIQ_TEST") .first() .specific ) @@ -44,13 +51,156 @@ class AttendanceExportTestCase(TestCase): test_student3 = User.objects.get(email="test-student3@example.com") + # Bern assignments update_assignment_completion( assignment_user=self.test_student1, - assignment=self.assignment, + assignment=self.casework, course_session=self.course_session_be, completion_data={}, evaluation_points=20, ) - def test_attendance_export_single_cs(self): - self.assertTrue(True) + update_assignment_completion( + assignment_user=self.test_student1, + assignment=self.edoniq_test, + course_session=self.course_session_be, + completion_data={}, + evaluation_points=14, + evaluation_passed=False, + ) + + update_assignment_completion( + assignment_user=self.test_student2, + assignment=self.edoniq_test, + course_session=self.course_session_be, + completion_data={}, + evaluation_points=24, + evaluation_passed=True, + ) + + self.expected_data_be = [ + self._make_header(), + [ + self.test_student1.first_name, + self.test_student1.last_name, + self.test_student1.email, + self.test_student1.additional_json_data["Lehrvertragsnummer"], + "Nicht bestanden", + 58, + "Bestanden", + 83, + ], + [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + self.test_student2.additional_json_data["Lehrvertragsnummer"], + "Bestanden", + 100, + "Keine Daten", + "Keine Daten", + ], + [ + test_student3.first_name, + test_student3.last_name, + test_student3.email, + None, + "Keine Daten", + "Keine Daten", + "Keine Daten", + "Keine Daten", + ], + ] + + def _generate_workbook(self, course_session_ids): + export_data = io.BytesIO( + export_competence_certificates(course_session_ids, save_as_file=False) + ) + return load_workbook(export_data) + + def _make_header( + self, + ): + casework_assignment = CourseSessionAssignment.objects.filter( + course_session__id=self.course_session_be.id, + learning_content__content_assignment__competence_certificate__isnull=False, + ).first() + + edoniq_assignment = CourseSessionEdoniqTest.objects.filter( + course_session__id=self.course_session_be.id, + learning_content__content_assignment__competence_certificate__isnull=False, + ).first() + + return [ + "Vorname", + "Nachname", + "Email", + "Lehrvertragsnummer", + f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} bestanden', + f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} Resultat %', + f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} bestanden', + f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} Resultat %', + ] + + def test_export_single_cs(self): + wb = self._generate_workbook([self.course_session_be.id]) + self.assertEqual(len(wb.sheetnames), 1) + self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") + + self._check_export(wb, self.expected_data_be, 4, 8) + + def test_export_only_kn_elements(self): + self.edoniq_test.competence_certificate = None + self.edoniq_test.save() + + expected_data = [] + for row in self.expected_data_be: + expected_data.append( + [cell for i, cell in enumerate(row) if (i != 4 and i != 5)] + ) + + wb = self._generate_workbook([self.course_session_be.id]) + self._check_export(wb, expected_data, 4, 6) + + def test_export_multiple_cs(self): + update_assignment_completion( + assignment_user=self.test_student2, + assignment=self.casework, + course_session=self.course_session_zh, + completion_data={}, + evaluation_points=18, + ) + + update_assignment_completion( + assignment_user=self.test_student2, + assignment=self.edoniq_test, + course_session=self.course_session_zh, + completion_data={}, + evaluation_points=22, + evaluation_passed=False, + ) + + expected_data = [ + [cell for i, cell in enumerate(self._make_header()) if (i != 4 and i != 5)], + [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + self.test_student2.additional_json_data["Lehrvertragsnummer"], + "Bestanden", + 75, + ], + ] + + wb = self._generate_workbook( + [self.course_session_be.id, self.course_session_zh.id] + ) + self.assertEqual(len(wb.sheetnames), 2) + self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") + self.assertEqual(wb.sheetnames[1], "Test Zürich 2022 a") + + self._check_export(wb, self.expected_data_be, 4, 5) + + wb.active = wb["Test Zürich 2022 a"] + + self._check_export(wb, expected_data, 2, 5) diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 968be54e..7b6c9bd4 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -282,6 +282,13 @@ def create_test_course( ) csac.due_date.save() + _csa = CourseSessionAssignment.objects.create( + course_session=cs_zurich, + learning_content=LearningContentAssignment.objects.get( + slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" + ), + ) + region1 = CourseSessionGroup.objects.create( name="Region 1", course=course, diff --git a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py index 49f1c085..40d82517 100644 --- a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py +++ b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py @@ -11,7 +11,16 @@ from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course_session.services.export_attendance import export_attendance -class AttendanceExportTestCase(TestCase): +class ExportBaseTextCase(TestCase): + def _check_export(self, wb, expected_data, max_row, max_col): + for row in wb.active.iter_rows(max_col=max_col, max_row=max_row): + for cell in row: + self.assertEqual( + cell.value, expected_data[row[0].row - 1][row.index(cell)] + ) + + +class AttendanceExportTestCase(ExportBaseTextCase): def setUp(self): create_default_users() create_test_course(include_vv=False, with_sessions=True) @@ -76,29 +85,20 @@ class AttendanceExportTestCase(TestCase): return load_workbook(export_data) def _make_header(self, csac): - return ( - [ - "Vorname", - "Nachname", - "Email", - "Lehrvertragsnummer", - f"Anwesenheit {csac.get_circle().title} {csac.attendance_course_zh.due_date.start.strftime('%d.%m.%Y')}", - ], - ) - - def _check_attendance_export(self, wb, expected_data, max_row, max_col): - for row in wb.active.iter_rows(max_col=max_col, max_row=max_row): - for cell in row: - self.assertEqual( - cell.value, expected_data[row[0].row - 1][row.index(cell)] - ) + return [ + "Vorname", + "Nachname", + "Email", + "Lehrvertragsnummer", + f"Anwesenheit {csac.get_circle().title} {csac.attendance_course_zh.due_date.start.strftime('%d.%m.%Y')}", + ] def test_attendance_export_single_cs(self): wb = self._generate_workbook([self.course_session_be.id]) self.assertEqual(len(wb.sheetnames), 1) self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") - self._check_attendance_export(wb, self.expected_data_be, 4, 5) + self._check_export(wb, self.expected_data_be, 4, 5) def test_attendance_export_multiple_cs(self): self.attendance_course_zh.attendance_user_list = [ @@ -131,5 +131,7 @@ class AttendanceExportTestCase(TestCase): self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") self.assertEqual(wb.sheetnames[1], "Test Zürich 2022 a") + self._check_export(wb, self.expected_data_be, 4, 5) + wb.active = wb["Test Zürich 2022 a"] - self._check_attendance_export(wb, expected_data_zh, 2, 5) + self._check_export(wb, expected_data_zh, 2, 5) From b2268f73a89f009e2e5e4f177cfd4529646251f7 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 4 Jun 2024 14:56:24 +0200 Subject: [PATCH 08/24] wip: Fix tests --- .../vbv_lernwelt/course_session/tests/test_attendance_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py index 40d82517..9966818c 100644 --- a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py +++ b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py @@ -90,7 +90,7 @@ class AttendanceExportTestCase(ExportBaseTextCase): "Nachname", "Email", "Lehrvertragsnummer", - f"Anwesenheit {csac.get_circle().title} {csac.attendance_course_zh.due_date.start.strftime('%d.%m.%Y')}", + f"Anwesenheit {csac.get_circle().title} {csac.due_date.start.strftime('%d.%m.%Y')}", ] def test_attendance_export_single_cs(self): From 742d12edaa8c7cd371b5751f7f198f144b3228e4 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 4 Jun 2024 21:15:51 +0200 Subject: [PATCH 09/24] wip: Add feedback tests --- .../test_assignment_completions_export.py | 4 +- .../tests/test_attendance_export.py | 4 +- .../feedback/tests/test_feedback_export.py | 178 ++++++++++++++++++ 3 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 server/vbv_lernwelt/feedback/tests/test_feedback_export.py diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py index ee21e875..da3abb1a 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py @@ -14,10 +14,10 @@ from vbv_lernwelt.course_session.models import ( CourseSessionAssignment, CourseSessionEdoniqTest, ) -from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTextCase +from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTestCase -class AssignmentCompletionExportTestCase(ExportBaseTextCase): +class AssignmentCompletionExportTestCase(ExportBaseTestCase): def setUp(self): create_default_users() self.course = create_test_course(include_vv=False, with_sessions=True) diff --git a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py index 9966818c..242febc3 100644 --- a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py +++ b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py @@ -11,7 +11,7 @@ from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course_session.services.export_attendance import export_attendance -class ExportBaseTextCase(TestCase): +class ExportBaseTestCase(TestCase): def _check_export(self, wb, expected_data, max_row, max_col): for row in wb.active.iter_rows(max_col=max_col, max_row=max_row): for cell in row: @@ -20,7 +20,7 @@ class ExportBaseTextCase(TestCase): ) -class AttendanceExportTestCase(ExportBaseTextCase): +class AttendanceExportTestCase(ExportBaseTestCase): def setUp(self): create_default_users() create_test_course(include_vv=False, with_sessions=True) diff --git a/server/vbv_lernwelt/feedback/tests/test_feedback_export.py b/server/vbv_lernwelt/feedback/tests/test_feedback_export.py new file mode 100644 index 00000000..8857c292 --- /dev/null +++ b/server/vbv_lernwelt/feedback/tests/test_feedback_export.py @@ -0,0 +1,178 @@ +import datetime +import io + +from openpyxl import load_workbook + +from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTestCase +from vbv_lernwelt.feedback.factories import FeedbackResponseFactory +from vbv_lernwelt.feedback.models import FeedbackResponse +from vbv_lernwelt.feedback.services import export_feedback +from vbv_lernwelt.learnpath.models import Circle + + +class FeedbackExportTestCase(ExportBaseTestCase): + def setUp(self): + super().setUp() + + create_default_users() + create_test_course(include_vv=True, with_sessions=True) + + self.course_session_be = CourseSession.objects.get(title="Test Bern 2022 a") + self.course_session_zh = CourseSession.objects.get(title="Test Zürich 2022 a") + + self.circle_fahrzeug = Circle.objects.get( + slug="test-lehrgang-lp-circle-fahrzeug" + ) + self.circle_reisen = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen") + + self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID) + self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID) + + self.course_sessions = [ + self.course_session_be, + self.course_session_be, + self.course_session_zh, + ] + self.feedback_data = { + "satisfaction": [1, 4, 2], + "goal_attainment": [2, 4, 3], + "proficiency": [20, 60, 80], + "preparation_task_clarity": [True, False, True], + "instructor_competence": [1, 2, 3], + "instructor_respect": [40, 80, 100], + "instructor_open_feedback": ["super", "ok", "naja"], + "would_recommend": [False, True, False], + "course_positive_feedback": ["Bla", "Katze", "Hund"], + "course_negative_feedback": ["Maus", "Hase", "Fuchs"], + } + + self.users = [self.test_student1, self.test_student2, self.test_student2] + self.circles = [self.circle_fahrzeug, self.circle_fahrzeug, self.circle_reisen] + + for i in range(3): + FeedbackResponseFactory( + circle=self.circles[i], + course_session=self.course_sessions[i], + data={ + "satisfaction": self.feedback_data["satisfaction"][i], + "goal_attainment": self.feedback_data["goal_attainment"][i], + "proficiency": self.feedback_data["proficiency"][i], + "preparation_task_clarity": self.feedback_data[ + "preparation_task_clarity" + ][i], + "instructor_competence": self.feedback_data[ + "instructor_competence" + ][i], + "instructor_open_feedback": self.feedback_data[ + "instructor_open_feedback" + ][i], + "would_recommend": self.feedback_data["would_recommend"][i], + "instructor_respect": self.feedback_data["instructor_respect"][i], + "course_positive_feedback": self.feedback_data[ + "course_positive_feedback" + ][i], + "course_negative_feedback": self.feedback_data[ + "course_negative_feedback" + ][i], + "feedback_type": "uk", + }, + feedback_user=self.users[i], + submitted=True, + ) + + ( + self.expected_data_fahrzeug, + self.expected_data_reisen, + ) = self._generate_expected_data() + + def _generate_expected_data(self): + feedback_data = [] + + for i in range(3): + feedback_data.append( + [ + self.course_sessions[i].title, + datetime.datetime.now().strftime("%d.%m.%Y"), + self.feedback_data["satisfaction"][i], + self.feedback_data["goal_attainment"][i], + self.feedback_data["proficiency"][i], + self.feedback_data["preparation_task_clarity"][i], + self.feedback_data["instructor_competence"][i], + self.feedback_data["instructor_respect"][i], + self.feedback_data["instructor_open_feedback"][i], + self.feedback_data["would_recommend"][i], + self.feedback_data["course_positive_feedback"][i], + self.feedback_data["course_negative_feedback"][i], + ] + ) + + expected_data_fahrzeug = [ + self._make_header(), + feedback_data[0], + feedback_data[1], + ] + + expected_data_reisen = [ + self._make_header(), + feedback_data[2], + ] + + return expected_data_fahrzeug, expected_data_reisen + + def _generate_workbook(self, course_session_ids): + export_data = io.BytesIO( + export_feedback(course_session_ids, save_as_file=False) + ) + return load_workbook(export_data) + + def _make_header(self): + return [ + "Durchführung", + "Datum", + "Zufriedenheit insgesamt", + "Zielerreichung insgesamt", + "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?", + "Waren die Vorbereitungsaufträge klar und verständlich?", + "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?", + "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?", + "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?", + "Würdest du den Kurs weiterempfehlen?", + "Was hat dir besonders gut gefallen?", + "Wo siehst du Verbesserungspotential?", + ] + + def test_export_feedback(self): + wb = self._generate_workbook( + [self.course_session_be.id, self.course_session_zh.id] + ) + self.assertEqual(len(wb.sheetnames), 2) + self.assertEqual(wb.sheetnames[0], "Fahrzeug") + self.assertEqual(wb.sheetnames[1], "Reisen") + + self._check_export(wb, self.expected_data_fahrzeug, 3, 12) + + wb.active = wb["Reisen"] + self._check_export(wb, self.expected_data_reisen, 2, 12) + + def test_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) From 90393e76d0c9e6fdb37161ca6a2147ce42c5c93c Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Sat, 8 Jun 2024 16:50:54 +0200 Subject: [PATCH 10/24] Fix cypress tests --- .../tests/test_assignment_completions_export.py | 8 ++++++++ server/vbv_lernwelt/course/creators/test_course.py | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py index da3abb1a..e706ef1f 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py @@ -15,6 +15,7 @@ from vbv_lernwelt.course_session.models import ( CourseSessionEdoniqTest, ) from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTestCase +from vbv_lernwelt.learnpath.models import LearningContentAssignment class AssignmentCompletionExportTestCase(ExportBaseTestCase): @@ -163,6 +164,13 @@ class AssignmentCompletionExportTestCase(ExportBaseTestCase): self._check_export(wb, expected_data, 4, 6) def test_export_multiple_cs(self): + csa = CourseSessionAssignment.objects.create( + course_session=self.course_session_zh, + learning_content=LearningContentAssignment.objects.get( + slug=f"{self.course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" + ), + ) + update_assignment_completion( assignment_user=self.test_student2, assignment=self.casework, diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 7b6c9bd4..968be54e 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -282,13 +282,6 @@ def create_test_course( ) csac.due_date.save() - _csa = CourseSessionAssignment.objects.create( - course_session=cs_zurich, - learning_content=LearningContentAssignment.objects.get( - slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" - ), - ) - region1 = CourseSessionGroup.objects.create( name="Region 1", course=course, From 672464b8c98cd3d8c892616ce32f288b4952fdd9 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 11 Jun 2024 21:04:23 +0200 Subject: [PATCH 11/24] Add circle permission check, refactor --- .../test_assignment_completions_export.py | 2 +- .../dashboard/tests/test_views.py | 62 +++++-- server/vbv_lernwelt/dashboard/views.py | 103 ++++++++--- server/vbv_lernwelt/feedback/export.py | 160 ++++++++++++++++++ server/vbv_lernwelt/feedback/services.py | 130 +------------- 5 files changed, 291 insertions(+), 166 deletions(-) create mode 100644 server/vbv_lernwelt/feedback/export.py diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py index e706ef1f..0a41ef98 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py @@ -164,7 +164,7 @@ class AssignmentCompletionExportTestCase(ExportBaseTestCase): self._check_export(wb, expected_data, 4, 6) def test_export_multiple_cs(self): - csa = CourseSessionAssignment.objects.create( + _csa = CourseSessionAssignment.objects.create( course_session=self.course_session_zh, learning_content=LearningContentAssignment.objects.get( slug=f"{self.course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index 015360d2..eb44dd98 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -24,14 +24,15 @@ from vbv_lernwelt.course.creators.test_utils import ( ) from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser from vbv_lernwelt.dashboard.views import ( - _get_allowed_course_session_ids_for_user, + _get_course_sessions_with_roles_for_user, _get_mentee_count, _get_mentor_open_tasks_count, + _get_permitted_circles_ids_for_user_and_course_session, get_course_config, get_course_sessions_with_roles_for_user, ) from vbv_lernwelt.learning_mentor.models import LearningMentor -from vbv_lernwelt.learnpath.models import LearningUnit +from vbv_lernwelt.learnpath.models import Circle, LearningUnit from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback @@ -445,34 +446,69 @@ class GetMentorOpenTasksTestCase(BaseMentorAssignmentTestCase): class ExportXlsTestCase(TestCase): def setUp(self): create_default_users() - create_test_course(include_vv=False, with_sessions=True) + create_test_course(include_vv=True, with_sessions=True) + self.ALLOWED_ROLES = ["EXPERT", "SUPERVISOR"] def test_can_export_cs_dats(self): # supervisor sees all cs in region supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID, TEST_COURSE_SESSION_BERN_ID] - allowed_cs_id = _get_allowed_course_session_ids_for_user( - supervisor, requested_cs_ids + allowed_csrs_ids = _get_course_sessions_with_roles_for_user( + supervisor, self.ALLOWED_ROLES, requested_cs_ids ) - self.assertCountEqual(requested_cs_ids, allowed_cs_id) + + self.assertCountEqual(requested_cs_ids, [csr.id for csr in allowed_csrs_ids]) def test_student_cannot_export_data(self): # student cannot export any data student = User.objects.get(id=TEST_STUDENT1_USER_ID) requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] - allowed_cs_id = _get_allowed_course_session_ids_for_user( - student, requested_cs_ids + allowed_csrs_ids = _get_course_sessions_with_roles_for_user( + student, self.ALLOWED_ROLES, requested_cs_ids ) - self.assertCountEqual([], allowed_cs_id) + self.assertCountEqual([], allowed_csrs_ids) def test_trainer_cannot_export_other_cs(self): # trainer can only export cs where she is assigned - student = User.objects.get(email="test-trainer2@example.com") + trainer = User.objects.get(email="test-trainer2@example.com") requested_cs_ids = [TEST_COURSE_SESSION_BERN_ID, TEST_COURSE_SESSION_ZURICH_ID] - allowed_cs_id = _get_allowed_course_session_ids_for_user( - student, requested_cs_ids + allowed_csrs_ids = _get_course_sessions_with_roles_for_user( + trainer, self.ALLOWED_ROLES, requested_cs_ids ) - self.assertCountEqual([TEST_COURSE_SESSION_ZURICH_ID], allowed_cs_id) + + self.assertCountEqual( + [TEST_COURSE_SESSION_ZURICH_ID], [csr.id for csr in allowed_csrs_ids] + ) + + def test_trainer_can_get_circles_where_expert(self): + trainer = User.objects.get(email="test-trainer2@example.com") + circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug") + requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] + + allowed_csrs_ids = _get_course_sessions_with_roles_for_user( + trainer, self.ALLOWED_ROLES, requested_cs_ids + ) + + allowed_circles = _get_permitted_circles_ids_for_user_and_course_session( + trainer, allowed_csrs_ids, [circle.id] + ) + self.assertEqual( + [(TEST_COURSE_SESSION_ZURICH_ID, [circle.id])], allowed_circles + ) + + def test_trainer_cannot_get_circles_where_not_expert(self): + trainer = User.objects.get(email="test-trainer2@example.com") + circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen") + requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] + + allowed_csrs_ids = _get_course_sessions_with_roles_for_user( + trainer, self.ALLOWED_ROLES, requested_cs_ids + ) + + allowed_circles = _get_permitted_circles_ids_for_user_and_course_session( + trainer, allowed_csrs_ids, [circle.id] + ) + self.assertEqual([(TEST_COURSE_SESSION_ZURICH_ID, [])], allowed_circles) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 5f660a0e..afa67e80 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -1,8 +1,9 @@ from dataclasses import asdict, dataclass from datetime import date from enum import Enum -from typing import List, Set +from typing import List, Set, Tuple +from django.db.models import Q from django.http import HttpResponse from rest_framework import status from rest_framework.decorators import api_view @@ -32,7 +33,9 @@ from vbv_lernwelt.course_session.services.export_attendance import ( from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.serializers import DueDateSerializer +from vbv_lernwelt.feedback.export import export_feedback_with_circle_restriction from vbv_lernwelt.learning_mentor.models import LearningMentor +from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback @@ -537,26 +540,54 @@ def _get_mentor_open_tasks_count(course_id: str, mentor: User) -> int: @api_view(["POST"]) def export_attendance_as_xsl(request): - return _generate_xls_export(request, export_attendance) + circle_ids = request.data.get("circleIds", None) + requested_course_session_ids = request.data.get("courseSessionIds", []) + course_sessions_with_roles = _get_permitted_courses_sessions_for_user( + request.user, requested_course_session_ids + ) + data = export_attendance( + [cs.id for cs in course_sessions_with_roles], circle_ids=circle_ids + ) + return _make_excel_response(data) @api_view(["POST"]) def export_competence_certificate_as_xsl(request): - return _generate_xls_export(request, export_competence_certificates) - - -def _generate_xls_export(request, export_fn) -> HttpResponse: - requested_course_session_ids = request.data.get("courseSessionIds", []) circle_ids = request.data.get("circleIds", None) - - if not requested_course_session_ids: - return Response({"error": "no_cs_ids"}, status=status.HTTP_400_BAD_REQUEST) - - course_session_ids = _get_allowed_course_session_ids_for_user( + requested_course_session_ids = request.data.get("courseSessionIds", []) + course_sessions_with_roles = _get_permitted_courses_sessions_for_user( request.user, requested_course_session_ids + ) + data = export_competence_certificates( + course_sessions_with_roles, circle_ids=circle_ids + ) + return _make_excel_response(data) + + +@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_roless = _get_permitted_courses_sessions_for_user( + request.user, requested_course_session_ids + ) + data = export_feedback_with_circle_restriction() + return _make_excel_response(data) + + +def _get_permitted_courses_sessions_for_user( + user: User, requested_coursesession_ids: List[str] +) -> List[CourseSessionWithRoles]: + ALLOWED_ROLES = ["EXPERT", "SUPERVISOR"] + + user_course_sessions_with_roles = _get_course_sessions_with_roles_for_user( + user, ALLOWED_ROLES, requested_coursesession_ids ) # noqa - data = export_fn(course_session_ids, circle_ids=circle_ids) + return user_course_sessions_with_roles + + +def _make_excel_response(data: bytes) -> HttpResponse: response = HttpResponse( data, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", @@ -565,16 +596,40 @@ def _generate_xls_export(request, export_fn) -> HttpResponse: return response -def _get_allowed_course_session_ids_for_user( - user: User, requested_cs_ids: List[str] -) -> List[str]: - ALLOWED_ROLES = ["EXPERT", "SUPERVISOR"] - # 1. get course sessions for user with allowed roles - # 2. get overlapping course sessions with given course_session_ids - # Note: We don't care about the circle_ids as it's ok-ish that trainers could export other data - all_cs_ids_for_user = [ - csr._original.id +def _get_course_sessions_with_roles_for_user( + user: User, allowed_roles: List[str], requested_cs_ids: List[str] +) -> List[CourseSessionWithRoles]: + all_cs_roles_for_user = [ + csr for csr in get_course_sessions_with_roles_for_user(user) - if any(role in ALLOWED_ROLES for role in csr.roles) + if any(role in allowed_roles for role in csr.roles) + and csr.id in requested_cs_ids ] # noqa - return list(set(requested_cs_ids) & set(all_cs_ids_for_user)) + + return all_cs_roles_for_user + + +def _get_permitted_circles_ids_for_user_and_course_session( + user: User, + user_course_sessions_with_roles: List[CourseSessionWithRoles], + requested_circle_ids: Tuple[int, int], +): + allowed_circles_for_sessions = [] + for cswr in user_course_sessions_with_roles: + if "SUPERVISOR" in cswr.roles: + allowed_circles_for_sessions.append(requested_circle_ids) + else: + course_session_users = CourseSessionUser.objects.filter( + course_session=cswr.id, + user=user, + ) + allowed_circles = ( + Circle.objects.filter( + Q(expert__in=course_session_users) & Q(id__in=requested_circle_ids) + ) + .distinct() + .values_list("id", flat=True) + ) + allowed_circles_for_sessions.append((cswr.id, list(allowed_circles))) + + return allowed_circles_for_sessions diff --git a/server/vbv_lernwelt/feedback/export.py b/server/vbv_lernwelt/feedback/export.py new file mode 100644 index 00000000..7917949a --- /dev/null +++ b/server/vbv_lernwelt/feedback/export.py @@ -0,0 +1,160 @@ +from io import BytesIO +from itertools import groupby +from operator import attrgetter +from typing import List, Tuple + +import structlog +from django.db.models import QuerySet +from openpyxl import Workbook + +from vbv_lernwelt.course_session.services.export_attendance import ( + make_export_filename, + sanitize_sheet_name, +) +from vbv_lernwelt.feedback.models import FeedbackResponse + +logger = structlog.get_logger(__name__) + +VV_FEEDBACK_QUESTIONS = [ + ("satisfaction", "Zufriedenheit insgesamt"), + ("goal_attainment", "Zielerreichung insgesamt"), + ( + "proficiency", + "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?", + ), + ("preparation_task_clarity", "Waren die Praxisaufträge klar und verständlich?"), + ("would_recommend", "Würdest du den Circle weiterempfehlen?"), + ("course_positive_feedback", "Was hat dir besonders gut gefallen?"), + ("course_negative_feedback", "Wo siehst du Verbesserungspotential?"), +] + +UK_FEEDBACK_QUESTIONS = [ + ("satisfaction", "Zufriedenheit insgesamt"), + ("goal_attainment", "Zielerreichung insgesamt"), + ( + "proficiency", + "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?", + ), + ( + "preparation_task_clarity", + "Waren die Vorbereitungsaufträge klar und verständlich?", + ), + ( + "instructor_competence", + "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?", + ), + ( + "instructor_respect", + "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?", + ), + ( + "instructor_open_feedback", + "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?", + ), + ("would_recommend", "Würdest du den Kurs weiterempfehlen?"), + ("course_positive_feedback", "Was hat dir besonders gut gefallen?"), + ("course_negative_feedback", "Wo siehst du Verbesserungspotential?"), +] + + +def export_feedback(course_session_ids: list[str], save_as_file: bool, circles=None): + """ + Export for django view, all circles are allowed + """ + if circles: + feedback_unordered = FeedbackResponse.objects.filter( + course_session_id__in=course_session_ids, + circle__in=circles, + submitted=True, + ) + else: + feedback_unordered = FeedbackResponse.objects.filter( + course_session_id__in=course_session_ids, + submitted=True, + ) + + return _generate_feedback_export(feedback_unordered, save_as_file) + + +def export_feedback_with_circle_restriction( + course_sessions_with_circles: List[Tuple[int, List[int]]], save_as_file: bool +): + """ + Export for user export, only circles in specified course sessions are allowed + """ + feedback_unordered = FeedbackResponse.objects.none() + + for course_session_with_circles in course_sessions_with_circles: + feedback_unordered = feedback_unordered | FeedbackResponse.objects.filter( + course_session_id__in=course_session_with_circles[0], + circle__in=course_session_with_circles[1], + submitted=True, + ) + + return _generate_feedback_export(feedback_unordered, save_as_file) + + +def _generate_feedback_export(feedback_unordered: QuerySet, save_as_file: bool): + wb = Workbook() + + # remove the first sheet is just easier than keeping track of the active sheet + wb.remove(wb.active) + + feedbacks = feedback_unordered.order_by("circle", "course_session", "updated_at") + grouped_feedbacks = groupby(feedbacks, key=attrgetter("circle")) + + for circle, group_feedbacks in grouped_feedbacks: + group_feedbacks = list(group_feedbacks) + logger.debug( + "export_feedback_for_circle", + data={ + "circle": circle.id, + "count": len(group_feedbacks), + }, + label="feedback_export", + ) + _create_sheet(wb, circle.title, group_feedbacks) + + if save_as_file: + wb.save(make_export_filename(name="feedback_export")) + else: + output = BytesIO() + wb.save(output) + + output.seek(0) + return output.getvalue() + + +def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]): + sheet = wb.create_sheet(title=sanitize_sheet_name(title)) + + if len(data) == 0: + return sheet + + # we instruct the users not to mix exports of different courses, so we can assume the questions are the same and of the first type + question_data = ( + UK_FEEDBACK_QUESTIONS + if data[0].data["feedback_type"] == "uk" + else VV_FEEDBACK_QUESTIONS + ) + + # add header + sheet.cell(row=1, column=1, value="Durchführung") + sheet.cell(row=1, column=2, value="Datum") + questions = [q[1] for q in question_data] + for col_idx, title in enumerate(questions, start=3): + sheet.cell(row=1, column=col_idx, value=title) + + _add_rows(sheet, data, question_data) + return sheet + + +def _add_rows(sheet, data, question_data): + for row_idx, feedback in enumerate(data, start=2): + sheet.cell(row=row_idx, column=1, value=feedback.course_session.title) + sheet.cell( + row=row_idx, column=2, value=feedback.updated_at.date().strftime("%d.%m.%Y") + ) + for col_idx, question in enumerate(question_data, start=3): + response = feedback.data.get(question[0], "") + sheet.cell(row=row_idx, column=col_idx, value=response) diff --git a/server/vbv_lernwelt/feedback/services.py b/server/vbv_lernwelt/feedback/services.py index e4b49cfd..d0fefe20 100644 --- a/server/vbv_lernwelt/feedback/services.py +++ b/server/vbv_lernwelt/feedback/services.py @@ -1,19 +1,13 @@ -from io import BytesIO -from itertools import groupby -from operator import attrgetter from typing import Union import structlog from django.http import HttpResponse -from openpyxl import Workbook from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession from vbv_lernwelt.course.services import mark_course_completion -from vbv_lernwelt.course_session.services.export_attendance import ( - make_export_filename, - sanitize_sheet_name, -) +from vbv_lernwelt.course_session.services.export_attendance import make_export_filename +from vbv_lernwelt.feedback.export import export_feedback from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.learnpath.models import ( LearningContentFeedbackUK, @@ -22,47 +16,6 @@ from vbv_lernwelt.learnpath.models import ( logger = structlog.get_logger(__name__) -VV_FEEDBACK_QUESTIONS = [ - ("satisfaction", "Zufriedenheit insgesamt"), - ("goal_attainment", "Zielerreichung insgesamt"), - ( - "proficiency", - "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?", - ), - ("preparation_task_clarity", "Waren die Praxisaufträge klar und verständlich?"), - ("would_recommend", "Würdest du den Circle weiterempfehlen?"), - ("course_positive_feedback", "Was hat dir besonders gut gefallen?"), - ("course_negative_feedback", "Wo siehst du Verbesserungspotential?"), -] - -UK_FEEDBACK_QUESTIONS = [ - ("satisfaction", "Zufriedenheit insgesamt"), - ("goal_attainment", "Zielerreichung insgesamt"), - ( - "proficiency", - "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?", - ), - ( - "preparation_task_clarity", - "Waren die Vorbereitungsaufträge klar und verständlich?", - ), - ( - "instructor_competence", - "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?", - ), - ( - "instructor_respect", - "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?", - ), - ( - "instructor_open_feedback", - "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?", - ), - ("would_recommend", "Würdest du den Kurs weiterempfehlen?"), - ("course_positive_feedback", "Was hat dir besonders gut gefallen?"), - ("course_negative_feedback", "Wo siehst du Verbesserungspotential?"), -] - def update_feedback_response( feedback_user: User, @@ -154,85 +107,6 @@ def initial_data_for_feedback_page( return {} -def export_feedback(course_session_ids: list[str], save_as_file: bool, circles=None): - wb = Workbook() - - # remove the first sheet is just easier than keeping track of the active sheet - wb.remove(wb.active) - - if circles: - feedback_unordered = FeedbackResponse.objects.filter( - course_session_id__in=course_session_ids, - circle__in=circles, - submitted=True, - ) - else: - feedback_unordered = FeedbackResponse.objects.filter( - course_session_id__in=course_session_ids, - submitted=True, - ) - - feedbacks = feedback_unordered.order_by("circle", "course_session", "updated_at") - grouped_feedbacks = groupby(feedbacks, key=attrgetter("circle")) - - for circle, group_feedbacks in grouped_feedbacks: - group_feedbacks = list(group_feedbacks) - logger.debug( - "export_feedback_for_circle", - data={ - "circle": circle.id, - "course_session_ids": course_session_ids, - "count": len(group_feedbacks), - }, - label="feedback_export", - ) - _create_sheet(wb, circle.title, group_feedbacks) - - if save_as_file: - wb.save(make_export_filename(name="feedback_export")) - else: - output = BytesIO() - wb.save(output) - - output.seek(0) - return output.getvalue() - - -def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]): - sheet = wb.create_sheet(title=sanitize_sheet_name(title)) - - if len(data) == 0: - return sheet - - # we instruct the users not to mix exports of different courses, so we can assume the questions are the same and of the first type - question_data = ( - UK_FEEDBACK_QUESTIONS - if data[0].data["feedback_type"] == "uk" - else VV_FEEDBACK_QUESTIONS - ) - - # add header - sheet.cell(row=1, column=1, value="Durchführung") - sheet.cell(row=1, column=2, value="Datum") - questions = [q[1] for q in question_data] - for col_idx, title in enumerate(questions, start=3): - sheet.cell(row=1, column=col_idx, value=title) - - _add_rows(sheet, data, question_data) - return sheet - - -def _add_rows(sheet, data, question_data): - for row_idx, feedback in enumerate(data, start=2): - sheet.cell(row=row_idx, column=1, value=feedback.course_session.title) - sheet.cell( - row=row_idx, column=2, value=feedback.updated_at.date().strftime("%d.%m.%Y") - ) - for col_idx, question in enumerate(question_data, start=3): - response = feedback.data.get(question[0], "") - sheet.cell(row=row_idx, column=col_idx, value=response) - - # used as admin action, that's why it's not in the views.py def get_feedbacks_for_course_sessions(_modeladmin, _request, queryset): file_name = "feedback_export_durchfuehrungen" From e955d6dddc432a02698fe04b2e1159c94d1fe5cd Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Wed, 12 Jun 2024 10:14:48 +0200 Subject: [PATCH 12/24] wip: Add tests --- .../dashboard/tests/test_views.py | 23 +++++++++++++++++ server/vbv_lernwelt/dashboard/views.py | 2 +- server/vbv_lernwelt/feedback/export.py | 4 +-- .../feedback/tests/test_feedback_export.py | 25 +++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index eb44dd98..0d7423bc 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -512,3 +512,26 @@ class ExportXlsTestCase(TestCase): trainer, allowed_csrs_ids, [circle.id] ) self.assertEqual([(TEST_COURSE_SESSION_ZURICH_ID, [])], allowed_circles) + + def test_supervisor_can_get_all_circles(self): + supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) + circle_reisen = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen") + circle_fahrzeug = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug") + requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] + + allowed_csrs_ids = _get_course_sessions_with_roles_for_user( + supervisor, self.ALLOWED_ROLES, requested_cs_ids + ) + + allowed_circles = _get_permitted_circles_ids_for_user_and_course_session( + supervisor, + allowed_csrs_ids, + [ + circle_fahrzeug.id, + circle_reisen.id, + ], + ) + self.assertEqual( + [(TEST_COURSE_SESSION_ZURICH_ID, [circle_fahrzeug.id, circle_reisen.id])], + allowed_circles, + ) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index afa67e80..f9aa54ec 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -617,7 +617,7 @@ def _get_permitted_circles_ids_for_user_and_course_session( allowed_circles_for_sessions = [] for cswr in user_course_sessions_with_roles: if "SUPERVISOR" in cswr.roles: - allowed_circles_for_sessions.append(requested_circle_ids) + allowed_circles_for_sessions.append((cswr.id, requested_circle_ids)) else: course_session_users = CourseSessionUser.objects.filter( course_session=cswr.id, diff --git a/server/vbv_lernwelt/feedback/export.py b/server/vbv_lernwelt/feedback/export.py index 7917949a..cd4ec5af 100644 --- a/server/vbv_lernwelt/feedback/export.py +++ b/server/vbv_lernwelt/feedback/export.py @@ -86,8 +86,8 @@ def export_feedback_with_circle_restriction( for course_session_with_circles in course_sessions_with_circles: feedback_unordered = feedback_unordered | FeedbackResponse.objects.filter( - course_session_id__in=course_session_with_circles[0], - circle__in=course_session_with_circles[1], + course_session_id=course_session_with_circles[0], + circle_id__in=course_session_with_circles[1], submitted=True, ) diff --git a/server/vbv_lernwelt/feedback/tests/test_feedback_export.py b/server/vbv_lernwelt/feedback/tests/test_feedback_export.py index 8857c292..c24a7f93 100644 --- a/server/vbv_lernwelt/feedback/tests/test_feedback_export.py +++ b/server/vbv_lernwelt/feedback/tests/test_feedback_export.py @@ -9,6 +9,7 @@ from vbv_lernwelt.core.models import User from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTestCase +from vbv_lernwelt.feedback.export import export_feedback_with_circle_restriction from vbv_lernwelt.feedback.factories import FeedbackResponseFactory from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.services import export_feedback @@ -159,6 +160,30 @@ class FeedbackExportTestCase(ExportBaseTestCase): wb.active = wb["Reisen"] self._check_export(wb, self.expected_data_reisen, 2, 12) + def test_export_feedback_with_cs_circle_pairs(self): + cs_circle_pairs = [ + ( + self.course_session_be.id, + [self.circle_fahrzeug.id, self.circle_reisen.id], + ), + ( + self.course_session_zh.id, + [self.circle_reisen.id, self.circle_fahrzeug.id], + ), + ] + export_data = io.BytesIO( + export_feedback_with_circle_restriction(cs_circle_pairs, save_as_file=False) + ) + wb = load_workbook(export_data) + self.assertEqual(len(wb.sheetnames), 2) + self.assertEqual(wb.sheetnames[0], "Fahrzeug") + self.assertEqual(wb.sheetnames[1], "Reisen") + + self._check_export(wb, self.expected_data_fahrzeug, 3, 12) + + wb.active = wb["Reisen"] + self._check_export(wb, self.expected_data_reisen, 2, 12) + def test_does_not_include_unsubmitted_feedback(self): feedback = FeedbackResponse.objects.get( circle=self.circle_reisen, From 06572c9e1f4969d1d67c73e5902fd360c9a1028e Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Thu, 13 Jun 2024 13:05:54 +0200 Subject: [PATCH 13/24] wip: Remove old code --- client/src/stores/dashboard.ts | 50 ++-------------------------------- 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/client/src/stores/dashboard.ts b/client/src/stores/dashboard.ts index da9d7417..dbcdcae3 100644 --- a/client/src/stores/dashboard.ts +++ b/client/src/stores/dashboard.ts @@ -4,17 +4,12 @@ import type { DashboardConfigType, } from "@/gql/graphql"; import type { DashboardCourseConfigType } from "@/services/dashboard"; -import { - fetchDashboardConfig, - fetchDashboardConfigv2, - fetchStatisticData, -} from "@/services/dashboard"; +import { fetchDashboardConfigv2, fetchStatisticData } from "@/services/dashboard"; import { defineStore } from "pinia"; import type { Ref } from "vue"; import { ref } from "vue"; export const useDashboardStore = defineStore("dashboard", () => { - const dashboardConfigs: Ref<DashboardConfigType[]> = ref([]); const dashboardConfigsv2: Ref<DashboardCourseConfigType[]> = ref([]); const currentDashboardConfig: Ref<DashboardConfigType | undefined> = ref(); const dashBoardDataCache: Record< @@ -25,55 +20,16 @@ export const useDashboardStore = defineStore("dashboard", () => { ref(null); const loading = ref(false); - // const loadDashboardData = async (type: DashboardType, id: string) => { - // let data; - // switch (type) { - // case "STATISTICS_DASHBOARD": - // data = await fetchStatisticData(id); - // break; - // case "PROGRESS_DASHBOARD": - // data = await fetchProgressData(id); - // break; - // default: - // return; - // } - // dashBoardDataCache[id] = data; - // currentDashBoardData.value = data; - // }; - const switchAndLoadDashboardConfig = async (config: DashboardConfigType) => { currentDashboardConfig.value = config; await loadDashboardDetails(); }; - const loadDashboardConfig = async () => { - if (dashboardConfigs.value.length > 0) return; - const configData = await fetchDashboardConfig(); - if (configData && configData.length > 0) { - dashboardConfigs.value = configData; - await switchAndLoadDashboardConfig(configData[0]); - } - }; - const loadDashboardDetails = async () => { loading.value = true; dashboardConfigsv2.value = await fetchDashboardConfigv2(); console.log("got dashboard config v2: ", dashboardConfigsv2.value); - try { - // if (!currentDashboardConfig.value) { - // await loadDashboardConfig(); - // return; - // } - // const { id, dashboard_type } = currentDashboardConfig.value; - // if (dashBoardDataCache[id]) { - // currentDashBoardData.value = dashBoardDataCache[id]; - // return; - // } - // // await loadDashboardData(dashboard_type, id); - } finally { - console.log("done loading dashboard details"); - loading.value = false; - } + loading.value = false; }; const loadStatisticsData = async (id: string) => { @@ -89,11 +45,9 @@ export const useDashboardStore = defineStore("dashboard", () => { }; return { - dashboardConfigs, dashboardConfigsv2, currentDashboardConfig, switchAndLoadDashboardConfig, - loadDashboardConfig, loadDashboardDetails, currentDashBoardData, loading, From b8813482b08327c350100339e6e3fcd1de822fc0 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Thu, 13 Jun 2024 14:45:53 +0200 Subject: [PATCH 14/24] Fix export, add client code and urls --- client/src/services/dashboard.ts | 23 ++++++++++++- server/config/urls.py | 6 +++- .../dashboard/tests/test_views.py | 18 ++++++---- server/vbv_lernwelt/dashboard/views.py | 34 ++++++++++++------- server/vbv_lernwelt/feedback/export.py | 1 + 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index 2abd9374..da2664bb 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -6,7 +6,7 @@ import { DASHBOARD_MENTOR_COMPETENCE_SUMMARY, } from "@/graphql/queries"; -import { itGetCached } from "@/fetchHelpers"; +import { itGetCached, itPost } from "@/fetchHelpers"; import type { AssignmentsStatisticsType, CourseProgressType, @@ -189,6 +189,27 @@ export async function fetchOpenTasksCount(courseId: string) { ); } +export async function exportFeedback(data: { + courseSessionIds: string[]; + circleIds: string[]; +}) { + return await itPost("/api/dashboard/export/feedback/", data); +} + +export async function exportAttendance(data: { + courseSessionIds: string[]; + circleIds: string[]; +}) { + return await itPost("/api/dashboard/export/attendance/", data); +} + +export async function exportCertificate(data: { + courseSessionIds: string[]; + circleIds: string[]; +}) { + return await itPost("/api/dashboard/export/certificate/", data); +} + export function courseIdForCourseSlug( dashboardConfigs: DashboardCourseConfigType[], courseSlug: string diff --git a/server/config/urls.py b/server/config/urls.py index c65d0665..4748e663 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -42,6 +42,8 @@ from vbv_lernwelt.course.views import ( from vbv_lernwelt.course_session.views import get_course_session_documents from vbv_lernwelt.dashboard.views import ( export_attendance_as_xsl, + export_competence_certificate_as_xsl, + export_feedback_as_xsl, get_dashboard_config, get_dashboard_due_dates, get_dashboard_persons, @@ -132,7 +134,9 @@ urlpatterns = [ path(r"api/dashboard/course/<str:course_id>/mentees/", get_mentee_count, name="get_mentee_count"), path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count, name="get_mentor_open_tasks_count"), - path(r"api/dashboard/export/attendance", export_attendance_as_xsl, name="export_attendance_as_xsl"), + path(r"api/dashboard/export/attendance/", export_attendance_as_xsl, name="export_attendance_as_xsl"), + path(r"api/dashboard/export/certificate/", export_competence_certificate_as_xsl, name="export_certificate_as_xsl"), + path(r"api/dashboard/export/feedback/", export_feedback_as_xsl, name="export_feedback_as_xsl"), # course path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index 0d7423bc..d3eaeae7 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -452,7 +452,10 @@ class ExportXlsTestCase(TestCase): def test_can_export_cs_dats(self): # supervisor sees all cs in region supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) - requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID, TEST_COURSE_SESSION_BERN_ID] + requested_cs_ids = [ + str(TEST_COURSE_SESSION_ZURICH_ID), + str(TEST_COURSE_SESSION_BERN_ID), + ] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( supervisor, self.ALLOWED_ROLES, requested_cs_ids @@ -463,7 +466,7 @@ class ExportXlsTestCase(TestCase): def test_student_cannot_export_data(self): # student cannot export any data student = User.objects.get(id=TEST_STUDENT1_USER_ID) - requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] + requested_cs_ids = [str(TEST_COURSE_SESSION_ZURICH_ID)] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( student, self.ALLOWED_ROLES, requested_cs_ids @@ -473,7 +476,10 @@ class ExportXlsTestCase(TestCase): def test_trainer_cannot_export_other_cs(self): # trainer can only export cs where she is assigned trainer = User.objects.get(email="test-trainer2@example.com") - requested_cs_ids = [TEST_COURSE_SESSION_BERN_ID, TEST_COURSE_SESSION_ZURICH_ID] + requested_cs_ids = [ + str(TEST_COURSE_SESSION_BERN_ID), + str(TEST_COURSE_SESSION_ZURICH_ID), + ] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( trainer, self.ALLOWED_ROLES, requested_cs_ids @@ -486,7 +492,7 @@ class ExportXlsTestCase(TestCase): def test_trainer_can_get_circles_where_expert(self): trainer = User.objects.get(email="test-trainer2@example.com") circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug") - requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] + requested_cs_ids = [str(TEST_COURSE_SESSION_ZURICH_ID)] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( trainer, self.ALLOWED_ROLES, requested_cs_ids @@ -502,7 +508,7 @@ class ExportXlsTestCase(TestCase): def test_trainer_cannot_get_circles_where_not_expert(self): trainer = User.objects.get(email="test-trainer2@example.com") circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen") - requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] + requested_cs_ids = [str(TEST_COURSE_SESSION_ZURICH_ID)] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( trainer, self.ALLOWED_ROLES, requested_cs_ids @@ -517,7 +523,7 @@ class ExportXlsTestCase(TestCase): supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) circle_reisen = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen") circle_fahrzeug = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug") - requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] + requested_cs_ids = [str(TEST_COURSE_SESSION_ZURICH_ID)] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( supervisor, self.ALLOWED_ROLES, requested_cs_ids diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index f9aa54ec..41000109 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -1,3 +1,4 @@ +import base64 from dataclasses import asdict, dataclass from datetime import date from enum import Enum @@ -546,7 +547,8 @@ def export_attendance_as_xsl(request): request.user, requested_course_session_ids ) data = export_attendance( - [cs.id for cs in course_sessions_with_roles], circle_ids=circle_ids + [cs.id for cs in course_sessions_with_roles], + circle_ids=[int(circle_id) for circle_id in circle_ids], ) return _make_excel_response(data) @@ -559,7 +561,8 @@ def export_competence_certificate_as_xsl(request): request.user, requested_course_session_ids ) data = export_competence_certificates( - course_sessions_with_roles, circle_ids=circle_ids + [cswr.id for cswr in course_sessions_with_roles], + circle_ids=[int(circle_id) for circle_id in circle_ids], ) return _make_excel_response(data) @@ -568,10 +571,17 @@ def export_competence_certificate_as_xsl(request): def export_feedback_as_xsl(request): circle_ids = request.data.get("circleIds", None) requested_course_session_ids = request.data.get("courseSessionIds", []) - course_sessions_with_roless = _get_permitted_courses_sessions_for_user( + course_sessions_with_roles = _get_permitted_courses_sessions_for_user( request.user, requested_course_session_ids - ) - data = export_feedback_with_circle_restriction() + ) # 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) @@ -588,12 +598,12 @@ def _get_permitted_courses_sessions_for_user( def _make_excel_response(data: bytes) -> HttpResponse: - response = HttpResponse( - data, - content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) - response["Content-Disposition"] = f'attachment; filename="{make_export_filename()}"' - return response + encoded_data = base64.b64encode(data).decode("utf-8") + + # Create the JSON response + response_data = {"encoded_data": encoded_data, "file_name": make_export_filename()} + + return Response(response_data, status=200) def _get_course_sessions_with_roles_for_user( @@ -603,7 +613,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 str(csr.id) in requested_cs_ids ] # noqa return all_cs_roles_for_user diff --git a/server/vbv_lernwelt/feedback/export.py b/server/vbv_lernwelt/feedback/export.py index cd4ec5af..bf81bbd7 100644 --- a/server/vbv_lernwelt/feedback/export.py +++ b/server/vbv_lernwelt/feedback/export.py @@ -118,6 +118,7 @@ def _generate_feedback_export(feedback_unordered: QuerySet, save_as_file: bool): if save_as_file: wb.save(make_export_filename(name="feedback_export")) else: + # todo handle IndexError output = BytesIO() wb.save(output) From d6293e879d2cff4864d839a249bb6e8f0c137781 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Thu, 13 Jun 2024 15:18:35 +0200 Subject: [PATCH 15/24] Fix test --- server/vbv_lernwelt/dashboard/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index d3eaeae7..2e4b76fe 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -461,7 +461,7 @@ class ExportXlsTestCase(TestCase): supervisor, self.ALLOWED_ROLES, requested_cs_ids ) - self.assertCountEqual(requested_cs_ids, [csr.id for csr in allowed_csrs_ids]) + self.assertCountEqual([int(cs) for cs in requested_cs_ids], [csr.id for csr in allowed_csrs_ids]) def test_student_cannot_export_data(self): # student cannot export any data From 50d5c4080d840c652cadeb0ab46b82b247c7eb01 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Thu, 13 Jun 2024 15:35:20 +0200 Subject: [PATCH 16/24] Revert "wip: Remove old code" This reverts commit 06572c9e1f4969d1d67c73e5902fd360c9a1028e. --- client/src/stores/dashboard.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/client/src/stores/dashboard.ts b/client/src/stores/dashboard.ts index dbcdcae3..69be8744 100644 --- a/client/src/stores/dashboard.ts +++ b/client/src/stores/dashboard.ts @@ -4,12 +4,17 @@ import type { DashboardConfigType, } from "@/gql/graphql"; import type { DashboardCourseConfigType } from "@/services/dashboard"; -import { fetchDashboardConfigv2, fetchStatisticData } from "@/services/dashboard"; +import { + fetchDashboardConfig, + fetchDashboardConfigv2, + fetchStatisticData, +} from "@/services/dashboard"; import { defineStore } from "pinia"; import type { Ref } from "vue"; import { ref } from "vue"; export const useDashboardStore = defineStore("dashboard", () => { + const dashboardConfigs: Ref<DashboardConfigType[]> = ref([]); const dashboardConfigsv2: Ref<DashboardCourseConfigType[]> = ref([]); const currentDashboardConfig: Ref<DashboardConfigType | undefined> = ref(); const dashBoardDataCache: Record< @@ -25,6 +30,15 @@ export const useDashboardStore = defineStore("dashboard", () => { await loadDashboardDetails(); }; + const loadDashboardConfig = async () => { + if (dashboardConfigs.value.length > 0) return; + const configData = await fetchDashboardConfig(); + if (configData && configData.length > 0) { + dashboardConfigs.value = configData; + await switchAndLoadDashboardConfig(configData[0]); + } + }; + const loadDashboardDetails = async () => { loading.value = true; dashboardConfigsv2.value = await fetchDashboardConfigv2(); @@ -45,9 +59,11 @@ export const useDashboardStore = defineStore("dashboard", () => { }; return { + dashboardConfigs, dashboardConfigsv2, currentDashboardConfig, switchAndLoadDashboardConfig, + loadDashboardConfig, loadDashboardDetails, currentDashBoardData, loading, From 7356056baf19e351377393ae8e116838722030b6 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Mon, 17 Jun 2024 09:20:47 +0200 Subject: [PATCH 17/24] wip: Add backend translations [skip ci] --- server/locale/de/LC_MESSAGES/django.mo | Bin 0 -> 380 bytes server/locale/de/LC_MESSAGES/django.po | 263 ++++++++++++++++++ server/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 2301 bytes server/locale/fr/LC_MESSAGES/django.po | 263 ++++++++++++++++++ server/locale/it/LC_MESSAGES/django.mo | Bin 0 -> 2191 bytes server/locale/it/LC_MESSAGES/django.po | 263 ++++++++++++++++++ server/vbv_lernwelt/assignment/export.py | 15 +- .../services/export_attendance.py | 15 +- .../dashboard/tests/test_views.py | 4 +- server/vbv_lernwelt/feedback/export.py | 29 +- .../vbv_lernwelt/feedback/graphql/queries.py | 38 +++ 11 files changed, 864 insertions(+), 26 deletions(-) create mode 100644 server/locale/de/LC_MESSAGES/django.mo create mode 100644 server/locale/de/LC_MESSAGES/django.po create mode 100644 server/locale/fr/LC_MESSAGES/django.mo create mode 100644 server/locale/fr/LC_MESSAGES/django.po create mode 100644 server/locale/it/LC_MESSAGES/django.mo create mode 100644 server/locale/it/LC_MESSAGES/django.po create mode 100644 server/vbv_lernwelt/feedback/graphql/queries.py diff --git a/server/locale/de/LC_MESSAGES/django.mo b/server/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..71cbdf3e9d8d54be31066ec4ad8628bc2c1f2845 GIT binary patch literal 380 zcmYL@K~KUk7=|%=+R?Lz&%}d9i{c3jGZa>EvE7z2Nc2{r&Y96JZ6W$Y{CoZuJ5A(G zp7i_Dx9RhJeDu}vIq;l#&OC>nD^HugXY4QU{MmN?lNtRkR}RH%w3NnHT4Bh@<dMuM zENMQE(<I1cqm4t{mem@Kx1~je#t>vF%H^(V-=Ii1iQ$Qo9Pt!I1Rhe%<ICqXp#c-t zp~47dYvHuPFkLVP3E>oml#`f^NEGFCKEL->Rc=KoQ6a?!10%_7(V7ey8`V`;n{war z20Z3;uifk31QV^CRQ|iq#``$=;jWunRB8aLH({)F;i8zL{=V00y-I_qTIqGAN(}v% i$^}`yHKImSZ8jEzYJOK6-VWez49^vuhS0kh1f3tbb!oc* literal 0 HcmV?d00001 diff --git a/server/locale/de/LC_MESSAGES/django.po b/server/locale/de/LC_MESSAGES/django.po new file mode 100644 index 00000000..115a551c --- /dev/null +++ b/server/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,263 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-17 08:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: vbv_lernwelt/assignment/export.py:181 +msgid "Bestanden" +msgstr "" + +#: vbv_lernwelt/assignment/export.py:181 +msgid "Nicht bestanden" +msgstr "" + +#: vbv_lernwelt/assignment/export.py:195 vbv_lernwelt/assignment/export.py:198 +#: vbv_lernwelt/assignment/export.py:199 +msgid "Keine Daten" +msgstr "" + +#: vbv_lernwelt/core/admin.py:32 +msgid "Personal info" +msgstr "" + +#: vbv_lernwelt/core/admin.py:34 +msgid "Permissions" +msgstr "" + +#: vbv_lernwelt/core/admin.py:45 +msgid "Important dates" +msgstr "" + +#: vbv_lernwelt/core/admin.py:47 +msgid "Profile" +msgstr "" + +#: vbv_lernwelt/core/admin.py:62 +msgid "Organisation" +msgstr "" + +#: vbv_lernwelt/core/admin.py:75 +msgid "Additional data" +msgstr "" + +#: vbv_lernwelt/course/models.py:24 vbv_lernwelt/course/models.py:62 +msgid "Titel" +msgstr "" + +#: vbv_lernwelt/course/models.py:26 +msgid "Kategorie-Name" +msgstr "" + +#: vbv_lernwelt/course/models.py:29 +msgid "Slug" +msgstr "" + +#: vbv_lernwelt/course/models.py:64 +msgid "Allgemein" +msgstr "" + +#: vbv_lernwelt/course/models.py:177 +msgid "Lehrgang-Seite" +msgstr "" + +#: vbv_lernwelt/course/models.py:272 +msgid "Teilnehmer" +msgstr "" + +#: vbv_lernwelt/course/models.py:273 +msgid "Experte/Trainer" +msgstr "" + +#: vbv_lernwelt/course/models.py:332 +msgid "Dokumente im Circle ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:336 +msgid "Lernmentor-Funktion ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:340 +msgid "Kompetenzweise ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:343 +msgid "Versicherungsvermittler-Lehrgang" +msgstr "" + +#: vbv_lernwelt/course/models.py:344 +msgid "ÜK-Lehrgang" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:111 +msgid "Anwesend" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:111 +msgid "Nicht anwesend" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:118 +msgid "Vorname" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:119 +msgid "Nachname" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:120 +msgid "Email" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:121 +#: vbv_lernwelt/course_session/services/export_attendance.py:133 +msgid "Lehrvertragsnummer" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:33 +msgid "Zufriedenheit insgesamt" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:34 +msgid "Zielerreichung insgesamt" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:37 +msgid "" +"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:41 +msgid "Waren die Vorbereitungsaufträge klar und verständlich?" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:45 +msgid "" +"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der " +"Kursleiterin?" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:49 +msgid "" +"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und " +"aufgegriffen?" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:53 +msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:55 +msgid "Würdest du den Kurs weiterempfehlen?" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:56 +msgid "Was hat dir besonders gut gefallen?" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:57 +msgid "Wo siehst du Verbesserungspotential?" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:144 +msgid "Durchführung" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:145 +msgid "Datum" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:31 +msgid "Internet" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:32 +msgid "Leaflet" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:33 +msgid "Newspaper" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:34 +msgid "Personal recommendation" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:35 +msgid "Public event" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:36 +msgid "Other" +msgstr "" + +#: vbv_lernwelt/media_files/models.py:27 vbv_lernwelt/media_files/models.py:54 +#: vbv_lernwelt/media_files/models.py:64 vbv_lernwelt/media_files/models.py:83 +msgid "file" +msgstr "" + +#: vbv_lernwelt/notify/models.py:9 +msgid "User Interaction" +msgstr "" + +#: vbv_lernwelt/notify/models.py:10 +msgid "Progress" +msgstr "" + +#: vbv_lernwelt/notify/models.py:11 +msgid "Information" +msgstr "" + +#: vbv_lernwelt/notify/models.py:16 +msgid "Attendance Course Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:18 +msgid "Assignment Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:21 +msgid "Casework Expert Evaluation Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:23 +msgid "Casework Submitted" +msgstr "" + +#: vbv_lernwelt/notify/models.py:24 +msgid "Casework Evaluated" +msgstr "" + +#: vbv_lernwelt/notify/models.py:25 +msgid "New Feedback" +msgstr "" + +#: vbv_lernwelt/notify/models.py:28 +msgid "Self Evaluation Feedback Requested" +msgstr "" + +#: vbv_lernwelt/notify/models.py:31 +msgid "Self Evaluation Feedback Provided" +msgstr "" + +#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:8 +msgid "Sites menu" +msgstr "" + +#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:14 +msgid "Edit this page" +msgstr "" diff --git a/server/locale/fr/LC_MESSAGES/django.mo b/server/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..5c10a8378da9bda52bcc4ae06a25c6dde22278e3 GIT binary patch literal 2301 zcmah~JB%Df5FNn$7R<+BAONcYBR*zqOk}Nnw%6W0bGqHV^ZhJNB)v1WGku=v9{OV& zi@+iyLr4T<1Qucm2nZmtNQMYOh>!pg5D*bS3JI@fZ@rcrFuI<%-BVrl>Qz_YZ~OLq z6ybRS<7tfVF`mJA_YS=9`~|!Z_&4x=;6K2-fqU+ZqP@TtkmpYU9{?Tz?gPF8<b4ak zhk%Q~hk={Gdw~Qz1}uTk13v|x0e%Nu0`9#linf6lfCqtJ0@r~*0bc+f+Jl(D^S~#8 zJHQUOy$9sD-+~m!`2mQf=tm&)`x%J8=nuS@*Pp=WfdB5!58M+)4`Lnz*-snDe$E2B zz-3?z`~bKG{08_Ia6gQ4pau4TzW^)Xvv6_{_~9;o4HTIF3IyHec^q$!!TQV|xQ!r1 z!<qTB_N?pdc@#gGCqIv2?8o35LY^i+t_?pe41{WOeS{Y}`Z77vX>^91mpUaKb)^qw z)D1St$Muce2AxH{QmP_aCZ$Q>JFNPY+e>7<m6_9_EXhVgndDlQ6b)69dm(48r_<5R z09(d3W*4L-EmB1SD<iU`JYqQ+#@^P~GZGgIX+_YfKp^Mq>pCsq`BZd4I+06{tyaLT z0d4C<7QD!4EQ^Bl)TXSz0ujU&X%H!uVmVk>APm_f*mBEMX#qDd)F#P=lUd^e%tjQf zCsoXRumI~IK{kgUe7;)W$e0ypg0YcP3DA(@W<-#=-4j-(wr=i?Q(eT)i>9myB433I zcMD5*8*8{W!YRt9{I&=>I!9bHR~fkQN)|8*mdP!mSO5;w(Uvk;VLDTS)6|rMKHHLI zIq=<}w$nW*Oh%bi<8h--eFIKsy6A?cxZH?KWi_U}DZ{p+f-Fj#<cKDeb{RQY`e-|h zts+EfbeylRTC+omkC)OI**Il%Tda1@E_crL#8z)@eQ9ObTBFKX9}irn()disT)bi0 zqP4mbuhAvNdtyGMZSiKWvlb6l;%-O0vC<wa9ok+Q_F8@Ee7s?$c7^oDw#DLRzb}TJ zK~J3M4LVEx)17X2t+&2@5~ex}5G~#yS++&LGd#D+OiuLsrweu1*lcbC27{$RZ%+H@ z;n!Mcjpi!E8}C%$<LQb&QWa9^<08qWb>tT|Hx}b#b33LrCL8y3Vp65Ewm3FYzO`Be zD~ov1*wVFyu9~S^c>TDT{#<xTi<9E$%g0;Ms;wtzX&<fCli-}<hNftxswY-@XybOf zxf7Cr)0`Tu>j|1Rl&~;34k7_**oyQ;Sykv!uN=2TG&CiD9?Mdp$)Z)XVKoa@zn|nL z)RQJW4h=2{C*b9h3|AVfO2Va47R&X-OU0_7uS5bl(7oglwq$o`C8tIO>`q1J!|&HV z#vzLn5^o13Jf>2>l~+k9RQ;LdcJnL(tk+G*CA!<iJ_xyE0va9LsK9{U#*wH$5e0mx zrYaDF^C{pFM-4i#m9!b;;2R?`xGvzf)A(z)fgt*8ci#v(h*H*Hc&pHJf+@SOL*6x9 zAtGm5PlOy9o1!QPqf?3Hf;FBxKM!hHc2m;a?<@oMkxnr2ggaG%zx#itkPz88Ip@S# zG>ni%eNA}eE72gYHllzmQ)Jeafgn@X6?oaEI->sETM`n-qELqG3@=<&X44!bF4{Ed z3|&24;tZzsQ58~k0x@F3acRm9F)9|z2?=I6Z;Eg{;07l}K`MMm46cjBPwRRr>dlHz z0!76a+(-O>3WjqLySSF%8rcJ6p<Hv&F-Q0jUngqpqPY=&c4LWU79@zQFe6z+{{fm^ B#0CHW literal 0 HcmV?d00001 diff --git a/server/locale/fr/LC_MESSAGES/django.po b/server/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..4fe78632 --- /dev/null +++ b/server/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,263 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-17 08:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: vbv_lernwelt/assignment/export.py:181 +msgid "Bestanden" +msgstr "Réussi" + +#: vbv_lernwelt/assignment/export.py:181 +msgid "Nicht bestanden" +msgstr "Échoué" + +#: vbv_lernwelt/assignment/export.py:195 vbv_lernwelt/assignment/export.py:198 +#: vbv_lernwelt/assignment/export.py:199 +msgid "Keine Daten" +msgstr "Aucune donnée" + +#: vbv_lernwelt/core/admin.py:32 +msgid "Personal info" +msgstr "" + +#: vbv_lernwelt/core/admin.py:34 +msgid "Permissions" +msgstr "" + +#: vbv_lernwelt/core/admin.py:45 +msgid "Important dates" +msgstr "" + +#: vbv_lernwelt/core/admin.py:47 +msgid "Profile" +msgstr "" + +#: vbv_lernwelt/core/admin.py:62 +msgid "Organisation" +msgstr "" + +#: vbv_lernwelt/core/admin.py:75 +msgid "Additional data" +msgstr "" + +#: vbv_lernwelt/course/models.py:24 vbv_lernwelt/course/models.py:62 +msgid "Titel" +msgstr "" + +#: vbv_lernwelt/course/models.py:26 +msgid "Kategorie-Name" +msgstr "" + +#: vbv_lernwelt/course/models.py:29 +msgid "Slug" +msgstr "" + +#: vbv_lernwelt/course/models.py:64 +msgid "Allgemein" +msgstr "" + +#: vbv_lernwelt/course/models.py:177 +msgid "Lehrgang-Seite" +msgstr "" + +#: vbv_lernwelt/course/models.py:272 +msgid "Teilnehmer" +msgstr "" + +#: vbv_lernwelt/course/models.py:273 +msgid "Experte/Trainer" +msgstr "" + +#: vbv_lernwelt/course/models.py:332 +msgid "Dokumente im Circle ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:336 +msgid "Lernmentor-Funktion ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:340 +msgid "Kompetenzweise ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:343 +msgid "Versicherungsvermittler-Lehrgang" +msgstr "" + +#: vbv_lernwelt/course/models.py:344 +msgid "ÜK-Lehrgang" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:111 +msgid "Anwesend" +msgstr "Présent" + +#: vbv_lernwelt/course_session/services/export_attendance.py:111 +msgid "Nicht anwesend" +msgstr "Pas présent" + +#: vbv_lernwelt/course_session/services/export_attendance.py:118 +msgid "Vorname" +msgstr "Prénom" + +#: vbv_lernwelt/course_session/services/export_attendance.py:119 +msgid "Nachname" +msgstr "Nom de famille" + +#: vbv_lernwelt/course_session/services/export_attendance.py:120 +msgid "Email" +msgstr "Email" + +#: vbv_lernwelt/course_session/services/export_attendance.py:121 +#: vbv_lernwelt/course_session/services/export_attendance.py:133 +msgid "Lehrvertragsnummer" +msgstr "Numéro de contrat d'apprentissage" + +#: vbv_lernwelt/feedback/export.py:33 +msgid "Zufriedenheit insgesamt" +msgstr "Degré de satisfaction au global" + +#: vbv_lernwelt/feedback/export.py:34 +msgid "Zielerreichung insgesamt" +msgstr "Degré de réalisation des objectifs" + +#: vbv_lernwelt/feedback/export.py:37 +msgid "" +"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?" +msgstr "As-tu l’impression de bien maîtriser les sujets qui ont été abordés pendant le cours ?" + +#: vbv_lernwelt/feedback/export.py:41 +msgid "Waren die Vorbereitungsaufträge klar und verständlich?" +msgstr "Les travaux préparatoires étaient-ils clairs et compréhensibles ?" + +#: vbv_lernwelt/feedback/export.py:45 +msgid "" +"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der " +"Kursleiterin?" +msgstr "Que penses-tu des compétences techniques de la personne chargée du cours et de sa maîtrise du sujet ?" + +#: vbv_lernwelt/feedback/export.py:49 +msgid "" +"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und " +"aufgegriffen?" +msgstr "Les questions et les suggestions des participants ont-elles été prises au sérieux et traitées correctement ?" + +#: vbv_lernwelt/feedback/export.py:53 +msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?" +msgstr "Souhaites-tu ajouter quelque chose à l’intention de la personne chargée du cours ?" + +#: vbv_lernwelt/feedback/export.py:55 +msgid "Würdest du den Kurs weiterempfehlen?" +msgstr "Est-ce que tu recommandes ce cours ?" + +#: vbv_lernwelt/feedback/export.py:56 +msgid "Was hat dir besonders gut gefallen?" +msgstr "Qu’est-ce qui t’a particulièrement plu ?" + +#: vbv_lernwelt/feedback/export.py:57 +msgid "Wo siehst du Verbesserungspotential?" +msgstr "À ton avis, quels sont les points qui pourraient être améliorés ?" + +#: vbv_lernwelt/feedback/export.py:144 +msgid "Durchführung" +msgstr "Opérations" + +#: vbv_lernwelt/feedback/export.py:145 +msgid "Datum" +msgstr "Date" + +#: vbv_lernwelt/feedback/models.py:31 +msgid "Internet" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:32 +msgid "Leaflet" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:33 +msgid "Newspaper" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:34 +msgid "Personal recommendation" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:35 +msgid "Public event" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:36 +msgid "Other" +msgstr "" + +#: vbv_lernwelt/media_files/models.py:27 vbv_lernwelt/media_files/models.py:54 +#: vbv_lernwelt/media_files/models.py:64 vbv_lernwelt/media_files/models.py:83 +msgid "file" +msgstr "" + +#: vbv_lernwelt/notify/models.py:9 +msgid "User Interaction" +msgstr "" + +#: vbv_lernwelt/notify/models.py:10 +msgid "Progress" +msgstr "" + +#: vbv_lernwelt/notify/models.py:11 +msgid "Information" +msgstr "" + +#: vbv_lernwelt/notify/models.py:16 +msgid "Attendance Course Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:18 +msgid "Assignment Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:21 +msgid "Casework Expert Evaluation Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:23 +msgid "Casework Submitted" +msgstr "" + +#: vbv_lernwelt/notify/models.py:24 +msgid "Casework Evaluated" +msgstr "" + +#: vbv_lernwelt/notify/models.py:25 +msgid "New Feedback" +msgstr "" + +#: vbv_lernwelt/notify/models.py:28 +msgid "Self Evaluation Feedback Requested" +msgstr "" + +#: vbv_lernwelt/notify/models.py:31 +msgid "Self Evaluation Feedback Provided" +msgstr "" + +#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:8 +msgid "Sites menu" +msgstr "" + +#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:14 +msgid "Edit this page" +msgstr "" diff --git a/server/locale/it/LC_MESSAGES/django.mo b/server/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..5cb0e69893ce98efd29312e247a4346e6dfe2d66 GIT binary patch literal 2191 zcma)+&yO256vqcBzXIh)`7IS>L@Uvj=?_jN+ijceCT-dzyCm6Nu{ZDJWyY<?9@(B0 zA`XcEfde-VEB*iuoc0!RL8#YW5C=|h0<Li3`zB3OR3sS5`Na0~dtbk2`==v^zKHO> zigpz32edcQK79l~_<jeU0{;M?2LA*f2M;|OMTfx}$o<#BXTZ0>Bj8Dp*R6ujfi3WP za0`4AB=9_#gYSS}gKOaTU>iLASQPDoJK!<!8*l*L1&@QL4k0G^A@~}26I_F~&q0p+ z9YitjfEbGIf~@aH@Copjd;Q-)f&O10>pF_bXTf72_s@a6{sV9Yya*l#r{Fd4E0A^n z3f>0a!=e-5J}ALE;2Ypsh&c&f2f1H@kXwB(<CkM`eik3xMMy=Znd5WrIj@WFMf_o% z{Jn(s3L4iC>8a|$wc)RZhEP=vUf_X-xlNwTC|V;Qq#2Qk)@3O3XuWhvHl7`1t~6=X z%%#%N4N`^#=3&*L%uUDzCsS|AJSP|RWRe+~Q`A#Q7KB{5o_F`|2iUf*GTV`kj2I~r z7}+C7Dj=4Z<v6(6eoEq&mQIvrBoN4l*}fTRc)k?vNG~!Ou+#~-wUF(+NXsBn8cVIo z%x&`7w-AE3VpNI|<>E%^yha#uXRzdft}+5{U}$WT2`|&i1(fYk=>jRe@WBZfKm@rA zelR_q9i*&^cY-#|C<hEuaX%u&JnRW0b6dak##?P+<t>{Rgd|Vl!atzWKcpJ2t?-Jn zxxQ^e9=#{7nJ+9{1SK_$O2^__C>FqB8pcryD|8o`;B6YpAfIi?aXK(>QQP?%6egwA zsqwhV&g=kA7kM$2OmVwPF6G6TvZ@TbiZnTtHpvi8DC1M|G7r&iId+N=smkMWeAC&R zl!UlFim@B7tf`C5#+4h5t4*=p><`)-y;`3N>q6Z1sT##=W$NRht&7^`M%<@~;x#cJ z(z^Jl+33gJjd;Bwu5Hx2?NhrOy=JW=eTaun8n0!rt}a?zosQ@=x=nGR*=@8tmmBNr z{pMhB5vC?Bk+gV7GOvqHqjz<Sm0aj_E^Bq#x^!s+1>JVHxs-kG^x4`KYq$#W@RI_1 z0(~6L6j~~?B9crxPhoXy*ox0D?O4{BT--E?9VwI6#rZuIYMZ)rQpYXpa$gswsJedj z{S`6)yZWvXZ?B4TCsu0FrsFFeC>oRnIc$-yw2TH5t5bzy1{F2q>O%K$SW27#w&|6) z!xnc+B+!gyq3Q(XY@&>^(G{EGFyMAuBZ@_c7OtD()jV@mD^xg<4%Jrd31)?Ka*B!& zPLwnj+g6wtgeW<tjaPe0t4r|ifBx*tOe!G@j8CV~B(ONB#NruX*|e~dIzZ{f;#~QF z1xS^X{2Gve9Jo<FxDp$1bmrb)NFtncjj+yJc9fHtNL^yR(jt^rXf+|Nt4;qyw=We6 zmt#Y=ux$MAF8*^l2|eF+b+t+Of)d<L2;>)l#r;3&>vjU^g!H0x)g_<84IC(uE4)R> z1!oJJ>|oI5{Hi10O87EO$<0py!YRBacq^+ULrEcVFp$6b_V_u(%Mv7yB`5(1QzV@I zXn{9IAwr+$stEH^EVI*>X{vDLt8gegs;zO_)C378s1$ZjQ3z_HqJbTalpo(aCQC~h F{S8}-ickOm literal 0 HcmV?d00001 diff --git a/server/locale/it/LC_MESSAGES/django.po b/server/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..c094423a --- /dev/null +++ b/server/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,263 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-17 08:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: vbv_lernwelt/assignment/export.py:181 +msgid "Bestanden" +msgstr "Superato" + +#: vbv_lernwelt/assignment/export.py:181 +msgid "Nicht bestanden" +msgstr "Fallito" + +#: vbv_lernwelt/assignment/export.py:195 vbv_lernwelt/assignment/export.py:198 +#: vbv_lernwelt/assignment/export.py:199 +msgid "Keine Daten" +msgstr "Nessun dato" + +#: vbv_lernwelt/core/admin.py:32 +msgid "Personal info" +msgstr "" + +#: vbv_lernwelt/core/admin.py:34 +msgid "Permissions" +msgstr "" + +#: vbv_lernwelt/core/admin.py:45 +msgid "Important dates" +msgstr "" + +#: vbv_lernwelt/core/admin.py:47 +msgid "Profile" +msgstr "" + +#: vbv_lernwelt/core/admin.py:62 +msgid "Organisation" +msgstr "" + +#: vbv_lernwelt/core/admin.py:75 +msgid "Additional data" +msgstr "" + +#: vbv_lernwelt/course/models.py:24 vbv_lernwelt/course/models.py:62 +msgid "Titel" +msgstr "" + +#: vbv_lernwelt/course/models.py:26 +msgid "Kategorie-Name" +msgstr "" + +#: vbv_lernwelt/course/models.py:29 +msgid "Slug" +msgstr "" + +#: vbv_lernwelt/course/models.py:64 +msgid "Allgemein" +msgstr "" + +#: vbv_lernwelt/course/models.py:177 +msgid "Lehrgang-Seite" +msgstr "" + +#: vbv_lernwelt/course/models.py:272 +msgid "Teilnehmer" +msgstr "" + +#: vbv_lernwelt/course/models.py:273 +msgid "Experte/Trainer" +msgstr "" + +#: vbv_lernwelt/course/models.py:332 +msgid "Dokumente im Circle ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:336 +msgid "Lernmentor-Funktion ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:340 +msgid "Kompetenzweise ein/aus" +msgstr "" + +#: vbv_lernwelt/course/models.py:343 +msgid "Versicherungsvermittler-Lehrgang" +msgstr "" + +#: vbv_lernwelt/course/models.py:344 +msgid "ÜK-Lehrgang" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:111 +msgid "Anwesend" +msgstr "Presente" + +#: vbv_lernwelt/course_session/services/export_attendance.py:111 +msgid "Nicht anwesend" +msgstr "Non presente" + +#: vbv_lernwelt/course_session/services/export_attendance.py:118 +msgid "Vorname" +msgstr "Nome" + +#: vbv_lernwelt/course_session/services/export_attendance.py:119 +msgid "Nachname" +msgstr "Cognome" + +#: vbv_lernwelt/course_session/services/export_attendance.py:120 +msgid "Email" +msgstr "E-mail" + +#: vbv_lernwelt/course_session/services/export_attendance.py:121 +#: vbv_lernwelt/course_session/services/export_attendance.py:133 +msgid "Lehrvertragsnummer" +msgstr "Numero di contratto di tirocinio" + +#: vbv_lernwelt/feedback/export.py:33 +msgid "Zufriedenheit insgesamt" +msgstr "Soddisfazione complessiva" + +#: vbv_lernwelt/feedback/export.py:34 +msgid "Zielerreichung insgesamt" +msgstr "Raggiungimento complessivo degli obiettivi" + +#: vbv_lernwelt/feedback/export.py:37 +msgid "" +"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?" +msgstr "Come valuti il tuo livello di preparazione sui temi dopo il corso?" + +#: vbv_lernwelt/feedback/export.py:41 +msgid "Waren die Vorbereitungsaufträge klar und verständlich?" +msgstr "Gli incarichi di preparazione erano chiari e comprensibili?" + +#: vbv_lernwelt/feedback/export.py:45 +msgid "" +"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der " +"Kursleiterin?" +msgstr "Come valuti il livello di preparazione sui temi e le competenze specialistiche dell’istruttore/istruttrice del corso?" + +#: vbv_lernwelt/feedback/export.py:49 +msgid "" +"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und " +"aufgegriffen?" +msgstr "Le domande e i suggerimenti dei/delle partecipanti al corso sono stati accolti e presi sul serio?" + +#: vbv_lernwelt/feedback/export.py:53 +msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?" +msgstr "Cos’altro vorresti ancora dire all’istruttore/istruttrice del corso?" + +#: vbv_lernwelt/feedback/export.py:55 +msgid "Würdest du den Kurs weiterempfehlen?" +msgstr "Raccomanderesti il corso?" + +#: vbv_lernwelt/feedback/export.py:56 +msgid "Was hat dir besonders gut gefallen?" +msgstr "Cos’hai apprezzato particolarmente?" + +#: vbv_lernwelt/feedback/export.py:57 +msgid "Wo siehst du Verbesserungspotential?" +msgstr "Dove vedi un potenziale di miglioramento?" + +#: vbv_lernwelt/feedback/export.py:144 +msgid "Durchführung" +msgstr "Svolgimenti" + +#: vbv_lernwelt/feedback/export.py:145 +msgid "Datum" +msgstr "Data" + +#: vbv_lernwelt/feedback/models.py:31 +msgid "Internet" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:32 +msgid "Leaflet" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:33 +msgid "Newspaper" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:34 +msgid "Personal recommendation" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:35 +msgid "Public event" +msgstr "" + +#: vbv_lernwelt/feedback/models.py:36 +msgid "Other" +msgstr "" + +#: vbv_lernwelt/media_files/models.py:27 vbv_lernwelt/media_files/models.py:54 +#: vbv_lernwelt/media_files/models.py:64 vbv_lernwelt/media_files/models.py:83 +msgid "file" +msgstr "" + +#: vbv_lernwelt/notify/models.py:9 +msgid "User Interaction" +msgstr "" + +#: vbv_lernwelt/notify/models.py:10 +msgid "Progress" +msgstr "" + +#: vbv_lernwelt/notify/models.py:11 +msgid "Information" +msgstr "" + +#: vbv_lernwelt/notify/models.py:16 +msgid "Attendance Course Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:18 +msgid "Assignment Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:21 +msgid "Casework Expert Evaluation Reminder" +msgstr "" + +#: vbv_lernwelt/notify/models.py:23 +msgid "Casework Submitted" +msgstr "" + +#: vbv_lernwelt/notify/models.py:24 +msgid "Casework Evaluated" +msgstr "" + +#: vbv_lernwelt/notify/models.py:25 +msgid "New Feedback" +msgstr "" + +#: vbv_lernwelt/notify/models.py:28 +msgid "Self Evaluation Feedback Requested" +msgstr "" + +#: vbv_lernwelt/notify/models.py:31 +msgid "Self Evaluation Feedback Provided" +msgstr "" + +#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:8 +msgid "Sites menu" +msgstr "" + +#: vbv_lernwelt/templates/wagtailadmin/pages/listing/_page_title_explore.html:14 +msgid "Edit this page" +msgstr "" diff --git a/server/vbv_lernwelt/assignment/export.py b/server/vbv_lernwelt/assignment/export.py index 39ff9247..8761dabe 100644 --- a/server/vbv_lernwelt/assignment/export.py +++ b/server/vbv_lernwelt/assignment/export.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from io import BytesIO import structlog +from django.utils.translation import gettext as _ from openpyxl import Workbook from vbv_lernwelt.assignment.models import ( @@ -138,13 +139,13 @@ def _create_sheet( sheet.cell( row=1, column=col_idx, - value=f"{col_prefix} bestanden", + value=f"{col_prefix} {_('bestanden')}", ) sheet.cell( row=1, column=col_idx + 1, - value=f"{col_prefix} Resultat %", + value=f"{col_prefix} {_('Resultat')} %", ) ordered_assignement_ids.append(cse.assignment.id) @@ -177,7 +178,9 @@ def _add_rows( if user_ac: status_text = ( - "Bestanden" if user_ac.evaluation_passed else "Nicht bestanden" + _("Bestanden") + if user_ac.evaluation_passed + else _("Nicht bestanden") ) sheet.cell(row=row_idx, column=col_idx, value=status_text) try: @@ -191,11 +194,11 @@ def _add_rows( ), ) except (ZeroDivisionError, TypeError): - sheet.cell(row=row_idx, column=col_idx + 1, value="Keine Daten") + sheet.cell(row=row_idx, column=col_idx + 1, value=_("Keine Daten")) else: - sheet.cell(row=row_idx, column=col_idx, value="Keine Daten") - sheet.cell(row=row_idx, column=col_idx + 1, value="Keine Daten") + sheet.cell(row=row_idx, column=col_idx, value=_("Keine Daten")) + sheet.cell(row=row_idx, column=col_idx + 1, value=_("Keine Daten")) col_idx += 2 diff --git a/server/vbv_lernwelt/course_session/services/export_attendance.py b/server/vbv_lernwelt/course_session/services/export_attendance.py index 92ec69f2..0fde37bb 100644 --- a/server/vbv_lernwelt/course_session/services/export_attendance.py +++ b/server/vbv_lernwelt/course_session/services/export_attendance.py @@ -4,6 +4,7 @@ from io import BytesIO from itertools import groupby import structlog +from django.utils.translation import gettext as _ from openpyxl import Workbook from vbv_lernwelt.course.models import CourseSessionUser @@ -88,7 +89,7 @@ def _create_sheet( sheet.cell( row=1, column=col_idx, - value=f"Anwesenheit {circle.title} {course.due_date.start.strftime('%d.%m.%Y')}", + value=f"{_('Anwesenheit')} {circle.title} {course.due_date.start.strftime('%d.%m.%Y')}", ) user_dict_map = {d["user_id"]: d for d in course.attendance_user_list} attendance_data[circle.title] = user_dict_map @@ -107,17 +108,17 @@ def _add_rows(sheet, users: list[CourseSessionUser], attendance_data): for key, user_dict_map in attendance_data.items(): user_dict = user_dict_map.get(str(user.user.id), {}) status = user_dict.get("status", "") if user_dict else "" - status_text = "Anwesend" if status == "PRESENT" else "Nicht anwesend" + status_text = _("Anwesend") if status == "PRESENT" else _("Nicht anwesend") sheet.cell(row=row_idx, column=col_idx, value=status_text) col_idx += 1 def add_user_headers(sheet): # todo: translate headers - sheet.cell(row=1, column=1, value="Vorname") - sheet.cell(row=1, column=2, value="Nachname") - sheet.cell(row=1, column=3, value="Email") - sheet.cell(row=1, column=4, value="Lehrvertragsnummer") + sheet.cell(row=1, column=1, value=_("Vorname")) + sheet.cell(row=1, column=2, value=_("Nachname")) + sheet.cell(row=1, column=3, value=_("Email")) + sheet.cell(row=1, column=4, value=_("Lehrvertragsnummer")) return 5 # return the next column index @@ -129,7 +130,7 @@ def add_user_export_data(sheet, user: CourseSessionUser, row_idx: int) -> int: sheet.cell( row=row_idx, column=4, - value=user.user.additional_json_data.get("Lehrvertragsnummer", ""), + value=user.user.additional_json_data.get(_("Lehrvertragsnummer"), ""), ) return 5 # return the next column index diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index 2e4b76fe..a556b809 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -461,7 +461,9 @@ class ExportXlsTestCase(TestCase): supervisor, self.ALLOWED_ROLES, requested_cs_ids ) - self.assertCountEqual([int(cs) for cs in requested_cs_ids], [csr.id for csr in allowed_csrs_ids]) + self.assertCountEqual( + [int(cs) for cs in requested_cs_ids], [csr.id for csr in allowed_csrs_ids] + ) def test_student_cannot_export_data(self): # student cannot export any data diff --git a/server/vbv_lernwelt/feedback/export.py b/server/vbv_lernwelt/feedback/export.py index bf81bbd7..0086d910 100644 --- a/server/vbv_lernwelt/feedback/export.py +++ b/server/vbv_lernwelt/feedback/export.py @@ -5,6 +5,7 @@ from typing import List, Tuple import structlog from django.db.models import QuerySet +from django.utils.translation import gettext as _ from openpyxl import Workbook from vbv_lernwelt.course_session.services.export_attendance import ( @@ -29,31 +30,35 @@ VV_FEEDBACK_QUESTIONS = [ ] UK_FEEDBACK_QUESTIONS = [ - ("satisfaction", "Zufriedenheit insgesamt"), - ("goal_attainment", "Zielerreichung insgesamt"), + ("satisfaction", _("Zufriedenheit insgesamt")), + ("goal_attainment", _("Zielerreichung insgesamt")), ( "proficiency", - "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?", + _("Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"), ), ( "preparation_task_clarity", - "Waren die Vorbereitungsaufträge klar und verständlich?", + _("Waren die Vorbereitungsaufträge klar und verständlich?"), ), ( "instructor_competence", - "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?", + _( + "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?" + ), ), ( "instructor_respect", - "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?", + _( + "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?" + ), ), ( "instructor_open_feedback", - "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?", + _("Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"), ), - ("would_recommend", "Würdest du den Kurs weiterempfehlen?"), - ("course_positive_feedback", "Was hat dir besonders gut gefallen?"), - ("course_negative_feedback", "Wo siehst du Verbesserungspotential?"), + ("would_recommend", _("Würdest du den Kurs weiterempfehlen?")), + ("course_positive_feedback", _("Was hat dir besonders gut gefallen?")), + ("course_negative_feedback", _("Wo siehst du Verbesserungspotential?")), ] @@ -140,8 +145,8 @@ def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]): ) # add header - sheet.cell(row=1, column=1, value="Durchführung") - sheet.cell(row=1, column=2, value="Datum") + sheet.cell(row=1, column=1, value=_("Durchführung")) + sheet.cell(row=1, column=2, value=_("Datum")) questions = [q[1] for q in question_data] for col_idx, title in enumerate(questions, start=3): sheet.cell(row=1, column=col_idx, value=title) diff --git a/server/vbv_lernwelt/feedback/graphql/queries.py b/server/vbv_lernwelt/feedback/graphql/queries.py new file mode 100644 index 00000000..78913462 --- /dev/null +++ b/server/vbv_lernwelt/feedback/graphql/queries.py @@ -0,0 +1,38 @@ +import itertools + +import graphene +from graphene import ObjectType + +from vbv_lernwelt.feedback.models import FeedbackResponse + + +class FeedbackSummary(ObjectType): + circle_id = graphene.Int() + count = graphene.Int() + + +class FeedbackForCourseQuery(object): + course_feedback_summary = graphene.List( + FeedbackSummary, course_id=graphene.Int(required=True) + ) + + def resolve_course_feedback_summary(self, info, **kwargs): + course_id = kwargs.get("course_id") + user = info.context.user + + feedbacks = FeedbackResponse.objects.filter( + course_session__course_id=course_id, circle__expert__user=user + ).order_by("circle_id") + summary = [] + + grouped_feedbacks = itertools.groupby(feedbacks, lambda x: x.circle_id) + + for key, feedbacks in grouped_feedbacks: + summary.append( + { + "circle_id": key, + "count": len(list(feedbacks)), + } + ) + + return summary From 0a9a4af5b265baebd01f06ef3a305a718e24c203 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Mon, 17 Jun 2024 14:21:47 +0200 Subject: [PATCH 18/24] wip: Use ints as input --- .gitignore | 1 - client/src/utils/export.ts | 4 ++++ .../vbv_lernwelt/dashboard/tests/test_views.py | 18 ++++++++---------- server/vbv_lernwelt/dashboard/views.py | 6 +++--- 4 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 client/src/utils/export.ts diff --git a/.gitignore b/.gitignore index 5eb128d8..3c4e6529 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,6 @@ coverage.xml .hypothesis/ # Translations -*.mo *.pot # Django stuff: diff --git a/client/src/utils/export.ts b/client/src/utils/export.ts new file mode 100644 index 00000000..17f0e1c6 --- /dev/null +++ b/client/src/utils/export.ts @@ -0,0 +1,4 @@ +/** + * Created by christiancueni on 17.06.2024. + * Copyright (c) 2024 ITerativ GmbH. All rights reserved. + */ diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index a556b809..c98d5ae1 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -453,17 +453,15 @@ class ExportXlsTestCase(TestCase): # supervisor sees all cs in region supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) requested_cs_ids = [ - str(TEST_COURSE_SESSION_ZURICH_ID), - str(TEST_COURSE_SESSION_BERN_ID), + TEST_COURSE_SESSION_ZURICH_ID, + TEST_COURSE_SESSION_BERN_ID, ] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( supervisor, self.ALLOWED_ROLES, requested_cs_ids ) - self.assertCountEqual( - [int(cs) for cs in requested_cs_ids], [csr.id for csr in allowed_csrs_ids] - ) + self.assertCountEqual(requested_cs_ids, [csr.id for csr in allowed_csrs_ids]) def test_student_cannot_export_data(self): # student cannot export any data @@ -479,8 +477,8 @@ class ExportXlsTestCase(TestCase): # trainer can only export cs where she is assigned trainer = User.objects.get(email="test-trainer2@example.com") requested_cs_ids = [ - str(TEST_COURSE_SESSION_BERN_ID), - str(TEST_COURSE_SESSION_ZURICH_ID), + TEST_COURSE_SESSION_BERN_ID, + TEST_COURSE_SESSION_ZURICH_ID, ] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( @@ -494,7 +492,7 @@ class ExportXlsTestCase(TestCase): def test_trainer_can_get_circles_where_expert(self): trainer = User.objects.get(email="test-trainer2@example.com") circle = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug") - requested_cs_ids = [str(TEST_COURSE_SESSION_ZURICH_ID)] + requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( trainer, self.ALLOWED_ROLES, requested_cs_ids @@ -510,7 +508,7 @@ class ExportXlsTestCase(TestCase): def test_trainer_cannot_get_circles_where_not_expert(self): trainer = User.objects.get(email="test-trainer2@example.com") circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen") - requested_cs_ids = [str(TEST_COURSE_SESSION_ZURICH_ID)] + requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( trainer, self.ALLOWED_ROLES, requested_cs_ids @@ -525,7 +523,7 @@ class ExportXlsTestCase(TestCase): supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) circle_reisen = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen") circle_fahrzeug = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug") - requested_cs_ids = [str(TEST_COURSE_SESSION_ZURICH_ID)] + requested_cs_ids = [TEST_COURSE_SESSION_ZURICH_ID] allowed_csrs_ids = _get_course_sessions_with_roles_for_user( supervisor, self.ALLOWED_ROLES, requested_cs_ids diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 41000109..05723e34 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -548,7 +548,7 @@ def export_attendance_as_xsl(request): ) data = export_attendance( [cs.id for cs in course_sessions_with_roles], - circle_ids=[int(circle_id) for circle_id in circle_ids], + circle_ids=circle_ids, ) return _make_excel_response(data) @@ -562,7 +562,7 @@ def export_competence_certificate_as_xsl(request): ) data = export_competence_certificates( [cswr.id for cswr in course_sessions_with_roles], - circle_ids=[int(circle_id) for circle_id in circle_ids], + circle_ids=circle_ids, ) return _make_excel_response(data) @@ -613,7 +613,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 str(csr.id) in requested_cs_ids + and csr.id in requested_cs_ids ] # noqa return all_cs_roles_for_user From 5b60e50ac4f4be04e122557c9ea71097c8f17624 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Mon, 17 Jun 2024 15:50:35 +0200 Subject: [PATCH 19/24] wip: Rename fns, add filename translations --- server/config/urls.py | 4 +- server/locale/de/LC_MESSAGES/django.po | 60 ++++++++------ server/locale/fr/LC_MESSAGES/django.mo | Bin 2301 -> 2510 bytes server/locale/fr/LC_MESSAGES/django.po | 76 +++++++++++------- server/locale/it/LC_MESSAGES/django.mo | Bin 2191 -> 2420 bytes server/locale/it/LC_MESSAGES/django.po | 68 ++++++++++------ server/vbv_lernwelt/assignment/export.py | 6 +- .../commands/export_assignment_completions.py | 4 +- .../test_assignment_completions_export.py | 4 +- .../services/export_attendance.py | 6 +- server/vbv_lernwelt/dashboard/views.py | 28 ++++--- server/vbv_lernwelt/feedback/export.py | 4 +- 12 files changed, 162 insertions(+), 98 deletions(-) diff --git a/server/config/urls.py b/server/config/urls.py index 4748e663..51627cce 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -42,7 +42,7 @@ from vbv_lernwelt.course.views import ( from vbv_lernwelt.course_session.views import get_course_session_documents from vbv_lernwelt.dashboard.views import ( export_attendance_as_xsl, - export_competence_certificate_as_xsl, + export_competence_elements_as_xsl, export_feedback_as_xsl, get_dashboard_config, get_dashboard_due_dates, @@ -135,7 +135,7 @@ urlpatterns = [ path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count, name="get_mentor_open_tasks_count"), path(r"api/dashboard/export/attendance/", export_attendance_as_xsl, name="export_attendance_as_xsl"), - path(r"api/dashboard/export/certificate/", export_competence_certificate_as_xsl, name="export_certificate_as_xsl"), + path(r"api/dashboard/export/certificate/", export_competence_elements_as_xsl, name="export_certificate_as_xsl"), path(r"api/dashboard/export/feedback/", export_feedback_as_xsl, name="export_feedback_as_xsl"), # course diff --git a/server/locale/de/LC_MESSAGES/django.po b/server/locale/de/LC_MESSAGES/django.po index 115a551c..3db23658 100644 --- a/server/locale/de/LC_MESSAGES/django.po +++ b/server/locale/de/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-06-17 08:25+0200\n" +"POT-Creation-Date: 2024-06-17 15:43+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" @@ -18,16 +18,20 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: vbv_lernwelt/assignment/export.py:181 +#: vbv_lernwelt/assignment/export.py:31 +msgid "export_kompetenznachweis_elemente" +msgstr "" + +#: vbv_lernwelt/assignment/export.py:183 msgid "Bestanden" msgstr "" -#: vbv_lernwelt/assignment/export.py:181 +#: vbv_lernwelt/assignment/export.py:185 msgid "Nicht bestanden" msgstr "" -#: vbv_lernwelt/assignment/export.py:195 vbv_lernwelt/assignment/export.py:198 -#: vbv_lernwelt/assignment/export.py:199 +#: vbv_lernwelt/assignment/export.py:199 vbv_lernwelt/assignment/export.py:202 +#: vbv_lernwelt/assignment/export.py:203 msgid "Keine Daten" msgstr "" @@ -103,81 +107,89 @@ msgstr "" msgid "ÜK-Lehrgang" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:111 +#: vbv_lernwelt/course_session/services/export_attendance.py:15 +msgid "export_anwesenheit" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:113 msgid "Anwesend" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:111 +#: vbv_lernwelt/course_session/services/export_attendance.py:113 msgid "Nicht anwesend" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:118 +#: vbv_lernwelt/course_session/services/export_attendance.py:120 msgid "Vorname" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:119 +#: vbv_lernwelt/course_session/services/export_attendance.py:121 msgid "Nachname" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:120 +#: vbv_lernwelt/course_session/services/export_attendance.py:122 msgid "Email" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:121 -#: vbv_lernwelt/course_session/services/export_attendance.py:133 +#: vbv_lernwelt/course_session/services/export_attendance.py:123 +#: vbv_lernwelt/course_session/services/export_attendance.py:135 msgid "Lehrvertragsnummer" msgstr "" -#: vbv_lernwelt/feedback/export.py:33 +#: vbv_lernwelt/feedback/export.py:19 +msgid "export_feedback" +msgstr "" + +#: vbv_lernwelt/feedback/export.py:35 msgid "Zufriedenheit insgesamt" msgstr "" -#: vbv_lernwelt/feedback/export.py:34 +#: vbv_lernwelt/feedback/export.py:36 msgid "Zielerreichung insgesamt" msgstr "" -#: vbv_lernwelt/feedback/export.py:37 +#: vbv_lernwelt/feedback/export.py:39 msgid "" "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?" msgstr "" -#: vbv_lernwelt/feedback/export.py:41 +#: vbv_lernwelt/feedback/export.py:43 msgid "Waren die Vorbereitungsaufträge klar und verständlich?" msgstr "" -#: vbv_lernwelt/feedback/export.py:45 +#: vbv_lernwelt/feedback/export.py:48 msgid "" "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der " "Kursleiterin?" msgstr "" -#: vbv_lernwelt/feedback/export.py:49 +#: vbv_lernwelt/feedback/export.py:54 msgid "" "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und " "aufgegriffen?" msgstr "" -#: vbv_lernwelt/feedback/export.py:53 +#: vbv_lernwelt/feedback/export.py:59 msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?" msgstr "" -#: vbv_lernwelt/feedback/export.py:55 +#: vbv_lernwelt/feedback/export.py:61 msgid "Würdest du den Kurs weiterempfehlen?" msgstr "" -#: vbv_lernwelt/feedback/export.py:56 +#: vbv_lernwelt/feedback/export.py:62 msgid "Was hat dir besonders gut gefallen?" msgstr "" -#: vbv_lernwelt/feedback/export.py:57 +#: vbv_lernwelt/feedback/export.py:63 msgid "Wo siehst du Verbesserungspotential?" msgstr "" -#: vbv_lernwelt/feedback/export.py:144 +#: vbv_lernwelt/feedback/export.py:150 msgid "Durchführung" msgstr "" -#: vbv_lernwelt/feedback/export.py:145 +#: vbv_lernwelt/feedback/export.py:151 msgid "Datum" msgstr "" diff --git a/server/locale/fr/LC_MESSAGES/django.mo b/server/locale/fr/LC_MESSAGES/django.mo index 5c10a8378da9bda52bcc4ae06a25c6dde22278e3..43c07650e89e0c7e53ad428649c6dfa12ffc78e3 100644 GIT binary patch delta 722 zcmZ9~IZFdU6u|LGT#fgUsPQU1BB|^xER4iLv5N?bvcxzNuiZ6SP*e!{0D=gyR}ew0 z3|a^_egFk4E3wehQai!_iyKfMy!>`HnVC0l^3`%5$~^~-f+9M}9`cnOBrkk?5Tkyj zsxgK&IEj@wjSV>Gjni1i*uo(0pv>FHMm)kMJjDQB;jmIUb<4v5FFtV$yDF5Lzy+Mc zGmN0$P%42@lmd6L1CKF`Hz*rFqHOerefWVf4ilzoDQLFpr+`H?*k7&kAeGxFC)-7- zIFJ2!;Ejt|&-e~yp$C+cJYgJPa0mx^IfDtD$8$_$fJx)%po7;KVSg2+uMwQWUfe`E z!J+2~b~C<2xfLn4m1Ms^K?6&{x~1Ufr2?C=oopfH%H)vch_&$~ZIh7yK!}vhe}}Z0 zTcBM(ossWqN$Ezo`0gwC<9ge%Gp?DmwscloX}#hq@71!_ON+_WT6w%?Z#df3){d1- trML7-*3|2ILtCyc=8ZjL#GB@1WTBKUPy4@&Hz{i_X)|S8ZpL2M>Kow}VI2Sf delta 526 zcmXxhy-R{o6vy%7<71hnrDir%^kPfl9zlcl0#Zvv1<}|85hPJVLxJGn9D)CWe?Sgx zO-)g55mYobHRNgpeSeocc<$%k%i}%wrIr3oRz6aG-G~u#jC>@=$;%K8@r_YzV;^=f zg1%=K#~5lniv2i&DV#(7ZwV8)hJ(0`y%^wvS;da&%rMcwB0gXV<6*Ns+{GN;;1<5% zEYABB#ZAoO5f<276;=42U8&F$GG))G4Zb2j`=m)w-@fQfGx6glI6;ru0OLGrp+(d} z%eaE;n8z#J!8_c?3?CihDX!uhp5PRV<na36TbyM4jv?y17^YPbT~Rk!*SHL~Yn?#X yudD4wl8!d2NRt^-&&UP26Y6;sF_K#D8HO}bD4z#sLHWRnL36{adChF(!v6!cwksw8 diff --git a/server/locale/fr/LC_MESSAGES/django.po b/server/locale/fr/LC_MESSAGES/django.po index 4fe78632..209a4b6e 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-06-17 08:25+0200\n" +"POT-Creation-Date: 2024-06-17 15:43+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" @@ -18,16 +18,20 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: vbv_lernwelt/assignment/export.py:181 +#: vbv_lernwelt/assignment/export.py:31 +msgid "export_kompetenznachweis_elemente" +msgstr "export_elements_de_controle" + +#: vbv_lernwelt/assignment/export.py:183 msgid "Bestanden" msgstr "Réussi" -#: vbv_lernwelt/assignment/export.py:181 +#: vbv_lernwelt/assignment/export.py:185 msgid "Nicht bestanden" msgstr "Échoué" -#: vbv_lernwelt/assignment/export.py:195 vbv_lernwelt/assignment/export.py:198 -#: vbv_lernwelt/assignment/export.py:199 +#: vbv_lernwelt/assignment/export.py:199 vbv_lernwelt/assignment/export.py:202 +#: vbv_lernwelt/assignment/export.py:203 msgid "Keine Daten" msgstr "Aucune donnée" @@ -103,81 +107,97 @@ msgstr "" msgid "ÜK-Lehrgang" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:111 +#: vbv_lernwelt/course_session/services/export_attendance.py:15 +msgid "export_anwesenheit" +msgstr "export_presence" + +#: vbv_lernwelt/course_session/services/export_attendance.py:113 msgid "Anwesend" msgstr "Présent" -#: vbv_lernwelt/course_session/services/export_attendance.py:111 +#: vbv_lernwelt/course_session/services/export_attendance.py:113 msgid "Nicht anwesend" msgstr "Pas présent" -#: vbv_lernwelt/course_session/services/export_attendance.py:118 +#: vbv_lernwelt/course_session/services/export_attendance.py:120 msgid "Vorname" msgstr "Prénom" -#: vbv_lernwelt/course_session/services/export_attendance.py:119 +#: vbv_lernwelt/course_session/services/export_attendance.py:121 msgid "Nachname" msgstr "Nom de famille" -#: vbv_lernwelt/course_session/services/export_attendance.py:120 +#: vbv_lernwelt/course_session/services/export_attendance.py:122 msgid "Email" msgstr "Email" -#: vbv_lernwelt/course_session/services/export_attendance.py:121 -#: vbv_lernwelt/course_session/services/export_attendance.py:133 +#: vbv_lernwelt/course_session/services/export_attendance.py:123 +#: vbv_lernwelt/course_session/services/export_attendance.py:135 msgid "Lehrvertragsnummer" msgstr "Numéro de contrat d'apprentissage" -#: vbv_lernwelt/feedback/export.py:33 +#: vbv_lernwelt/feedback/export.py:19 +msgid "export_feedback" +msgstr "export_feedback" + +#: vbv_lernwelt/feedback/export.py:35 msgid "Zufriedenheit insgesamt" msgstr "Degré de satisfaction au global" -#: vbv_lernwelt/feedback/export.py:34 +#: vbv_lernwelt/feedback/export.py:36 msgid "Zielerreichung insgesamt" msgstr "Degré de réalisation des objectifs" -#: vbv_lernwelt/feedback/export.py:37 +#: vbv_lernwelt/feedback/export.py:39 msgid "" "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?" -msgstr "As-tu l’impression de bien maîtriser les sujets qui ont été abordés pendant le cours ?" +msgstr "" +"As-tu l’impression de bien maîtriser les sujets qui ont été abordés pendant " +"le cours ?" -#: vbv_lernwelt/feedback/export.py:41 +#: vbv_lernwelt/feedback/export.py:43 msgid "Waren die Vorbereitungsaufträge klar und verständlich?" msgstr "Les travaux préparatoires étaient-ils clairs et compréhensibles ?" -#: vbv_lernwelt/feedback/export.py:45 +#: vbv_lernwelt/feedback/export.py:48 msgid "" "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der " "Kursleiterin?" -msgstr "Que penses-tu des compétences techniques de la personne chargée du cours et de sa maîtrise du sujet ?" +msgstr "" +"Que penses-tu des compétences techniques de la personne chargée du cours et " +"de sa maîtrise du sujet ?" -#: vbv_lernwelt/feedback/export.py:49 +#: vbv_lernwelt/feedback/export.py:54 msgid "" "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und " "aufgegriffen?" -msgstr "Les questions et les suggestions des participants ont-elles été prises au sérieux et traitées correctement ?" +msgstr "" +"Les questions et les suggestions des participants ont-elles été prises au " +"sérieux et traitées correctement ?" -#: vbv_lernwelt/feedback/export.py:53 +#: vbv_lernwelt/feedback/export.py:59 msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?" -msgstr "Souhaites-tu ajouter quelque chose à l’intention de la personne chargée du cours ?" +msgstr "" +"Souhaites-tu ajouter quelque chose à l’intention de la personne chargée du " +"cours ?" -#: vbv_lernwelt/feedback/export.py:55 +#: vbv_lernwelt/feedback/export.py:61 msgid "Würdest du den Kurs weiterempfehlen?" msgstr "Est-ce que tu recommandes ce cours ?" -#: vbv_lernwelt/feedback/export.py:56 +#: vbv_lernwelt/feedback/export.py:62 msgid "Was hat dir besonders gut gefallen?" msgstr "Qu’est-ce qui t’a particulièrement plu ?" -#: vbv_lernwelt/feedback/export.py:57 +#: vbv_lernwelt/feedback/export.py:63 msgid "Wo siehst du Verbesserungspotential?" msgstr "À ton avis, quels sont les points qui pourraient être améliorés ?" -#: vbv_lernwelt/feedback/export.py:144 +#: vbv_lernwelt/feedback/export.py:150 msgid "Durchführung" msgstr "Opérations" -#: vbv_lernwelt/feedback/export.py:145 +#: vbv_lernwelt/feedback/export.py:151 msgid "Datum" msgstr "Date" diff --git a/server/locale/it/LC_MESSAGES/django.mo b/server/locale/it/LC_MESSAGES/django.mo index 5cb0e69893ce98efd29312e247a4346e6dfe2d66..0993e981009f21a06a5b10d1af13dc9147f90847 100644 GIT binary patch delta 742 zcmY+>KTH!*9Ki8k>*1e*U<+7k7&Kyx?k0+hB@D>a1g0kBavsn0!tt)Qm(-lmhJ=;K zQg;U9;z9@mCJqcbI>-(Sj&A7e_jg_c{iVPA+<Upc_j|whzIayZ9+#Y+5EXKoJSFdu zpL0BjXL*sUSjB6&h(oxH*Rh_B8#v6^VhIz}|83zYe!wyOh*xkQAB%M5D=&AM_=|IR z>ypTGT){ei!WqmvB1?E5wZJzxg*#ZrFQ^;;MBV5Y-oZaO$6?A;tp(LD|JO%{{Uzi@ zD}ROj<ZU+I#M_LwQ7hiX5j@Do-;uTD2R_9k)WXL2cmW@w9_Sr*@DLy16RPmg#|rz) zH(nm%iB4dFK2PFI<}7N5YREy-VkeOO27(5r!7^#^d0O}lyh#>Gy)wPNepGcMZJUN} zK2B=;E{3+5TS!l<cd1bzwHxKV-??6X-n?!`t=RQ!$Fz-Ym>`yH>ZLKOFFb#(KVFO0 zn<h3kv7X=Pn4s;N(5xF9o4pNZ)Tx=auH+>_WR2Tw>2irD1GDE#Ui>y&CU93x==zb3 JTTvKB@(+H*XUzZr delta 526 zcmXxhu}i~H5XbRrOs%a(+gPna6~T6}I_RcI1v7|>i--trf>{K+NGXUAaBy{UcM$)8 z_HS?z>Don{byC4e7r(y;J-B?{dm*{w-cNX2NZvyK&WK5}Oumpa<W-6X@r6V9jamG~ zH2R)dfH~BD3WsqPL#&~`tK%qc;TRrb24h?`OYEG*JO_^$;WO@FFkp6!M_9o&9$*iv zxad<9_pyX$7}4A{s_+wC$u4qaJye6QIEe3VpO}5JnB(9N)mfRhBUnN0S5QA#!*$%i zDqi9?-lH1-z!om?(E>i;33hQBm+7X4yQqB=Q`C1+<f&pBQ9t;fF$~wW8qoMP+I|#R ss8Pi@IYH_g8IZdYU5_G1Qp;UKUI&|vR(ugRPA!T%d)|%LDWwDdABbuxc>n+a diff --git a/server/locale/it/LC_MESSAGES/django.po b/server/locale/it/LC_MESSAGES/django.po index c094423a..bd4d61d5 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-06-17 08:25+0200\n" +"POT-Creation-Date: 2024-06-17 15:43+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" @@ -18,16 +18,20 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: vbv_lernwelt/assignment/export.py:181 +#: vbv_lernwelt/assignment/export.py:31 +msgid "export_kompetenznachweis_elemente" +msgstr "esportazione_elementi_del_controllo" + +#: vbv_lernwelt/assignment/export.py:183 msgid "Bestanden" msgstr "Superato" -#: vbv_lernwelt/assignment/export.py:181 +#: vbv_lernwelt/assignment/export.py:185 msgid "Nicht bestanden" msgstr "Fallito" -#: vbv_lernwelt/assignment/export.py:195 vbv_lernwelt/assignment/export.py:198 -#: vbv_lernwelt/assignment/export.py:199 +#: vbv_lernwelt/assignment/export.py:199 vbv_lernwelt/assignment/export.py:202 +#: vbv_lernwelt/assignment/export.py:203 msgid "Keine Daten" msgstr "Nessun dato" @@ -103,81 +107,93 @@ msgstr "" msgid "ÜK-Lehrgang" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:111 +#: vbv_lernwelt/course_session/services/export_attendance.py:15 +msgid "export_anwesenheit" +msgstr "esportazione_presenza" + +#: vbv_lernwelt/course_session/services/export_attendance.py:113 msgid "Anwesend" msgstr "Presente" -#: vbv_lernwelt/course_session/services/export_attendance.py:111 +#: vbv_lernwelt/course_session/services/export_attendance.py:113 msgid "Nicht anwesend" msgstr "Non presente" -#: vbv_lernwelt/course_session/services/export_attendance.py:118 +#: vbv_lernwelt/course_session/services/export_attendance.py:120 msgid "Vorname" msgstr "Nome" -#: vbv_lernwelt/course_session/services/export_attendance.py:119 +#: vbv_lernwelt/course_session/services/export_attendance.py:121 msgid "Nachname" msgstr "Cognome" -#: vbv_lernwelt/course_session/services/export_attendance.py:120 +#: vbv_lernwelt/course_session/services/export_attendance.py:122 msgid "Email" msgstr "E-mail" -#: vbv_lernwelt/course_session/services/export_attendance.py:121 -#: vbv_lernwelt/course_session/services/export_attendance.py:133 +#: vbv_lernwelt/course_session/services/export_attendance.py:123 +#: vbv_lernwelt/course_session/services/export_attendance.py:135 msgid "Lehrvertragsnummer" msgstr "Numero di contratto di tirocinio" -#: vbv_lernwelt/feedback/export.py:33 +#: vbv_lernwelt/feedback/export.py:19 +msgid "export_feedback" +msgstr "esportazione_feedback" + +#: vbv_lernwelt/feedback/export.py:35 msgid "Zufriedenheit insgesamt" msgstr "Soddisfazione complessiva" -#: vbv_lernwelt/feedback/export.py:34 +#: vbv_lernwelt/feedback/export.py:36 msgid "Zielerreichung insgesamt" msgstr "Raggiungimento complessivo degli obiettivi" -#: vbv_lernwelt/feedback/export.py:37 +#: vbv_lernwelt/feedback/export.py:39 msgid "" "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?" msgstr "Come valuti il tuo livello di preparazione sui temi dopo il corso?" -#: vbv_lernwelt/feedback/export.py:41 +#: vbv_lernwelt/feedback/export.py:43 msgid "Waren die Vorbereitungsaufträge klar und verständlich?" msgstr "Gli incarichi di preparazione erano chiari e comprensibili?" -#: vbv_lernwelt/feedback/export.py:45 +#: vbv_lernwelt/feedback/export.py:48 msgid "" "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der " "Kursleiterin?" -msgstr "Come valuti il livello di preparazione sui temi e le competenze specialistiche dell’istruttore/istruttrice del corso?" +msgstr "" +"Come valuti il livello di preparazione sui temi e le competenze " +"specialistiche dell’istruttore/istruttrice del corso?" -#: vbv_lernwelt/feedback/export.py:49 +#: vbv_lernwelt/feedback/export.py:54 msgid "" "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und " "aufgegriffen?" -msgstr "Le domande e i suggerimenti dei/delle partecipanti al corso sono stati accolti e presi sul serio?" +msgstr "" +"Le domande e i suggerimenti dei/delle partecipanti al corso sono stati " +"accolti e presi sul serio?" -#: vbv_lernwelt/feedback/export.py:53 +#: vbv_lernwelt/feedback/export.py:59 msgid "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?" msgstr "Cos’altro vorresti ancora dire all’istruttore/istruttrice del corso?" -#: vbv_lernwelt/feedback/export.py:55 +#: vbv_lernwelt/feedback/export.py:61 msgid "Würdest du den Kurs weiterempfehlen?" msgstr "Raccomanderesti il corso?" -#: vbv_lernwelt/feedback/export.py:56 +#: vbv_lernwelt/feedback/export.py:62 msgid "Was hat dir besonders gut gefallen?" msgstr "Cos’hai apprezzato particolarmente?" -#: vbv_lernwelt/feedback/export.py:57 +#: vbv_lernwelt/feedback/export.py:63 msgid "Wo siehst du Verbesserungspotential?" msgstr "Dove vedi un potenziale di miglioramento?" -#: vbv_lernwelt/feedback/export.py:144 +#: vbv_lernwelt/feedback/export.py:150 msgid "Durchführung" msgstr "Svolgimenti" -#: vbv_lernwelt/feedback/export.py:145 +#: vbv_lernwelt/feedback/export.py:151 msgid "Datum" msgstr "Data" diff --git a/server/vbv_lernwelt/assignment/export.py b/server/vbv_lernwelt/assignment/export.py index 8761dabe..f8cbc205 100644 --- a/server/vbv_lernwelt/assignment/export.py +++ b/server/vbv_lernwelt/assignment/export.py @@ -28,6 +28,8 @@ from vbv_lernwelt.learnpath.models import LearningContent logger = structlog.get_logger(__name__) +COMPETENCE_ELEMENT_EXPORT_FILE_NAME = _("export_kompetenznachweis_elemente") + @dataclass class CompetenceCertificateElement: @@ -37,7 +39,7 @@ class CompetenceCertificateElement: course_session: CourseSession -def export_competence_certificates( +def export_competence_elements( course_session_ids: list[str], circle_ids: list[int] = None, save_as_file: bool = False, @@ -99,7 +101,7 @@ def export_competence_certificates( ) if save_as_file: - wb.save(make_export_filename(name="competence_certificate_export")) + wb.save(make_export_filename(COMPETENCE_ELEMENT_EXPORT_FILE_NAME)) else: output = BytesIO() wb.save(output) diff --git a/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py b/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py index 0cd3e291..6b74e782 100644 --- a/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py +++ b/server/vbv_lernwelt/assignment/management/commands/export_assignment_completions.py @@ -1,7 +1,7 @@ import djclick as click import structlog -from vbv_lernwelt.assignment.export import export_competence_certificates +from vbv_lernwelt.assignment.export import export_competence_elements logger = structlog.get_logger(__name__) @@ -15,4 +15,4 @@ logger = structlog.get_logger(__name__) ) def command(course_session_id, save_as_file): # using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function - export_competence_certificates([course_session_id], save_as_file=save_as_file) + export_competence_elements([course_session_id], save_as_file=save_as_file) diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py index 0a41ef98..550bf67f 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py @@ -2,7 +2,7 @@ import io from openpyxl import load_workbook -from vbv_lernwelt.assignment.export import export_competence_certificates +from vbv_lernwelt.assignment.export import export_competence_elements from vbv_lernwelt.assignment.models import Assignment from vbv_lernwelt.assignment.services import update_assignment_completion from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID @@ -115,7 +115,7 @@ class AssignmentCompletionExportTestCase(ExportBaseTestCase): def _generate_workbook(self, course_session_ids): export_data = io.BytesIO( - export_competence_certificates(course_session_ids, save_as_file=False) + export_competence_elements(course_session_ids, save_as_file=False) ) return load_workbook(export_data) diff --git a/server/vbv_lernwelt/course_session/services/export_attendance.py b/server/vbv_lernwelt/course_session/services/export_attendance.py index 0fde37bb..4ce81de1 100644 --- a/server/vbv_lernwelt/course_session/services/export_attendance.py +++ b/server/vbv_lernwelt/course_session/services/export_attendance.py @@ -12,6 +12,8 @@ from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse logger = structlog.get_logger(__name__) +ATTENDANCE_EXPORT_FILENAME = _("export_anwesenheit") + def export_attendance( course_session_ids: list[str], @@ -56,7 +58,7 @@ def export_attendance( ) if save_as_file: - wb.save(make_export_filename()) + wb.save(make_export_filename(ATTENDANCE_EXPORT_FILENAME)) else: output = BytesIO() wb.save(output) @@ -155,7 +157,7 @@ def group_by_session_title(items): } -def make_export_filename(name: str = "attendance_export"): +def make_export_filename(name: str): today_date = datetime.today().strftime("%Y-%m-%d") return f"{name}_{today_date}.xlsx" diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 05723e34..33962032 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -11,7 +11,10 @@ from rest_framework.decorators import api_view from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response -from vbv_lernwelt.assignment.export import export_competence_certificates +from vbv_lernwelt.assignment.export import ( + COMPETENCE_ELEMENT_EXPORT_FILE_NAME, + export_competence_elements, +) from vbv_lernwelt.assignment.models import ( AssignmentCompletion, AssignmentCompletionStatus, @@ -28,13 +31,17 @@ from vbv_lernwelt.course.models import ( ) from vbv_lernwelt.course.views import logger from vbv_lernwelt.course_session.services.export_attendance import ( + ATTENDANCE_EXPORT_FILENAME, export_attendance, make_export_filename, ) from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.serializers import DueDateSerializer -from vbv_lernwelt.feedback.export import export_feedback_with_circle_restriction +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 @@ -550,21 +557,21 @@ def export_attendance_as_xsl(request): [cs.id for cs in course_sessions_with_roles], circle_ids=circle_ids, ) - return _make_excel_response(data) + return _make_excel_response(data, file_name=ATTENDANCE_EXPORT_FILENAME) @api_view(["POST"]) -def export_competence_certificate_as_xsl(request): +def export_competence_elements_as_xsl(request): circle_ids = request.data.get("circleIds", None) requested_course_session_ids = request.data.get("courseSessionIds", []) course_sessions_with_roles = _get_permitted_courses_sessions_for_user( request.user, requested_course_session_ids ) - data = export_competence_certificates( + data = export_competence_elements( [cswr.id for cswr in course_sessions_with_roles], circle_ids=circle_ids, ) - return _make_excel_response(data) + return _make_excel_response(data, COMPETENCE_ELEMENT_EXPORT_FILE_NAME) @api_view(["POST"]) @@ -582,7 +589,7 @@ def export_feedback_as_xsl(request): ) # noqa data = export_feedback_with_circle_restriction(allowed_circles, False) - return _make_excel_response(data) + return _make_excel_response(data, FEEDBACK_EXPORT_FILE_NAME) def _get_permitted_courses_sessions_for_user( @@ -597,11 +604,14 @@ def _get_permitted_courses_sessions_for_user( return user_course_sessions_with_roles -def _make_excel_response(data: bytes) -> HttpResponse: +def _make_excel_response(data: bytes, file_name: str) -> HttpResponse: encoded_data = base64.b64encode(data).decode("utf-8") # Create the JSON response - response_data = {"encoded_data": encoded_data, "file_name": make_export_filename()} + response_data = { + "encoded_data": encoded_data, + "file_name": make_export_filename(file_name), + } return Response(response_data, status=200) diff --git a/server/vbv_lernwelt/feedback/export.py b/server/vbv_lernwelt/feedback/export.py index 0086d910..2dab9cb6 100644 --- a/server/vbv_lernwelt/feedback/export.py +++ b/server/vbv_lernwelt/feedback/export.py @@ -16,6 +16,8 @@ from vbv_lernwelt.feedback.models import FeedbackResponse logger = structlog.get_logger(__name__) +FEEDBACK_EXPORT_FILE_NAME = _("export_feedback") + VV_FEEDBACK_QUESTIONS = [ ("satisfaction", "Zufriedenheit insgesamt"), ("goal_attainment", "Zielerreichung insgesamt"), @@ -121,7 +123,7 @@ def _generate_feedback_export(feedback_unordered: QuerySet, save_as_file: bool): _create_sheet(wb, circle.title, group_feedbacks) if save_as_file: - wb.save(make_export_filename(name="feedback_export")) + wb.save(make_export_filename(FEEDBACK_EXPORT_FILE_NAME)) else: # todo handle IndexError output = BytesIO() From 033886f00bdf4cdad72807df9f71f149c9f09ecc Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Mon, 17 Jun 2024 16:32:27 +0200 Subject: [PATCH 20/24] Add frontend export --- .../dashboard/StatisticFilterList.vue | 16 ++--- .../dashboard/statistic/AssignmentList.vue | 18 ++++++ .../dashboard/statistic/AttendanceList.vue | 18 ++++++ .../dashboard/statistic/FeedbackList.vue | 18 ++++++ client/src/services/dashboard.ts | 30 ++++----- client/src/types.ts | 17 +++++ client/src/utils/export.ts | 62 +++++++++++++++++-- server/config/urls.py | 3 +- 8 files changed, 155 insertions(+), 27 deletions(-) diff --git a/client/src/components/dashboard/StatisticFilterList.vue b/client/src/components/dashboard/StatisticFilterList.vue index 7ec9b451..cda10a88 100644 --- a/client/src/components/dashboard/StatisticFilterList.vue +++ b/client/src/components/dashboard/StatisticFilterList.vue @@ -3,21 +3,17 @@ import { computed, ref, watch } from "vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import { useTranslation } from "i18next-vue"; import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql"; +import type { StatisticsFilterItem } from "@/types"; const { t } = useTranslation(); -interface Item { - _id: string; - course_session_id: string; - generation: string; - circle_id: string; -} - const props = defineProps<{ - items: Item[]; + items: StatisticsFilterItem[]; courseSessionProperties: StatisticsCourseSessionPropertiesType; }>(); +defineExpose({ getFilteredItems }); + const sessionFilter = computed(() => { const f = props.courseSessionProperties.sessions.map((session) => ({ name: `${t("a.Durchfuehrung")}: ${session.name}`, @@ -71,6 +67,10 @@ const filteredItems = computed(() => { return sessionMatch && generationMatch && circleMatch; }); }); + +function getFilteredItems() { + return filteredItems.value; +} </script> <template> diff --git a/client/src/pages/dashboard/statistic/AssignmentList.vue b/client/src/pages/dashboard/statistic/AssignmentList.vue index b89316f9..b07e8c54 100644 --- a/client/src/pages/dashboard/statistic/AssignmentList.vue +++ b/client/src/pages/dashboard/statistic/AssignmentList.vue @@ -9,6 +9,9 @@ import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue" import { getDateString } from "@/components/dueDates/dueDatesUtils"; import dayjs from "dayjs"; import ItProgress from "@/components/ui/ItProgress.vue"; +import { type Ref, ref } from "vue"; +import { exportDataAsXls } from "@/utils/export"; +import { exportCompetenceElements } from "@/services/dashboard"; // eslint-disable-next-line @typescript-eslint/no-unused-vars const props = defineProps<{ @@ -17,6 +20,8 @@ const props = defineProps<{ circleMeta: (circleId: string) => StatisticsCircleDataType; }>(); +const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null); + const assignmentStats = (metrics: AssignmentCompletionMetricsType) => { if (!metrics.ranking_completed) { return { @@ -36,15 +41,28 @@ const assignmentStats = (metrics: AssignmentCompletionMetricsType) => { const total = (metrics: AssignmentCompletionMetricsType) => { return metrics.passed_count + metrics.failed_count + metrics.unranked_count; }; + +async function exportData() { + if (!statisticFilter.value) { + return; + } + const filteredItems = statisticFilter.value.getFilteredItems(); + await exportDataAsXls(filteredItems, exportCompetenceElements); +} </script> <template> <main> <div class="mb-10 flex items-center justify-between"> <h3>{{ $t("a.Kompetenznachweis-Elemente") }}</h3> + <button class="flex" @click="exportData"> + <it-icon-export></it-icon-export> + <span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span> + </button> </div> <div v-if="courseStatistics?.assignments.records" class="mt-8 bg-white"> <StatisticFilterList + ref="statisticFilter" :course-session-properties="courseStatistics?.course_session_properties" :items="courseStatistics.assignments.records" > diff --git a/client/src/pages/dashboard/statistic/AttendanceList.vue b/client/src/pages/dashboard/statistic/AttendanceList.vue index 09231812..884bf568 100644 --- a/client/src/pages/dashboard/statistic/AttendanceList.vue +++ b/client/src/pages/dashboard/statistic/AttendanceList.vue @@ -8,6 +8,9 @@ import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue" import ItProgress from "@/components/ui/ItProgress.vue"; import { getDateString } from "@/components/dueDates/dueDatesUtils"; import dayjs from "dayjs"; +import { ref, type Ref } from "vue"; +import { exportDataAsXls } from "@/utils/export"; +import { exportAttendance } from "@/services/dashboard"; // eslint-disable-next-line @typescript-eslint/no-unused-vars const props = defineProps<{ @@ -16,6 +19,8 @@ const props = defineProps<{ circleMeta: (circleId: string) => StatisticsCircleDataType; }>(); +const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null); + const attendanceStats = (present: number, total: number) => { return { SUCCESS: present, @@ -23,18 +28,31 @@ const attendanceStats = (present: number, total: number) => { UNKNOWN: 0, }; }; + +async function exportData() { + if (!statisticFilter.value) { + return; + } + const filteredItems = statisticFilter.value.getFilteredItems(); + await exportDataAsXls(filteredItems, exportAttendance); +} </script> <template> <main> <div class="mb-10 flex items-center justify-between"> <h3>{{ $t("Anwesenheit") }}</h3> + <button class="flex" @click="exportData"> + <it-icon-export></it-icon-export> + <span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span> + </button> </div> <div v-if="courseStatistics?.attendance_day_presences.records" class="mt-8 bg-white" > <StatisticFilterList + ref="statisticFilter" :course-session-properties="courseStatistics.course_session_properties" :items="courseStatistics.attendance_day_presences.records" > diff --git a/client/src/pages/dashboard/statistic/FeedbackList.vue b/client/src/pages/dashboard/statistic/FeedbackList.vue index 27917ee0..c84d3b8c 100644 --- a/client/src/pages/dashboard/statistic/FeedbackList.vue +++ b/client/src/pages/dashboard/statistic/FeedbackList.vue @@ -7,6 +7,9 @@ import type { } from "@/gql/graphql"; import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue"; import { getBlendedColorForRating } from "@/utils/ratingToColor"; +import { ref, type Ref } from "vue"; +import { exportDataAsXls } from "@/utils/export"; +import { exportFeedback } from "@/services/dashboard"; // eslint-disable-next-line @typescript-eslint/no-unused-vars const props = defineProps<{ @@ -14,15 +17,30 @@ const props = defineProps<{ courseSessionName: (sessionId: string) => string; circleMeta: (circleId: string) => StatisticsCircleDataType; }>(); + +const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null); + +async function exportData() { + if (!statisticFilter.value) { + return; + } + const filteredItems = statisticFilter.value.getFilteredItems(); + await exportDataAsXls(filteredItems, exportFeedback); +} </script> <template> <main> <div class="mb-10 flex items-center justify-between"> <h3>{{ $t("a.Feedback Teilnehmer") }}</h3> + <button class="flex" @click="exportData"> + <it-icon-export></it-icon-export> + <span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span> + </button> </div> <div v-if="courseStatistics?.feedback_responses.records" class="mt-8 bg-white"> <StatisticFilterList + ref="statisticFilter" :course-session-properties="courseStatistics.course_session_properties" :items="courseStatistics.feedback_responses.records" > diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index da2664bb..82bab340 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -13,7 +13,12 @@ import type { CourseStatisticsType, DashboardConfigType, } from "@/gql/graphql"; -import type { DashboardPersonsPageMode, DueDate } from "@/types"; +import type { + DashboardPersonsPageMode, + DueDate, + XlsExportRequestData, + XlsExportResponseData, +} from "@/types"; export type DashboardPersonRoleType = | "SUPERVISOR" @@ -189,25 +194,22 @@ export async function fetchOpenTasksCount(courseId: string) { ); } -export async function exportFeedback(data: { - courseSessionIds: string[]; - circleIds: string[]; -}) { +export async function exportFeedback( + data: XlsExportRequestData +): Promise<XlsExportResponseData> { return await itPost("/api/dashboard/export/feedback/", data); } -export async function exportAttendance(data: { - courseSessionIds: string[]; - circleIds: string[]; -}) { +export async function exportAttendance( + data: XlsExportRequestData +): Promise<XlsExportResponseData> { return await itPost("/api/dashboard/export/attendance/", data); } -export async function exportCertificate(data: { - courseSessionIds: string[]; - circleIds: string[]; -}) { - return await itPost("/api/dashboard/export/certificate/", data); +export async function exportCompetenceElements( + data: XlsExportRequestData +): Promise<XlsExportResponseData> { + return await itPost("/api/dashboard/export/competence_elements/", data); } export function courseIdForCourseSlug( diff --git a/client/src/types.ts b/client/src/types.ts index c55bb33d..afdf8556 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -618,3 +618,20 @@ export type User = { }; export type DashboardPersonsPageMode = "default" | "competenceMetrics"; + +export interface StatisticsFilterItem { + _id: string; + course_session_id: string; + generation: string; + circle_id: string; +} + +export interface XlsExportRequestData { + courseSessionIds: number[]; + circleIds: number[]; +} + +export interface XlsExportResponseData { + encoded_data: string; + file_name: string; +} diff --git a/client/src/utils/export.ts b/client/src/utils/export.ts index 17f0e1c6..67069455 100644 --- a/client/src/utils/export.ts +++ b/client/src/utils/export.ts @@ -1,4 +1,58 @@ -/** - * Created by christiancueni on 17.06.2024. - * Copyright (c) 2024 ITerativ GmbH. All rights reserved. - */ +import type { + StatisticsFilterItem, + XlsExportRequestData, + XlsExportResponseData, +} from "@/types"; + +interface exportApiCall { + (data: XlsExportRequestData): Promise<XlsExportResponseData>; +} + +export async function exportDataAsXls( + items: StatisticsFilterItem[], + apiCall: exportApiCall +) { + const itemIds = extractUniqueIds(items); + const data = await apiCall(itemIds); + openDataAsXls(data.encoded_data, data.file_name); +} + +export async function openDataAsXls(encodedData: string, filename: string) { + // Decode base64 string to binary data + const byteCharacters = atob(encodedData); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: "application/octet-stream" }); + + // Create a link element and trigger download + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); +} + +// Function to extract unique circle_ids and course_session_ids +export function extractUniqueIds(data: StatisticsFilterItem[]): { + circleIds: number[]; + courseSessionIds: number[]; +} { + const circleIdsSet = new Set<number>(); + const courseSessionIdsSet = new Set<number>(); + + data.forEach((item) => { + circleIdsSet.add(Number(item.circle_id)); + courseSessionIdsSet.add(Number(item.course_session_id)); + }); + + return { + circleIds: Array.from(circleIdsSet), + courseSessionIds: Array.from(courseSessionIdsSet), + }; +} diff --git a/server/config/urls.py b/server/config/urls.py index 51627cce..259d5c3b 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -135,7 +135,8 @@ urlpatterns = [ path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count, name="get_mentor_open_tasks_count"), path(r"api/dashboard/export/attendance/", export_attendance_as_xsl, name="export_attendance_as_xsl"), - path(r"api/dashboard/export/certificate/", export_competence_elements_as_xsl, name="export_certificate_as_xsl"), + path(r"api/dashboard/export/competence_elements/", export_competence_elements_as_xsl, + name="export_certificate_as_xsl"), path(r"api/dashboard/export/feedback/", export_feedback_as_xsl, name="export_feedback_as_xsl"), # course From 232959b92d5d3f89b235be62dc4d4bc937e9e9e5 Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Mon, 17 Jun 2024 20:07:47 +0200 Subject: [PATCH 21/24] Display export only to experts --- client/src/pages/dashboard/statistic/AssignmentList.vue | 8 +++++++- client/src/pages/dashboard/statistic/AttendanceList.vue | 8 +++++++- client/src/pages/dashboard/statistic/FeedbackList.vue | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/client/src/pages/dashboard/statistic/AssignmentList.vue b/client/src/pages/dashboard/statistic/AssignmentList.vue index b07e8c54..28473d3b 100644 --- a/client/src/pages/dashboard/statistic/AssignmentList.vue +++ b/client/src/pages/dashboard/statistic/AssignmentList.vue @@ -12,6 +12,7 @@ import ItProgress from "@/components/ui/ItProgress.vue"; import { type Ref, ref } from "vue"; import { exportDataAsXls } from "@/utils/export"; import { exportCompetenceElements } from "@/services/dashboard"; +import { useUserStore } from "@/stores/user"; // eslint-disable-next-line @typescript-eslint/no-unused-vars const props = defineProps<{ @@ -21,6 +22,7 @@ const props = defineProps<{ }>(); const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null); +const userStore = useUserStore(); const assignmentStats = (metrics: AssignmentCompletionMetricsType) => { if (!metrics.ranking_completed) { @@ -55,7 +57,11 @@ async function exportData() { <main> <div class="mb-10 flex items-center justify-between"> <h3>{{ $t("a.Kompetenznachweis-Elemente") }}</h3> - <button class="flex" @click="exportData"> + <button + v-if="userStore.course_session_experts.length > 0" + class="flex" + @click="exportData" + > <it-icon-export></it-icon-export> <span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span> </button> diff --git a/client/src/pages/dashboard/statistic/AttendanceList.vue b/client/src/pages/dashboard/statistic/AttendanceList.vue index 884bf568..bdbe53d5 100644 --- a/client/src/pages/dashboard/statistic/AttendanceList.vue +++ b/client/src/pages/dashboard/statistic/AttendanceList.vue @@ -11,6 +11,7 @@ import dayjs from "dayjs"; import { ref, type Ref } from "vue"; import { exportDataAsXls } from "@/utils/export"; import { exportAttendance } from "@/services/dashboard"; +import { useUserStore } from "@/stores/user"; // eslint-disable-next-line @typescript-eslint/no-unused-vars const props = defineProps<{ @@ -20,6 +21,7 @@ const props = defineProps<{ }>(); const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null); +const userStore = useUserStore(); const attendanceStats = (present: number, total: number) => { return { @@ -42,7 +44,11 @@ async function exportData() { <main> <div class="mb-10 flex items-center justify-between"> <h3>{{ $t("Anwesenheit") }}</h3> - <button class="flex" @click="exportData"> + <button + v-if="userStore.course_session_experts.length > 0" + class="flex" + @click="exportData" + > <it-icon-export></it-icon-export> <span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span> </button> diff --git a/client/src/pages/dashboard/statistic/FeedbackList.vue b/client/src/pages/dashboard/statistic/FeedbackList.vue index c84d3b8c..72238905 100644 --- a/client/src/pages/dashboard/statistic/FeedbackList.vue +++ b/client/src/pages/dashboard/statistic/FeedbackList.vue @@ -10,6 +10,7 @@ import { getBlendedColorForRating } from "@/utils/ratingToColor"; import { ref, type Ref } from "vue"; import { exportDataAsXls } from "@/utils/export"; import { exportFeedback } from "@/services/dashboard"; +import { useUserStore } from "@/stores/user"; // eslint-disable-next-line @typescript-eslint/no-unused-vars const props = defineProps<{ @@ -19,6 +20,7 @@ const props = defineProps<{ }>(); const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null); +const userStore = useUserStore(); async function exportData() { if (!statisticFilter.value) { @@ -33,7 +35,11 @@ async function exportData() { <main> <div class="mb-10 flex items-center justify-between"> <h3>{{ $t("a.Feedback Teilnehmer") }}</h3> - <button class="flex" @click="exportData"> + <button + v-if="userStore.course_session_experts.length > 0" + class="flex" + @click="exportData" + > <it-icon-export></it-icon-export> <span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span> </button> From 7c21070b96adcedd9149b1a608719e6b7fd18e4a Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 18 Jun 2024 08:26:30 +0200 Subject: [PATCH 22/24] Add frontend tests, add translations --- .../dashboard/statistic/AssignmentList.vue | 1 + .../dashboard/statistic/AttendanceList.vue | 1 + .../dashboard/statistic/FeedbackList.vue | 1 + cypress/e2e/dashboard/dashboardExport.cy.js | 87 ++++++++++++++++++ server/locale/de/LC_MESSAGES/django.mo | Bin 380 -> 416 bytes server/locale/de/LC_MESSAGES/django.po | 2 +- 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 cypress/e2e/dashboard/dashboardExport.cy.js diff --git a/client/src/pages/dashboard/statistic/AssignmentList.vue b/client/src/pages/dashboard/statistic/AssignmentList.vue index 28473d3b..3d72eff4 100644 --- a/client/src/pages/dashboard/statistic/AssignmentList.vue +++ b/client/src/pages/dashboard/statistic/AssignmentList.vue @@ -60,6 +60,7 @@ async function exportData() { <button v-if="userStore.course_session_experts.length > 0" class="flex" + data-cy="export-button" @click="exportData" > <it-icon-export></it-icon-export> diff --git a/client/src/pages/dashboard/statistic/AttendanceList.vue b/client/src/pages/dashboard/statistic/AttendanceList.vue index bdbe53d5..c8b57ed1 100644 --- a/client/src/pages/dashboard/statistic/AttendanceList.vue +++ b/client/src/pages/dashboard/statistic/AttendanceList.vue @@ -47,6 +47,7 @@ async function exportData() { <button v-if="userStore.course_session_experts.length > 0" class="flex" + data-cy="export-button" @click="exportData" > <it-icon-export></it-icon-export> diff --git a/client/src/pages/dashboard/statistic/FeedbackList.vue b/client/src/pages/dashboard/statistic/FeedbackList.vue index 72238905..1bacde78 100644 --- a/client/src/pages/dashboard/statistic/FeedbackList.vue +++ b/client/src/pages/dashboard/statistic/FeedbackList.vue @@ -38,6 +38,7 @@ async function exportData() { <button v-if="userStore.course_session_experts.length > 0" class="flex" + data-cy="export-button" @click="exportData" > <it-icon-export></it-icon-export> diff --git a/cypress/e2e/dashboard/dashboardExport.cy.js b/cypress/e2e/dashboard/dashboardExport.cy.js new file mode 100644 index 00000000..565955db --- /dev/null +++ b/cypress/e2e/dashboard/dashboardExport.cy.js @@ -0,0 +1,87 @@ +import { login } from "../helpers"; + +const path = require("path"); + +// ignore automatic import mess-up... + +// const getDashboardStatistics = (what) => { +// return cy.get(`[data-cy="dashboard.stats.${what}"]`); +// }; +// +// const clickOnDetailsLink = (within) => { +// cy.get(`[data-cy="dashboard.stats.${within}"]`).within(() => { +// cy.get('[data-cy="basebox.detailsLink"]').click(); +// }); +// }; +// + +function getCurrentDate() { + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function verifyExportFileExists(fileName) { + const downloadsFolder = Cypress.config("downloadsFolder"); + cy.readFile( + path.join(downloadsFolder, `${fileName}_${getCurrentDate()}.xlsx`) + ).should("exist"); +} + +function testExport(url, fileName) { + cy.visit(url); + cy.get('[data-cy="export-button"]').click(); + verifyExportFileExists(fileName); +} + +describe("dashboardExport.cy.js", () => { + beforeEach(() => { + cy.manageCommand( + "cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days" + ); + }); + + describe("as supervisor", () => { + beforeEach(() => { + login("test-supervisor1@example.com", "test"); + }); + + it("should download the attendance export", () => { + testExport("/statistic/test-lehrgang/attendance", "export_anwesenheit"); + }); + + it("should download the competence elements export", () => { + testExport( + "/statistic/test-lehrgang/assignment", + "export_kompetenznachweis_elemente" + ); + }); + + it("should download the feedback export", () => { + testExport("/statistic/test-lehrgang/feedback", "export_feedback"); + }); + }); + + describe("as trainer", () => { + beforeEach(() => { + login("test-trainer1@example.com", "test"); + }); + + it("should download the attendance export", () => { + testExport("/statistic/test-lehrgang/attendance", "export_anwesenheit"); + }); + + it("should download the competence elements export", () => { + testExport( + "/statistic/test-lehrgang/assignment", + "export_kompetenznachweis_elemente" + ); + }); + + it("should download the feedback export", () => { + testExport("/statistic/test-lehrgang/feedback", "export_feedback"); + }); + }); +}); diff --git a/server/locale/de/LC_MESSAGES/django.mo b/server/locale/de/LC_MESSAGES/django.mo index 71cbdf3e9d8d54be31066ec4ad8628bc2c1f2845..1de8004b13ba791b2bcf4a541221dc092613da50 100644 GIT binary patch delta 102 zcmeyvw17G6o)F7a1|VPpVi_RT0b*7lwgF-g2moRbKM;tW7#SGCq5N4uHW0x9NIw|3 Q<|bz5Y_xD;WCODp01_Aq^8f$< delta 65 wcmZ3${D;Zno)F7a1|VPrVi_P-0b*t#)&XJ=umEBwprj>`2C0F8jbSc~0QW)#)Bpeg diff --git a/server/locale/de/LC_MESSAGES/django.po b/server/locale/de/LC_MESSAGES/django.po index 3db23658..abac3a2b 100644 --- a/server/locale/de/LC_MESSAGES/django.po +++ b/server/locale/de/LC_MESSAGES/django.po @@ -129,7 +129,7 @@ msgstr "" #: vbv_lernwelt/course_session/services/export_attendance.py:122 msgid "Email" -msgstr "" +msgstr "Email" #: vbv_lernwelt/course_session/services/export_attendance.py:123 #: vbv_lernwelt/course_session/services/export_attendance.py:135 From 7a8ee5610987201f0180120cb732a9d63e73c9cd Mon Sep 17 00:00:00 2001 From: Christian Cueni <christian.cueni@iterativ.ch> Date: Tue, 18 Jun 2024 15:38:43 +0200 Subject: [PATCH 23/24] Add language to request, add language backend tests, add icon --- .../dashboard/statistic/AssignmentList.vue | 2 +- .../dashboard/statistic/AttendanceList.vue | 2 +- .../dashboard/statistic/FeedbackList.vue | 2 +- client/src/services/dashboard.ts | 21 ++- client/src/utils/export.ts | 7 +- server/locale/de/LC_MESSAGES/django.po | 35 +++-- server/locale/fr/LC_MESSAGES/django.mo | Bin 2510 -> 2618 bytes server/locale/fr/LC_MESSAGES/django.po | 36 +++-- server/locale/it/LC_MESSAGES/django.mo | Bin 2420 -> 2527 bytes server/locale/it/LC_MESSAGES/django.po | 36 +++-- server/vbv_lernwelt/assignment/export.py | 22 +-- .../test_assignment_completions_export.py | 136 ++++++++++++++++-- .../services/export_attendance.py | 22 +-- .../tests/test_attendance_export.py | 89 +++++++++++- server/vbv_lernwelt/feedback/export.py | 8 +- .../feedback/tests/test_feedback_export.py | 51 +++++++ .../vbv_lernwelt/static/icons/icon-export.svg | 4 + 17 files changed, 387 insertions(+), 86 deletions(-) create mode 100644 server/vbv_lernwelt/static/icons/icon-export.svg diff --git a/client/src/pages/dashboard/statistic/AssignmentList.vue b/client/src/pages/dashboard/statistic/AssignmentList.vue index 3d72eff4..6e5b528f 100644 --- a/client/src/pages/dashboard/statistic/AssignmentList.vue +++ b/client/src/pages/dashboard/statistic/AssignmentList.vue @@ -49,7 +49,7 @@ async function exportData() { return; } const filteredItems = statisticFilter.value.getFilteredItems(); - await exportDataAsXls(filteredItems, exportCompetenceElements); + await exportDataAsXls(filteredItems, exportCompetenceElements, userStore.language); } </script> diff --git a/client/src/pages/dashboard/statistic/AttendanceList.vue b/client/src/pages/dashboard/statistic/AttendanceList.vue index c8b57ed1..f5369042 100644 --- a/client/src/pages/dashboard/statistic/AttendanceList.vue +++ b/client/src/pages/dashboard/statistic/AttendanceList.vue @@ -36,7 +36,7 @@ async function exportData() { return; } const filteredItems = statisticFilter.value.getFilteredItems(); - await exportDataAsXls(filteredItems, exportAttendance); + await exportDataAsXls(filteredItems, exportAttendance, userStore.language); } </script> diff --git a/client/src/pages/dashboard/statistic/FeedbackList.vue b/client/src/pages/dashboard/statistic/FeedbackList.vue index 1bacde78..efd01dea 100644 --- a/client/src/pages/dashboard/statistic/FeedbackList.vue +++ b/client/src/pages/dashboard/statistic/FeedbackList.vue @@ -27,7 +27,7 @@ async function exportData() { return; } const filteredItems = statisticFilter.value.getFilteredItems(); - await exportDataAsXls(filteredItems, exportFeedback); + await exportDataAsXls(filteredItems, exportFeedback, userStore.language); } </script> diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index 82bab340..422bf2dc 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -195,21 +195,30 @@ export async function fetchOpenTasksCount(courseId: string) { } export async function exportFeedback( - data: XlsExportRequestData + data: XlsExportRequestData, + language: string ): Promise<XlsExportResponseData> { - return await itPost("/api/dashboard/export/feedback/", data); + return await itPost("/api/dashboard/export/feedback/", data, { + headers: { "Accept-Language": language }, + }); } export async function exportAttendance( - data: XlsExportRequestData + data: XlsExportRequestData, + language: string ): Promise<XlsExportResponseData> { - return await itPost("/api/dashboard/export/attendance/", data); + return await itPost("/api/dashboard/export/attendance/", data, { + headers: { "Accept-Language": language }, + }); } export async function exportCompetenceElements( - data: XlsExportRequestData + data: XlsExportRequestData, + language: string ): Promise<XlsExportResponseData> { - return await itPost("/api/dashboard/export/competence_elements/", data); + return await itPost("/api/dashboard/export/competence_elements/", data, { + headers: { "Accept-Language": language }, + }); } export function courseIdForCourseSlug( diff --git a/client/src/utils/export.ts b/client/src/utils/export.ts index 67069455..bf72e286 100644 --- a/client/src/utils/export.ts +++ b/client/src/utils/export.ts @@ -5,15 +5,16 @@ import type { } from "@/types"; interface exportApiCall { - (data: XlsExportRequestData): Promise<XlsExportResponseData>; + (data: XlsExportRequestData, language: string): Promise<XlsExportResponseData>; } export async function exportDataAsXls( items: StatisticsFilterItem[], - apiCall: exportApiCall + apiCall: exportApiCall, + language: string ) { const itemIds = extractUniqueIds(items); - const data = await apiCall(itemIds); + const data = await apiCall(itemIds, language); openDataAsXls(data.encoded_data, data.file_name); } diff --git a/server/locale/de/LC_MESSAGES/django.po b/server/locale/de/LC_MESSAGES/django.po index abac3a2b..826c9f35 100644 --- a/server/locale/de/LC_MESSAGES/django.po +++ b/server/locale/de/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-06-17 15:43+0200\n" +"POT-Creation-Date: 2024-06-18 15:24+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -22,16 +22,24 @@ msgstr "" msgid "export_kompetenznachweis_elemente" msgstr "" -#: vbv_lernwelt/assignment/export.py:183 +#: vbv_lernwelt/assignment/export.py:142 +msgid "Resultat" +msgstr "" + +#: vbv_lernwelt/assignment/export.py:143 +msgid "bestanden" +msgstr "" + +#: vbv_lernwelt/assignment/export.py:187 msgid "Bestanden" msgstr "" -#: vbv_lernwelt/assignment/export.py:185 +#: vbv_lernwelt/assignment/export.py:189 msgid "Nicht bestanden" msgstr "" -#: vbv_lernwelt/assignment/export.py:199 vbv_lernwelt/assignment/export.py:202 -#: vbv_lernwelt/assignment/export.py:203 +#: vbv_lernwelt/assignment/export.py:203 vbv_lernwelt/assignment/export.py:206 +#: vbv_lernwelt/assignment/export.py:207 msgid "Keine Daten" msgstr "" @@ -111,28 +119,31 @@ msgstr "" msgid "export_anwesenheit" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:113 +#: vbv_lernwelt/course_session/services/export_attendance.py:92 +msgid "Anwesenheit" +msgstr "" + +#: vbv_lernwelt/course_session/services/export_attendance.py:116 msgid "Anwesend" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:113 +#: vbv_lernwelt/course_session/services/export_attendance.py:116 msgid "Nicht anwesend" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:120 +#: vbv_lernwelt/course_session/services/export_attendance.py:123 msgid "Vorname" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:121 +#: vbv_lernwelt/course_session/services/export_attendance.py:124 msgid "Nachname" msgstr "" -#: vbv_lernwelt/course_session/services/export_attendance.py:122 +#: vbv_lernwelt/course_session/services/export_attendance.py:125 msgid "Email" msgstr "Email" -#: vbv_lernwelt/course_session/services/export_attendance.py:123 -#: vbv_lernwelt/course_session/services/export_attendance.py:135 +#: vbv_lernwelt/course_session/services/export_attendance.py:126 msgid "Lehrvertragsnummer" msgstr "" diff --git a/server/locale/fr/LC_MESSAGES/django.mo b/server/locale/fr/LC_MESSAGES/django.mo index 43c07650e89e0c7e53ad428649c6dfa12ffc78e3..4bf66c9da0ddbb21cd9b59cb4283183ae62f6714 100644 GIT binary patch delta 730 zcmY+?&nrYx6u|K_&lqMfejC4%pJijAEEE=_k?b_eLP~=tlWD>mDVy0T+0euvAPcN$ zM464UVWViIBxR$Njg5tb@0ocEZgbz~p7-9p_uMn@y8Nsn{#54N5?a08zp#z|=-@(| zH$?Jr0Sj;i-MEUmxPe8uZO6N)?;T(no}kXZLNDH8F+OMS$K{<t9ydO*8;u;17VN=( zoWLR6!7+TpRvdJSj9>&c;d89U7xdu|YN9@})I^$4H}1kF44~f;7nxz88Sc5V6OK?5 zN+D~=1?q_|kyj;+dXfih##cKoWKtn<9qM}R=)q3pC%s$(7{Cr(!%_B^QwHPcW!5at z;Q*#_9&0&9E3VrfVk7Y#GF3iMPxfu=W!F052GoC1ld7UqxPOg;OT2~)^D-sVY;{2` zmzI+KGYfOTOw(_qlwOeyr4}pcT8+O=H`mH(wMy&<T0#9z$~E>!mrcuzPK)1@9nG5I zSYpVC<OfY_DH03ClE+4a;hr?DSSUJeMw2(rO^17U@nWB8Q)VLMTJ{79GVfZ+dsoWH N<KdEJg%g|ZzF%$NRQdn_ delta 621 zcmY+?Jxjx25Ww+kOdIRFZMD9F)s#-|4jqi>py*T)EV_t=B2p`Yf*p#3qfjimD~KRY z1|1?UegFk0Cvnh^po4?>zqEoLTz`4eCU?(0^<odv@=L@#5~7zJAm7L&c^Tk?7&k;3 z(ZMjzU=ZiA6|?nq9-CPgFoL_N&mCYJ9%DP6p@r8tCQ_C=E=IWVi4)k@ATo{1n8kCn z(J)0am_lu^ggtnIar96NKcN<S#{_<$!)D@itqm=PY7MNQNqt%4LOb6;-B}5><2@Y4 zN`38O3+sE-7d@ix<QXUN6-P12%>~Th5?){)EgqV}A{OxmZR$&kx!O33L%4~$gTuO~ z*w6Y3^;ERk7)iZ<!2tc>y?*d>zrhadCOb(zGTr2F%yn_4W7E+8K$O(te?!O2DKM_u sWc0fxQm3J#7pJh3+sYMItNX^f`(+FoZX#F-RBuDuZrbvU>UTK(18H40R{#J2 diff --git a/server/locale/fr/LC_MESSAGES/django.po b/server/locale/fr/LC_MESSAGES/django.po index 209a4b6e..d338278e 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-06-17 15:43+0200\n" +"POT-Creation-Date: 2024-06-18 15:24+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -22,16 +22,24 @@ msgstr "" msgid "export_kompetenznachweis_elemente" msgstr "export_elements_de_controle" -#: vbv_lernwelt/assignment/export.py:183 +#: vbv_lernwelt/assignment/export.py:142 +msgid "Resultat" +msgstr "Résultats" + +#: vbv_lernwelt/assignment/export.py:143 +msgid "bestanden" +msgstr "réussi" + +#: vbv_lernwelt/assignment/export.py:187 msgid "Bestanden" msgstr "Réussi" -#: vbv_lernwelt/assignment/export.py:185 +#: vbv_lernwelt/assignment/export.py:189 msgid "Nicht bestanden" msgstr "Échoué" -#: vbv_lernwelt/assignment/export.py:199 vbv_lernwelt/assignment/export.py:202 -#: vbv_lernwelt/assignment/export.py:203 +#: vbv_lernwelt/assignment/export.py:203 vbv_lernwelt/assignment/export.py:206 +#: vbv_lernwelt/assignment/export.py:207 msgid "Keine Daten" msgstr "Aucune donnée" @@ -111,28 +119,32 @@ msgstr "" msgid "export_anwesenheit" msgstr "export_presence" -#: vbv_lernwelt/course_session/services/export_attendance.py:113 +#: vbv_lernwelt/course_session/services/export_attendance.py:92 +#| msgid "Anwesend" +msgid "Anwesenheit" +msgstr "Présence" + +#: vbv_lernwelt/course_session/services/export_attendance.py:116 msgid "Anwesend" msgstr "Présent" -#: vbv_lernwelt/course_session/services/export_attendance.py:113 +#: vbv_lernwelt/course_session/services/export_attendance.py:116 msgid "Nicht anwesend" msgstr "Pas présent" -#: vbv_lernwelt/course_session/services/export_attendance.py:120 +#: vbv_lernwelt/course_session/services/export_attendance.py:123 msgid "Vorname" msgstr "Prénom" -#: vbv_lernwelt/course_session/services/export_attendance.py:121 +#: vbv_lernwelt/course_session/services/export_attendance.py:124 msgid "Nachname" msgstr "Nom de famille" -#: vbv_lernwelt/course_session/services/export_attendance.py:122 +#: vbv_lernwelt/course_session/services/export_attendance.py:125 msgid "Email" msgstr "Email" -#: vbv_lernwelt/course_session/services/export_attendance.py:123 -#: vbv_lernwelt/course_session/services/export_attendance.py:135 +#: vbv_lernwelt/course_session/services/export_attendance.py:126 msgid "Lehrvertragsnummer" msgstr "Numéro de contrat d'apprentissage" diff --git a/server/locale/it/LC_MESSAGES/django.mo b/server/locale/it/LC_MESSAGES/django.mo index 0993e981009f21a06a5b10d1af13dc9147f90847..e565176b12302822ffe11e760d3c45cc798eda61 100644 GIT binary patch delta 727 zcmY+?J1j#{7{Kw<tE#PMThuF}sYjR$B$7^TB-lJA7P(EYXmV4nUXfV3btM`MHep~? zv2-yoh>64|9*Y<(5|c>yf9)MO={dji-JW~ycfNZ+{ZE11qu;rsXkB*y#y<LkgMpUT zlyYMRYjFcTn8#||!g}1b<3p5nC+Np>l>4u-0q?L8Un<vg>VruQ7rt-|wJN2;7{M4W z;uId>0={D}PB@jCLlY(83v9zz4B`(;qCv8hL_#PJ9>EZfW6Y6LYKe(tc<icpYFk3~ zP?sntx<-EL-i{xzhxiTU03L=q^r6Jv43a=U%09yw!BOnP3{LWVwZ}wG`i$!sCYwRr zM;))Q9S7KL2%{*miIR98TX56%2*boBl(#5JwbCivzed4TPBvm)xny1>hjO{3l(M;$ zOinFLz7Ze2Spr%IT^=m&^>6DWkP}l_)nxx66_oEJAaOLcZe)#ALPfom*|L$e3R9Zt zo;9*-rlniOGp$?m#Eq<_rxHf0c<bDDxTja;t_{5qcbPS_$@0qLjcZ<WXV=oks&1_m H!k(F55qVIl delta 621 zcmY+>zb^w}7{KwT=h|xhC|6X4M57{17KtB=c0wX)LJXG}q?$-Hmx!SWp$nrR7j}bK zEOId*1_PskT^IfX3?|>_oP;NR?sM<ude8GdZ!P(5RbMRgNQgAqO}>)-<avk-F=>d@ zV-90DgJGP<CbWa~3O2BI(83MW|LtKj9%BMeF^ZQsDpHkOZu)reiFxd*6Pd<EwDAnH zXqX~%IDp#VCU)WprtliI@DpmGH_YG%=Gjb&uC*aM@?#NA>Pwj$?R*XS$yTu5!Cuw} zs2yKmBVGmT2jtuGh!gmX+E{{@r*H&yL)%!vI~>F@x>&*@rl~LY+zeq&A7Gq0w_`SN z9CbnkWFvmFZAiU;!2tc>Gk);8-*^jlkgcR1nI7M7%xWPWn}!xok~+S>p=0J0{N3tV qYQ#yMhK^oxPNlS7a+cj)qwIYdJ%*PFABNnU$f`FST`=7JSm7Hb^fa&l diff --git a/server/locale/it/LC_MESSAGES/django.po b/server/locale/it/LC_MESSAGES/django.po index bd4d61d5..522df647 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-06-17 15:43+0200\n" +"POT-Creation-Date: 2024-06-18 15:24+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -22,16 +22,24 @@ msgstr "" msgid "export_kompetenznachweis_elemente" msgstr "esportazione_elementi_del_controllo" -#: vbv_lernwelt/assignment/export.py:183 +#: vbv_lernwelt/assignment/export.py:142 +msgid "Resultat" +msgstr "Risultato" + +#: vbv_lernwelt/assignment/export.py:143 +msgid "bestanden" +msgstr "superato" + +#: vbv_lernwelt/assignment/export.py:187 msgid "Bestanden" msgstr "Superato" -#: vbv_lernwelt/assignment/export.py:185 +#: vbv_lernwelt/assignment/export.py:189 msgid "Nicht bestanden" msgstr "Fallito" -#: vbv_lernwelt/assignment/export.py:199 vbv_lernwelt/assignment/export.py:202 -#: vbv_lernwelt/assignment/export.py:203 +#: vbv_lernwelt/assignment/export.py:203 vbv_lernwelt/assignment/export.py:206 +#: vbv_lernwelt/assignment/export.py:207 msgid "Keine Daten" msgstr "Nessun dato" @@ -111,28 +119,32 @@ msgstr "" msgid "export_anwesenheit" msgstr "esportazione_presenza" -#: vbv_lernwelt/course_session/services/export_attendance.py:113 +#: vbv_lernwelt/course_session/services/export_attendance.py:92 +#| msgid "Anwesend" +msgid "Anwesenheit" +msgstr "Presenza" + +#: vbv_lernwelt/course_session/services/export_attendance.py:116 msgid "Anwesend" msgstr "Presente" -#: vbv_lernwelt/course_session/services/export_attendance.py:113 +#: vbv_lernwelt/course_session/services/export_attendance.py:116 msgid "Nicht anwesend" msgstr "Non presente" -#: vbv_lernwelt/course_session/services/export_attendance.py:120 +#: vbv_lernwelt/course_session/services/export_attendance.py:123 msgid "Vorname" msgstr "Nome" -#: vbv_lernwelt/course_session/services/export_attendance.py:121 +#: vbv_lernwelt/course_session/services/export_attendance.py:124 msgid "Nachname" msgstr "Cognome" -#: vbv_lernwelt/course_session/services/export_attendance.py:122 +#: vbv_lernwelt/course_session/services/export_attendance.py:125 msgid "Email" msgstr "E-mail" -#: vbv_lernwelt/course_session/services/export_attendance.py:123 -#: vbv_lernwelt/course_session/services/export_attendance.py:135 +#: vbv_lernwelt/course_session/services/export_attendance.py:126 msgid "Lehrvertragsnummer" msgstr "Numero di contratto di tirocinio" diff --git a/server/vbv_lernwelt/assignment/export.py b/server/vbv_lernwelt/assignment/export.py index f8cbc205..230cb661 100644 --- a/server/vbv_lernwelt/assignment/export.py +++ b/server/vbv_lernwelt/assignment/export.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from io import BytesIO import structlog -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ from openpyxl import Workbook from vbv_lernwelt.assignment.models import ( @@ -138,16 +138,20 @@ def _create_sheet( col_prefix = f'Circle "{circle.title}" {cse.learning_content.title}' + # add translation strings here as they are not picked up in f-strings + result_str = str(_("Resultat")) + success_str = str(_("bestanden")) + sheet.cell( row=1, column=col_idx, - value=f"{col_prefix} {_('bestanden')}", + value=f"{col_prefix} {success_str}", ) sheet.cell( row=1, column=col_idx + 1, - value=f"{col_prefix} {_('Resultat')} %", + value=f"{col_prefix} {result_str} %", ) ordered_assignement_ids.append(cse.assignment.id) @@ -180,9 +184,9 @@ def _add_rows( if user_ac: status_text = ( - _("Bestanden") + str(_("Bestanden")) if user_ac.evaluation_passed - else _("Nicht bestanden") + else str(_("Nicht bestanden")) ) sheet.cell(row=row_idx, column=col_idx, value=status_text) try: @@ -196,11 +200,13 @@ def _add_rows( ), ) except (ZeroDivisionError, TypeError): - sheet.cell(row=row_idx, column=col_idx + 1, value=_("Keine Daten")) + sheet.cell( + row=row_idx, column=col_idx + 1, value=str(_("Keine Daten")) + ) else: - sheet.cell(row=row_idx, column=col_idx, value=_("Keine Daten")) - sheet.cell(row=row_idx, column=col_idx + 1, value=_("Keine Daten")) + sheet.cell(row=row_idx, column=col_idx, value=str(_("Keine Daten"))) + sheet.cell(row=row_idx, column=col_idx + 1, value=str(_("Keine Daten"))) col_idx += 2 diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py index 550bf67f..95528bcc 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_completions_export.py @@ -1,5 +1,6 @@ import io +from django.utils.translation import activate from openpyxl import load_workbook from vbv_lernwelt.assignment.export import export_competence_elements @@ -50,7 +51,7 @@ class AssignmentCompletionExportTestCase(ExportBaseTestCase): self.test_student2.additional_json_data = {"Lehrvertragsnummer": 1987654321} self.test_student2.save() - test_student3 = User.objects.get(email="test-student3@example.com") + self.test_student3 = User.objects.get(email="test-student3@example.com") # Bern assignments update_assignment_completion( @@ -102,9 +103,9 @@ class AssignmentCompletionExportTestCase(ExportBaseTestCase): "Keine Daten", ], [ - test_student3.first_name, - test_student3.last_name, - test_student3.email, + self.test_student3.first_name, + self.test_student3.last_name, + self.test_student3.email, None, "Keine Daten", "Keine Daten", @@ -122,15 +123,7 @@ class AssignmentCompletionExportTestCase(ExportBaseTestCase): def _make_header( self, ): - casework_assignment = CourseSessionAssignment.objects.filter( - course_session__id=self.course_session_be.id, - learning_content__content_assignment__competence_certificate__isnull=False, - ).first() - - edoniq_assignment = CourseSessionEdoniqTest.objects.filter( - course_session__id=self.course_session_be.id, - learning_content__content_assignment__competence_certificate__isnull=False, - ).first() + casework_assignment, edoniq_assignment = self._get_assignments() return [ "Vorname", @@ -143,6 +136,19 @@ class AssignmentCompletionExportTestCase(ExportBaseTestCase): f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} Resultat %', ] + def _get_assignments(self): + casework_assignment = CourseSessionAssignment.objects.filter( + course_session__id=self.course_session_be.id, + learning_content__content_assignment__competence_certificate__isnull=False, + ).first() + + edoniq_assignment = CourseSessionEdoniqTest.objects.filter( + course_session__id=self.course_session_be.id, + learning_content__content_assignment__competence_certificate__isnull=False, + ).first() + + return casework_assignment, edoniq_assignment + def test_export_single_cs(self): wb = self._generate_workbook([self.course_session_be.id]) self.assertEqual(len(wb.sheetnames), 1) @@ -212,3 +218,107 @@ class AssignmentCompletionExportTestCase(ExportBaseTestCase): wb.active = wb["Test Zürich 2022 a"] self._check_export(wb, expected_data, 2, 5) + + def test_french_export(self): + activate("fr") + wb = self._generate_workbook([self.course_session_be.id]) + + casework_assignment, edoniq_assignment = self._get_assignments() + + header = [ + "Prénom", + "Nom de famille", + "E-mail", + "Numéro de contrat d'apprentissage", + f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} réussi', + f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} Résultats %', + f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} réussi', + f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} Résultats %', + ] + + expected_data_be = [ + header, + [ + self.test_student1.first_name, + self.test_student1.last_name, + self.test_student1.email, + self.test_student1.additional_json_data["Lehrvertragsnummer"], + "Échoué", + 58, + "Réussi", + 83, + ], + [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + self.test_student2.additional_json_data["Lehrvertragsnummer"], + "Réussi", + 100, + "Aucune donnée", + "Aucune donnée", + ], + [ + self.test_student3.first_name, + self.test_student3.last_name, + self.test_student3.email, + None, + "Aucune donnée", + "Aucune donnée", + "Aucune donnée", + "Aucune donnée", + ], + ] + self._check_export(wb, expected_data_be, 4, 8) + + def test_italian_export(self): + activate("it") + wb = self._generate_workbook([self.course_session_be.id]) + + casework_assignment, edoniq_assignment = self._get_assignments() + + header = [ + "Nome", + "Cognome", + "Email", + "Numero di contratto di tirocinio", + f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} superato', + f'Circle "{self.edoniq_test.get_attached_circle_title()}" {edoniq_assignment.learning_content.title} Risultato %', + f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} superato', + f'Circle "{self.casework.get_attached_circle_title()}" {casework_assignment.learning_content.title} Risultato %', + ] + + expected_data_be = [ + header, + [ + self.test_student1.first_name, + self.test_student1.last_name, + self.test_student1.email, + self.test_student1.additional_json_data["Lehrvertragsnummer"], + "Fallito", + 58, + "Superato", + 83, + ], + [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + self.test_student2.additional_json_data["Lehrvertragsnummer"], + "Superato", + 100, + "Nessun dato", + "Nessun dato", + ], + [ + self.test_student3.first_name, + self.test_student3.last_name, + self.test_student3.email, + None, + "Nessun dato", + "Nessun dato", + "Nessun dato", + "Nessun dato", + ], + ] + self._check_export(wb, expected_data_be, 4, 8) diff --git a/server/vbv_lernwelt/course_session/services/export_attendance.py b/server/vbv_lernwelt/course_session/services/export_attendance.py index 4ce81de1..71587217 100644 --- a/server/vbv_lernwelt/course_session/services/export_attendance.py +++ b/server/vbv_lernwelt/course_session/services/export_attendance.py @@ -4,7 +4,7 @@ from io import BytesIO from itertools import groupby import structlog -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ from openpyxl import Workbook from vbv_lernwelt.course.models import CourseSessionUser @@ -83,15 +83,18 @@ def _create_sheet( # common user headers..., <attendance_course> <date>, status <attendance_course>, .. col_idx = add_user_headers(sheet) attendance_data = {} + for course in attendance_courses: circle = course.get_circle() if circle_ids and circle.id not in circle_ids: continue + presence_str = str(_("Anwesenheit")) # f-strings are not picked up by gettext + sheet.cell( row=1, column=col_idx, - value=f"{_('Anwesenheit')} {circle.title} {course.due_date.start.strftime('%d.%m.%Y')}", + value=f"{presence_str} {circle.title} {course.due_date.start.strftime('%d.%m.%Y')}", ) user_dict_map = {d["user_id"]: d for d in course.attendance_user_list} attendance_data[circle.title] = user_dict_map @@ -110,17 +113,18 @@ def _add_rows(sheet, users: list[CourseSessionUser], attendance_data): for key, user_dict_map in attendance_data.items(): user_dict = user_dict_map.get(str(user.user.id), {}) status = user_dict.get("status", "") if user_dict else "" - status_text = _("Anwesend") if status == "PRESENT" else _("Nicht anwesend") + status_text = ( + str(_("Anwesend")) if status == "PRESENT" else str(_("Nicht anwesend")) + ) sheet.cell(row=row_idx, column=col_idx, value=status_text) col_idx += 1 def add_user_headers(sheet): - # todo: translate headers - sheet.cell(row=1, column=1, value=_("Vorname")) - sheet.cell(row=1, column=2, value=_("Nachname")) - sheet.cell(row=1, column=3, value=_("Email")) - sheet.cell(row=1, column=4, value=_("Lehrvertragsnummer")) + sheet.cell(row=1, column=1, value=str(_("Vorname"))) + sheet.cell(row=1, column=2, value=str(_("Nachname"))) + sheet.cell(row=1, column=3, value=str(_("Email"))) + sheet.cell(row=1, column=4, value=str(_("Lehrvertragsnummer"))) return 5 # return the next column index @@ -132,7 +136,7 @@ def add_user_export_data(sheet, user: CourseSessionUser, row_idx: int) -> int: sheet.cell( row=row_idx, column=4, - value=user.user.additional_json_data.get(_("Lehrvertragsnummer"), ""), + value=user.user.additional_json_data.get("Lehrvertragsnummer", ""), ) return 5 # return the next column index diff --git a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py index 242febc3..350ffaa4 100644 --- a/server/vbv_lernwelt/course_session/tests/test_attendance_export.py +++ b/server/vbv_lernwelt/course_session/tests/test_attendance_export.py @@ -1,6 +1,7 @@ import io from django.test import TestCase +from django.utils.translation import activate, deactivate from openpyxl import load_workbook from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID @@ -19,6 +20,10 @@ class ExportBaseTestCase(TestCase): cell.value, expected_data[row[0].row - 1][row.index(cell)] ) + def tearDown(self): + # Deactivate the language after the test + deactivate() + class AttendanceExportTestCase(ExportBaseTestCase): def setUp(self): @@ -41,7 +46,7 @@ class AttendanceExportTestCase(ExportBaseTestCase): self.test_student2.additional_json_data = {"Lehrvertragsnummer": 1987654321} self.test_student2.save() - test_student3 = User.objects.get(email="test-student3@example.com") + self.test_student3 = User.objects.get(email="test-student3@example.com") self.attendance_course_be.attendance_user_list = [ { "email": self.test_student1.email, @@ -69,9 +74,9 @@ class AttendanceExportTestCase(ExportBaseTestCase): "Nicht anwesend", ], [ - test_student3.first_name, - test_student3.last_name, - test_student3.email, + self.test_student3.first_name, + self.test_student3.last_name, + self.test_student3.email, None, "Nicht anwesend", ], @@ -135,3 +140,79 @@ class AttendanceExportTestCase(ExportBaseTestCase): wb.active = wb["Test Zürich 2022 a"] self._check_export(wb, expected_data_zh, 2, 5) + + def test_french_export(self): + activate("fr") + wb = self._generate_workbook([self.course_session_be.id]) + + header = [ + "Prénom", + "Nom de famille", + "E-mail", + "Numéro de contrat d'apprentissage", + f"Présence {self.attendance_course_be.get_circle().title} {self.attendance_course_be.due_date.start.strftime('%d.%m.%Y')}", + ] + + expected_data_be = [ + header, + [ + self.test_student1.first_name, + self.test_student1.last_name, + self.test_student1.email, + self.test_student1.additional_json_data["Lehrvertragsnummer"], + "Présent", + ], + [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + self.test_student2.additional_json_data["Lehrvertragsnummer"], + "Pas présent", + ], + [ + self.test_student3.first_name, + self.test_student3.last_name, + self.test_student3.email, + None, + "Pas présent", + ], + ] + self._check_export(wb, expected_data_be, 4, 5) + + def test_italian_export(self): + activate("it") + wb = self._generate_workbook([self.course_session_be.id]) + + header = [ + "Nome", + "Cognome", + "Email", + "Numero di contratto di tirocinio", + f"Presenza {self.attendance_course_be.get_circle().title} {self.attendance_course_be.due_date.start.strftime('%d.%m.%Y')}", + ] + + expected_data_be = [ + header, + [ + self.test_student1.first_name, + self.test_student1.last_name, + self.test_student1.email, + self.test_student1.additional_json_data["Lehrvertragsnummer"], + "Presente", + ], + [ + self.test_student2.first_name, + self.test_student2.last_name, + self.test_student2.email, + self.test_student2.additional_json_data["Lehrvertragsnummer"], + "Non presente", + ], + [ + self.test_student3.first_name, + self.test_student3.last_name, + self.test_student3.email, + None, + "Non presente", + ], + ] + self._check_export(wb, expected_data_be, 4, 5) diff --git a/server/vbv_lernwelt/feedback/export.py b/server/vbv_lernwelt/feedback/export.py index 2dab9cb6..879906a7 100644 --- a/server/vbv_lernwelt/feedback/export.py +++ b/server/vbv_lernwelt/feedback/export.py @@ -5,7 +5,7 @@ from typing import List, Tuple import structlog from django.db.models import QuerySet -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ from openpyxl import Workbook from vbv_lernwelt.course_session.services.export_attendance import ( @@ -147,11 +147,11 @@ def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]): ) # add header - sheet.cell(row=1, column=1, value=_("Durchführung")) - sheet.cell(row=1, column=2, value=_("Datum")) + sheet.cell(row=1, column=1, value=str(_("Durchführung"))) + sheet.cell(row=1, column=2, value=str(_("Datum"))) questions = [q[1] for q in question_data] for col_idx, title in enumerate(questions, start=3): - sheet.cell(row=1, column=col_idx, value=title) + sheet.cell(row=1, column=col_idx, value=str(title)) _add_rows(sheet, data, question_data) return sheet diff --git a/server/vbv_lernwelt/feedback/tests/test_feedback_export.py b/server/vbv_lernwelt/feedback/tests/test_feedback_export.py index c24a7f93..d9785dac 100644 --- a/server/vbv_lernwelt/feedback/tests/test_feedback_export.py +++ b/server/vbv_lernwelt/feedback/tests/test_feedback_export.py @@ -1,6 +1,7 @@ import datetime import io +from django.utils.translation import activate from openpyxl import load_workbook from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USER_ID @@ -201,3 +202,53 @@ class FeedbackExportTestCase(ExportBaseTestCase): 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/static/icons/icon-export.svg b/server/vbv_lernwelt/static/icons/icon-export.svg new file mode 100644 index 00000000..9e90e74d --- /dev/null +++ b/server/vbv_lernwelt/static/icons/icon-export.svg @@ -0,0 +1,4 @@ +<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M14.0002 3.57463C13.8229 3.57463 13.6456 3.63996 13.5056 3.77996L6.55224 10.7333L7.54158 11.7226L13.3002 5.96396L13.3002 20.8226L14.7002 20.8226L14.7002 5.96396L20.4589 11.7226L21.4482 10.7333L14.4949 3.77996C14.3549 3.63996 14.1776 3.57463 14.0002 3.57463V3.57463Z" fill="#00224D"/> +<path d="M23.3802 23.184H4.61084V24.584H23.3802V23.184Z" fill="#00224D"/> +</svg> From de6b659ea7d47b9cff482358cc3b4681dccf78db Mon Sep 17 00:00:00 2001 From: Daniel Egger <daniel.egger@gmail.com> Date: Wed, 19 Jun 2024 16:24:37 +0200 Subject: [PATCH 24/24] Try to fix flaky cypress test --- client/src/pages/dashboard/DashboardDueDatesPage.vue | 2 +- cypress/e2e/dueDates.cy.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/pages/dashboard/DashboardDueDatesPage.vue b/client/src/pages/dashboard/DashboardDueDatesPage.vue index 34c5e05d..c8fdf2e6 100644 --- a/client/src/pages/dashboard/DashboardDueDatesPage.vue +++ b/client/src/pages/dashboard/DashboardDueDatesPage.vue @@ -218,7 +218,7 @@ watch(selectedCourse, async () => { <span class="inline">{{ $t("general.back") }}</span> </router-link> - <h2 class="my-4">{{ $t("a.Termine") }}</h2> + <h2 class="my-4" data-cy="title">{{ $t("a.Termine") }}</h2> <div class="bg-white px-4 py-2"> <section v-if="filtersVisible" diff --git a/cypress/e2e/dueDates.cy.js b/cypress/e2e/dueDates.cy.js index 7da34778..8939158b 100644 --- a/cypress/e2e/dueDates.cy.js +++ b/cypress/e2e/dueDates.cy.js @@ -3,6 +3,7 @@ import { login } from "./helpers"; function selectDropboxItem(dropboxSelector, item) { cy.get(dropboxSelector).click(); cy.get(dropboxSelector).contains(item).click(); + cy.get('[data-cy="title"]').should("contain", "Termine"); } describe("dueDates.cy.js", () => {