Merged develop into fix/floating
This commit is contained in:
commit
be6fcfa779
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 d’assurance 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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue