Merged develop into fix/floating

This commit is contained in:
Christian Cueni 2024-02-29 06:13:12 +00:00
commit be6fcfa779
10 changed files with 257 additions and 2 deletions

View File

@ -24,5 +24,8 @@ fi
# Create Prüfungslehrgang
python /app/manage.py create_vermittler_pruefung
# Create Motorfahrzeug Prüfungslehrgang
python /app/manage.py create_motorfahrzeug_pruefung
# Set the command to run supervisord
/home/django/.local/bin/supervisord -c /app/supervisord.conf

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

@ -9,3 +9,4 @@ COURSE_UK_TRAINING_IT = -9
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID = -12
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID = -13

View File

@ -49,6 +49,7 @@ from vbv_lernwelt.core.constants import TEST_MENTOR1_USER_ID
from vbv_lernwelt.core.create_default_users import default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.consts import (
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
COURSE_TEST_ID,
COURSE_UK,
COURSE_UK_FR,
@ -95,6 +96,7 @@ from vbv_lernwelt.importer.services import (
)
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
create_vv_motorfahrzeug_pruefung_learning_path,
create_vv_new_learning_path,
create_vv_pruefung_learning_path,
)
@ -309,6 +311,34 @@ def create_versicherungsvermittlerin_pruefung_course(
create_vv_pruefung_learning_path(course_id=course_id)
def create_motorfahrzeug_pruefung_course(
course_id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID, language="de"
):
names = {
"de": "Motorfahrzeug Versicherungsvermittler/-in VBV Prüfung",
"fr": "Véhicules à moteur Intermédiaire dassurance AFA Examen",
"it": "Veicolo a motore Intermediario/a assicurativo/a AFA Esame",
}
# Versicherungsvermittler/in mit neuen Circles
course = create_versicherungsvermittlerin_with_categories(
course_id=course_id,
title=names[language],
)
# assignments create assignments parent page
_assignment_list_page = AssignmentListPageFactory(
parent=course.coursepage,
)
create_vv_new_competence_profile(course_id=course_id)
create_default_media_library(course_id=course_id)
create_vv_reflection(course_id=course_id)
CourseSession.objects.create(course_id=course_id, title=names[language])
create_vv_motorfahrzeug_pruefung_learning_path(course_id=course_id)
def create_course_uk_de(course_id=COURSE_UK, lang="de"):
names = {
"de": "Überbetriebliche Kurse",

View File

@ -0,0 +1,23 @@
import djclick as click
from vbv_lernwelt.course.consts import COURSE_MOTORFAHRZEUG_PRUEFUNG_ID
from vbv_lernwelt.course.management.commands.create_default_courses import (
create_motorfahrzeug_pruefung_course,
)
from vbv_lernwelt.course.models import Course
ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
@click.command()
def command():
print(
"Creating Motorfahrzeug Vermittler Prüfung course",
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
)
if Course.objects.filter(id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID).exists():
print("Course already exists, skipping")
return
create_motorfahrzeug_pruefung_course()

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

View File

@ -12,7 +12,11 @@ from vbv_lernwelt.competence.factories import (
)
from vbv_lernwelt.competence.models import ActionCompetence
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.consts import (
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID,
)
from vbv_lernwelt.course.models import CourseCategory, CoursePage
from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
@ -89,7 +93,7 @@ def create_vv_new_learning_path(
def create_vv_pruefung_learning_path(
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, user=None
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID, user=None
):
if user is None:
user = User.objects.get(username="info@iterativ.ch")
@ -108,6 +112,25 @@ def create_vv_pruefung_learning_path(
Page.objects.update(owner=user)
def create_vv_motorfahrzeug_pruefung_learning_path(
course_id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID, user=None
):
if user is None:
user = User.objects.get(username="info@iterativ.ch")
course_page = CoursePage.objects.get(course_id=course_id)
lp = LearningPathFactory(
title="Lernpfad",
parent=course_page,
)
TopicFactory(title="Fahrzeug", parent=lp)
create_circle_fahrzeug(lp, course_page=course_page)
# all pages belong to 'admin' by default
Page.objects.update(owner=user)
def create_circle_basis(lp, title="Basis", course_page=None):
circle = CircleFactory(
title=title,