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//mentees/", get_mentee_count, name="get_mentee_count"), path(r"api/dashboard/course//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):