Merged develop into feature/VBV-635-invalid-emails

This commit is contained in:
Christian Cueni 2024-01-24 06:08:38 +00:00
commit b1631b6b28
16 changed files with 405 additions and 173 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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");
}
);
}

View File

@ -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",

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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'),

View File

@ -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",
},
)

View File

@ -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):

View File

@ -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()

View File

@ -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,
)
)

View File

@ -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)