Merge branch 'develop' into feat/course-feature-toggles

This commit is contained in:
Livio Bieri 2024-02-29 09:47:48 +01:00
commit e64bab918e
17 changed files with 644 additions and 6 deletions

View File

@ -549,8 +549,6 @@ type AssignmentCompletionObjectType {
submitted_at: DateTime
evaluation_submitted_at: DateTime
evaluation_user: UserObjectType
evaluation_points: Float
evaluation_max_points: Float
evaluation_passed: Boolean
edoniq_extended_time_flag: Boolean!
assignment_user: UserObjectType!
@ -561,6 +559,8 @@ type AssignmentCompletionObjectType {
additional_json_data: JSONString!
task_completion_data: GenericScalar
learning_content_page_id: ID
evaluation_points: Float
evaluation_max_points: Float
}
"""

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

@ -737,6 +737,7 @@ CONSTANCE_CONFIG = {
"Default value is empty and will not send any emails. (No regex support!)",
),
}
TRACKING_TAG = env("IT_TRACKING_TAG", default="")
if APP_ENVIRONMENT == "local":
# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development

View File

@ -25,6 +25,7 @@ from vbv_lernwelt.core.views import (
check_rate_limit,
cypress_reset_view,
generate_web_component_icons,
iterativ_test_coursesessions_reset_view,
permission_denied_view,
rate_limit_exceeded_view,
vue_home,
@ -209,6 +210,13 @@ urlpatterns = [
name="t2l_sync",
),
# iterativ Test course sessions
path(
r"api/core/resetiterativsessions/",
iterativ_test_coursesessions_reset_view,
name="iterativ_test_coursesessions_reset_view",
),
path("server/graphql/",
csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
# testing and debug

View File

@ -17,6 +17,10 @@ class AssignmentCompletionObjectType(DjangoObjectType):
task_completion_data = GenericScalar()
learning_content_page_id = graphene.ID(source="learning_content_page_id")
# rounded to sensible representation
evaluation_points = graphene.Float()
evaluation_max_points = graphene.Float()
class Meta:
model = AssignmentCompletion
fields = (
@ -34,12 +38,20 @@ class AssignmentCompletionObjectType(DjangoObjectType):
"evaluation_user",
"additional_json_data",
"edoniq_extended_time_flag",
"evaluation_points",
"evaluation_passed",
"evaluation_max_points",
"task_completion_data",
)
def resolve_evaluation_points(self, info):
if self.evaluation_points:
return round(self.evaluation_points, 1) # noqa
return None
def resolve_evaluation_max_points(self, info):
if self.evaluation_max_points:
return round(self.evaluation_max_points, 1) # noqa
return None
class AssignmentObjectType(DjangoObjectType):
tasks = JSONStreamField()

View File

@ -0,0 +1,336 @@
from datetime import datetime, time, timedelta
import djclick as click
import structlog
from django.utils import timezone
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.course.consts import (
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
)
from vbv_lernwelt.course.models import (
Course,
CourseCompletion,
CourseSession,
CourseSessionUser,
)
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.notify.models import Notification
logger = structlog.get_logger(__name__)
from vbv_lernwelt.importer.services import (
create_or_update_course_session,
get_uk_course,
LP_DATA,
TRANSLATIONS,
)
IT_VV_TEST_COURSE = "Iterativ VV Testkurs"
IT_UK_TEST_COURSE = "Iterativ üK Testkurs"
IT_UK_TEST_REGION = "Iterativ Region"
TIME_FORMAT = "%d.%m.%Y, %H:%M"
PASSWORD = "KqaDm3-x8zhCKHLWDV_oiqFrYWHg"
logger = structlog.get_logger(__name__)
@click.command()
def command():
create_or_update_uk()
create_or_update_vv()
def create_or_update_uk(language="de"):
uk_course = get_uk_course(language)
uk_circle_keys = [
"Kickoff",
"Basis",
"Fahrzeug",
"Haushalt Teil 1",
"Haushalt Teil 2",
]
data = create_uk_data(language)
create_or_update_course_session(
uk_course,
data,
language,
circle_keys=uk_circle_keys,
)
cs = CourseSession.objects.get(import_id=data["ID"])
members, trainer, regionenleiter = get_or_create_users_uk()
delete_cs_data(cs, members + [trainer, regionenleiter])
add_to_course_session(cs, members)
add_trainers_to_course_session(cs, [trainer], uk_circle_keys, language)
create_and_add_to_cs_group(cs.course, IT_UK_TEST_REGION, [cs], regionenleiter)
def create_or_update_vv(language="de"):
vv_course = get_vv_course(language)
cs, _created = CourseSession.objects.get_or_create(
course=vv_course, import_id=IT_VV_TEST_COURSE
)
cs.title = IT_VV_TEST_COURSE
cs.save()
create_or_update_assignment_course_session(cs)
members, member_with_mentor, mentor = get_or_create_users_vv()
delete_cs_data(cs, members + [member_with_mentor, mentor])
add_to_course_session(cs, members + [member_with_mentor])
add_mentor_to_course_session(cs, [(mentor, member_with_mentor)])
def delete_cs_data(cs: CourseSession, users: list[User]):
if cs:
CourseCompletion.objects.filter(course_session=cs).delete()
Notification.objects.filter(course_session=cs).delete()
AssignmentCompletion.objects.filter(course_session=cs).delete()
CourseSessionAttendanceCourse.objects.filter(course_session=cs).update(
attendance_user_list=[]
)
CourseSessionEdoniqTest.objects.filter(course_session=cs).delete()
CourseSessionUser.objects.filter(course_session=cs).delete()
learning_mentor_ids = (
LearningMentor.objects.filter(participants__course_session=cs)
.values_list("id", flat=True)
.distinct()
| LearningMentor.objects.filter(mentor__in=users)
.values_list("id", flat=True)
.distinct()
)
# cannot call delete on distinct objects
LearningMentor.objects.filter(id__in=list(learning_mentor_ids)).delete()
else:
logger.info("no_course_session_found", import_id=cs.import_id)
FeedbackResponse.objects.filter(feedback_user__in=users).delete()
def add_to_course_session(
course_session: CourseSession,
members: list[User],
role=CourseSessionUser.Role.MEMBER,
):
if course_session:
for user in members:
csu, _created = CourseSessionUser.objects.get_or_create(
course_session_id=course_session.id, user_id=user.id, role=role
)
csu.save()
def add_mentor_to_course_session(
course_session: CourseSession, mentor_mentee_pairs: list[tuple[User, User]]
):
for mentor, mentee in mentor_mentee_pairs:
lm = LearningMentor.objects.create(
course=course_session.course,
mentor=mentor,
)
lm.participants.add(
CourseSessionUser.objects.get(
user__id=mentee.id,
course_session=course_session,
)
)
def add_trainers_to_course_session(
course_session: CourseSession,
trainers: list[User],
circle_keys: list[str],
language,
):
add_to_course_session(course_session, trainers, CourseSessionUser.Role.EXPERT)
for user in trainers:
for circle_key in circle_keys:
circle_name = LP_DATA[circle_key][language]["title"]
circle = Circle.objects.filter(
slug=f"{course_session.course.slug}-lp-circle-{circle_name.lower()}"
).first()
if course_session and circle:
csu = CourseSessionUser.objects.filter(
course_session_id=course_session.id, user_id=user.id
).first()
if csu:
csu.expert.add(circle)
csu.save()
def get_or_create_users_uk():
members = [
_create_or_update_user(
f"teilnehmer{n}.uk@iterativ.ch", "Teilnehmer üK", "Iterativ", PASSWORD, "de"
)
for n in range(1, 10)
]
trainer = _create_or_update_user(
"trainer1.uk@iterativ.ch", "Trainer üK", "Iterativ", PASSWORD, "de"
)
regionenleiter = _create_or_update_user(
"regionenleiter1.uk@iterativ.ch",
"Regionenleiter üK",
"Iterativ",
PASSWORD,
"de",
)
return (
members,
trainer,
regionenleiter,
)
def get_or_create_users_vv():
members = [
_create_or_update_user(
f"teilnehmer{n}.vv@iterativ.ch", "Teilnehmer VV", "Iterativ", PASSWORD, "de"
)
for n in range(1, 10)
]
member_with_mentor = _create_or_update_user(
"teilnehmer1.vv.lb@iterativ.ch",
"Teilnehmer VV mit LB",
"Iterativ",
PASSWORD,
"de",
)
mentor = _create_or_update_user(
"lernbegleitung1.vv@iterativ.ch",
"Lernbegleitung VV",
"Iterativ",
PASSWORD,
"de",
)
return members, member_with_mentor, mentor
def _create_or_update_user(email, first_name, last_name, password, language):
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
user = User(
email=email,
username=email,
)
user.email = email
user.first_name = first_name or user.first_name
user.last_name = last_name or user.last_name
user.username = email
user.language = language
user.set_password(password)
user.save()
return user
def create_uk_data(language):
return {
"Klasse": IT_UK_TEST_COURSE,
"ID": IT_UK_TEST_COURSE,
"Generation": 2024,
"Region": "Bern",
"Sprache": language,
f"Kickoff {TRANSLATIONS[language]['start']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=2)).date(), time(9, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Kickoff {TRANSLATIONS[language]['ende']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=2)).date(), time(16, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Kickoff {TRANSLATIONS[language]['raum']}": "Raum 1",
f"Kickoff {TRANSLATIONS[language]['standort']}": "Bern",
f"Kickoff {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
f"Basis {TRANSLATIONS[language]['start']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=4)).date(), time(9, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Basis {TRANSLATIONS[language]['ende']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=4)).date(), time(16, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Basis {TRANSLATIONS[language]['raum']}": "Raum 1",
f"Basis {TRANSLATIONS[language]['standort']}": "Bern",
f"Basis {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
f"Fahrzeug {TRANSLATIONS[language]['start']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=6)).date(), time(9, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Fahrzeug {TRANSLATIONS[language]['ende']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=6)).date(), time(16, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Fahrzeug {TRANSLATIONS[language]['raum']}": "Raum 1",
f"Fahrzeug {TRANSLATIONS[language]['standort']}": "Bern",
f"Fahrzeug {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
f"Haushalt Teil 1 {TRANSLATIONS[language]['start']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=8)).date(), time(9, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Haushalt Teil 1 {TRANSLATIONS[language]['ende']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=8)).date(), time(16, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Haushalt Teil 1 {TRANSLATIONS[language]['raum']}": "Raum 1",
f"Haushalt Teil 1 {TRANSLATIONS[language]['standort']}": "Bern",
f"Haushalt Teil 1 {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
f"Haushalt Teil 2 {TRANSLATIONS[language]['start']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=10)).date(), time(9, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Haushalt Teil 2 {TRANSLATIONS[language]['ende']}": timezone.make_aware(
datetime.combine((timezone.now() + timedelta(weeks=10)).date(), time(16, 0))
).strftime("%d.%m.%Y, %H:%M"),
f"Haushalt Teil 2 {TRANSLATIONS[language]['raum']}": "Raum 1",
f"Haushalt Teil 2 {TRANSLATIONS[language]['standort']}": "Bern",
f"Haushalt Teil 2 {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
}
def create_and_add_to_cs_group(
course: Course, name: str, course_sessions: list[CourseSession], supervisor: User
):
region, _ = CourseSessionGroup.objects.get_or_create(
name=name,
course=course,
)
for cs in course_sessions:
region.course_session.add(cs)
region.supervisor.add(supervisor)
def get_vv_course(language: str) -> Course:
if language == "fr":
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID
elif language == "it":
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID
else:
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_ID
return Course.objects.get(id=course_id)
def create_or_update_assignment_course_session(cs: CourseSession):
# not nice but works for now
for assignment in Assignment.objects.all():
if assignment.get_course().id == cs.course.id:
logger.debug(
"create_course_session_assigments",
assignment=assignment,
label="reset_test_courses",
)
for lca in assignment.learningcontentassignment_set.all():
_csa, _created = CourseSessionAssignment.objects.get_or_create(
course_session=cs,
learning_content=lca,
)

View File

@ -58,6 +58,12 @@ def vue_home(request, *args):
# render index.html from `npm run build`
content = loader.render_to_string("vue/index.html", context={}, request=request)
# inject Plausible tracking tag
if settings.TRACKING_TAG:
content = content.replace(
"</head>",
f"\n{settings.TRACKING_TAG}\n</head>",
)
return HttpResponse(content)
@ -179,6 +185,17 @@ def cypress_reset_view(request):
return HttpResponseRedirect("/server/admin/")
@api_view(["POST"])
@authentication_classes((authentication.SessionAuthentication,))
@permission_classes((IsAdminUser,))
def iterativ_test_coursesessions_reset_view(request):
call_command(
"reset_iterativ_test_sessions",
)
return HttpResponseRedirect("/server/admin/")
@django_view_authentication_exempt
def generate_web_component_icons(request):
svg_files = []

View File

@ -6,8 +6,15 @@ from vbv_lernwelt.course.models import (
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(CourseConfiguration)
class CourseConfigurationAdmin(admin.ModelAdmin):
@ -27,6 +34,7 @@ class CourseAdmin(admin.ModelAdmin):
"category_name",
"slug",
]
actions = [get_feedbacks_for_courses]
@admin.register(CourseSession)
@ -41,6 +49,7 @@ class CourseSessionAdmin(admin.ModelAdmin):
"created_at",
"updated_at",
]
actions = [get_feedbacks_for_course_sessions]
@admin.register(CourseSessionUser)

View File

@ -9,12 +9,14 @@ 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
VV_COURSE_IDS = [
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID,
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
]
UK_COURSE_IDS = [

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,
@ -93,6 +94,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,
)
@ -307,6 +309,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,

View File

@ -45,6 +45,14 @@
<hr style="margin: 24px 0">
<form action="/api/core/resetiterativsessions/" method="post">
{% csrf_token %}
<p>Zurücksetzen der Iterativ Testdurchführungen (üK: "Iterativ üK Testkurs", VV: "Iterativ VV Testkurs")</p>
<button class="btn">Iterativ Testdurchführungen zurücksetzen</button>
</form>
<hr style="margin: 24px 0">
<form action="/api/core/cypressreset/" method="post">
{% csrf_token %}
<label>