Merged develop into feature/VBV-635-invalid-emails
This commit is contained in:
commit
b1631b6b28
|
|
@ -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 }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <router-link :to="{name: 'cockpitUserProfile', params: {userId: participant.id}}" class="underline">-->
|
||||
<!-- {{ $t("a.Profil anzeigen") }}-->
|
||||
<!-- </router-link>-->
|
||||
<router-link
|
||||
:to="{ name: 'cockpitUserProfile', params: { userId: participant.id } }"
|
||||
class="underline"
|
||||
>
|
||||
{{ $t("cockpit.profileLink") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</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;
|
||||
circle: CircleType;
|
||||
readonly?: boolean;
|
||||
hideLinks?: boolean;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
@ -229,7 +230,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="belongsToCompetenceCertificate(learningContent)"
|
||||
v-if="belongsToCompetenceCertificate(learningContent) && !hideLinks"
|
||||
class="ml-16 text-sm text-gray-800"
|
||||
>
|
||||
{{
|
||||
|
|
@ -298,11 +299,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
<div>{{ $t("a.Selbsteinschätzung") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />-->
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ function handleFinishedLearningContent() {
|
|||
props.learningContent,
|
||||
props.circle,
|
||||
previousRoute,
|
||||
(lc: LearningContentWithCompletion) => {
|
||||
courseCompletionData.markCompletion(lc, "SUCCESS");
|
||||
async (lc: LearningContentWithCompletion) => {
|
||||
await courseCompletionData.markCompletion(lc, "SUCCESS");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,9 +221,19 @@ const router = createRouter({
|
|||
},
|
||||
{
|
||||
path: "profile/:userId",
|
||||
component: () => import("@/pages/cockpit/CockpitUserProfilePage.vue"),
|
||||
component: () =>
|
||||
import("@/pages/cockpit/profilePage/CockpitUserProfilePage.vue"),
|
||||
props: true,
|
||||
name: "cockpitUserProfile",
|
||||
children: [
|
||||
{
|
||||
path: "learning-path",
|
||||
component: () =>
|
||||
import("@/pages/cockpit/profilePage/LearningPathProfilePage.vue"),
|
||||
props: true,
|
||||
name: "cockpitProfileLearningPath",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "profile/:userId/:circleSlug",
|
||||
|
|
|
|||
|
|
@ -73,16 +73,18 @@ export const useCircleStore = defineStore({
|
|||
});
|
||||
}
|
||||
},
|
||||
continueFromLearningContent(
|
||||
async continueFromLearningContent(
|
||||
currentLearningContent: LearningContentWithCompletion,
|
||||
circle: CircleType,
|
||||
returnRoute?: RouteLocationNormalized,
|
||||
markCompletionFn?: (learningContent: LearningContentWithCompletion) => void
|
||||
markCompletionFn?: (
|
||||
learningContent: LearningContentWithCompletion
|
||||
) => Promise<void>
|
||||
) {
|
||||
if (currentLearningContent) {
|
||||
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
||||
if (markCompletionFn) {
|
||||
markCompletionFn(currentLearningContent);
|
||||
await markCompletionFn(currentLearningContent);
|
||||
}
|
||||
}
|
||||
this.closeLearningContent(currentLearningContent, circle, returnRoute);
|
||||
|
|
|
|||
|
|
@ -9,71 +9,71 @@ describe("circle.cy.js", () => {
|
|||
});
|
||||
|
||||
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", () => {
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
|
||||
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");
|
||||
|
||||
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();
|
||||
|
||||
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");
|
||||
|
||||
// completion data should still be there after reload
|
||||
cy.reload();
|
||||
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");
|
||||
});
|
||||
|
||||
it("can open learning contents and complete them by continuing", () => {
|
||||
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();
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
"contain",
|
||||
"Verschaffe dir einen Überblick"
|
||||
);
|
||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
|
||||
cy.get('[data-cy="ls-continue-button"]').click();
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").click({ force: true });
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
"contain",
|
||||
"Handlungsfeld «Fahrzeug»"
|
||||
);
|
||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
|
||||
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");
|
||||
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");
|
||||
});
|
||||
|
||||
it("continue button works", () => {
|
||||
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\"]").should("contain", "Los geht's");
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
||||
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
"contain",
|
||||
"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"]').click();
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Weiter geht's");
|
||||
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
"contain",
|
||||
"Handlungsfeld «Fahrzeug»"
|
||||
);
|
||||
|
|
@ -81,43 +81,43 @@ describe("circle.cy.js", () => {
|
|||
|
||||
it("can open learning content by url", () => {
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
cy.get("[data-cy=\"lc-title\"]").should(
|
||||
"contain",
|
||||
"Handlungsfeld «Fahrzeug»"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="close-learning-content"]').click();
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
cy.get("[data-cy=\"close-learning-content\"]").click();
|
||||
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||
});
|
||||
|
||||
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"]')
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||
.first()
|
||||
.should("contain", "Vorbereitung");
|
||||
cy.get('[data-cy="lp-learning-sequence"]')
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||
.eq(1)
|
||||
.should("contain", "Training");
|
||||
cy.get('[data-cy="lp-learning-sequence"]')
|
||||
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||
.last()
|
||||
.should("contain", "Transfer");
|
||||
|
||||
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\"]").should("have.length", 10);
|
||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||
.first()
|
||||
.should("contain", "Verschaffe dir einen Überblick");
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||
.eq(4)
|
||||
.should("contain", "Präsenzkurs Fahrzeug");
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||
.eq(7)
|
||||
.should("contain", "Reflexion");
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||
.last()
|
||||
.should("contain", "Feedback");
|
||||
|
||||
cy.visit("/course/test-lehrgang/learn/reisen");
|
||||
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-sequence\"]").should("have.length", 3);
|
||||
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 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.core.middleware.auth import django_view_authentication_exempt
|
||||
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/avatar/$', post_avatar, name='post_avatar'),
|
||||
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),
|
||||
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.course.models import Course, CourseSessionUser
|
||||
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.media_files.models import UserImage
|
||||
|
||||
|
|
@ -61,6 +62,19 @@ def get_cockpit_type(request, course_id: int):
|
|||
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"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
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.services import FileDirectUploadService
|
||||
from vbv_lernwelt.iam.permissions import (
|
||||
can_view_course_completions,
|
||||
course_sessions_for_user_qs,
|
||||
has_course_access,
|
||||
has_course_access_by_page_request,
|
||||
is_circle_expert,
|
||||
is_course_session_expert,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
|
||||
|
|
@ -76,8 +76,8 @@ def request_course_completion(request, course_session_id):
|
|||
|
||||
@api_view(["GET"])
|
||||
def request_course_completion_for_user(request, course_session_id, user_id):
|
||||
if request.user.id == user_id or is_course_session_expert(
|
||||
request.user, course_session_id
|
||||
if can_view_course_completions(
|
||||
user=request.user, course_session_id=course_session_id, target_user_id=user_id
|
||||
):
|
||||
return _request_course_completion(course_session_id, user_id)
|
||||
raise PermissionDenied()
|
||||
|
|
|
|||
|
|
@ -52,12 +52,14 @@ def is_course_session_expert(user, course_session_id: int):
|
|||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
course_session = CourseSession.objects.get(id=course_session_id)
|
||||
|
||||
is_supervisor = CourseSessionGroup.objects.filter(
|
||||
supervisor=user, course_session__id=course_session_id
|
||||
supervisor=user, course_session=course_session
|
||||
).exists()
|
||||
|
||||
is_expert = CourseSessionUser.objects.filter(
|
||||
course_session_id=course_session_id,
|
||||
course_session=course_session,
|
||||
user=user,
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
).exists()
|
||||
|
|
@ -174,3 +176,34 @@ def has_role_in_course(user: User, course: Course) -> bool:
|
|||
return True
|
||||
|
||||
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