diff --git a/server/config/urls.py b/server/config/urls.py index 1f2b36fa..2d96e5cb 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -10,6 +10,9 @@ from django.views import defaults as default_views from django.views.decorators.csrf import csrf_exempt from django_ratelimit.exceptions import Ratelimited from graphene_django.views import GraphQLView +from wagtail import urls as wagtail_urls +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.documents import urls as media_library_urls from vbv_lernwelt.api.directory import list_entities from vbv_lernwelt.api.user import ( @@ -52,6 +55,7 @@ from vbv_lernwelt.edoniq_test.views import ( from vbv_lernwelt.feedback.views import ( get_expert_feedbacks_for_course, get_feedback_for_circle, + export_feedback_for_course_session, ) from vbv_lernwelt.files.views import presign from vbv_lernwelt.importer.views import ( @@ -61,9 +65,6 @@ from vbv_lernwelt.importer.views import ( ) from vbv_lernwelt.media_files.views import user_image from vbv_lernwelt.notify.views import email_notification_settings -from wagtail import urls as wagtail_urls -from wagtail.admin import urls as wagtailadmin_urls -from wagtail.documents import urls as media_library_urls class SignedIntConverter(IntConverter): @@ -171,6 +172,9 @@ urlpatterns = [ name='storage_presign'), # feedback + path(r'api/core/feedback/export//', + export_feedback_for_course_session, + name='export_feedback_for_course_session'), path(r'api/core/feedback//summary/', get_expert_feedbacks_for_course, name='feedback_summary'), diff --git a/server/vbv_lernwelt/feedback/management/commands/export_feedback.py b/server/vbv_lernwelt/feedback/management/commands/export_feedback.py index 011275f3..8864f956 100644 --- a/server/vbv_lernwelt/feedback/management/commands/export_feedback.py +++ b/server/vbv_lernwelt/feedback/management/commands/export_feedback.py @@ -1,4 +1,5 @@ from datetime import datetime +from io import BytesIO from itertools import groupby from operator import attrgetter @@ -28,32 +29,48 @@ logger = structlog.get_logger(__name__) @click.command() @click.argument("course_session_id") -def command(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_feedback(course_session_id, save_as_file) + + +def export_feedback(course_session_id: str, save_as_file: bool): wb = Workbook() + + # remove the first sheet is just easier than keeping track of the active sheet wb.remove_sheet(wb.active) - # Create another sheet + feedbacks = FeedbackResponse.objects.filter( course_session_id=course_session_id ).order_by("circle") 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.title, "course_session_id": course_session_id, - "count": sum(1 for _ in group_feedbacks), + "count": len(group_feedbacks), }, label="feedback_export", ) create_sheet(wb, circle.title, group_feedbacks) - today_date = datetime.today().strftime("%Y-%m-%d") + if save_as_file: + wb.save(make_export_filename()) + else: + output = BytesIO() + wb.save(output) - # Set the filename with today's date - filename = f"feedback_export_{today_date}.xlsx" - wb.save("example.xlsx") + output.seek(0) + return output.getvalue() def create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]): @@ -72,3 +89,8 @@ def add_rows(sheet, data): for col_idx, question in enumerate(VV_FEEDBACK_QUESTIONS, start=2): response = feedback.data.get(question[0], "") sheet.cell(row=row_idx, column=col_idx, value=response) + + +def make_export_filename(): + today_date = datetime.today().strftime("%Y-%m-%d") + return f"feedback_export_{today_date}.xlsx" diff --git a/server/vbv_lernwelt/feedback/views.py b/server/vbv_lernwelt/feedback/views.py index ecd11483..13dbe37c 100644 --- a/server/vbv_lernwelt/feedback/views.py +++ b/server/vbv_lernwelt/feedback/views.py @@ -1,10 +1,14 @@ import itertools import structlog -from rest_framework.decorators import api_view +from django.http import HttpResponse +from rest_framework import authentication +from rest_framework.decorators import api_view, authentication_classes, permission_classes from rest_framework.exceptions import PermissionDenied +from rest_framework.permissions import IsAdminUser from rest_framework.response import Response +from vbv_lernwelt.feedback.management.commands.export_feedback import export_feedback, make_export_filename from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.utils import feedback_users from vbv_lernwelt.iam.permissions import is_course_session_expert @@ -78,3 +82,16 @@ def get_feedback_for_circle(request, course_session_id, circle_id): feedback_data["questions"][field].append(data) return Response(status=200, data=feedback_data) + + +@api_view(["GET"]) +@authentication_classes((authentication.SessionAuthentication,)) +@permission_classes((IsAdminUser,)) +def export_feedback_for_course_session(request, course_session_id): + excel_bytes = export_feedback(course_session_id, False) + + response = HttpResponse(content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + response['Content-Disposition'] = f"attachment; filename={make_export_filename()}" + response.write(excel_bytes) + + return response