Merged develop into feature/mf-exam-course

This commit is contained in:
Christian Cueni 2024-02-28 16:27:20 +00:00
commit 72e87c345a
5 changed files with 175 additions and 0 deletions

View File

@ -1,8 +1,15 @@
from django.contrib import admin
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
from vbv_lernwelt.feedback.services import (
get_feedbacks_for_course_sessions,
get_feedbacks_for_courses,
)
from vbv_lernwelt.learnpath.models import Circle
get_feedbacks_for_course_sessions.short_description = "Feedback export"
get_feedbacks_for_courses.short_description = "Feedback export"
@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
@ -12,6 +19,7 @@ class CourseAdmin(admin.ModelAdmin):
"category_name",
"slug",
]
actions = [get_feedbacks_for_courses]
@admin.register(CourseSession)
@ -26,6 +34,7 @@ class CourseSessionAdmin(admin.ModelAdmin):
"created_at",
"updated_at",
]
actions = [get_feedbacks_for_course_sessions]
@admin.register(CourseSessionUser)

View File

@ -0,0 +1,18 @@
import djclick as click
import structlog
from vbv_lernwelt.feedback.services import export_feedback
logger = structlog.get_logger(__name__)
@click.command()
@click.argument("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)

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
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,47 @@ 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?"),
]
UK_FEEDBACK_QUESTIONS = [
("satisfaction", "Zufriedenheit insgesamt"),
("goal_attainment", "Zielerreichung insgesamt"),
(
"proficiency",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?",
),
(
"preparation_task_clarity",
"Waren die Vorbereitungsaufträge klar und verständlich?",
),
(
"instructor_competence",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?",
),
(
"instructor_respect",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?",
),
(
"instructor_open_feedback",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?",
),
("would_recommend", "Würdest du den Kurs 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 +147,104 @@ def initial_data_for_feedback_page(
"feedback_type": "vv",
}
return {}
def export_feedback(course_session_ids: list[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__in=course_session_ids,
submitted=True,
).order_by("circle", "course_session", "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_ids": course_session_ids,
"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)
if len(data) == 0:
return sheet
# we instruct the users not to mix exports of different courses, so we can assume the questions are the same and of the first type
question_data = (
UK_FEEDBACK_QUESTIONS
if data[0].data["feedback_type"] == "uk"
else VV_FEEDBACK_QUESTIONS
)
# add header
sheet.cell(row=1, column=1, value="Durchführung")
sheet.cell(row=1, column=2, value="Datum")
questions = [q[1] for q in question_data]
for col_idx, title in enumerate(questions, start=3):
sheet.cell(row=1, column=col_idx, value=title)
_add_rows(sheet, data, question_data)
return sheet
def _add_rows(sheet, data, question_data):
for row_idx, feedback in enumerate(data, start=2):
sheet.cell(row=row_idx, column=1, value=feedback.course_session.title)
sheet.cell(
row=row_idx, column=2, value=feedback.updated_at.date().strftime("%d.%m.%Y")
)
for col_idx, question in enumerate(question_data, start=3):
response = feedback.data.get(question[0], "")
sheet.cell(row=row_idx, column=col_idx, value=response)
def make_export_filename(name: str = "feedback_export"):
today_date = datetime.today().strftime("%Y-%m-%d")
return f"{name}_{today_date}.xlsx"
# used as admin action, that's why it's not in the views.py
def get_feedbacks_for_course_sessions(_modeladmin, _request, queryset):
file_name = "feedback_export_durchfuehrungen"
return _handle_feedback_export_action(queryset, file_name)
def get_feedbacks_for_courses(_modeladmin, _request, queryset):
course_sessions = CourseSession.objects.filter(course__in=queryset)
file_name = "feedback_export_lehrgaenge"
return _handle_feedback_export_action(course_sessions, file_name)
def _handle_feedback_export_action(course_seesions, file_name):
excel_bytes = export_feedback(course_seesions, False)
response = HttpResponse(
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
response[
"Content-Disposition"
] = f"attachment; filename={make_export_filename(file_name)}"
response.write(excel_bytes)
return response