From 798c159ee75050974602cc5489a6252d047b38b8 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 28 Feb 2024 11:40:10 +0100 Subject: [PATCH] Move code to service, add admin action --- server/config/urls.py | 10 +- server/vbv_lernwelt/course/admin.py | 4 + .../management/commands/export_feedback.py | 80 +--------------- server/vbv_lernwelt/feedback/services.py | 94 +++++++++++++++++++ server/vbv_lernwelt/feedback/views.py | 19 +--- 5 files changed, 103 insertions(+), 104 deletions(-) diff --git a/server/config/urls.py b/server/config/urls.py index 2d96e5cb..1f2b36fa 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -10,9 +10,6 @@ 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 ( @@ -55,7 +52,6 @@ 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 ( @@ -65,6 +61,9 @@ 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): @@ -172,9 +171,6 @@ 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/course/admin.py b/server/vbv_lernwelt/course/admin.py index 581266a9..1285f3e8 100644 --- a/server/vbv_lernwelt/course/admin.py +++ b/server/vbv_lernwelt/course/admin.py @@ -1,8 +1,11 @@ from django.contrib import admin from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser +from vbv_lernwelt.feedback.services import get_feedbacks_for_course from vbv_lernwelt.learnpath.models import Circle +get_feedbacks_for_course.short_description = "Feedback export" + @admin.register(Course) class CourseAdmin(admin.ModelAdmin): @@ -26,6 +29,7 @@ class CourseSessionAdmin(admin.ModelAdmin): "created_at", "updated_at", ] + actions = [get_feedbacks_for_course] @admin.register(CourseSessionUser) diff --git a/server/vbv_lernwelt/feedback/management/commands/export_feedback.py b/server/vbv_lernwelt/feedback/management/commands/export_feedback.py index 8864f956..f0cae06b 100644 --- a/server/vbv_lernwelt/feedback/management/commands/export_feedback.py +++ b/server/vbv_lernwelt/feedback/management/commands/export_feedback.py @@ -1,28 +1,7 @@ -from datetime import datetime -from io import BytesIO -from itertools import groupby -from operator import attrgetter - import djclick as click import structlog -from openpyxl import Workbook -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?"), -] +from vbv_lernwelt.feedback.services import export_feedback logger = structlog.get_logger(__name__) @@ -37,60 +16,3 @@ 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_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) - - 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": len(group_feedbacks), - }, - label="feedback_export", - ) - create_sheet(wb, circle.title, group_feedbacks) - - 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, data: list[FeedbackResponse]): - sheet = wb.create_sheet(title=title) - # add header - questions = [q[1] for q in VV_FEEDBACK_QUESTIONS] - for col_idx, title in enumerate(questions, start=2): - sheet.cell(row=1, column=col_idx, value=title) - - add_rows(sheet, data) - return sheet - - -def add_rows(sheet, data): - for row_idx, feedback in enumerate(data, start=2): - 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/services.py b/server/vbv_lernwelt/feedback/services.py index 6d6979f7..265139d1 100644 --- a/server/vbv_lernwelt/feedback/services.py +++ b/server/vbv_lernwelt/feedback/services.py @@ -1,6 +1,12 @@ +from datetime import datetime +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 @@ -13,6 +19,19 @@ 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?"), +] + def update_feedback_response( feedback_user: User, @@ -100,3 +119,78 @@ def initial_data_for_feedback_page( "feedback_type": "vv", } return {} + + +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) + + feedbacks = FeedbackResponse.objects.filter( + course_session_id=course_session_id, + submitted=True, + ).order_by("circle", "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_id": course_session_id, + "count": len(group_feedbacks), + }, + label="feedback_export", + ) + _create_sheet(wb, circle.title, group_feedbacks) + + 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, data: list[FeedbackResponse]): + sheet = wb.create_sheet(title=title) + # add header + sheet.cell(row=1, column=1, value="Datum") + questions = [q[1] for q in VV_FEEDBACK_QUESTIONS] + for col_idx, title in enumerate(questions, start=2): + sheet.cell(row=1, column=col_idx, value=title) + + _add_rows(sheet, data) + return sheet + + +def _add_rows(sheet, data): + for row_idx, feedback in enumerate(data, start=2): + sheet.cell( + row=row_idx, column=1, value=feedback.updated_at.date().strftime("%d.%m.%Y") + ) + 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" + + +# used as admin action, that's why it's not in the views.py +def get_feedbacks_for_course(_modeladmin, _request, queryset): + excel_bytes = export_feedback(queryset.first(), 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 diff --git a/server/vbv_lernwelt/feedback/views.py b/server/vbv_lernwelt/feedback/views.py index 13dbe37c..ecd11483 100644 --- a/server/vbv_lernwelt/feedback/views.py +++ b/server/vbv_lernwelt/feedback/views.py @@ -1,14 +1,10 @@ import itertools import structlog -from django.http import HttpResponse -from rest_framework import authentication -from rest_framework.decorators import api_view, authentication_classes, permission_classes +from rest_framework.decorators import api_view 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 @@ -82,16 +78,3 @@ 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