Merged develop into feature/VBV-698-optional-flag

This commit is contained in:
Christian Cueni 2024-07-29 07:56:02 +00:00
commit 29fe1bdf83
9 changed files with 185 additions and 120 deletions

View File

@ -6,6 +6,7 @@ import type { CourseStatisticsType } from "@/gql/graphql";
import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue"; import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue";
import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.vue"; import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.vue";
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue"; import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
const props = defineProps<{ const props = defineProps<{
courseId: string; courseId: string;
@ -95,4 +96,7 @@ onMounted(async () => {
/> />
</div> </div>
</div> </div>
<div v-else class="flex w-full flex-row justify-center">
<LoadingSpinner />
</div>
</template> </template>

View File

@ -3,6 +3,7 @@ import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPa
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils"; import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import { computed } from "vue"; import { computed } from "vue";
import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables"; import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall"; export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall";
@ -54,23 +55,25 @@ const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
<template> <template>
<div> <div>
<h4 <div v-if="circlesCount > 0">
v-if="diagramType === 'horizontal' && circles.length > 0" <h4 v-if="diagramType === 'horizontal'" class="mb-4 font-bold">
class="mb-4 font-bold" {{
> $t("learningPathPage.progressText", {
{{ inProgressCount: inProgressCirclesCount,
$t("learningPathPage.progressText", { allCount: circlesCount,
inProgressCount: inProgressCirclesCount, })
allCount: circlesCount, }}
}) </h4>
}} <div :class="wrapperClasses">
</h4> <LearningPathCircle
<div :class="wrapperClasses"> v-for="circle in circles"
<LearningPathCircle :key="circle.id"
v-for="circle in circles" :sectors="calculateCircleSectorData(circle)"
:key="circle.id" ></LearningPathCircle>
:sectors="calculateCircleSectorData(circle)" </div>
></LearningPathCircle> </div>
<div v-else class="flex justify-center">
<LoadingSpinner />
</div> </div>
</div> </div>
</template> </template>

View File

@ -265,11 +265,10 @@ const executePayment = async () => {
<p v-if="paymentError" class="text-bold mt-12 text-lg text-red-700"> <p v-if="paymentError" class="text-bold mt-12 text-lg text-red-700">
{{ {{
$t("a.Fehler bei der Zahlung. Bitte versuche es erneut oder kontaktiere uns") $t(
}}: "a.Fehler bei der Zahlung. Bitte versuche es erneut oder wähle eine andere Zahlungsmethode."
<a href="mailto:vermittler@vbv-afa.ch" class="underline"> )
vermittler@vbv-afa.ch }}
</a>
</p> </p>
<h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3> <h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3>

View File

@ -201,6 +201,7 @@ MIDDLEWARE = [
"vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware", "vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware",
"vbv_lernwelt.core.middleware.auth.UserLoggedInCookieMiddleWare", "vbv_lernwelt.core.middleware.auth.UserLoggedInCookieMiddleWare",
# "vbv_lernwelt.debugtools.middleware.QueryCountDebugMiddleware",
] ]
# STATIC # STATIC

View File

@ -17,6 +17,11 @@ def query_competence_course_session_assignments(course_session_ids, circle_ids=N
AssignmentType.CASEWORK.value, AssignmentType.CASEWORK.value,
], ],
learning_content__content_assignment__competence_certificate__isnull=False, learning_content__content_assignment__competence_certificate__isnull=False,
).select_related(
"submission_deadline",
"learning_content",
"course_session",
"learning_content__content_assignment",
): ):
if circle_ids and csa.learning_content.get_circle().id not in circle_ids: if circle_ids and csa.learning_content.get_circle().id not in circle_ids:
continue continue
@ -34,6 +39,11 @@ def query_competence_course_session_edoniq_tests(course_session_ids, circle_ids=
for cset in CourseSessionEdoniqTest.objects.filter( for cset in CourseSessionEdoniqTest.objects.filter(
course_session_id__in=course_session_ids, course_session_id__in=course_session_ids,
learning_content__content_assignment__competence_certificate__isnull=False, learning_content__content_assignment__competence_certificate__isnull=False,
).select_related(
"deadline",
"learning_content",
"course_session",
"learning_content__content_assignment",
): ):
if circle_ids and cset.learning_content.get_circle().id not in circle_ids: if circle_ids and cset.learning_content.get_circle().id not in circle_ids:
continue continue

View File

@ -1,5 +1,5 @@
import math import math
from typing import List from typing import List, Tuple
import graphene import graphene
@ -7,7 +7,6 @@ import vbv_lernwelt.assignment.models
from vbv_lernwelt.assignment.models import ( from vbv_lernwelt.assignment.models import (
AssignmentCompletion, AssignmentCompletion,
AssignmentCompletionStatus, AssignmentCompletionStatus,
AssignmentType,
) )
from vbv_lernwelt.competence.services import ( from vbv_lernwelt.competence.services import (
query_competence_course_session_assignments, query_competence_course_session_assignments,
@ -98,14 +97,23 @@ def get_assignment_completion_metrics(
assignment: vbv_lernwelt.assignment.models.Assignment, assignment: vbv_lernwelt.assignment.models.Assignment,
user_selection_ids: List[str] | None, user_selection_ids: List[str] | None,
urql_id_postfix: str = "", urql_id_postfix: str = "",
context=None,
) -> AssignmentCompletionMetricsType: ) -> AssignmentCompletionMetricsType:
if not context:
context = {}
if user_selection_ids: if user_selection_ids:
course_session_users = user_selection_ids course_session_users = user_selection_ids
else: else:
course_session_users = CourseSessionUser.objects.filter( key = f"CourseSessionUser_{course_session.id}"
course_session=course_session, if not key in context:
role=CourseSessionUser.Role.MEMBER, course_session_users = CourseSessionUser.objects.filter(
).values_list("user", flat=True) course_session=course_session,
role=CourseSessionUser.Role.MEMBER,
).values_list("user", flat=True)
context[key] = course_session_users
else:
course_session_users = context[key]
evaluation_results = AssignmentCompletion.objects.filter( evaluation_results = AssignmentCompletion.objects.filter(
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
@ -139,33 +147,54 @@ def create_record(
course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest, course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest,
user_selection_ids: List[str] | None, user_selection_ids: List[str] | None,
urql_id_postfix: str = "", urql_id_postfix: str = "",
) -> AssignmentStatisticsRecordType: context=None,
if isinstance(course_session_assignment, CourseSessionAssignment): ) -> Tuple[AssignmentStatisticsRecordType, dict]:
due_date = course_session_assignment.submission_deadline if not context:
else: context = {}
due_date = course_session_assignment.deadline
assignment_type = (
"CourseSessionAssignment"
if isinstance(course_session_assignment, CourseSessionAssignment)
else "CourseSessionEdoniqTest"
)
due_date = (
course_session_assignment.submission_deadline
if assignment_type == "CourseSessionAssignment"
else course_session_assignment.deadline
)
key = f"{assignment_type}_{course_session_assignment.learning_content.id}"
if not key in context:
context[key] = course_session_assignment.learning_content.get_circle().id
circle_id = context[key]
learning_content = course_session_assignment.learning_content learning_content = course_session_assignment.learning_content
return AssignmentStatisticsRecordType( return (
# make sure it's unique, across all types of assignments! AssignmentStatisticsRecordType(
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}", # make sure it's unique, across all types of assignments!
# noqa _id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}",
course_session_id=str(course_session_assignment.course_session.id), # noqa # noqa
circle_id=learning_content.get_circle().id, # noqa course_session_id=str(course_session_assignment.course_session.id), # noqa
course_session_assignment_id=str(course_session_assignment.id), # noqa circle_id=circle_id, # noqa
generation=course_session_assignment.course_session.generation, # noqa course_session_assignment_id=str(course_session_assignment.id), # noqa
assignment_type_translation_key=due_date.assignment_type_translation_key, generation=course_session_assignment.course_session.generation, # noqa
# noqa assignment_type_translation_key=due_date.assignment_type_translation_key,
assignment_title=learning_content.content_assignment.title, # noqa # noqa
metrics=get_assignment_completion_metrics( # noqa assignment_title=learning_content.content_assignment.title, # noqa
course_session=course_session_assignment.course_session, # noqa metrics=get_assignment_completion_metrics( # noqa
assignment=learning_content.content_assignment, # noqa course_session=course_session_assignment.course_session, # noqa
user_selection_ids=user_selection_ids, # noqa assignment=learning_content.content_assignment, # noqa
urql_id_postfix=urql_id_postfix, # noqa user_selection_ids=user_selection_ids, # noqa
urql_id_postfix=urql_id_postfix, # noqa
context=context, # noqa
),
details_url=due_date.url_expert, # noqa
deadline=due_date.start, # noqa
), ),
details_url=due_date.url_expert, # noqa context,
deadline=due_date.start, # noqa
) )
@ -178,28 +207,26 @@ def assignments(
) -> AssignmentsStatisticsType: ) -> AssignmentsStatisticsType:
if urql_id is None: if urql_id is None:
urql_id = str(course_id) urql_id = str(course_id)
course_sessions = CourseSession.objects.filter( course_sessions = CourseSession.objects.filter(
id__in=course_session_selection_ids, id__in=course_session_selection_ids,
) )
records: List[AssignmentStatisticsRecordType] = [] records: List[AssignmentStatisticsRecordType] = []
context = {}
for course_session in course_sessions: csas = query_competence_course_session_assignments(course_sessions, circle_ids)
for csa in query_competence_course_session_assignments( csets = query_competence_course_session_edoniq_tests(course_sessions, circle_ids)
[course_session.id], circle_ids
):
record = create_record(csa, user_selection_ids, urql_id_postfix=urql_id)
records.append(record)
for cset in query_competence_course_session_edoniq_tests( for csa in csas:
[course_session.id], circle_ids record, context = create_record(
): csa, user_selection_ids, urql_id_postfix=urql_id, context=context
record = create_record( )
course_session_assignment=cset, records.append(record)
user_selection_ids=user_selection_ids,
urql_id_postfix=urql_id, for cset in csets:
) record, context = create_record(
records.append(record) cset, user_selection_ids, urql_id_postfix=urql_id, context=context
)
records.append(record)
return AssignmentsStatisticsType( return AssignmentsStatisticsType(
_id=urql_id, # noqa _id=urql_id, # noqa

View File

@ -41,25 +41,23 @@ def competences(
completions = CourseCompletion.objects.filter( completions = CourseCompletion.objects.filter(
course_session_id__in=course_session_selection_ids, course_session_id__in=course_session_selection_ids,
page_type="competence.PerformanceCriteria", page_type="competence.PerformanceCriteria",
).prefetch_related("course_session", "page") ).select_related("course_session", "page")
if user_selection_ids is not None: if user_selection_ids is not None:
completions = completions.filter(user_id__in=user_selection_ids) completions = completions.filter(user_id__in=user_selection_ids)
competence_records = {} page_ids = completions.values_list("page_id", flat=True).distinct()
pages = Page.objects.filter(id__in=page_ids).specific()
learning_units = {page.id: page.specific.learning_unit for page in pages}
unique_page_ids = {completion.page.id for completion in completions}
learning_units = {
page_id: Page.objects.get(id=page_id).specific.learning_unit
for page_id in unique_page_ids
}
circles = { circles = {
lu.id: c lu.id: c
for lu in learning_units.values() for lu in learning_units.values()
if (lu is not None and (c := lu.get_circle()) is not None) if lu and (c := lu.get_circle()) and (circle_ids is None or c.id in circle_ids)
and (circle_ids is None or c.id in circle_ids)
} }
competence_records = {}
for completion in completions: for completion in completions:
learning_unit = learning_units.get(completion.page.id) learning_unit = learning_units.get(completion.page.id)
@ -73,20 +71,23 @@ def competences(
combined_id = f"{circle.id}-{completion.course_session.id}@{urql_id_postfix}" combined_id = f"{circle.id}-{completion.course_session.id}@{urql_id_postfix}"
competence_records.setdefault(combined_id, {}).setdefault( if combined_id not in competence_records:
learning_unit, competence_records[combined_id] = {}
CompetenceRecordStatisticsType(
_id=combined_id, # noqa if learning_unit not in competence_records[combined_id]:
title=learning_unit.title, # noqa competence_records[combined_id][
course_session_id=completion.course_session.id, # noqa learning_unit
generation=completion.course_session.generation, # noqa ] = CompetenceRecordStatisticsType(
circle_id=circle.id, # noqa _id=combined_id,
success_count=0, # noqa title=learning_unit.title,
fail_count=0, # noqa course_session_id=completion.course_session.id,
generation=completion.course_session.generation,
circle_id=circle.id,
success_count=0,
fail_count=0,
details_url=f"/course/{course_slug}/cockpit?courseSessionId={completion.course_session.id}", details_url=f"/course/{course_slug}/cockpit?courseSessionId={completion.course_session.id}",
# noqa # noqa
), )
)
if completion.completion_status == CourseCompletionStatus.SUCCESS.value: if completion.completion_status == CourseCompletionStatus.SUCCESS.value:
competence_records[combined_id][learning_unit].success_count += 1 competence_records[combined_id][learning_unit].success_count += 1
@ -99,7 +100,7 @@ def competences(
for record in circle_records.values() for record in circle_records.values()
] ]
success_count = sum([c.success_count for c in values]) success_count = sum(c.success_count for c in values)
fail_count = sum([c.fail_count for c in values]) fail_count = sum(c.fail_count for c in values)
return values, success_count, fail_count return values, success_count, fail_count

View File

@ -183,7 +183,7 @@ def _create_person_list_with_roles(user):
if has_cs_role(cs.roles) and cs.course.configuration.is_uk: if has_cs_role(cs.roles) and cs.course.configuration.is_uk:
course_session_users = CourseSessionUser.objects.filter( course_session_users = CourseSessionUser.objects.filter(
course_session=cs.id course_session=cs.id
) ).select_related("user")
my_role = user_role(cs.roles) my_role = user_role(cs.roles)
for csu in course_session_users: for csu in course_session_users:
person_data = result_persons.get( person_data = result_persons.get(
@ -239,7 +239,6 @@ def _create_person_list_with_roles(user):
result_persons[mentor_relation.mentor.id]["course_sessions"].append( result_persons[mentor_relation.mentor.id]["course_sessions"].append(
course_session_entry course_session_entry
) )
return result_persons.values() return result_persons.values()
@ -307,45 +306,28 @@ def get_dashboard_due_dates(request):
course_sessions = get_course_sessions_with_roles_for_user(request.user) course_sessions = get_course_sessions_with_roles_for_user(request.user)
course_session_ids = [cs.id for cs in course_sessions] course_session_ids = [cs.id for cs in course_sessions]
all_due_dates = DueDate.objects.filter(
course_session__id__in=course_session_ids
)
# filter only future due dates
due_dates = []
today = date.today() today = date.today()
for due_date in all_due_dates:
# due_dates.append(due_date)
if due_date.end:
if due_date.end.date() >= today:
due_dates.append(due_date)
elif due_date.start:
if due_date.start.date() >= today:
due_dates.append(due_date)
due_dates.sort(key=lambda x: x.start) # Fetch future due dates in a single query using Q objects for complex filtering
future_due_dates = DueDate.objects.filter(
# find course session by id in `course_sessions` Q(course_session_id__in=course_session_ids),
Q(end__gte=today) | Q(start__gte=today),
).select_related("course_session")
result_due_dates = [] result_due_dates = []
for due_date in due_dates: course_session_map = {cs.id: cs for cs in course_sessions}
data = DueDateSerializer(due_date).data
for due_date in sorted(future_due_dates, key=lambda x: x.start):
data = DueDateSerializer(due_date).data
cs = course_session_map.get(due_date.course_session_id)
cs = next(
course_session
for course_session in course_sessions
if course_session.id == due_date.course_session.id
)
if cs: if cs:
data["course_session"] = _create_course_session_dict( data["course_session"] = _create_course_session_dict(
cs, my_role=user_role(cs.roles), user_role="" cs, my_role=user_role(cs.roles), user_role=""
) )
result_due_dates.append(data) result_due_dates.append(data)
return Response( return Response(status=200, data=result_due_dates)
status=200,
data=result_due_dates,
)
except PermissionDenied as e: except PermissionDenied as e:
raise e raise e

View File

@ -0,0 +1,38 @@
import time
import structlog
from django.db import connection, reset_queries
logger = structlog.get_logger(__name__)
class QueryCountDebugMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not request.path.startswith("/api/") and not request.path.startswith(
"/server/"
):
return self.get_response(request)
reset_queries()
start_queries = len(connection.queries)
start_time = time.time()
response = self.get_response(request)
end_queries = len(connection.queries)
end_time = time.time()
total_queries = end_queries - start_queries
duration = end_time - start_time
logger.debug(
"query_count_middleware",
request_path=request.path,
queries=total_queries,
duration=duration,
)
return response