269 lines
8.5 KiB
Python
269 lines
8.5 KiB
Python
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
|
|
from vbv_lernwelt.course.services import mark_course_completion
|
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
|
from vbv_lernwelt.learnpath.models import (
|
|
LearningContentFeedbackUK,
|
|
LearningContentFeedbackVV,
|
|
)
|
|
|
|
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,
|
|
course_session: CourseSession,
|
|
learning_content_feedback_page: Union[
|
|
LearningContentFeedbackUK, LearningContentFeedbackVV
|
|
],
|
|
submitted: bool,
|
|
validated_data: dict,
|
|
):
|
|
circle = learning_content_feedback_page.get_circle()
|
|
|
|
feedback_response, _ = FeedbackResponse.objects.get_or_create(
|
|
feedback_user_id=feedback_user.id,
|
|
circle_id=circle.id,
|
|
course_session=course_session,
|
|
)
|
|
|
|
original_data = feedback_response.data
|
|
updated_data = validated_data
|
|
initial_data = initial_data_for_feedback_page(learning_content_feedback_page)
|
|
|
|
merged_data = initial_data | {
|
|
key: updated_data[key]
|
|
if updated_data.get(key, "") != ""
|
|
else original_data.get(key)
|
|
for key in initial_data.keys()
|
|
}
|
|
|
|
feedback_response.data = merged_data
|
|
|
|
# save the response before completion mark,
|
|
# because who knows what could happen in between...
|
|
if submitted:
|
|
feedback_response.submitted = submitted
|
|
feedback_response.save()
|
|
|
|
if submitted:
|
|
mark_course_completion(
|
|
user=feedback_user,
|
|
page=learning_content_feedback_page,
|
|
course_session=course_session,
|
|
completion_status=CourseCompletionStatus.SUCCESS.value,
|
|
)
|
|
|
|
logger.info(
|
|
"feedback successfully created",
|
|
label="feedback",
|
|
feedback_user_id=feedback_user.id,
|
|
circle_title=circle.title,
|
|
course_session_id=course_session.id,
|
|
)
|
|
|
|
return feedback_response
|
|
|
|
|
|
def initial_data_for_feedback_page(
|
|
learning_content_feedback_page: Union[
|
|
LearningContentFeedbackUK, LearningContentFeedbackVV
|
|
]
|
|
):
|
|
if hasattr(learning_content_feedback_page, "learningcontentfeedbackuk"):
|
|
return {
|
|
"satisfaction": None,
|
|
"goal_attainment": None,
|
|
"proficiency": None,
|
|
"preparation_task_clarity": None,
|
|
"instructor_competence": None,
|
|
"instructor_respect": None,
|
|
"instructor_open_feedback": "",
|
|
"would_recommend": None,
|
|
"course_negative_feedback": "",
|
|
"course_positive_feedback": "",
|
|
"feedback_type": "uk",
|
|
}
|
|
if hasattr(learning_content_feedback_page, "learningcontentfeedbackvv"):
|
|
return {
|
|
"satisfaction": None,
|
|
"goal_attainment": None,
|
|
"proficiency": None,
|
|
"preparation_task_clarity": None,
|
|
"would_recommend": None,
|
|
"course_negative_feedback": "",
|
|
"course_positive_feedback": "",
|
|
"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=_sanitize_sheet_name(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 _sanitize_sheet_name(text, default_name="DefaultSheet"):
|
|
if text is None:
|
|
return default_name
|
|
|
|
prohibited_chars = ["\\", "/", "*", "?", ":", "[", "]"]
|
|
for char in prohibited_chars:
|
|
text = text.replace(char, "")
|
|
|
|
text = text.strip("'")
|
|
|
|
text = text[:31]
|
|
|
|
if len(text) == 0:
|
|
return default_name
|
|
|
|
return text
|
|
|
|
|
|
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
|