Merge branch 'develop' into feat/geteilter-bereich

This commit is contained in:
Livio Bieri 2024-03-27 15:40:50 +01:00
commit 3eb098be35
14 changed files with 124 additions and 59 deletions

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import ItButton from "@/components/ui/ItButton.vue";
import type { Assignment } from "@/types";
const props = defineProps<{
assignment: Assignment;
}>();
const openSolutionSample = () => {
const url = props.assignment.solution_sample?.url;
if (url) {
window.open(url, "_blank");
}
};
</script>
<template>
<div v-if="assignment.solution_sample" data-cy="show-sample-solution">
<p>
{{ $t("assignment.submissionShowSampleSolutionText") }}
</p>
<ItButton
class="mt-6"
variant="primary"
size="normal"
:disabled="false"
data-cy="show-sample-solution-button"
@click="openSolutionSample"
>
<p>{{ $t("assignment.submissionShowSampleSolution") }}</p>
</ItButton>
</div>
</template>
<style scoped></style>

View File

@ -14,6 +14,7 @@ import type { Assignment } from "@/types";
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue"; import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import { useMyLearningMentors } from "@/composables"; import { useMyLearningMentors } from "@/composables";
import NoMentorInformationPanel from "@/components/learningMentor/NoMentorInformationPanel.vue"; import NoMentorInformationPanel from "@/components/learningMentor/NoMentorInformationPanel.vue";
import SampleSolution from "@/components/assignment/SampleSolution.vue";
const props = defineProps<{ const props = defineProps<{
submissionDeadlineStart?: string | null; submissionDeadlineStart?: string | null;
@ -98,4 +99,9 @@ const onSubmit = async () => {
> >
<p>{{ $t("a.Ergebnisse teilen") }}</p> <p>{{ $t("a.Ergebnisse teilen") }}</p>
</ItButton> </ItButton>
<SampleSolution
v-if="learningMentors?.length == 0"
class="pt-8"
:assignment="assignment"
/>
</template> </template>

View File

@ -13,11 +13,11 @@ export function assignmentsMaxEvaluationPoints(
} }
export function assignmentsUserPoints(assignments: CompetenceCertificateAssignment[]) { export function assignmentsUserPoints(assignments: CompetenceCertificateAssignment[]) {
return _.sum( return +_.sum(
assignments assignments
.filter((a) => a.completion?.completion_status === "EVALUATION_SUBMITTED") .filter((a) => a.completion?.completion_status === "EVALUATION_SUBMITTED")
.map((a) => a.completion?.evaluation_points ?? 0) .map((a) => a.completion?.evaluation_points ?? 0)
); ).toFixed(1);
} }
export function competenceCertificateProgressStatusCount( export function competenceCertificateProgressStatusCount(

View File

@ -41,7 +41,6 @@ const someFinished = computed(() => {
if (props.learningSequence) { if (props.learningSequence) {
return someFinishedInLearningSequence(props.learningSequence); return someFinishedInLearningSequence(props.learningSequence);
} }
return false; return false;
}); });
@ -235,7 +234,11 @@ type LearninContentWithCompetenceCertificate =
: 'no-status' : 'no-status'
" "
> >
<CompletionStatus /> <CompletionStatus
:completed="
performanceCriteriaHasStatus(learningUnit.performance_criteria)
"
/>
{{ $t("a.Selbsteinschätzung") }} {{ $t("a.Selbsteinschätzung") }}
</div> </div>

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import ItButton from "@/components/ui/ItButton.vue";
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue"; import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
import { import {
useCourseData, useCourseData,
@ -14,6 +13,7 @@ import type { AssignmentAssignmentAssignmentTypeChoices } from "@/gql/graphql";
import CaseWorkSubmit from "@/components/learningPath/assignment/CaseWorkSubmit.vue"; import CaseWorkSubmit from "@/components/learningPath/assignment/CaseWorkSubmit.vue";
import SimpleSubmit from "@/components/learningPath/assignment/SimpleSubmit.vue"; import SimpleSubmit from "@/components/learningPath/assignment/SimpleSubmit.vue";
import PraxisAssignmentSubmit from "@/components/learningPath/assignment/PraxisAssignmentSubmit.vue"; import PraxisAssignmentSubmit from "@/components/learningPath/assignment/PraxisAssignmentSubmit.vue";
import SampleSolution from "@/components/assignment/SampleSolution.vue";
const props = defineProps<{ const props = defineProps<{
assignment: Assignment; assignment: Assignment;
@ -78,14 +78,6 @@ const isPraxisAssignment = computed(() => checkAssignmentType(["PRAXIS_ASSIGNMEN
const onEditTask = (task: AssignmentTask) => { const onEditTask = (task: AssignmentTask) => {
emit("editTask", task); emit("editTask", task);
}; };
const openSolutionSample = () => {
const url = props.assignment.solution_sample?.url ?? "";
if (props.assignment.solution_sample) {
window.open(url, "_blank");
}
};
</script> </script>
<template> <template>
<div class="w-full border border-gray-400 p-8" data-cy="confirm-container"> <div class="w-full border border-gray-400 p-8" data-cy="confirm-container">
@ -134,26 +126,7 @@ const openSolutionSample = () => {
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName }) $t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
}} }}
</p> </p>
<div <SampleSolution class="pt-2" :assignment="assignment" />
v-if="assignment.solution_sample"
class="pt-2"
data-cy="show-sample-solution"
>
<p>
{{ $t("assignment.submissionShowSampleSolutionText") }}
</p>
<ItButton
class="mt-6"
variant="primary"
size="normal"
:disabled="false"
data-cy="show-sample-solution-button"
@click="openSolutionSample"
>
<p>{{ $t("assignment.submissionShowSampleSolution") }}</p>
</ItButton>
</div>
</div> </div>
</div> </div>
<AssignmentSubmissionResponses <AssignmentSubmissionResponses

View File

@ -6,10 +6,9 @@ const props = defineProps<{
content: LearningContent; content: LearningContent;
}>(); }>();
</script> </script>
<template> <template>
<LearningContentSimpleLayout :learning-content="props.content"> <LearningContentSimpleLayout :learning-content="props.content">
<div class="h-screen"> <div class="h-[calc(100vh-222px)] md:h-[calc(100vh-238px)]">
<iframe width="100%" height="100%" :src="props.content.content_url" /> <iframe width="100%" height="100%" :src="props.content.content_url" />
</div> </div>
</LearningContentSimpleLayout> </LearningContentSimpleLayout>

View File

@ -43,10 +43,24 @@ export function someFinishedInLearningSequence(ls: LearningSequence) {
}); });
} }
/**
* Completed is defined as having completed all learning contents, ranked at
* least one performance criteria (but not necessarily all) as success or fail.
*/
export function allFinishedInLearningSequence(ls: LearningSequence) { export function allFinishedInLearningSequence(ls: LearningSequence) {
return learningSequenceFlatChildren(ls).every((lc) => { const allLearningContentsCompleted = ls.learning_units
return lc.completion_status === "SUCCESS"; .flatMap((lu) => lu.learning_contents)
}); .every((lc) => lc.completion_status === "SUCCESS");
const performanceCriteria = ls.learning_units.flatMap(
(lu) => lu.performance_criteria
);
const somePerformanceCriteriaRanked =
performanceCriteriaHasStatus(performanceCriteria) ||
performanceCriteria.length === 0; // -> treat as completed
return allLearningContentsCompleted && somePerformanceCriteriaRanked;
} }
export function performanceCriteriaStatusCount( export function performanceCriteriaStatusCount(

Binary file not shown.

View File

@ -9,6 +9,7 @@ from vbv_lernwelt.assignment.models import (
AssignmentCompletion, AssignmentCompletion,
AssignmentCompletionAuditLog, AssignmentCompletionAuditLog,
AssignmentCompletionStatus, AssignmentCompletionStatus,
AssignmentType,
is_valid_assignment_completion_status, is_valid_assignment_completion_status,
) )
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
@ -208,10 +209,14 @@ def update_assignment_completion(
ac.save() ac.save()
if completion_status in [ if (
AssignmentCompletionStatus.EVALUATION_SUBMITTED, completion_status
AssignmentCompletionStatus.SUBMITTED, in [
]: AssignmentCompletionStatus.EVALUATION_SUBMITTED,
AssignmentCompletionStatus.SUBMITTED,
]
and assignment.assignment_type != AssignmentType.EDONIQ_TEST.value
):
acl = AssignmentCompletionAuditLog.objects.create( acl = AssignmentCompletionAuditLog.objects.create(
assignment_user=assignment_user, assignment_user=assignment_user,
assignment=assignment, assignment=assignment,

View File

@ -1,3 +1,4 @@
# Course IDs
COURSE_TEST_ID = -1 COURSE_TEST_ID = -1
COURSE_UK = -3 COURSE_UK = -3
COURSE_VERSICHERUNGSVERMITTLERIN_ID = -4 COURSE_VERSICHERUNGSVERMITTLERIN_ID = -4

View File

@ -88,9 +88,10 @@ def create_user(username: str) -> User:
def create_course_session( def create_course_session(
course: Course, title: str, generation: str = "2023" course: Course, title: str, generation: str = "2023", _id=None
) -> CourseSession: ) -> CourseSession:
return CourseSession.objects.create( return CourseSession.objects.create(
id=_id,
course=course, course=course,
title=title, title=title,
import_id=title, import_id=title,

View File

@ -185,7 +185,7 @@ def export_feedback(course_session_ids: list[str], save_as_file: bool):
def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]): def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]):
sheet = wb.create_sheet(title=title) sheet = wb.create_sheet(title=_sanitize_sheet_name(title))
if len(data) == 0: if len(data) == 0:
return sheet return sheet
@ -208,6 +208,24 @@ def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]):
return sheet 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): def _add_rows(sheet, data, question_data):
for row_idx, feedback in enumerate(data, start=2): 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=1, value=feedback.course_session.title)

