Move code to service, add admin action

This commit is contained in:
Christian Cueni 2024-02-28 11:40:10 +01:00
parent 603c0544c2
commit 798c159ee7
5 changed files with 103 additions and 104 deletions

View File

@ -10,9 +10,6 @@ from django.views import defaults as default_views
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django_ratelimit.exceptions import Ratelimited from django_ratelimit.exceptions import Ratelimited
from graphene_django.views import GraphQLView 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.directory import list_entities
from vbv_lernwelt.api.user import ( from vbv_lernwelt.api.user import (
@ -55,7 +52,6 @@ from vbv_lernwelt.edoniq_test.views import (
from vbv_lernwelt.feedback.views import ( from vbv_lernwelt.feedback.views import (
get_expert_feedbacks_for_course, get_expert_feedbacks_for_course,
get_feedback_for_circle, get_feedback_for_circle,
export_feedback_for_course_session,
) )
from vbv_lernwelt.files.views import presign from vbv_lernwelt.files.views import presign
from vbv_lernwelt.importer.views import ( 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.media_files.views import user_image
from vbv_lernwelt.notify.views import email_notification_settings 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): class SignedIntConverter(IntConverter):
@ -172,9 +171,6 @@ urlpatterns = [
name='storage_presign'), name='storage_presign'),
# feedback # feedback
path(r'api/core/feedback/export/<str:course_session_id>/',
export_feedback_for_course_session,
name='export_feedback_for_course_session'),
path(r'api/core/feedback/<str:course_session_id>/summary/', path(r'api/core/feedback/<str:course_session_id>/summary/',
get_expert_feedbacks_for_course, get_expert_feedbacks_for_course,
name='feedback_summary'), name='feedback_summary'),

View File

@ -1,8 +1,11 @@
from django.contrib import admin from django.contrib import admin
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser 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 from vbv_lernwelt.learnpath.models import Circle
get_feedbacks_for_course.short_description = "Feedback export"
@admin.register(Course) @admin.register(Course)
class CourseAdmin(admin.ModelAdmin): class CourseAdmin(admin.ModelAdmin):
@ -26,6 +29,7 @@ class CourseSessionAdmin(admin.ModelAdmin):
"created_at", "created_at",
"updated_at", "updated_at",
] ]
actions = [get_feedbacks_for_course]
@admin.register(CourseSessionUser) @admin.register(CourseSessionUser)

View File

@ -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 djclick as click
import structlog import structlog
from openpyxl import Workbook
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.services import export_feedback
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?"),
]
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -37,60 +16,3 @@ logger = structlog.get_logger(__name__)
def command(course_session_id, 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 # 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) 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"

View File

@ -1,6 +1,12 @@
from datetime import datetime
from io import BytesIO
from itertools import groupby
from operator import attrgetter
from typing import Union from typing import Union
import structlog import structlog
from django.http import HttpResponse
from openpyxl import Workbook
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
@ -13,6 +19,19 @@ from vbv_lernwelt.learnpath.models import (
logger = structlog.get_logger(__name__) 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( def update_feedback_response(
feedback_user: User, feedback_user: User,
@ -100,3 +119,78 @@ def initial_data_for_feedback_page(
"feedback_type": "vv", "feedback_type": "vv",
} }
return {} 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

View File

@ -1,14 +1,10 @@
import itertools import itertools
import structlog import structlog
from django.http import HttpResponse from rest_framework.decorators import api_view
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.exceptions import PermissionDenied
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response 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.models import FeedbackResponse
from vbv_lernwelt.feedback.utils import feedback_users from vbv_lernwelt.feedback.utils import feedback_users
from vbv_lernwelt.iam.permissions import is_course_session_expert 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) feedback_data["questions"][field].append(data)
return Response(status=200, data=feedback_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