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