View File

@ -35,7 +35,13 @@ class DatatransWebhookTestCase(APITestCase):
_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, _id=COURSE_VERSICHERUNGSVERMITTLERIN_ID,
) )
create_course_session(course=course, title="Versicherungsvermittler/-in DE") create_course_session(
course=course,
title="Versicherungsvermittler/-in DE",
# MUST be 1 -> this is the correct
# VV DE course session ID in production
_id=1,
)
self.user = User.objects.create_user( self.user = User.objects.create_user(
username="testuser", username="testuser",
@ -145,14 +151,21 @@ class DatatransWebhookTestCase(APITestCase):
CourseSessionUser.objects.count(), CourseSessionUser.objects.count(),
) )
csu = CourseSessionUser.objects.first()
self.assertEqual( self.assertEqual(
self.user, self.user,
CourseSessionUser.objects.first().user, csu.user,
)
self.assertEqual(
1,
csu.course_session.id,
) )
self.assertEqual( self.assertEqual(
COURSE_VERSICHERUNGSVERMITTLERIN_ID, COURSE_VERSICHERUNGSVERMITTLERIN_ID,
CourseSessionUser.objects.first().course_session.course.id, csu.course_session.course.id,
) )
self.assertEqual( self.assertEqual(

View File

@ -8,11 +8,6 @@ from rest_framework.response import Response
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
from vbv_lernwelt.core.models import Country, User from vbv_lernwelt.core.models import Country, User
from vbv_lernwelt.course.consts import (
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
)
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
from vbv_lernwelt.shop.const import ( from vbv_lernwelt.shop.const import (
@ -37,10 +32,11 @@ from vbv_lernwelt.shop.services import (
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
PRODUCT_SKU_TO_COURSE = { PRODUCT_SKU_TO_COURSE_SESSION_ID = {
VV_DE_PRODUCT_SKU: COURSE_VERSICHERUNGSVERMITTLERIN_ID, # CourseSession IDs PROD/STAGING
VV_FR_PRODUCT_SKU: COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID, VV_DE_PRODUCT_SKU: 1, # vv-de
VV_IT_PRODUCT_SKU: COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID, VV_FR_PRODUCT_SKU: 2, # vv-fr
VV_IT_PRODUCT_SKU: 3, # vv-it
} }
@ -229,9 +225,9 @@ def create_vv_course_session_user(checkout_info: CheckoutInformation):
_, created = CourseSessionUser.objects.get_or_create( _, created = CourseSessionUser.objects.get_or_create(
user=checkout_info.user, user=checkout_info.user,
role=CourseSessionUser.Role.MEMBER, role=CourseSessionUser.Role.MEMBER,
course_session=CourseSession.objects.filter( course_session=CourseSession.objects.get(
course_id=PRODUCT_SKU_TO_COURSE[checkout_info.product_sku] id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku]
).first(), ),
) )
if created: if created: