Merged in feature/VBV-621-teilnehmer-profil (pull request #274)
Cockpit Teilnehmer Profil Approved-by: Christian Cueni
This commit is contained in:
commit
b2c1e30b47
|
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex-none border-r bg-white p-4 lg:p-8">
|
||||||
|
<slot name="side"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 p-4 lg:p-8">
|
||||||
|
<slot name="main"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import * as log from "loglevel";
|
|
||||||
import { computed, onMounted } from "vue";
|
|
||||||
|
|
||||||
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
|
|
||||||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
|
||||||
import {
|
|
||||||
useCourseDataWithCompletion,
|
|
||||||
useCourseSessionDetailQuery,
|
|
||||||
} from "@/composables";
|
|
||||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
userId: string;
|
|
||||||
courseSlug: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
log.debug("CockpitUserProfilePage created", props.userId);
|
|
||||||
|
|
||||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
|
||||||
|
|
||||||
const courseCompletionData = useCourseDataWithCompletion(
|
|
||||||
props.courseSlug,
|
|
||||||
props.userId
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
log.debug("CockpitUserProfilePage mounted");
|
|
||||||
});
|
|
||||||
|
|
||||||
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug, props.userId);
|
|
||||||
|
|
||||||
const learningPath = computed(() => {
|
|
||||||
return lpQueryResult.learningPath.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { findUser } = useCourseSessionDetailQuery();
|
|
||||||
|
|
||||||
const user = computed(() => {
|
|
||||||
return findUser(props.userId);
|
|
||||||
});
|
|
||||||
|
|
||||||
function setActiveClasses(isActive: boolean) {
|
|
||||||
return isActive ? ["border-blue-900", "border-b-2"] : ["text-bg-900"];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="!loading" class="bg-gray-200">
|
|
||||||
<div v-if="user" class="container-large">
|
|
||||||
<nav class="py-4 pb-4">
|
|
||||||
<router-link
|
|
||||||
class="btn-text inline-flex items-center pl-0"
|
|
||||||
:to="`/course/${props.courseSlug}/cockpit`"
|
|
||||||
>
|
|
||||||
<it-icon-arrow-left />
|
|
||||||
<span>{{ $t("general.back") }}</span>
|
|
||||||
</router-link>
|
|
||||||
</nav>
|
|
||||||
<header class="mb-12 flex flex-row items-center">
|
|
||||||
<img class="mr-8 h-44 w-44 rounded-full" :src="user.avatar_url" />
|
|
||||||
<div>
|
|
||||||
<h1 class="mb-2">{{ user.first_name }} {{ user.last_name }}</h1>
|
|
||||||
<p class="mb-2">{{ user.email }}</p>
|
|
||||||
<p class="bg-message bg-[center_left_-4px] bg-no-repeat pl-6">
|
|
||||||
{{ $t("messages.sendMessage") }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div v-if="learningPath" class="mb-8 w-full bg-white pb-1 pt-2">
|
|
||||||
<!-- TODO: rework this section with next redesign -->
|
|
||||||
<LearningPathPathView
|
|
||||||
:use-mobile-layout="false"
|
|
||||||
:hide-buttons="true"
|
|
||||||
:learning-path="learningPath"
|
|
||||||
:next-learning-content="undefined"
|
|
||||||
:override-circle-url-base="`/course/${props.courseSlug}/cockpit/profile/${props.userId}`"
|
|
||||||
></LearningPathPathView>
|
|
||||||
</div>
|
|
||||||
<ul class="mb-5 flex flex-row border-b-2">
|
|
||||||
<li class="relative top-px mr-12 pb-3" :class="setActiveClasses(true)">
|
|
||||||
<button>{{ $t("competences.competences") }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="mr-12">
|
|
||||||
<button>{{ $t("general.transferTask_other") }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="mr-12">
|
|
||||||
<button>{{ $t("general.exam_other") }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="mr-12">
|
|
||||||
<button>{{ $t("general.certificate_other") }}</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<ul class="bg-white px-8">
|
|
||||||
<li
|
|
||||||
v-for="competence in courseCompletionData.actionCompetences.value ?? []"
|
|
||||||
:key="competence.id"
|
|
||||||
class="border-b border-gray-500 p-8 last:border-0"
|
|
||||||
>
|
|
||||||
<CompetenceDetail
|
|
||||||
:competence="competence"
|
|
||||||
:course-slug="props.courseSlug"
|
|
||||||
:show-assess-again="false"
|
|
||||||
:is-inline="true"
|
|
||||||
></CompetenceDetail>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
@ -27,9 +27,12 @@ const { summary } = useMentorCockpit(courseSession.value.id);
|
||||||
{{ participant.email }}
|
{{ participant.email }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <router-link :to="{name: 'cockpitUserProfile', params: {userId: participant.id}}" class="underline">-->
|
<router-link
|
||||||
<!-- {{ $t("a.Profil anzeigen") }}-->
|
:to="{ name: 'cockpitUserProfile', params: { userId: participant.id } }"
|
||||||
<!-- </router-link>-->
|
class="underline"
|
||||||
|
>
|
||||||
|
{{ $t("cockpit.profileLink") }}
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import { useFetch } from "@vueuse/core";
|
||||||
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userId: string;
|
||||||
|
courseSlug: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const pages = ref([
|
||||||
|
{ label: t("general.learningPath"), route: "cockpitProfileLearningPath" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
const { data: user } = useFetch(
|
||||||
|
`/api/core/profile/${courseSession.value.id}/${props.userId}`
|
||||||
|
).json();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// if current route name not in pages, redirect to first page
|
||||||
|
if (route.name && !pages.value.find((page) => page.route === route.name)) {
|
||||||
|
router.push({
|
||||||
|
name: pages.value[0].route,
|
||||||
|
params: { userId: props.userId, courseSlug: props.courseSlug },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="user" class="flex flex-col bg-gray-200">
|
||||||
|
<div class="relative border-b bg-white shadow-md">
|
||||||
|
<div class="container-large pb-0">
|
||||||
|
<router-link
|
||||||
|
class="btn-text inline-flex items-center pl-0"
|
||||||
|
:to="`/course/${props.courseSlug}/cockpit`"
|
||||||
|
>
|
||||||
|
<it-icon-arrow-left />
|
||||||
|
<span>{{ $t("general.back") }}</span>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<div class="mb-12 mt-2 flex items-center">
|
||||||
|
<img class="mr-8 h-48 w-48 rounded-full" :src="user.avatar_url" />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h2 class="mb-2">{{ user.first_name }} {{ user.last_name }}</h2>
|
||||||
|
<p class="mb-2">{{ user.email }}</p>
|
||||||
|
<p class="text-gray-800">{{ $t("a.Teilnehmer") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-row px-4 lg:px-8">
|
||||||
|
<li
|
||||||
|
v-for="page in pages"
|
||||||
|
:key="page.route"
|
||||||
|
class="relative top-px mr-12 pb-3"
|
||||||
|
:class="[route.name === page.route ? 'border-b-2 border-blue-900 pb-3' : '']"
|
||||||
|
>
|
||||||
|
<router-link :to="{ name: page.route }">
|
||||||
|
{{ page.label }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<router-view class="flex flex-grow py-0" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||||
|
import { useCourseDataWithCompletion } from "@/composables";
|
||||||
|
import CockpitProfileContent from "@/components/cockpit/profile/CockpitProfileContent.vue";
|
||||||
|
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||||
|
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import type { CircleType } from "@/types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userId: string;
|
||||||
|
courseSlug: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedCircle = ref();
|
||||||
|
|
||||||
|
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug, props.userId);
|
||||||
|
|
||||||
|
function selectCircle(circle: CircleType) {
|
||||||
|
selectedCircle.value = circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(lpQueryResult.learningPath, () => {
|
||||||
|
if (lpQueryResult.learningPath?.value?.topics?.length) {
|
||||||
|
selectCircle(lpQueryResult.learningPath.value.topics[0].circles[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CockpitProfileContent>
|
||||||
|
<template #side>
|
||||||
|
<div
|
||||||
|
v-for="topic in lpQueryResult.learningPath?.value?.topics ?? []"
|
||||||
|
:key="topic.id"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<h4 class="mb-1 font-semibold text-gray-800">
|
||||||
|
{{ topic.title }}
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
v-for="circle in topic.circles"
|
||||||
|
:key="circle.id"
|
||||||
|
class="flex w-full items-center space-x-2 p-2 pr-4 hover:bg-gray-200 lg:pr-8"
|
||||||
|
:class="{ 'bg-gray-200': selectedCircle === circle }"
|
||||||
|
@click="selectCircle(circle)"
|
||||||
|
>
|
||||||
|
<LearningPathCircle
|
||||||
|
:sectors="calculateCircleSectorData(circle)"
|
||||||
|
class="h-10 w-10 snap-center rounded-full bg-white p-0.5"
|
||||||
|
></LearningPathCircle>
|
||||||
|
<span>{{ circle.title }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #main>
|
||||||
|
<ol v-if="selectedCircle" class="flex-auto bg-gray-200 px-6 py-4 lg:px-16">
|
||||||
|
<li
|
||||||
|
v-for="learningSequence in selectedCircle.learning_sequences ?? []"
|
||||||
|
:key="learningSequence.id"
|
||||||
|
>
|
||||||
|
<LearningSequence
|
||||||
|
:course-slug="props.courseSlug"
|
||||||
|
:circle="selectedCircle"
|
||||||
|
:learning-sequence="learningSequence"
|
||||||
|
readonly
|
||||||
|
hide-links
|
||||||
|
></LearningSequence>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</template>
|
||||||
|
</CockpitProfileContent>
|
||||||
|
</template>
|
||||||
|
|
@ -32,6 +32,7 @@ type Props = {
|
||||||
learningSequence: LearningSequence;
|
learningSequence: LearningSequence;
|
||||||
circle: CircleType;
|
circle: CircleType;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
hideLinks?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -229,7 +230,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="belongsToCompetenceCertificate(learningContent)"
|
v-if="belongsToCompetenceCertificate(learningContent) && !hideLinks"
|
||||||
class="ml-16 text-sm text-gray-800"
|
class="ml-16 text-sm text-gray-800"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
|
|
@ -298,11 +299,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
||||||
<div>{{ $t("a.Selbsteinschätzung") }}</div>
|
<div>{{ $t("a.Selbsteinschätzung") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />-->
|
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,8 @@ function handleFinishedLearningContent() {
|
||||||
props.learningContent,
|
props.learningContent,
|
||||||
props.circle,
|
props.circle,
|
||||||
previousRoute,
|
previousRoute,
|
||||||
(lc: LearningContentWithCompletion) => {
|
async (lc: LearningContentWithCompletion) => {
|
||||||
courseCompletionData.markCompletion(lc, "SUCCESS");
|
await courseCompletionData.markCompletion(lc, "SUCCESS");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,9 +221,19 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "profile/:userId",
|
path: "profile/:userId",
|
||||||
component: () => import("@/pages/cockpit/CockpitUserProfilePage.vue"),
|
component: () =>
|
||||||
|
import("@/pages/cockpit/profilePage/CockpitUserProfilePage.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
name: "cockpitUserProfile",
|
name: "cockpitUserProfile",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "learning-path",
|
||||||
|
component: () =>
|
||||||
|
import("@/pages/cockpit/profilePage/LearningPathProfilePage.vue"),
|
||||||
|
props: true,
|
||||||
|
name: "cockpitProfileLearningPath",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "profile/:userId/:circleSlug",
|
path: "profile/:userId/:circleSlug",
|
||||||
|
|
|
||||||
|
|
@ -73,16 +73,18 @@ export const useCircleStore = defineStore({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
continueFromLearningContent(
|
async continueFromLearningContent(
|
||||||
currentLearningContent: LearningContentWithCompletion,
|
currentLearningContent: LearningContentWithCompletion,
|
||||||
circle: CircleType,
|
circle: CircleType,
|
||||||
returnRoute?: RouteLocationNormalized,
|
returnRoute?: RouteLocationNormalized,
|
||||||
markCompletionFn?: (learningContent: LearningContentWithCompletion) => void
|
markCompletionFn?: (
|
||||||
|
learningContent: LearningContentWithCompletion
|
||||||
|
) => Promise<void>
|
||||||
) {
|
) {
|
||||||
if (currentLearningContent) {
|
if (currentLearningContent) {
|
||||||
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
||||||
if (markCompletionFn) {
|
if (markCompletionFn) {
|
||||||
markCompletionFn(currentLearningContent);
|
await markCompletionFn(currentLearningContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.closeLearningContent(currentLearningContent, circle, returnRoute);
|
this.closeLearningContent(currentLearningContent, circle, returnRoute);
|
||||||
|
|
|
||||||
|
|
@ -9,71 +9,71 @@ describe("circle.cy.js", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open circle page", () => {
|
it("can open circle page", () => {
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can toggle learning content", () => {
|
it("can toggle learning content", () => {
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).should("have.class", "cy-unchecked");
|
).should("have.class", "cy-unchecked");
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).click();
|
).click();
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
|
|
||||||
// completion data should still be there after reload
|
// completion data should still be there after reload
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open learning contents and complete them by continuing", () => {
|
it("can open learning contents and complete them by continuing", () => {
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick\"]"
|
||||||
).click();
|
).click();
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Verschaffe dir einen Überblick"
|
"Verschaffe dir einen Überblick"
|
||||||
);
|
);
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
|
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get("[data-cy=\"ls-continue-button\"]").click({ force: true });
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox\"]"
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("continue button works", () => {
|
it("continue button works", () => {
|
||||||
cy.get('[data-cy="ls-continue-button"]').should("contain", "Los geht's");
|
cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Los geht's");
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
||||||
|
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Verschaffe dir einen Überblick"
|
"Verschaffe dir einen Überblick"
|
||||||
);
|
);
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||||
|
|
||||||
cy.get('[data-cy="ls-continue-button"]').should("contain", "Weiter geht's");
|
cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Weiter geht's");
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
|
|
@ -81,43 +81,43 @@ describe("circle.cy.js", () => {
|
||||||
|
|
||||||
it("can open learning content by url", () => {
|
it("can open learning content by url", () => {
|
||||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
|
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('[data-cy="close-learning-content"]').click();
|
cy.get("[data-cy=\"close-learning-content\"]").click();
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checks number of sequences and contents", () => {
|
it("checks number of sequences and contents", () => {
|
||||||
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
|
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
|
||||||
cy.get('[data-cy="lp-learning-sequence"]')
|
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||||
.first()
|
.first()
|
||||||
.should("contain", "Vorbereitung");
|
.should("contain", "Vorbereitung");
|
||||||
cy.get('[data-cy="lp-learning-sequence"]')
|
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.should("contain", "Training");
|
.should("contain", "Training");
|
||||||
cy.get('[data-cy="lp-learning-sequence"]')
|
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||||
.last()
|
.last()
|
||||||
.should("contain", "Transfer");
|
.should("contain", "Transfer");
|
||||||
|
|
||||||
cy.get('[data-cy="lp-learning-content"]').should("have.length", 10);
|
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 10);
|
||||||
cy.get('[data-cy="lp-learning-content"]')
|
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||||
.first()
|
.first()
|
||||||
.should("contain", "Verschaffe dir einen Überblick");
|
.should("contain", "Verschaffe dir einen Überblick");
|
||||||
cy.get('[data-cy="lp-learning-content"]')
|
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||||
.eq(4)
|
.eq(4)
|
||||||
.should("contain", "Präsenzkurs Fahrzeug");
|
.should("contain", "Präsenzkurs Fahrzeug");
|
||||||
cy.get('[data-cy="lp-learning-content"]')
|
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||||
.eq(7)
|
.eq(7)
|
||||||
.should("contain", "Reflexion");
|
.should("contain", "Reflexion");
|
||||||
cy.get('[data-cy="lp-learning-content"]')
|
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||||
.last()
|
.last()
|
||||||
.should("contain", "Feedback");
|
.should("contain", "Feedback");
|
||||||
|
|
||||||
cy.visit("/course/test-lehrgang/learn/reisen");
|
cy.visit("/course/test-lehrgang/learn/reisen");
|
||||||
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
|
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
|
||||||
cy.get('[data-cy="lp-learning-content"]').should("have.length", 9);
|
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 9);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,12 @@ from django_ratelimit.exceptions import Ratelimited
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
from vbv_lernwelt.api.directory import list_entities
|
from vbv_lernwelt.api.directory import list_entities
|
||||||
from vbv_lernwelt.api.user import get_cockpit_type, me_user_view, post_avatar
|
from vbv_lernwelt.api.user import (
|
||||||
|
get_cockpit_type,
|
||||||
|
get_profile,
|
||||||
|
me_user_view,
|
||||||
|
post_avatar,
|
||||||
|
)
|
||||||
from vbv_lernwelt.assignment.views import request_assignment_completion_status
|
from vbv_lernwelt.assignment.views import request_assignment_completion_status
|
||||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||||
from vbv_lernwelt.core.schema import schema
|
from vbv_lernwelt.core.schema import schema
|
||||||
|
|
@ -102,6 +107,7 @@ urlpatterns = [
|
||||||
re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
|
re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
|
||||||
re_path(r'api/core/avatar/$', post_avatar, name='post_avatar'),
|
re_path(r'api/core/avatar/$', post_avatar, name='post_avatar'),
|
||||||
re_path(r'api/core/entities/$', list_entities, name='list_entities'),
|
re_path(r'api/core/entities/$', list_entities, name='list_entities'),
|
||||||
|
path(r'api/core/profile/<signed_int:course_session_id>/<uuid:user_id>', get_profile, name='get_profile_view'),
|
||||||
|
|
||||||
re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login),
|
re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login),
|
||||||
name='vue_login'),
|
name='vue_login'),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_user,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileViewTest(APITestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.course, _ = create_course("Test Course")
|
||||||
|
self.course_session = create_course_session(
|
||||||
|
course=self.course, title="Test Session"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user = create_user("user")
|
||||||
|
add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
self.user,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_user_profile(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
url = reverse(
|
||||||
|
"get_profile_view",
|
||||||
|
kwargs={
|
||||||
|
"course_session_id": self.course_session.id,
|
||||||
|
"user_id": self.user.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
profile = response.data
|
||||||
|
self.assertEqual(
|
||||||
|
profile,
|
||||||
|
{
|
||||||
|
"id": str(self.user.id),
|
||||||
|
"first_name": self.user.first_name,
|
||||||
|
"last_name": self.user.last_name,
|
||||||
|
"email": self.user.email,
|
||||||
|
"username": self.user.username,
|
||||||
|
"avatar_url": "/static/avatars/myvbv-default-avatar.png",
|
||||||
|
"organisation": None,
|
||||||
|
"is_superuser": False,
|
||||||
|
"course_session_experts": [],
|
||||||
|
"language": "de",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -7,6 +7,7 @@ from rest_framework.response import Response
|
||||||
from vbv_lernwelt.core.serializers import UserSerializer
|
from vbv_lernwelt.core.serializers import UserSerializer
|
||||||
from vbv_lernwelt.course.models import Course, CourseSessionUser
|
from vbv_lernwelt.course.models import Course, CourseSessionUser
|
||||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.iam.permissions import can_view_profile
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
from vbv_lernwelt.media_files.models import UserImage
|
from vbv_lernwelt.media_files.models import UserImage
|
||||||
|
|
||||||
|
|
@ -61,6 +62,19 @@ def get_cockpit_type(request, course_id: int):
|
||||||
return Response({"type": cockpit_type})
|
return Response({"type": cockpit_type})
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET"])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def get_profile(request, course_session_id: int, user_id: str):
|
||||||
|
course_session_user = get_object_or_404(
|
||||||
|
CourseSessionUser, course_session_id=course_session_id, user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not can_view_profile(request.user, course_session_user):
|
||||||
|
return Response(status=403)
|
||||||
|
|
||||||
|
return Response(UserSerializer(course_session_user.user).data)
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def post_avatar(request):
|
def post_avatar(request):
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
from vbv_lernwelt.files.models import UploadFile
|
from vbv_lernwelt.files.models import UploadFile
|
||||||
from vbv_lernwelt.files.services import FileDirectUploadService
|
from vbv_lernwelt.files.services import FileDirectUploadService
|
||||||
from vbv_lernwelt.iam.permissions import (
|
from vbv_lernwelt.iam.permissions import (
|
||||||
|
can_view_course_completions,
|
||||||
course_sessions_for_user_qs,
|
course_sessions_for_user_qs,
|
||||||
has_course_access,
|
has_course_access,
|
||||||
has_course_access_by_page_request,
|
has_course_access_by_page_request,
|
||||||
is_circle_expert,
|
is_circle_expert,
|
||||||
is_course_session_expert,
|
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
|
|
||||||
|
|
@ -76,8 +76,8 @@ def request_course_completion(request, course_session_id):
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def request_course_completion_for_user(request, course_session_id, user_id):
|
def request_course_completion_for_user(request, course_session_id, user_id):
|
||||||
if request.user.id == user_id or is_course_session_expert(
|
if can_view_course_completions(
|
||||||
request.user, course_session_id
|
user=request.user, course_session_id=course_session_id, target_user_id=user_id
|
||||||
):
|
):
|
||||||
return _request_course_completion(course_session_id, user_id)
|
return _request_course_completion(course_session_id, user_id)
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,14 @@ def is_course_session_expert(user, course_session_id: int):
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
course_session = CourseSession.objects.get(id=course_session_id)
|
||||||
|
|
||||||
is_supervisor = CourseSessionGroup.objects.filter(
|
is_supervisor = CourseSessionGroup.objects.filter(
|
||||||
supervisor=user, course_session__id=course_session_id
|
supervisor=user, course_session=course_session
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
is_expert = CourseSessionUser.objects.filter(
|
is_expert = CourseSessionUser.objects.filter(
|
||||||
course_session_id=course_session_id,
|
course_session=course_session,
|
||||||
user=user,
|
user=user,
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
@ -174,3 +176,34 @@ def has_role_in_course(user: User, course: Course) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool:
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if user == profile_user.user:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if is_course_session_expert(user, profile_user.course_session.id) or is_user_mentor(
|
||||||
|
mentor=user,
|
||||||
|
participant_user_id=profile_user.user.id,
|
||||||
|
course_session_id=profile_user.course_session.id,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def can_view_course_completions(
|
||||||
|
user: User, course_session_id: int, target_user_id: str
|
||||||
|
) -> bool:
|
||||||
|
return (
|
||||||
|
user.id == target_user_id
|
||||||
|
or is_course_session_expert(user=user, course_session_id=course_session_id)
|
||||||
|
or is_user_mentor(
|
||||||
|
mentor=user,
|
||||||
|
participant_user_id=target_user_id,
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.iam.permissions import is_course_session_expert
|
||||||
|
|
||||||
|
|
||||||
|
class ExpertTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.course, _ = create_course("Test Course")
|
||||||
|
self.course_session = create_course_session(
|
||||||
|
course=self.course, title="Test Session"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user = create_user("user")
|
||||||
|
|
||||||
|
def test_member(self):
|
||||||
|
# GIVEN
|
||||||
|
csu = CourseSessionUser.objects.create(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=self.user,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
is_expert = is_course_session_expert(
|
||||||
|
user=csu.user, course_session_id=self.course_session.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertFalse(is_expert)
|
||||||
|
|
||||||
|
def test_supervisor(self):
|
||||||
|
# GIVEN
|
||||||
|
csg = CourseSessionGroup.objects.create(name="Test Group", course=self.course)
|
||||||
|
csg.course_session.add(self.course_session)
|
||||||
|
csg.supervisor.add(self.user)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
is_expert = is_course_session_expert(
|
||||||
|
user=self.user, course_session_id=self.course_session.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(is_expert)
|
||||||
|
|
||||||
|
def test_expert(self):
|
||||||
|
# GIVEN
|
||||||
|
csu = CourseSessionUser.objects.create(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=self.user,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
is_expert = is_course_session_expert(
|
||||||
|
user=csu.user, course_session_id=self.course_session.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(is_expert)
|
||||||
Loading…
Reference in New Issue