Merged in feature/VBV-597-umsetzung-cockpit-lernbegleitung (pull request #248)
Cockpit & Management Lernbegleitung Approved-by: Daniel Egger
This commit is contained in:
commit
cbc89b7641
|
|
@ -0,0 +1,54 @@
|
|||
<script setup lang="ts">
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
taskTitle: string;
|
||||
circleTitle: string;
|
||||
pendingTasks: number;
|
||||
pendingTasksLabel: string;
|
||||
taskLink: RouteLocationRaw;
|
||||
taskLinkLabel: string;
|
||||
taskLinkPendingLabel: string;
|
||||
}>();
|
||||
|
||||
const linkLabel = computed(() => {
|
||||
if (!props.taskLinkPendingLabel) {
|
||||
return props.taskLinkLabel;
|
||||
}
|
||||
return props.pendingTasks > 0 ? props.taskLinkPendingLabel : props.taskLinkLabel;
|
||||
});
|
||||
|
||||
const hasPendingTasks = computed(() => props.pendingTasks > 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-start justify-between gap-4 border-b py-2 pl-5 pr-5 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
|
||||
>
|
||||
<div class="flex flex-grow flex-row items-center justify-start">
|
||||
<div class="w-80">
|
||||
<div class="font-bold">{{ taskTitle }}</div>
|
||||
<div class="text-small text-gray-900">
|
||||
{{ $t("a.Circle") }} «{{ circleTitle }}»
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-grow flex-row items-center justify-start space-x-2 pl-20">
|
||||
<template v-if="hasPendingTasks">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full border-2 border-green-500 px-3 py-1 text-sm font-bold"
|
||||
>
|
||||
<span>{{ pendingTasks }}</span>
|
||||
</div>
|
||||
<span>{{ pendingTasksLabel }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<router-link
|
||||
:class="[hasPendingTasks ? 'btn-primary' : 'underline']"
|
||||
:to="taskLink"
|
||||
>
|
||||
{{ linkLabel }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import AssignmentItem from "@/components/cockpit/mentor/AssignmentItem.vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
|
||||
defineProps<{
|
||||
taskTitle: string;
|
||||
circleTitle: string;
|
||||
pendingTasks: number;
|
||||
taskLink: RouteLocationRaw;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AssignmentItem
|
||||
:task-title="`${$t('a.Praxisauftrag')}: ${taskTitle}`"
|
||||
:circle-title="circleTitle"
|
||||
:pending-tasks="pendingTasks"
|
||||
:task-link="taskLink"
|
||||
:pending-tasks-label="$t('a.Ergebnisse abgegeben')"
|
||||
:task-link-pending-label="$t('a.Ergebnisse bewerten')"
|
||||
:task-link-label="$t('a.Praxisaufträge anschauen')"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -18,9 +18,12 @@ import CoursePreviewBar from "@/components/header/CoursePreviewBar.vue";
|
|||
import {
|
||||
getCockpitUrl,
|
||||
getCompetenceNaviUrl,
|
||||
getLearningMentorManagementUrl,
|
||||
getLearningPathUrl,
|
||||
getMediaCenterUrl,
|
||||
} from "@/utils/utils";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { VV_COURSE_IDS } from "@/constants";
|
||||
|
||||
log.debug("MainNavigationBar created");
|
||||
|
||||
|
|
@ -31,6 +34,7 @@ const notificationsStore = useNotificationsStore();
|
|||
const {
|
||||
inCockpit,
|
||||
inCompetenceProfile,
|
||||
inLearningMentor,
|
||||
inCourse,
|
||||
inLearningPath,
|
||||
inMediaLibrary,
|
||||
|
|
@ -66,6 +70,42 @@ const appointmentsUrl = computed(() => {
|
|||
onMounted(() => {
|
||||
log.debug("MainNavigationBar mounted");
|
||||
});
|
||||
|
||||
const hasMediaLibraryMenu = computed(() => {
|
||||
if (useCockpitStore().hasMentorCockpitType) {
|
||||
return false;
|
||||
}
|
||||
return inCourse() && Boolean(courseSessionsStore.currentCourseSession);
|
||||
});
|
||||
|
||||
const hasCockpitMenu = computed(() => {
|
||||
return courseSessionsStore.currentCourseSessionHasCockpit;
|
||||
});
|
||||
|
||||
const hasPreviewMenu = computed(() => {
|
||||
return useCockpitStore().hasExpertCockpitType;
|
||||
});
|
||||
|
||||
const hasAppointmentsMenu = computed(() => {
|
||||
if (useCockpitStore().hasMentorCockpitType) {
|
||||
return false;
|
||||
}
|
||||
return userStore.loggedIn;
|
||||
});
|
||||
|
||||
const hasNotificationsMenu = computed(() => {
|
||||
return userStore.loggedIn;
|
||||
});
|
||||
|
||||
const hasMentorManagementMenu = computed(() => {
|
||||
if (courseSessionsStore.currentCourseSessionHasCockpit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// learning mentor management is only available for VV courses
|
||||
const currentCourseId = courseSessionsStore.currentCourseSession?.course.id || "";
|
||||
return inCourse() && VV_COURSE_IDS.includes(currentCourseId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -76,6 +116,9 @@ onMounted(() => {
|
|||
v-if="userStore.loggedIn"
|
||||
:show="state.showMobileNavigationMenu"
|
||||
:course-session="courseSessionsStore.currentCourseSession"
|
||||
:has-media-library-menu="hasMediaLibraryMenu"
|
||||
:has-cockpit-menu="hasCockpitMenu"
|
||||
:has-preview-menu="hasPreviewMenu"
|
||||
:media-url="
|
||||
getMediaCenterUrl(courseSessionsStore.currentCourseSession?.course?.slug)
|
||||
"
|
||||
|
|
@ -135,6 +178,7 @@ onMounted(() => {
|
|||
<div class="hidden space-x-8 lg:flex">
|
||||
<template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
|
||||
<router-link
|
||||
v-if="hasCockpitMenu"
|
||||
data-cy="navigation-cockpit-link"
|
||||
:to="
|
||||
getCockpitUrl(
|
||||
|
|
@ -148,6 +192,7 @@ onMounted(() => {
|
|||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="hasPreviewMenu"
|
||||
data-cy="navigation-preview-link"
|
||||
:to="
|
||||
getLearningPathUrl(
|
||||
|
|
@ -189,6 +234,20 @@ onMounted(() => {
|
|||
>
|
||||
{{ t("competences.title") }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="hasMentorManagementMenu"
|
||||
data-cy="navigation-learning-mentor-link"
|
||||
:to="
|
||||
getLearningMentorManagementUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
)
|
||||
"
|
||||
class="nav-item"
|
||||
:class="{ 'nav-item--active': inLearningMentor() }"
|
||||
>
|
||||
{{ t("a.Lernbegleitung") }}
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -196,10 +255,10 @@ onMounted(() => {
|
|||
|
||||
<div class="flex items-stretch justify-start space-x-8">
|
||||
<router-link
|
||||
v-if="inCourse() && courseSessionsStore.currentCourseSession"
|
||||
v-if="hasMediaLibraryMenu"
|
||||
:to="
|
||||
getMediaCenterUrl(
|
||||
courseSessionsStore.currentCourseSession.course.slug
|
||||
courseSessionsStore.currentCourseSession?.course.slug
|
||||
)
|
||||
"
|
||||
data-cy="medialibrary-link"
|
||||
|
|
@ -210,7 +269,7 @@ onMounted(() => {
|
|||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="userStore.loggedIn"
|
||||
v-if="hasAppointmentsMenu"
|
||||
:to="appointmentsUrl"
|
||||
data-cy="all-duedates-link"
|
||||
class="nav-item"
|
||||
|
|
@ -220,7 +279,7 @@ onMounted(() => {
|
|||
</router-link>
|
||||
|
||||
<!-- Notification Bell & Menu -->
|
||||
<div v-if="userStore.loggedIn" class="nav-item leading-none">
|
||||
<div v-if="hasNotificationsMenu" class="nav-item leading-none">
|
||||
<NotificationPopover>
|
||||
<template #toggleButtonContent>
|
||||
<div class="relative h-8 w-8">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ const router = useRouter();
|
|||
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
hasMediaLibraryMenu: boolean;
|
||||
hasPreviewMenu: boolean;
|
||||
hasCockpitMenu: boolean;
|
||||
courseSession: CourseSession | undefined;
|
||||
mediaUrl?: string;
|
||||
user: UserState | undefined;
|
||||
|
|
@ -62,7 +65,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
<h4 class="text-sm text-gray-900">{{ courseSession.course.title }}</h4>
|
||||
<ul class="mt-6">
|
||||
<template v-if="courseSessionsStore.currentCourseSessionHasCockpit">
|
||||
<li class="mb-6">
|
||||
<li v-if="hasCockpitMenu" class="mb-6">
|
||||
<button
|
||||
data-cy="navigation-mobile-cockpit-link"
|
||||
@click="clickLink(getCockpitUrl(courseSession.course.slug))"
|
||||
|
|
@ -70,7 +73,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
{{ $t("cockpit.title") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="mb-6">
|
||||
<li v-if="hasPreviewMenu" class="mb-6">
|
||||
<button
|
||||
data-cy="navigation-mobile-preview-link"
|
||||
@click="clickLink(getLearningPathUrl(courseSession.course.slug))"
|
||||
|
|
@ -97,7 +100,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
</button>
|
||||
</li>
|
||||
</template>
|
||||
<li class="mb-6">
|
||||
<li v-if="hasMediaLibraryMenu" class="mb-6">
|
||||
<button
|
||||
data-cy="medialibrary-link"
|
||||
@click="clickLink(getMediaCenterUrl(courseSession.course.slug))"
|
||||
|
|
@ -110,9 +113,7 @@ const courseSessionsStore = useCourseSessionsStore();
|
|||
<div class="mt-6 border-b">
|
||||
<ul>
|
||||
<li class="mb-6">
|
||||
<button data-cy="medialibrary-link" @click="clickLink('/')">
|
||||
myVBV
|
||||
</button>
|
||||
<button data-cy="dashboard-link" @click="clickLink('/')">myVBV</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
<script setup lang="ts">
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import type { CourseSessionUserObjectsType } from "@/gql/graphql";
|
||||
import dayjs from "dayjs";
|
||||
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
||||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { bustItGetCache } from "@/fetchHelpers";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import log from "loglevel";
|
||||
import type { Assignment } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
evaluationDocumentUrl: string;
|
||||
submissionDeadlineStart?: string | null;
|
||||
circleExpert?: CourseSessionUserObjectsType;
|
||||
courseSessionId: string;
|
||||
assignment: Assignment;
|
||||
learningContentId: string;
|
||||
}>();
|
||||
|
||||
const state = reactive({
|
||||
confirmInput: false,
|
||||
confirmPerson: false,
|
||||
});
|
||||
|
||||
const circleExpertName = computed(() => {
|
||||
return `${props.circleExpert?.first_name} ${props.circleExpert?.last_name}`;
|
||||
});
|
||||
|
||||
const cannotSubmit = computed(() => {
|
||||
return !state.confirmInput || !state.confirmPerson;
|
||||
});
|
||||
|
||||
const upsertAssignmentCompletionMutation = useMutation(
|
||||
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
|
||||
);
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignment.id,
|
||||
courseSessionId: props.courseSessionId,
|
||||
learningContentId: props.learningContentId,
|
||||
completionDataString: JSON.stringify({}),
|
||||
completionStatus: "SUBMITTED",
|
||||
});
|
||||
bustItGetCache(
|
||||
`/api/course/completion/${props.courseSessionId}/${useUserStore().id}/`
|
||||
);
|
||||
// if solution sample is available, do not close the assigment automatically
|
||||
if (!props.assignment.solution_sample) {
|
||||
eventBus.emit("finishedLearningContent", true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Could not submit assignment", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ItCheckbox
|
||||
class="w-full border-b border-gray-400 py-10 sm:py-6"
|
||||
:checkbox-item="{
|
||||
label: $t('assignment.confirmSubmitResults'),
|
||||
value: 'value',
|
||||
checked: state.confirmInput,
|
||||
}"
|
||||
data-cy="confirm-submit-results"
|
||||
@toggle="state.confirmInput = !state.confirmInput"
|
||||
></ItCheckbox>
|
||||
<div class="w-full border-b border-gray-400">
|
||||
<ItCheckbox
|
||||
class="py-6"
|
||||
:checkbox-item="{
|
||||
label: $t('assignment.confirmSubmitPerson'),
|
||||
value: 'value',
|
||||
checked: state.confirmPerson,
|
||||
}"
|
||||
data-cy="confirm-submit-person"
|
||||
@toggle="state.confirmPerson = !state.confirmPerson"
|
||||
></ItCheckbox>
|
||||
<div v-if="circleExpert" class="flex flex-row items-center pb-6 pl-[49px]">
|
||||
<img
|
||||
alt="Notification icon"
|
||||
class="mr-2 h-[45px] min-w-[45px] rounded-full"
|
||||
:src="circleExpert.avatar_url"
|
||||
/>
|
||||
<p class="text-base font-bold">
|
||||
{{ circleExpertName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-x-2 pt-6 text-base sm:flex-row">
|
||||
<p>{{ $t("assignment.assessmentDocumentDisclaimer") }}</p>
|
||||
<a :href="evaluationDocumentUrl" class="underline">
|
||||
{{ $t("assignment.showAssessmentDocument") }}
|
||||
</a>
|
||||
</div>
|
||||
<p v-if="submissionDeadlineStart" class="pt-6">
|
||||
{{ $t("assignment.dueDateSubmission") }}
|
||||
<DateEmbedding
|
||||
:single-date="dayjs(props.submissionDeadlineStart)"
|
||||
></DateEmbedding>
|
||||
</p>
|
||||
<ItButton
|
||||
class="mt-6"
|
||||
variant="blue"
|
||||
size="large"
|
||||
:disabled="cannotSubmit"
|
||||
data-cy="submit-assignment"
|
||||
@click="onSubmit"
|
||||
>
|
||||
<p>{{ $t("assignment.submitAssignment") }}</p>
|
||||
</ItButton>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<script setup lang="ts">
|
||||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import { ref } from "vue";
|
||||
import { bustItGetCache, useCSRFFetch } from "@/fetchHelpers";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import log from "loglevel";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import type { Assignment } from "@/types";
|
||||
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
||||
const currentCourseSession = useCurrentCourseSession();
|
||||
|
||||
const props = defineProps<{
|
||||
submissionDeadlineStart?: string | null;
|
||||
courseSessionId: string;
|
||||
assignment: Assignment;
|
||||
learningContentId: string;
|
||||
}>();
|
||||
|
||||
const confirmPerson = ref(false);
|
||||
|
||||
const upsertAssignmentCompletionMutation = useMutation(
|
||||
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
|
||||
);
|
||||
|
||||
const { data: learningMentors } = useCSRFFetch(
|
||||
`/api/mentor/${props.courseSessionId}/mentors`
|
||||
).json();
|
||||
|
||||
const selectedLearningMentor = ref();
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignment.id,
|
||||
courseSessionId: props.courseSessionId,
|
||||
learningContentId: props.learningContentId,
|
||||
completionDataString: JSON.stringify({}),
|
||||
completionStatus: "SUBMITTED",
|
||||
evaluationUserId: selectedLearningMentor.value,
|
||||
});
|
||||
bustItGetCache(
|
||||
`/api/course/completion/${props.courseSessionId}/${useUserStore().id}/`
|
||||
);
|
||||
// if solution sample is available, do not close the assigment automatically
|
||||
if (!props.assignment.solution_sample) {
|
||||
eventBus.emit("finishedLearningContent", true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Could not submit assignment", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full border-b border-gray-400">
|
||||
<div v-if="learningMentors?.length" class="my-6">
|
||||
<ItCheckbox
|
||||
class="py-6"
|
||||
:checkbox-item="{
|
||||
label: $t('a.confirmSubmitPersonPraxisAssignment'),
|
||||
value: 'value',
|
||||
checked: confirmPerson,
|
||||
}"
|
||||
data-cy="confirm-submit-person"
|
||||
@toggle="confirmPerson = !confirmPerson"
|
||||
></ItCheckbox>
|
||||
|
||||
<div>
|
||||
<select v-model="selectedLearningMentor" data-cy="select-learning-mentor">
|
||||
<option
|
||||
v-for="learningMentor in learningMentors"
|
||||
:key="learningMentor.id"
|
||||
:value="learningMentor.mentor.id"
|
||||
>
|
||||
{{ learningMentor.mentor.first_name }} {{ learningMentor.mentor.last_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="my-6">
|
||||
<div class="flex space-x-2 bg-sky-200 p-4">
|
||||
<it-icon-info class="it-icon h-6 w-6 text-sky-700" />
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
{{
|
||||
$t(
|
||||
"a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen. Lade jetzt jemanden ein."
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'learningMentorManagement',
|
||||
params: { courseSlug: currentCourseSession.course.slug },
|
||||
}"
|
||||
class="btn-blue px-4 py-2 font-bold"
|
||||
>
|
||||
{{ $t("a.Lernbegleitung einladen") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="props.submissionDeadlineStart" class="pt-6">
|
||||
{{ $t("assignment.dueDateSubmission") }}
|
||||
<DateEmbedding :single-date="dayjs(props.submissionDeadlineStart)"></DateEmbedding>
|
||||
</p>
|
||||
<ItButton
|
||||
class="mt-6"
|
||||
variant="primary"
|
||||
size="large"
|
||||
:disabled="!confirmPerson || !selectedLearningMentor"
|
||||
data-cy="submit-assignment"
|
||||
@click="onSubmit"
|
||||
>
|
||||
<p>{{ $t("a.Ergebnisse teilen") }}</p>
|
||||
</ItButton>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<script setup lang="ts">
|
||||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import { ref } from "vue";
|
||||
import { bustItGetCache } from "@/fetchHelpers";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import log from "loglevel";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import type { Assignment } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSessionId: string;
|
||||
assignment: Assignment;
|
||||
learningContentId: string;
|
||||
}>();
|
||||
|
||||
const upsertAssignmentCompletionMutation = useMutation(
|
||||
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
|
||||
);
|
||||
|
||||
const confirmInput = ref(false);
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignment.id,
|
||||
courseSessionId: props.courseSessionId,
|
||||
learningContentId: props.learningContentId,
|
||||
completionDataString: JSON.stringify({}),
|
||||
completionStatus: "SUBMITTED",
|
||||
});
|
||||
bustItGetCache(
|
||||
`/api/course/completion/${props.courseSessionId}/${useUserStore().id}/`
|
||||
);
|
||||
// if solution sample is available, do not close the assigment automatically
|
||||
if (!props.assignment.solution_sample) {
|
||||
eventBus.emit("finishedLearningContent", true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Could not submit assignment", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ItCheckbox
|
||||
class="w-full border-b border-gray-400 py-10 sm:py-6"
|
||||
:checkbox-item="{
|
||||
label: $t('assignment.confirmSubmitResults'),
|
||||
value: 'value',
|
||||
checked: confirmInput,
|
||||
}"
|
||||
data-cy="confirm-submit-results"
|
||||
@toggle="confirmInput = !confirmInput"
|
||||
></ItCheckbox>
|
||||
<ItButton
|
||||
class="mt-6"
|
||||
variant="blue"
|
||||
size="large"
|
||||
:disabled="!confirmInput"
|
||||
data-cy="submit-assignment"
|
||||
@click="onSubmit"
|
||||
>
|
||||
<p>{{ $t("assignment.submitAssignment") }}</p>
|
||||
</ItButton>
|
||||
</template>
|
||||
|
|
@ -3,3 +3,9 @@ export const itCheckboxDefaultIconCheckedTailwindClass =
|
|||
|
||||
export const itCheckboxDefaultIconUncheckedTailwindClass =
|
||||
"bg-[url(/static/icons/icon-checkbox-unchecked.svg)] hover:bg-[url(/static/icons/icon-checkbox-unchecked-hover.svg)]";
|
||||
|
||||
export const VV_COURSE_IDS = [
|
||||
"-4", // vv-de
|
||||
"-10", // vv-fr
|
||||
"-11", // vv-it
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { getCookieValue } from "@/router/guards";
|
||||
import { createFetch } from "@vueuse/core";
|
||||
|
||||
class FetchError extends Error {
|
||||
response: Response;
|
||||
|
|
@ -92,3 +93,13 @@ export const itGetCached = (
|
|||
|
||||
return itGetPromiseCache.get(url.toString()) as Promise<any>;
|
||||
};
|
||||
|
||||
export const useCSRFFetch = createFetch({
|
||||
options: {
|
||||
async beforeFetch({ options }) {
|
||||
const headers = options.headers as Record<string, string>;
|
||||
headers["X-CSRFToken"] = getCookieValue("csrftoken");
|
||||
return { options };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
|
|||
*/
|
||||
const documents = {
|
||||
"\n mutation AttendanceCheckMutation(\n $attendanceCourseId: ID!\n $attendanceUserList: [AttendanceUserInputType]!\n ) {\n update_course_session_attendance_course_users(\n id: $attendanceCourseId\n attendance_user_list: $attendanceUserList\n ) {\n course_session_attendance_course {\n id\n attendance_user_list {\n user_id\n first_name\n last_name\n email\n status\n }\n }\n }\n }\n": types.AttendanceCheckMutationDocument,
|
||||
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
|
||||
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
|
||||
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
|
||||
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
|
||||
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
||||
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n circle_contact_type\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
||||
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
|
||||
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
|
||||
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
|
||||
|
|
@ -48,7 +48,7 @@ export function graphql(source: "\n mutation AttendanceCheckMutation(\n $att
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n"): (typeof documents)["\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n"): (typeof documents)["\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
@ -68,11 +68,11 @@ export function graphql(source: "\n query competenceCertificateQuery($courseSlu
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n circle_contact_type\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n circle_contact_type\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n circle_contact_type\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -237,10 +237,20 @@ type CourseObjectType {
|
|||
category_name: String!
|
||||
slug: String!
|
||||
enable_circle_documents: Boolean!
|
||||
circle_contact_type: CourseCourseCircleContactTypeChoices!
|
||||
learning_path: LearningPathObjectType!
|
||||
action_competences: [ActionCompetenceObjectType!]!
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
enum CourseCourseCircleContactTypeChoices {
|
||||
"""EXPERT"""
|
||||
EXPERT
|
||||
|
||||
"""LEARNING_MENTOR"""
|
||||
LEARNING_MENTOR
|
||||
}
|
||||
|
||||
type ActionCompetenceObjectType implements CoursePageInterface {
|
||||
competence_id: String!
|
||||
id: ID!
|
||||
|
|
@ -863,7 +873,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
|
|||
type Mutation {
|
||||
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
|
||||
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
|
||||
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
||||
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, evaluation_user_id: ID, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
||||
}
|
||||
|
||||
type SendFeedbackMutation {
|
||||
|
|
@ -903,4 +913,4 @@ enum AssignmentCompletionStatus {
|
|||
SUBMITTED
|
||||
EVALUATION_IN_PROGRESS
|
||||
EVALUATION_SUBMITTED
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
|
|||
export const CompetencesStatisticsType = "CompetencesStatisticsType";
|
||||
export const ContentDocumentObjectType = "ContentDocumentObjectType";
|
||||
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
||||
export const CourseCourseCircleContactTypeChoices = "CourseCourseCircleContactTypeChoices";
|
||||
export const CourseObjectType = "CourseObjectType";
|
||||
export const CoursePageInterface = "CoursePageInterface";
|
||||
export const CourseProgressType = "CourseProgressType";
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(`
|
|||
$completionDataString: String!
|
||||
$evaluationPoints: Float
|
||||
$initializeCompletion: Boolean
|
||||
$evaluationUserId: ID
|
||||
) {
|
||||
upsert_assignment_completion(
|
||||
assignment_id: $assignmentId
|
||||
|
|
@ -43,6 +44,7 @@ export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(`
|
|||
completion_data_string: $completionDataString
|
||||
evaluation_points: $evaluationPoints
|
||||
initialize_completion: $initializeCompletion
|
||||
evaluation_user_id: $evaluationUserId
|
||||
) {
|
||||
assignment_completion {
|
||||
id
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
|
|||
title
|
||||
slug
|
||||
enable_circle_documents
|
||||
circle_contact_type
|
||||
}
|
||||
users {
|
||||
id
|
||||
|
|
@ -206,6 +207,7 @@ export const COURSE_QUERY = graphql(`
|
|||
slug
|
||||
category_name
|
||||
enable_circle_documents
|
||||
circle_contact_type
|
||||
action_competences {
|
||||
competence_id
|
||||
...CoursePageFields
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
useCourseDataWithCompletion,
|
||||
useCourseSessionDetailQuery,
|
||||
} from "@/composables";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import * as log from "loglevel";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
log.debug("CockpitParentPage created");
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
}>();
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const loaded = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("CockpitParentPage mounted", props.courseSlug);
|
||||
|
||||
try {
|
||||
await courseSessionDetailResult.waitForData();
|
||||
await cockpitStore.loadCircles(
|
||||
props.courseSlug,
|
||||
courseSessionDetailResult.findCurrentUser()
|
||||
);
|
||||
|
||||
// workaround so that the completion data is loaded before display
|
||||
const userDataPromises = courseSessionDetailResult.filterMembers().map((m) => {
|
||||
const completionData = useCourseDataWithCompletion(props.courseSlug, m.id);
|
||||
return completionData.resultPromise;
|
||||
});
|
||||
await Promise.all(userDataPromises);
|
||||
|
||||
loaded.value = true;
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<main>
|
||||
<div v-if="loaded">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -3,6 +3,7 @@ import CirclePage from "@/pages/learningPath/circlePage/CirclePage.vue";
|
|||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string;
|
||||
|
|
@ -12,6 +13,8 @@ const props = defineProps<{
|
|||
|
||||
log.debug("CockpitUserCirclePage created", props.userId, props.circleSlug);
|
||||
|
||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("CockpitUserCirclePage mounted");
|
||||
});
|
||||
|
|
@ -25,7 +28,7 @@ const user = computed(() => {
|
|||
|
||||
<template>
|
||||
<CirclePage
|
||||
v-if="user"
|
||||
v-if="user && !loading"
|
||||
:course-slug="props.courseSlug"
|
||||
:circle-slug="props.circleSlug"
|
||||
:profile-user="user"
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import { computed, onMounted } from "vue";
|
|||
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
|
||||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
||||
import {
|
||||
useCourseSessionDetailQuery,
|
||||
useCourseDataWithCompletion,
|
||||
useCourseSessionDetailQuery,
|
||||
} from "@/composables";
|
||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string;
|
||||
|
|
@ -16,6 +17,8 @@ const props = defineProps<{
|
|||
|
||||
log.debug("CockpitUserProfilePage created", props.userId);
|
||||
|
||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
||||
|
||||
const courseCompletionData = useCourseDataWithCompletion(
|
||||
props.courseSlug,
|
||||
props.userId
|
||||
|
|
@ -43,7 +46,7 @@ function setActiveClasses(isActive: boolean) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div v-if="!loading" class="bg-gray-200">
|
||||
<div v-if="user" class="container-large">
|
||||
<nav class="py-4 pb-4">
|
||||
<router-link
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div v-if="!loading" class="bg-gray-200">
|
||||
<div class="container-large">
|
||||
<nav class="py-4 pb-4">
|
||||
<router-link
|
||||
|
|
@ -36,6 +36,7 @@ import { onMounted, ref } from "vue";
|
|||
import type { FeedbackData, FeedbackType } from "@/types";
|
||||
import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue";
|
||||
import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue";
|
||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -43,7 +44,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
log.debug("FeedbackPage created", props.circleId);
|
||||
|
||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const feedbackData = ref<FeedbackData | undefined>(undefined);
|
||||
const feedbackType = ref<FeedbackType | undefined>(undefined);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { computed, onMounted } from "vue";
|
|||
import { useRouter } from "vue-router";
|
||||
import { getPreviousRoute } from "@/router/history";
|
||||
import { getAssignmentTypeTitle } from "../../../utils/utils";
|
||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -19,6 +20,7 @@ const props = defineProps<{
|
|||
|
||||
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId);
|
||||
|
||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -62,7 +64,7 @@ const assignment = computed(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute bottom-0 top-0 z-10 w-full bg-white">
|
||||
<div v-if="!loading" class="absolute bottom-0 top-0 z-10 w-full bg-white">
|
||||
<div v-if="queryResult.fetching.value"></div>
|
||||
<div v-else-if="queryResult.error.value">{{ queryResult.error.value }}</div>
|
||||
<div v-else>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import AssignmentDetails from "@/pages/cockpit/assignmentsPage/AssignmentDetails
|
|||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import type { LearningContentAssignment, LearningContentEdoniqTest } from "@/types";
|
||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -13,6 +14,7 @@ const props = defineProps<{
|
|||
log.debug("AssignmentsPage created", props.courseSlug);
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("AssignmentsPage mounted");
|
||||
|
|
@ -26,7 +28,7 @@ const learningContentAssignment = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div v-if="!loading" class="bg-gray-200">
|
||||
<div class="container-large">
|
||||
<nav class="py-4 pb-4">
|
||||
<router-link
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
import { useCurrentCourseSession } from "@/composables";
|
||||
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
|
||||
import { computed } from "vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useExpertCockpitStore } from "@/stores/expertCockpit";
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const expertCockpitStore = useExpertCockpitStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const circleDates = computed(() => {
|
||||
const dueDates = courseSession.value.due_dates.filter((dueDate) => {
|
||||
if (!cockpitStore.currentCircle) return false;
|
||||
return cockpitStore.currentCircle.id == dueDate?.circle?.id;
|
||||
if (!expertCockpitStore.currentCircle) return false;
|
||||
return expertCockpitStore.currentCircle.id == dueDate?.circle?.id;
|
||||
});
|
||||
return dueDates.slice(0, 4);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
|||
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useExpertCockpitStore } from "@/stores/expertCockpit";
|
||||
import log from "loglevel";
|
||||
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import UserStatusCount from "@/pages/cockpit/cockpitPage/UserStatusCount.vue";
|
||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -16,22 +17,24 @@ const props = defineProps<{
|
|||
|
||||
log.debug("CockpitIndexPage created", props.courseSlug);
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
||||
|
||||
const expertCockpitStore = useExpertCockpitStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div v-if="cockpitStore.circles?.length">
|
||||
<div v-if="cockpitStore.currentCircle" class="container-large">
|
||||
<div v-if="!loading" class="bg-gray-200">
|
||||
<div v-if="expertCockpitStore.circles?.length">
|
||||
<div v-if="expertCockpitStore.currentCircle" class="container-large">
|
||||
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
||||
<h1>Cockpit</h1>
|
||||
<ItDropdownSelect
|
||||
:model-value="cockpitStore.currentCircle"
|
||||
:model-value="expertCockpitStore.currentCircle"
|
||||
class="mt-4 w-full lg:mt-0 lg:w-96"
|
||||
:items="cockpitStore.circles"
|
||||
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
|
||||
:items="expertCockpitStore.circles"
|
||||
@update:model-value="expertCockpitStore.setCurrentCourseCircleFromEvent"
|
||||
></ItDropdownSelect>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
|
|
@ -113,7 +116,7 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
|
|||
</div>
|
||||
<SubmissionsOverview
|
||||
:course-session="courseSession"
|
||||
:selected-circle="cockpitStore.currentCircle.id"
|
||||
:selected-circle="expertCockpitStore.currentCircle.id"
|
||||
></SubmissionsOverview>
|
||||
<div class="pt-4">
|
||||
<!-- progress -->
|
||||
|
|
@ -137,12 +140,12 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
|
|||
:course-session-id="courseSession.id"
|
||||
:course-slug="props.courseSlug"
|
||||
:user-id="csu.user_id"
|
||||
:show-circle-slugs="[cockpitStore.currentCircle.slug]"
|
||||
:show-circle-slugs="[expertCockpitStore.currentCircle.slug]"
|
||||
diagram-type="singleSmall"
|
||||
class="mr-4"
|
||||
></LearningPathDiagram>
|
||||
<p class="lg:min-w-[150px]">
|
||||
{{ cockpitStore.currentCircle.title }}
|
||||
{{ expertCockpitStore.currentCircle.title }}
|
||||
</p>
|
||||
<UserStatusCount
|
||||
:course-slug="props.courseSlug"
|
||||
|
|
@ -164,11 +167,19 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
|
|||
</div>
|
||||
</div>
|
||||
<div v-else class="container-large mt-4">
|
||||
<!-- No circle selected -->
|
||||
<span class="text-lg text-orange-600">
|
||||
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="container-large mt-4">
|
||||
<span class="text-lg text-orange-600">
|
||||
<!-- No circle at all (should never happen, mostly
|
||||
for us to reduce confusion why the cockpit is just empty...) -->
|
||||
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<nav class="border-b bg-white px-4 lg:px-8">
|
||||
<ul class="flex flex-col lg:flex-row">
|
||||
<li
|
||||
class="border-t-2 border-t-transparent"
|
||||
:class="{
|
||||
'border-b-2 border-b-blue-900':
|
||||
route.name === 'mentorCockpitOverview' || route.name === 'mentorCockpit',
|
||||
}"
|
||||
>
|
||||
<router-link :to="{ name: 'mentorCockpitOverview' }" class="block py-3">
|
||||
{{ $t("a.Übersicht") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
class="border-t-2 border-t-transparent lg:ml-12"
|
||||
:class="{
|
||||
'border-b-2 border-b-blue-900': route.name === 'mentorCockpitParticipants',
|
||||
}"
|
||||
>
|
||||
<router-link :to="{ name: 'mentorCockpitParticipants' }" class="block py-3">
|
||||
{{ $t("a.Teilnehmer") }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="container-large">
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
useCourseSessionDetailQuery,
|
||||
} from "@/composables";
|
||||
import { circleFlatLearningContents } from "@/services/circle";
|
||||
import { getCockpitUrl } from "@/utils/utils";
|
||||
|
||||
interface Submittable {
|
||||
id: string;
|
||||
|
|
@ -126,10 +127,12 @@ const getShowDetailsText = (lc: LearningContent) => {
|
|||
};
|
||||
|
||||
const getDetailsLink = (lc: LearningContent, circleId: string) => {
|
||||
const base = getCockpitUrl(props.courseSession.course.slug);
|
||||
|
||||
if (isFeedback(lc)) {
|
||||
return `cockpit/feedback/${circleId}`;
|
||||
return `${base}/feedback/${circleId}`;
|
||||
} else if (isAssignment(lc) || isEdoniqTest(lc)) {
|
||||
return `cockpit/assignment/${lc.id}`;
|
||||
return `${base}/assignment/${lc.id}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
|
@ -191,18 +194,18 @@ const getIconName = (lc: LearningContent) => {
|
|||
class="grow pr-8"
|
||||
></FeedbackSubmissionProgress>
|
||||
<div class="flex items-center lg:w-1/4 lg:justify-end">
|
||||
<button v-if="submittable.detailsLink" class="btn-primary">
|
||||
<router-link
|
||||
:to="submittable.detailsLink"
|
||||
:data-cy="
|
||||
isFeedback(submittable.content)
|
||||
? `show-feedback-btn-${submittable.content.slug}`
|
||||
: `show-details-btn-${submittable.content.slug}`
|
||||
"
|
||||
>
|
||||
{{ submittable.showDetailsText }}
|
||||
</router-link>
|
||||
</button>
|
||||
<router-link
|
||||
v-if="submittable.detailsLink"
|
||||
class="btn-primary"
|
||||
:to="submittable.detailsLink"
|
||||
:data-cy="
|
||||
isFeedback(submittable.content)
|
||||
? `show-feedback-btn-${submittable.content.slug}`
|
||||
: `show-details-btn-${submittable.content.slug}`
|
||||
"
|
||||
>
|
||||
{{ submittable.showDetailsText }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
useCourseDataWithCompletion,
|
||||
useCourseSessionDetailQuery,
|
||||
} from "@/composables";
|
||||
import { useExpertCockpitStore } from "@/stores/expertCockpit";
|
||||
import { ref } from "vue";
|
||||
|
||||
export function useExpertCockpitPageData(courseSlug: string) {
|
||||
const loading = ref(true);
|
||||
|
||||
const cockpitStore = useExpertCockpitStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
await courseSessionDetailResult.waitForData();
|
||||
await cockpitStore.loadCircles(
|
||||
courseSlug,
|
||||
courseSessionDetailResult.findCurrentUser()
|
||||
);
|
||||
|
||||
const userDataPromises = courseSessionDetailResult.filterMembers().map((m) => {
|
||||
const completionData = useCourseDataWithCompletion(courseSlug, m.id);
|
||||
return completionData.resultPromise;
|
||||
});
|
||||
|
||||
await Promise.all(userDataPromises);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
return {
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
:to="{ name: 'mentorCockpitOverview' }"
|
||||
class="btn-text mb-4 inline-flex items-center pl-0"
|
||||
>
|
||||
<it-icon-arrow-left class="it-icon"></it-icon-arrow-left>
|
||||
{{ $t("general.back") }}
|
||||
</router-link>
|
||||
|
||||
<div class="bg-white">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script setup lang="ts">
|
||||
import type { PraxisAssignment } from "@/services/mentorCockpit";
|
||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { computed, type Ref, ref } from "vue";
|
||||
import PraxisAssignmentItem from "@/components/cockpit/mentor/PraxisAssignmentItem.vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
|
||||
const { t } = useTranslation();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
|
||||
const summary = mentorCockpitStore.summary;
|
||||
|
||||
const statusFilterValue = ref({ name: t("Alle"), id: "_all" });
|
||||
|
||||
const statusFilter = ref([
|
||||
{ name: t("Alle"), id: "_all" },
|
||||
{ name: t("a.Zu erledigen"), id: "todo" },
|
||||
]);
|
||||
|
||||
const circleFilterValue = ref({ name: t("a.AlleCircle"), id: "_all" });
|
||||
|
||||
const circleFilter = computed(() => {
|
||||
if (!summary.value) return [];
|
||||
|
||||
return [
|
||||
{ name: t("a.AlleCircle"), id: "_all" },
|
||||
...summary.value.circles.map((circle) => ({
|
||||
name: `Circle: ${circle.title}`,
|
||||
id: circle.id,
|
||||
})),
|
||||
];
|
||||
});
|
||||
|
||||
const filteredAssignments: Ref<PraxisAssignment[]> = computed(() => {
|
||||
if (!summary.value) return [];
|
||||
|
||||
let filtered = summary.value.assignments;
|
||||
|
||||
if (statusFilterValue.value.id !== "_all") {
|
||||
filtered = filtered.filter((item) => item.pending_evaluations > 0);
|
||||
}
|
||||
|
||||
if (circleFilterValue.value.id !== "_all") {
|
||||
filtered = filtered.filter(
|
||||
(item) => item.circle_id === String(circleFilterValue.value.id)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="summary" class="bg-white">
|
||||
<div class="flex flex-col space-x-2 lg:flex-row">
|
||||
<ItDropdownSelect
|
||||
v-model="statusFilterValue"
|
||||
class="min-w-[10rem]"
|
||||
:items="statusFilter"
|
||||
borderless
|
||||
></ItDropdownSelect>
|
||||
<ItDropdownSelect
|
||||
v-model="circleFilterValue"
|
||||
class="min-w-[18rem]"
|
||||
:items="circleFilter"
|
||||
borderless
|
||||
></ItDropdownSelect>
|
||||
</div>
|
||||
<template v-for="item in filteredAssignments" :key="item.id">
|
||||
<PraxisAssignmentItem
|
||||
v-if="item.type === 'praxis_assignment'"
|
||||
:circle-title="mentorCockpitStore.getCircleTitleById(item.circle_id)"
|
||||
:pending-tasks="item.pending_evaluations"
|
||||
:task-link="{
|
||||
name: 'mentorCockpitPraxisAssignments',
|
||||
params: { praxisAssignmentId: item.id },
|
||||
}"
|
||||
:task-title="item.title"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<script setup lang="ts">
|
||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const { summary } = useMentorCockpit(courseSession.value.id);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="summary" class="bg-white px-4 py-2">
|
||||
<div
|
||||
v-for="participant in summary.participants"
|
||||
:key="participant.id"
|
||||
class="flex flex-col items-start justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:items-center md:gap-16"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<img
|
||||
:alt="participant.last_name"
|
||||
class="h-11 w-11 rounded-full"
|
||||
:src="participant.avatar_url || '/static/avatars/myvbv-default-avatar.png'"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-bold">
|
||||
{{ participant.first_name }}
|
||||
{{ participant.last_name }}
|
||||
</div>
|
||||
{{ participant.email }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <router-link :to="{name: 'cockpitUserProfile', params: {userId: participant.id}}" class="underline">-->
|
||||
<!-- {{ $t("a.Profil anzeigen") }}-->
|
||||
<!-- </router-link>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<script setup lang="ts">
|
||||
import type { Participant, PraxisAssignment } from "@/services/mentorCockpit";
|
||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||
import { computed, type Ref } from "vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
praxisAssignmentId: string;
|
||||
}>();
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
|
||||
|
||||
const praxisAssignment: Ref<PraxisAssignment | null> = computed(() =>
|
||||
mentorCockpitStore.getPraxisAssignmentById(props.praxisAssignmentId)
|
||||
);
|
||||
|
||||
const getParticipantById = (id: string): Participant | null => {
|
||||
if (mentorCockpitStore.summary.value?.participants) {
|
||||
const found = mentorCockpitStore.summary.value.participants.find(
|
||||
(item) => item.id === id
|
||||
);
|
||||
return found || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="praxisAssignment">
|
||||
<div class="p-6">
|
||||
<h2 class="mb-2">{{ $t("a.Praxisauftrag") }}: {{ praxisAssignment.title }}</h2>
|
||||
<span class="text-gray-800">
|
||||
Circle «{{ mentorCockpitStore.getCircleTitleById(praxisAssignment.circle_id) }}»
|
||||
</span>
|
||||
<template v-if="praxisAssignment.pending_evaluations > 0">
|
||||
<div class="flex flex-row items-center space-x-2 pt-4">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full border-2 border-green-500 px-3 text-sm font-bold"
|
||||
>
|
||||
<span>{{ praxisAssignment.pending_evaluations }}</span>
|
||||
</div>
|
||||
<span>{{ $t("a.Ergebnisse abgegeben") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="border-t">
|
||||
<div
|
||||
v-for="item in praxisAssignment.completions"
|
||||
:key="item.user_id"
|
||||
class="flex flex-col items-start justify-between gap-4 border-b py-2 pl-5 pr-5 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
|
||||
>
|
||||
<!-- Left -->
|
||||
<div class="flex flex-grow flex-row items-center justify-start">
|
||||
<div class="w-80">
|
||||
<div class="flex items-center space-x-2">
|
||||
<img
|
||||
:alt="item.last_name"
|
||||
class="h-11 w-11 rounded-full"
|
||||
:src="
|
||||
getParticipantById(item.user_id)?.avatar_url ||
|
||||
'/static/avatars/myvbv-default-avatar.png'
|
||||
"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-bold">
|
||||
{{ getParticipantById(item.user_id)?.first_name }}
|
||||
{{ getParticipantById(item.user_id)?.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Center -->
|
||||
<div
|
||||
class="flex flex-grow flex-row items-center justify-start space-x-2 pl-20"
|
||||
>
|
||||
<template v-if="item.status == 'SUBMITTED'">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full border-2 border-green-500 px-3 py-1 text-sm font-bold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<it-icon-check class="h-5 w-5"></it-icon-check>
|
||||
</span>
|
||||
</div>
|
||||
<span>{{ $t("a.Ergebnisse abgegeben") }}</span>
|
||||
</template>
|
||||
<template v-if="item.status == 'EVALUATED'">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full border-2 border-green-500 bg-green-500 px-3 py-1 text-sm font-bold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<it-icon-check class="h-5 w-5"></it-icon-check>
|
||||
</span>
|
||||
</div>
|
||||
<span>{{ $t("a.Bewertung freigeben") }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Right -->
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession, useCourseData } from "@/composables";
|
||||
import { useCourseData, useCurrentCourseSession } from "@/composables";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useExpertCockpitStore } from "@/stores/expertCockpit";
|
||||
import ItModal from "@/components/ui/ItModal.vue";
|
||||
import DocumentUploadForm from "@/pages/cockpit/documentPage/DocumentUploadForm.vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
|
|
@ -16,14 +16,17 @@ import {
|
|||
} from "@/services/files";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const cockpitStore = useExpertCockpitStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseData = useCourseData(courseSession.value?.course.slug);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useExpertCockpitPageData(courseData.course.value?.slug || "");
|
||||
|
||||
const showUploadModal = ref(false);
|
||||
const showUploadErrorMessage = ref(false);
|
||||
const isUploading = ref(false);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<main>
|
||||
<div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -37,35 +37,44 @@ onMounted(async () => {
|
|||
<nav class="border-b bg-white px-4 lg:px-8">
|
||||
<ul class="flex flex-col lg:flex-row">
|
||||
<li
|
||||
class="inline-block border-t-2 border-t-transparent py-3"
|
||||
class="border-t-2 border-t-transparent"
|
||||
:class="{ 'border-b-2 border-b-blue-900': routeInOverview() }"
|
||||
>
|
||||
<router-link :to="`/course/${courseSlug}/competence`">
|
||||
<router-link :to="`/course/${courseSlug}/competence`" class="block py-3">
|
||||
{{ $t("a.Übersicht") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
class="inline-block border-t-2 border-t-transparent py-3 lg:ml-12"
|
||||
class="border-t-2 border-t-transparent lg:ml-12"
|
||||
:class="{ 'border-b-2 border-b-blue-900': routeInCompetenceCertificate() }"
|
||||
>
|
||||
<router-link :to="`/course/${courseSlug}/competence/certificates`">
|
||||
<router-link
|
||||
:to="`/course/${courseSlug}/competence/certificates`"
|
||||
class="block py-3"
|
||||
>
|
||||
{{ $t("a.Kompetenznachweise") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
class="inline-block border-t-2 border-t-transparent py-3 lg:ml-12"
|
||||
class="border-t-2 border-t-transparent lg:ml-12"
|
||||
:class="{ 'border-b-2 border-b-blue-900': routeInPerformanceCriteria() }"
|
||||
>
|
||||
<router-link :to="`/course/${courseSlug}/competence/criteria`">
|
||||
<router-link
|
||||
:to="`/course/${courseSlug}/competence/criteria`"
|
||||
class="block py-3"
|
||||
>
|
||||
{{ $t("a.Selbsteinschätzungen") }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="inline-block border-t-2 border-t-transparent py-3 lg:ml-12"
|
||||
class="border-t-2 border-t-transparent lg:ml-12"
|
||||
:class="{ 'border-b-2 border-b-blue-900': routeInActionCompetences() }"
|
||||
>
|
||||
<router-link :to="`/course/${courseSlug}/competence/competences`">
|
||||
<router-link
|
||||
:to="`/course/${courseSlug}/competence/competences`"
|
||||
class="block py-3"
|
||||
>
|
||||
{{ $t("a.Handlungskompetenzen") }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
} from "@/gql/graphql";
|
||||
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
|
||||
import AssignmentProgressSummaryBox from "@/components/dashboard/AssignmentProgressSummaryBox.vue";
|
||||
import { VV_COURSE_IDS } from "@/constants";
|
||||
|
||||
const dashboardStore = useDashboardStore();
|
||||
|
||||
|
|
@ -46,6 +47,13 @@ const competenceCertificateUrl = computed(() => {
|
|||
const competenceCriteriaUrl = computed(() => {
|
||||
return `/course/${courseSlug.value}/competence/criteria?courseSessionId=${courseSessionProgress.value?.session_to_continue_id}`;
|
||||
});
|
||||
|
||||
const isVVCourse = computed(() => {
|
||||
if (!dashboardStore.currentDashboardConfig) {
|
||||
return false;
|
||||
}
|
||||
return VV_COURSE_IDS.includes(dashboardStore.currentDashboardConfig.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -74,6 +82,7 @@ const competenceCriteriaUrl = computed(() => {
|
|||
</div>
|
||||
<div class="grid auto-rows-fr grid-cols-1 gap-8 xl:grid-cols-2">
|
||||
<AssignmentProgressSummaryBox
|
||||
v-if="!isVVCourse"
|
||||
:total-assignments="assignment.total_count"
|
||||
:achieved-points-count="assignment.points_achieved_count"
|
||||
:max-points-count="assignment.points_max_count"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import { useCSRFFetch } from "@/fetchHelpers";
|
||||
import { getCockpitUrl } from "@/utils/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
courseId: string;
|
||||
invitationId: string;
|
||||
}>();
|
||||
|
||||
const { data, error } = useCSRFFetch(
|
||||
`/api/mentor/${props.courseId}/invitations/accept`,
|
||||
{
|
||||
onFetchError(ctx) {
|
||||
ctx.error = ctx.data;
|
||||
return ctx;
|
||||
},
|
||||
}
|
||||
)
|
||||
.post({
|
||||
invitation_id: props.invitationId,
|
||||
})
|
||||
.json();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow bg-gray-200">
|
||||
<div class="container-large">
|
||||
<header class="mb-8 mt-12">
|
||||
<h1 class="mb-8">{{ $t("a.Einladung") }}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div class="bg-white p-6">
|
||||
<template v-if="error">
|
||||
{{
|
||||
$t(
|
||||
"a.Die Einladung konnte nicht akzeptiert werden. Bitte melde dich beim Support."
|
||||
)
|
||||
}}
|
||||
<div>
|
||||
<a class="underline" href="mailto:help@vbv.ch">help@vbv.ch</a>
|
||||
</div>
|
||||
<div v-if="error.message" class="my-4">
|
||||
{{ $t("a.Fehlermeldung") }}: {{ error.message }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i18next
|
||||
:translation="
|
||||
$t('a.Du hast die Einladung von {name} erfolgreich akzeptiert.')
|
||||
"
|
||||
>
|
||||
<template #name>
|
||||
<b>{{ data.user.first_name }} {{ data.user.last_name }}</b>
|
||||
</template>
|
||||
</i18next>
|
||||
<div class="mt-4">
|
||||
<a class="underline" :href="getCockpitUrl(data.course_slug)">
|
||||
{{ $t("a.Cockpit anschauen") }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import ItModal from "@/components/ui/ItModal.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useCSRFFetch } from "@/fetchHelpers";
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const showInvitationModal = ref(false);
|
||||
const inviteeEmail = ref("");
|
||||
|
||||
const { execute: refreshMentors, data: mentors } = useCSRFFetch(
|
||||
`/api/mentor/${courseSession.value.id}/mentors`
|
||||
).json();
|
||||
|
||||
const { execute: refreshInvitations, data: invitations } = useCSRFFetch(
|
||||
`/api/mentor/${courseSession.value.id}/invitations`
|
||||
).json();
|
||||
|
||||
const hasMentors = computed(() => {
|
||||
return (
|
||||
(mentors.value && mentors.value.length > 0) ||
|
||||
(invitations.value && invitations.value.length > 0)
|
||||
);
|
||||
});
|
||||
|
||||
const validEmail = computed(() => {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return emailRegex.test(inviteeEmail.value);
|
||||
});
|
||||
|
||||
const removeInvitation = async (invitationId: string) => {
|
||||
await useCSRFFetch(
|
||||
`/api/mentor/${courseSession.value.id}/invitations/${invitationId}/delete`
|
||||
).delete();
|
||||
await refreshInvitations();
|
||||
};
|
||||
|
||||
const removeMentor = async (mentorId: string) => {
|
||||
await useCSRFFetch(
|
||||
`/api/mentor/${courseSession.value.id}/mentors/${mentorId}/leave`
|
||||
).delete();
|
||||
await refreshMentors();
|
||||
};
|
||||
|
||||
const inviteMentor = async () => {
|
||||
await useCSRFFetch(`/api/mentor/${courseSession.value.id}/invitations/create`).post({
|
||||
email: inviteeEmail.value,
|
||||
});
|
||||
await refreshInvitations();
|
||||
showInvitationModal.value = false;
|
||||
inviteeEmail.value = "";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div class="container-large">
|
||||
<header class="mb-8 mt-12">
|
||||
<h1 class="mb-8">{{ $t("a.Lernbegleitung") }}</h1>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"a.Hier kannst du Personen einladen, damit sie deine Lernbegleitung werden. Zudem siehst du jederzeit eine Übersicht aller Personen, die du bereits als Lernbegleitung hinzugefügt hast."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</header>
|
||||
<main>
|
||||
<div class="bg-white p-6">
|
||||
<div class="mb-8">
|
||||
<button
|
||||
class="btn-secondary flex items-center"
|
||||
@click="showInvitationModal = true"
|
||||
>
|
||||
<it-icon-add class="it-icon mr-2 h-6 w-6" />
|
||||
{{ $t("a.Neue Lernbegleitung einladen") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="border-t">
|
||||
<div
|
||||
v-for="invitation in invitations"
|
||||
:key="invitation.id"
|
||||
class="flex flex-col justify-between gap-4 border-b py-4 md:flex-row md:gap-16"
|
||||
>
|
||||
<div class="flex flex-col justify-between md:flex-grow md:flex-row">
|
||||
{{ invitation.email }}
|
||||
<div class="flex items-center">
|
||||
<it-icon-info class="it-icon mr-2 h-6 w-6" />
|
||||
{{ $t("a.Die Einladung wurde noch nicht angenommen.") }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="underline" @click="removeInvitation(invitation.id)">
|
||||
{{ $t("a.Entfernen") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="learningMentor in mentors"
|
||||
:key="learningMentor.id"
|
||||
class="flex flex-col justify-between gap-4 border-b py-4 md:flex-row md:gap-16"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<img
|
||||
:alt="learningMentor.mentor.last_name"
|
||||
class="h-11 w-11 rounded-full"
|
||||
:src="learningMentor.mentor.avatar_url"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-bold">
|
||||
{{ learningMentor.mentor.first_name }}
|
||||
{{ learningMentor.mentor.last_name }}
|
||||
</div>
|
||||
{{ learningMentor.mentor.email }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="underline" @click="removeMentor(learningMentor.id)">
|
||||
{{ $t("a.Entfernen") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!hasMentors" class="mt-8 flex items-center">
|
||||
<it-icon-info class="it-icon mr-2 h-6 w-6" />
|
||||
{{
|
||||
$t("a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen.")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<ItModal v-model="showInvitationModal">
|
||||
<template #title>{{ $t("a.Neue Lernbegleitung einladen") }}</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<label for="mentor-email">{{ $t("a.E-Mail Adresse") }}</label>
|
||||
<input id="mentor-email" v-model="inviteeEmail" type="email" />
|
||||
|
||||
<button
|
||||
:disabled="!validEmail"
|
||||
class="btn-primary mt-8"
|
||||
@click="inviteMentor()"
|
||||
>
|
||||
{{ $t("a.Einladung abschicken") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</ItModal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -9,10 +9,12 @@ import DocumentSection from "./DocumentSection.vue";
|
|||
import {
|
||||
useCourseDataWithCompletion,
|
||||
useCourseSessionDetailQuery,
|
||||
useCurrentCourseSession,
|
||||
} from "@/composables";
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
|
||||
import { useCSRFFetch } from "@/fetchHelpers";
|
||||
|
||||
export interface Props {
|
||||
courseSlug: string;
|
||||
|
|
@ -21,6 +23,8 @@ export interface Props {
|
|||
readonly?: boolean;
|
||||
}
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const route = useRoute();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
const circleStore = useCircleStore();
|
||||
|
|
@ -67,6 +71,43 @@ const showDocumentSection = computed(() => {
|
|||
return lpQueryResult.course.value?.enable_circle_documents && !props.readonly;
|
||||
});
|
||||
|
||||
const courseConfig = computed(() => {
|
||||
if (lpQueryResult.course.value?.circle_contact_type === "EXPERT") {
|
||||
return {
|
||||
contactDescription: "circlePage.contactExpertDescription",
|
||||
contactButton: "circlePage.contactExpertButton",
|
||||
showContact: true,
|
||||
};
|
||||
} else if (lpQueryResult.course.value?.circle_contact_type === "LEARNING_MENTOR") {
|
||||
return {
|
||||
contactDescription: "circlePage.contactLearningMentorDescription",
|
||||
contactButton: "circlePage.contactLearningMentorButton",
|
||||
showContact: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
contactDescription: "",
|
||||
contactButton: "",
|
||||
showContact: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const { data: mentors } = useCSRFFetch(
|
||||
`/api/mentor/${courseSession.value.id}/mentors`
|
||||
).json();
|
||||
|
||||
const expert = computed(() => {
|
||||
if (courseConfig.value.showContact) {
|
||||
if (lpQueryResult.course.value?.circle_contact_type === "EXPERT") {
|
||||
return circleExperts.value[0];
|
||||
} else if (lpQueryResult.course.value?.circle_contact_type === "LEARNING_MENTOR") {
|
||||
return mentors.value?.[0].mentor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => circle.value,
|
||||
() => {
|
||||
|
|
@ -198,29 +239,28 @@ watch(
|
|||
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
|
||||
<div class="mt-4 leading-relaxed">
|
||||
{{
|
||||
$t("circlePage.contactExpertDescription", {
|
||||
$t(courseConfig.contactDescription, {
|
||||
circleName: circle?.title,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div v-for="expert in circleExperts" :key="expert.user_id">
|
||||
<div class="mb-2 mt-2 flex flex-row items-center">
|
||||
<template v-if="expert">
|
||||
<div class="mb-6 mt-4 flex flex-row items-center">
|
||||
<img
|
||||
class="mr-2 h-[45px] rounded-full"
|
||||
:src="expert.avatar_url"
|
||||
:src="
|
||||
expert.avatar_url ||
|
||||
'/static/avatars/myvbv-default-avatar.png'
|
||||
"
|
||||
/>
|
||||
<p class="lg:leading-[45px]">
|
||||
{{ expert.first_name }} {{ expert.last_name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
v-if="circleExperts.length > 0"
|
||||
:href="'mailto:' + circleExperts[0].email"
|
||||
class="btn-secondary mt-4 text-xl"
|
||||
>
|
||||
{{ $t("circlePage.contactExpertButton") }}
|
||||
</a>
|
||||
<a :href="'mailto:' + expert.email" class="btn-secondary text-xl">
|
||||
{{ $t(courseConfig.contactButton) }}
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
||||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||
import {
|
||||
useCourseData,
|
||||
useCourseSessionDetailQuery,
|
||||
useCurrentCourseSession,
|
||||
} from "@/composables";
|
||||
import { bustItGetCache } from "@/fetchHelpers";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { Assignment, AssignmentCompletion, AssignmentTask } from "@/types";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import dayjs from "dayjs";
|
||||
import type { AssignmentAssignmentAssignmentTypeChoices } from "@/gql/graphql";
|
||||
import CaseWorkSubmit from "@/components/learningPath/assignment/CaseWorkSubmit.vue";
|
||||
import SimpleSubmit from "@/components/learningPath/assignment/SimpleSubmit.vue";
|
||||
import PraxisAssignmentSubmit from "@/components/learningPath/assignment/PraxisAssignmentSubmit.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
|
|
@ -39,11 +33,6 @@ const courseData = useCourseData(courseSession.value.course.slug);
|
|||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const state = reactive({
|
||||
confirmInput: false,
|
||||
confirmPerson: false,
|
||||
});
|
||||
|
||||
const learningContent = computed(() => {
|
||||
return courseData.findLearningContent(props.learningContentId);
|
||||
});
|
||||
|
|
@ -77,13 +66,6 @@ const completionTaskData = computed(() => {
|
|||
return props.assignmentCompletion?.task_completion_data ?? {};
|
||||
});
|
||||
|
||||
const cannotSubmit = computed(() => {
|
||||
return (
|
||||
(!state.confirmInput && !isPraxisAssignment.value) ||
|
||||
(props.assignment.assignment_type === "CASEWORK" && !state.confirmPerson)
|
||||
);
|
||||
});
|
||||
|
||||
function checkAssignmentType(
|
||||
assignmentType: AssignmentAssignmentAssignmentTypeChoices[]
|
||||
) {
|
||||
|
|
@ -91,15 +73,8 @@ function checkAssignmentType(
|
|||
}
|
||||
|
||||
const isCasework = computed(() => checkAssignmentType(["CASEWORK"]));
|
||||
const mayBeEvaluated = computed(() =>
|
||||
checkAssignmentType(["CASEWORK", "PRAXIS_ASSIGNMENT"])
|
||||
);
|
||||
const isPraxisAssignment = computed(() => checkAssignmentType(["PRAXIS_ASSIGNMENT"]));
|
||||
|
||||
const upsertAssignmentCompletionMutation = useMutation(
|
||||
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
|
||||
);
|
||||
|
||||
const onEditTask = (task: AssignmentTask) => {
|
||||
emit("editTask", task);
|
||||
};
|
||||
|
|
@ -111,97 +86,43 @@ const openSolutionSample = () => {
|
|||
window.open(url, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.assignment.id,
|
||||
courseSessionId: courseSession.value.id,
|
||||
learningContentId: props.learningContentId,
|
||||
completionDataString: JSON.stringify({}),
|
||||
completionStatus: "SUBMITTED",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
id: props.assignmentCompletion?.id,
|
||||
});
|
||||
bustItGetCache(
|
||||
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
|
||||
);
|
||||
// if solution sample is available, do not close the assigment automatically
|
||||
if (!props.assignment.solution_sample) {
|
||||
eventBus.emit("finishedLearningContent", true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Could not submit assignment", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="w-full border border-gray-400 p-8" data-cy="confirm-container">
|
||||
<h3 class="heading-3 border-b border-gray-400 pb-6">
|
||||
{{ $t("assignment.submitAssignment") }}
|
||||
<template v-if="isPraxisAssignment">
|
||||
{{ $t("a.Ergebnisse teilen") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("assignment.submitAssignment") }}
|
||||
</template>
|
||||
</h3>
|
||||
|
||||
<div v-if="completionStatus === 'IN_PROGRESS'">
|
||||
<ItCheckbox
|
||||
v-if="!isPraxisAssignment"
|
||||
class="w-full border-b border-gray-400 py-10 sm:py-6"
|
||||
:checkbox-item="{
|
||||
label: $t('assignment.confirmSubmitResults'),
|
||||
value: 'value',
|
||||
checked: state.confirmInput,
|
||||
}"
|
||||
data-cy="confirm-submit-results"
|
||||
@toggle="state.confirmInput = !state.confirmInput"
|
||||
></ItCheckbox>
|
||||
<div v-if="mayBeEvaluated" class="w-full border-b border-gray-400">
|
||||
<ItCheckbox
|
||||
v-if="mayBeEvaluated"
|
||||
class="py-6"
|
||||
:checkbox-item="{
|
||||
label: isPraxisAssignment
|
||||
? $t('a.confirmSubmitPersonPraxisAssignment')
|
||||
: $t('assignment.confirmSubmitPerson'),
|
||||
value: 'value',
|
||||
checked: state.confirmPerson,
|
||||
}"
|
||||
data-cy="confirm-submit-person"
|
||||
@toggle="state.confirmPerson = !state.confirmPerson"
|
||||
></ItCheckbox>
|
||||
<div v-if="circleExpert" class="flex flex-row items-center pb-6 pl-[49px]">
|
||||
<img
|
||||
alt="Notification icon"
|
||||
class="mr-2 h-[45px] min-w-[45px] rounded-full"
|
||||
:src="circleExpert.avatar_url"
|
||||
/>
|
||||
<p class="text-base font-bold">
|
||||
{{ circleExpertName }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- TODO: find way to find user that will do the corrections -->
|
||||
</div>
|
||||
<div v-if="isCasework" class="flex flex-col space-x-2 pt-6 text-base sm:flex-row">
|
||||
<p>{{ $t("assignment.assessmentDocumentDisclaimer") }}</p>
|
||||
<a :href="props.assignment.evaluation_document_url" class="underline">
|
||||
{{ $t("assignment.showAssessmentDocument") }}
|
||||
</a>
|
||||
</div>
|
||||
<p v-if="mayBeEvaluated && props.submissionDeadlineStart" class="pt-6">
|
||||
{{ $t("assignment.dueDateSubmission") }}
|
||||
<DateEmbedding
|
||||
:single-date="dayjs(props.submissionDeadlineStart)"
|
||||
></DateEmbedding>
|
||||
</p>
|
||||
<ItButton
|
||||
class="mt-6"
|
||||
variant="blue"
|
||||
size="large"
|
||||
:disabled="cannotSubmit"
|
||||
data-cy="submit-assignment"
|
||||
@click="onSubmit"
|
||||
>
|
||||
<p>{{ $t("assignment.submitAssignment") }}</p>
|
||||
</ItButton>
|
||||
<CaseWorkSubmit
|
||||
v-if="isCasework"
|
||||
:course-session-id="courseSessionId"
|
||||
:assignment="assignment"
|
||||
:learning-content-id="learningContentId"
|
||||
:circle-expert="circleExpert"
|
||||
:evaluation-document-url="assignment.evaluation_document_url"
|
||||
:submission-deadline-start="submissionDeadlineStart"
|
||||
/>
|
||||
|
||||
<PraxisAssignmentSubmit
|
||||
v-else-if="isPraxisAssignment"
|
||||
:course-session-id="courseSessionId"
|
||||
:assignment="assignment"
|
||||
:learning-content-id="learningContentId"
|
||||
:submission-deadline-start="submissionDeadlineStart"
|
||||
/>
|
||||
|
||||
<SimpleSubmit
|
||||
v-else
|
||||
:course-session-id="courseSessionId"
|
||||
:assignment="assignment"
|
||||
:learning-content-id="learningContentId"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="pt-6">
|
||||
<ItSuccessAlert
|
||||
|
|
@ -214,7 +135,7 @@ const onSubmit = async () => {
|
|||
}}
|
||||
</p>
|
||||
<div
|
||||
v-if="props.assignment.solution_sample"
|
||||
v-if="assignment.solution_sample"
|
||||
class="pt-2"
|
||||
data-cy="show-sample-solution"
|
||||
>
|
||||
|
|
@ -236,7 +157,7 @@ const onSubmit = async () => {
|
|||
</div>
|
||||
</div>
|
||||
<AssignmentSubmissionResponses
|
||||
:assignment="props.assignment"
|
||||
:assignment="assignment"
|
||||
:assignment-completion-data="completionData"
|
||||
:assignment-task-completion-data="completionTaskData"
|
||||
:allow-edit="completionStatus === 'IN_PROGRESS'"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import WizardPage from "@/components/onboarding/WizardPage.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { getLoginURL } from "@/router/utils";
|
||||
import { getLoginURL, getSignUpURL } from "@/router/utils";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
courseType: {
|
||||
|
|
@ -9,7 +11,27 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = useUserStore();
|
||||
const route = useRoute();
|
||||
|
||||
function constructParams() {
|
||||
const params: { lang: string; next?: string; course?: string } = {
|
||||
lang: user.language,
|
||||
};
|
||||
|
||||
const nextValue = route.query.next;
|
||||
if (nextValue && typeof nextValue === "string") {
|
||||
params.next = nextValue;
|
||||
} else {
|
||||
params.course = props.courseType;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
const loginURL = computed(() => getLoginURL(constructParams()));
|
||||
const signUpURL = computed(() => getSignUpURL(constructParams()));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -19,23 +41,12 @@ const user = useUserStore();
|
|||
<p class="mb-4">
|
||||
{{ $t("a.Damit du myVBV nutzen kannst, brauchst du ein Konto.") }}
|
||||
</p>
|
||||
<a
|
||||
:href="`/sso/signup?course=${props.courseType}&lang=${user.language}`"
|
||||
class="btn-primary"
|
||||
>
|
||||
<a :href="signUpURL" class="btn-primary">
|
||||
{{ $t("a.Konto erstellen") }}
|
||||
</a>
|
||||
|
||||
<p class="mb-4 mt-12">{{ $t("a.Hast du schon ein Konto?") }}</p>
|
||||
<a
|
||||
:href="
|
||||
getLoginURL({
|
||||
course: props.courseType,
|
||||
lang: user.language,
|
||||
})
|
||||
"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<a :href="loginURL" class="btn-secondary">
|
||||
{{ $t("a.Anmelden") }}
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { getLoginURLNext, shouldUseSSO } from "@/router/utils";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { NavigationGuard, RouteLocationNormalized } from "vue-router";
|
||||
|
|
@ -110,3 +111,41 @@ export async function handleCourseSessionAsQueryParam(to: RouteLocationNormalize
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleCockpit(to: RouteLocationNormalized) {
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseId = courseSessionsStore.currentCourseSession?.course.id || null;
|
||||
const cockpitStore = useCockpitStore();
|
||||
await cockpitStore.fetchCockpitType(courseId);
|
||||
|
||||
if (to.name === "cockpit") {
|
||||
if (cockpitStore.hasExpertCockpitType) {
|
||||
return { name: "expertCockpit", params: to.params };
|
||||
} else if (cockpitStore.hasMentorCockpitType) {
|
||||
return { name: "mentorCockpitOverview", params: to.params };
|
||||
}
|
||||
}
|
||||
|
||||
const cockpitType = to.meta?.cockpitType;
|
||||
|
||||
if (!cockpitType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cockpitType !== cockpitStore.cockpitType) {
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleAcceptLearningMentorInvitation(
|
||||
to: RouteLocationNormalized
|
||||
) {
|
||||
const user = useUserStore();
|
||||
if (user.loggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `/onboarding/vv-${user.language}/account/create?next=${encodeURIComponent(
|
||||
to.fullPath
|
||||
)}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import GuestStartPage from "@/pages/start/GuestStartPage.vue";
|
|||
import UKStartPage from "@/pages/start/UKStartPage.vue";
|
||||
import VVStartPage from "@/pages/start/VVStartPage.vue";
|
||||
import {
|
||||
handleAcceptLearningMentorInvitation,
|
||||
handleCockpit,
|
||||
handleCourseSessionAsQueryParam,
|
||||
handleCurrentCourseSession,
|
||||
redirectToLoginIfRequired,
|
||||
|
|
@ -139,19 +141,89 @@ const router = createRouter({
|
|||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/course/:courseSlug/cockpit",
|
||||
path: "/course/:courseSlug/mentor",
|
||||
component: () => import("@/pages/learningMentor/MentorManagementPage.vue"),
|
||||
props: true,
|
||||
component: () => import("@/pages/cockpit/CockpitParentPage.vue"),
|
||||
name: "learningMentorManagement",
|
||||
},
|
||||
{
|
||||
path: "/lernbegleitung/:courseId/invitation/:invitationId",
|
||||
component: () => import("@/pages/learningMentor/InvitationAcceptPage.vue"),
|
||||
props: true,
|
||||
beforeEnter: handleAcceptLearningMentorInvitation,
|
||||
meta: {
|
||||
// The login redirect is handled in the beforeEnter guard
|
||||
public: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/course/:courseSlug/cockpit",
|
||||
name: "cockpit",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: () => import("@/pages/cockpit/cockpitPage/CockpitPage.vue"),
|
||||
path: "expert",
|
||||
component: () => import("@/pages/cockpit/cockpitPage/CockpitExpertPage.vue"),
|
||||
props: true,
|
||||
name: "expertCockpit",
|
||||
meta: {
|
||||
cockpitType: "expert",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "mentor",
|
||||
component: () => import("@/pages/cockpit/cockpitPage/CockpitMentorPage.vue"),
|
||||
name: "mentorCockpit",
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: () =>
|
||||
import("@/pages/cockpit/cockpitPage/mentor/MentorOverview.vue"),
|
||||
name: "mentorCockpitOverview",
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "participants",
|
||||
component: () =>
|
||||
import("@/pages/cockpit/cockpitPage/mentor/MentorParticipants.vue"),
|
||||
name: "mentorCockpitParticipants",
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "details",
|
||||
component: () =>
|
||||
import("@/pages/cockpit/cockpitPage/mentor/MentorDetailParentPage.vue"),
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "praxis-assignments/:praxisAssignmentId",
|
||||
component: () =>
|
||||
import(
|
||||
"@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignment.vue"
|
||||
),
|
||||
name: "mentorCockpitPraxisAssignments",
|
||||
meta: {
|
||||
cockpitType: "mentor",
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "profile/:userId",
|
||||
component: () => import("@/pages/cockpit/CockpitUserProfilePage.vue"),
|
||||
props: true,
|
||||
name: "cockpitUserProfile",
|
||||
},
|
||||
{
|
||||
path: "profile/:userId/:circleSlug",
|
||||
|
|
@ -312,6 +384,8 @@ router.beforeEach(redirectToLoginIfRequired);
|
|||
router.beforeEach(handleCurrentCourseSession);
|
||||
router.beforeEach(handleCourseSessionAsQueryParam);
|
||||
|
||||
router.beforeEach(handleCockpit);
|
||||
|
||||
router.beforeEach(addToHistory);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,18 @@ export function shouldUseSSO() {
|
|||
return appEnv.startsWith("prod") || appEnv.startsWith("stage");
|
||||
}
|
||||
|
||||
export function getLoginURL(params = {}) {
|
||||
let url = shouldUseSSO() ? "/sso/login/" : "/login-local";
|
||||
|
||||
function constructURL(basePath: string, params = {}) {
|
||||
const queryParams = new URLSearchParams(params);
|
||||
if (queryParams.toString()) {
|
||||
url += `?${queryParams}`;
|
||||
}
|
||||
return `${basePath}${queryParams.toString() ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
export function getLoginURL(params = {}) {
|
||||
const basePath = shouldUseSSO() ? "/sso/login" : "/login-local";
|
||||
return constructURL(basePath, params);
|
||||
}
|
||||
|
||||
export function getSignUpURL(params = {}) {
|
||||
return constructURL("/sso/signup", params);
|
||||
}
|
||||
|
||||
export function getLoginURLNext() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
import { itGetCached } from "@/fetchHelpers";
|
||||
import type { Ref } from "vue";
|
||||
import { ref, watchEffect } from "vue";
|
||||
|
||||
export interface Participant {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
interface Circle {
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
enum CompletionStatus {
|
||||
UNKNOWN = "UNKNOWN",
|
||||
SUBMITTED = "SUBMITTED",
|
||||
EVALUATED = "EVALUATED",
|
||||
}
|
||||
|
||||
interface Completion {
|
||||
status: CompletionStatus;
|
||||
user_id: string;
|
||||
last_name: string;
|
||||
}
|
||||
|
||||
export interface PraxisAssignment {
|
||||
id: string;
|
||||
title: string;
|
||||
circle_id: string;
|
||||
pending_evaluations: number;
|
||||
completions: Completion[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Summary {
|
||||
participants: Participant[];
|
||||
circles: Circle[];
|
||||
assignments: PraxisAssignment[];
|
||||
}
|
||||
|
||||
export const useMentorCockpit = (
|
||||
courseSessionId: string | Ref<string> | (() => string)
|
||||
) => {
|
||||
const isLoading = ref(false);
|
||||
const summary: Ref<Summary | null> = ref(null);
|
||||
const error = ref(null);
|
||||
|
||||
const getCircleTitleById = (id: string): string => {
|
||||
if (summary.value?.circles) {
|
||||
const circle = summary.value.circles.find((circle) => String(circle.id) === id);
|
||||
return circle ? circle.title : "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const getPraxisAssignmentById = (id: string): PraxisAssignment | null => {
|
||||
if (summary.value?.assignments) {
|
||||
const found = summary.value.assignments.find(
|
||||
(assignment) => assignment.id === id
|
||||
);
|
||||
return found ? found : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
summary.value = null;
|
||||
error.value = null;
|
||||
|
||||
itGetCached(`/api/mentor/${courseSessionId}/summary`)
|
||||
.then((response) => {
|
||||
summary.value = response;
|
||||
})
|
||||
.catch((err) => (error.value = err))
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
summary,
|
||||
error,
|
||||
getCircleTitleById,
|
||||
getPraxisAssignmentById,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,76 +1,37 @@
|
|||
import { useCourseData } from "@/composables";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { itGetCached } from "@/fetchHelpers";
|
||||
import { defineStore } from "pinia";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
type CircleCockpit = CircleLight & {
|
||||
name: string;
|
||||
};
|
||||
type CockpitType = "mentor" | "expert" | null;
|
||||
|
||||
export type CockpitStoreState = {
|
||||
courseSessionMembers: CourseSessionUser[] | undefined;
|
||||
circles: CircleCockpit[] | undefined;
|
||||
currentCircle: CircleCockpit | undefined;
|
||||
};
|
||||
export const useCockpitStore = defineStore("cockpit", () => {
|
||||
const cockpitType: Ref<CockpitType> = ref(null);
|
||||
const isLoading = ref(false);
|
||||
|
||||
export const useCockpitStore = defineStore({
|
||||
id: "cockpit",
|
||||
state: () => {
|
||||
return {
|
||||
courseSessionMembers: undefined,
|
||||
circles: [],
|
||||
currentCircle: undefined,
|
||||
} as CockpitStoreState;
|
||||
},
|
||||
actions: {
|
||||
async loadCircles(
|
||||
courseSlug: string,
|
||||
currentCourseSessionUser: CourseSessionUser | undefined
|
||||
) {
|
||||
log.debug("loadCircles called", courseSlug);
|
||||
const hasExpertCockpitType = computed(() => cockpitType.value === "expert");
|
||||
const hasMentorCockpitType = computed(() => cockpitType.value === "mentor");
|
||||
const hasNoCockpitType = computed(() => cockpitType.value === null);
|
||||
|
||||
this.circles = await courseCircles(courseSlug, currentCourseSessionUser);
|
||||
async function fetchCockpitType(courseId: string | null) {
|
||||
if (!courseId) {
|
||||
cockpitType.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.circles?.length) {
|
||||
await this.setCurrentCourseCircle(this.circles[0].slug);
|
||||
}
|
||||
},
|
||||
async setCurrentCourseCircle(circleSlug: string) {
|
||||
this.currentCircle = this.circles?.find((c) => c.slug === circleSlug);
|
||||
},
|
||||
async setCurrentCourseCircleFromEvent(event: CircleLight) {
|
||||
await this.setCurrentCourseCircle(event.slug);
|
||||
},
|
||||
},
|
||||
isLoading.value = true;
|
||||
const url = `/api/course/${courseId}/cockpit/`;
|
||||
const response = await itGetCached(url);
|
||||
cockpitType.value = response.type;
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
fetchCockpitType,
|
||||
hasExpertCockpitType,
|
||||
hasMentorCockpitType,
|
||||
hasNoCockpitType,
|
||||
isLoading,
|
||||
cockpitType,
|
||||
};
|
||||
});
|
||||
|
||||
async function courseCircles(
|
||||
courseSlug: string,
|
||||
currentCourseSessionUser: CourseSessionUser | undefined
|
||||
) {
|
||||
if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") {
|
||||
const expert = currentCourseSessionUser as ExpertSessionUser;
|
||||
return expert.circles.map((c) => {
|
||||
return { ...c, name: c.title };
|
||||
});
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
// Return all circles from learning path for admin users
|
||||
if (userStore.is_superuser) {
|
||||
const lpQueryResult = useCourseData(courseSlug);
|
||||
await lpQueryResult.resultPromise;
|
||||
|
||||
return (lpQueryResult.circles.value ?? []).map((c) => {
|
||||
return {
|
||||
id: c.id,
|
||||
slug: c.slug,
|
||||
title: c.title,
|
||||
name: c.title,
|
||||
} as const;
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { itGetCached } from "@/fetchHelpers";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import type { CourseSession, DueDate } from "@/types";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import { useRouteLookups } from "@/utils/route";
|
||||
|
|
@ -149,6 +150,9 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
function hasCockpit(courseSession: CourseSession) {
|
||||
const userStore = useUserStore();
|
||||
return (
|
||||
useCockpitStore().hasMentorCockpitType ||
|
||||
useCockpitStore().hasExpertCockpitType ||
|
||||
// for legacy reasons: don't forget course session supervisors!
|
||||
userStore.course_session_experts.includes(courseSession.id) ||
|
||||
userStore.is_superuser
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
import { useCourseData } from "@/composables";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
type CircleExpertCockpit = CircleLight & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ExpertCockpitStoreState = {
|
||||
courseSessionMembers: CourseSessionUser[] | undefined;
|
||||
circles: CircleExpertCockpit[] | undefined;
|
||||
currentCircle: CircleExpertCockpit | undefined;
|
||||
};
|
||||
|
||||
export const useExpertCockpitStore = defineStore({
|
||||
id: "expertCockpit",
|
||||
state: () => {
|
||||
return {
|
||||
courseSessionMembers: undefined,
|
||||
circles: [],
|
||||
currentCircle: undefined,
|
||||
} as ExpertCockpitStoreState;
|
||||
},
|
||||
actions: {
|
||||
async loadCircles(
|
||||
courseSlug: string,
|
||||
currentCourseSessionUser: CourseSessionUser | undefined
|
||||
) {
|
||||
log.debug("loadCircles called", courseSlug);
|
||||
|
||||
this.circles = await courseCircles(courseSlug, currentCourseSessionUser);
|
||||
|
||||
if (this.circles?.length) {
|
||||
await this.setCurrentCourseCircle(this.circles[0].slug);
|
||||
}
|
||||
},
|
||||
async setCurrentCourseCircle(circleSlug: string) {
|
||||
this.currentCircle = this.circles?.find((c) => c.slug === circleSlug);
|
||||
},
|
||||
async setCurrentCourseCircleFromEvent(event: CircleLight) {
|
||||
await this.setCurrentCourseCircle(event.slug);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
async function courseCircles(
|
||||
courseSlug: string,
|
||||
currentCourseSessionUser: CourseSessionUser | undefined
|
||||
) {
|
||||
if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") {
|
||||
const expert = currentCourseSessionUser as ExpertSessionUser;
|
||||
return expert.circles.map((c) => {
|
||||
return { ...c, name: c.title };
|
||||
});
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
// Return all circles from learning path for admin users
|
||||
if (userStore.is_superuser) {
|
||||
const lpQueryResult = useCourseData(courseSlug);
|
||||
await lpQueryResult.resultPromise;
|
||||
|
||||
return (lpQueryResult.circles.value ?? []).map((c) => {
|
||||
return {
|
||||
id: c.id,
|
||||
slug: c.slug,
|
||||
title: c.title,
|
||||
name: c.title,
|
||||
} as const;
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import type {
|
|||
AssignmentCompletionStatus as AssignmentCompletionStatusGenerated,
|
||||
AssignmentObjectType,
|
||||
CircleObjectType,
|
||||
CourseCourseCircleContactTypeChoices,
|
||||
CourseSessionObjectType,
|
||||
CourseSessionUserObjectsType,
|
||||
LearningContentAssignmentObjectType,
|
||||
|
|
@ -195,6 +196,7 @@ export interface Course {
|
|||
category_name: string;
|
||||
slug: string;
|
||||
enable_circle_documents: boolean;
|
||||
circle_contact_type: CourseCourseCircleContactTypeChoices;
|
||||
}
|
||||
|
||||
export interface CourseCategory {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ export function useRouteLookups() {
|
|||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inLearningMentor() {
|
||||
const regex = new RegExp("/course/[^/]+/mentor");
|
||||
return regex.test(route.path);
|
||||
}
|
||||
|
||||
function inMediaLibrary() {
|
||||
const regex = new RegExp("/course/[^/]+/media");
|
||||
return regex.test(route.path);
|
||||
|
|
@ -37,6 +42,7 @@ export function useRouteLookups() {
|
|||
inCockpit,
|
||||
inLearningPath,
|
||||
inCompetenceProfile,
|
||||
inLearningMentor,
|
||||
inCourse,
|
||||
inAppointments: inAppointments,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function createCourseUrl(courseSlug: string | undefined, specificSub: string): s
|
|||
return "/";
|
||||
}
|
||||
|
||||
if (["learn", "media", "competence", "cockpit"].includes(specificSub)) {
|
||||
if (["learn", "media", "competence", "cockpit", "mentor"].includes(specificSub)) {
|
||||
return `/course/${courseSlug}/${specificSub}`;
|
||||
}
|
||||
return `/course/${courseSlug}`;
|
||||
|
|
@ -36,6 +36,10 @@ export function getCockpitUrl(courseSlug: string | undefined): string {
|
|||
return createCourseUrl(courseSlug, "cockpit");
|
||||
}
|
||||
|
||||
export function getLearningMentorManagementUrl(courseSlug: string | undefined): string {
|
||||
return createCourseUrl(courseSlug, "mentor");
|
||||
}
|
||||
|
||||
export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
||||
import { login } from "../helpers";
|
||||
|
||||
function completePraxisAssignment(selectExpert = false) {
|
||||
function completePraxisAssignment() {
|
||||
cy.visit("/course/test-lehrgang/learn/reisen/mein-kundenstamm");
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
cy.testLearningContentTitle(
|
||||
"Teilaufgabe 1: Filtere nach Kundeneigenschaften"
|
||||
);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 1.1");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-2"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-2\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 1.2");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-3"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-3\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 1.3");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-4"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-4\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 1.4");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-5"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-5\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 1.5");
|
||||
// wait because of input debounce
|
||||
|
|
@ -32,19 +32,19 @@ function completePraxisAssignment(selectExpert = false) {
|
|||
|
||||
// step 2
|
||||
cy.testLearningContentTitle("Teilaufgabe 2: Filtere nach Versicherungen");
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 2.1");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-2"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-2\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 2.2");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-3"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-3\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 2.3");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-4"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-4\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 2.4");
|
||||
// wait because of input debounce
|
||||
|
|
@ -66,11 +66,11 @@ function completePraxisAssignment(selectExpert = false) {
|
|||
cy.testLearningContentTitle(
|
||||
"Teilaufgabe 3: Filtere nach besonderen Ereignissen"
|
||||
);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 3.1");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-2"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-2\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 3.2");
|
||||
// wait because of input debounce
|
||||
|
|
@ -79,7 +79,7 @@ function completePraxisAssignment(selectExpert = false) {
|
|||
|
||||
// step 4
|
||||
cy.testLearningContentTitle("Teilaufgabe 4: Kundentelefonate");
|
||||
cy.get('[data-cy="it-textarea-user-text-input-0"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-0\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 4.1");
|
||||
// wait because of input debounce
|
||||
|
|
@ -88,23 +88,24 @@ function completePraxisAssignment(selectExpert = false) {
|
|||
|
||||
// step 5
|
||||
cy.testLearningContentTitle("Teilaufgabe 5: Kundentelefonate2");
|
||||
cy.get('[data-cy="it-textarea-user-text-input-0"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-0\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 5.1");
|
||||
// wait because of input debounce
|
||||
cy.wait(550);
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
|
||||
cy.get('[data-cy="confirm-submit-results"]').should("not.exist");
|
||||
cy.get('[data-cy="confirm-submit-person"]').should(
|
||||
cy.get("[data-cy=\"confirm-submit-results\"]").should("not.exist");
|
||||
cy.get("[data-cy=\"confirm-submit-person\"]").should(
|
||||
"contain",
|
||||
"Folgende Person soll mir Feedback zu meinen Ergebnissen geben."
|
||||
);
|
||||
if (selectExpert) {
|
||||
cy.get('[data-cy="confirm-submit-person"]').click();
|
||||
}
|
||||
cy.get('[data-cy="submit-assignment"]').click();
|
||||
cy.get('[data-cy="success-text"]').should("exist");
|
||||
|
||||
cy.get("[data-cy=\"confirm-submit-person\"]").click();
|
||||
cy.get("[data-cy='select-learning-mentor']").should("be.visible").select(0);
|
||||
|
||||
cy.get("[data-cy=\"submit-assignment\"]").click();
|
||||
cy.get("[data-cy=\"success-text\"]").should("exist");
|
||||
|
||||
// app goes back to circle view -> check if assignment is marked as completed
|
||||
cy.url().should((url) => {
|
||||
|
|
@ -112,13 +113,13 @@ function completePraxisAssignment(selectExpert = false) {
|
|||
});
|
||||
cy.reload();
|
||||
cy.get(
|
||||
'[data-cy="test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm-checkbox"]'
|
||||
"[data-cy=\"test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm-checkbox\"]"
|
||||
).should("have.class", "cy-checked");
|
||||
}
|
||||
|
||||
describe("assignmentStudent.cy.js", () => {
|
||||
beforeEach(() => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
cy.manageCommand("cypress_reset --create-learning-mentor");
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit(
|
||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||
|
|
@ -162,10 +163,10 @@ describe("assignmentStudent.cy.js", () => {
|
|||
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
|
||||
);
|
||||
// Click confirmation
|
||||
cy.get('[data-cy="it-checkbox-confirmation-1"]').click();
|
||||
cy.get("[data-cy=\"it-checkbox-confirmation-1\"]").click();
|
||||
|
||||
cy.reload();
|
||||
cy.get('[data-cy="it-checkbox-confirmation-1"]').should(
|
||||
cy.get("[data-cy=\"it-checkbox-confirmation-1\"]").should(
|
||||
"have.class",
|
||||
"cy-checked"
|
||||
);
|
||||
|
|
@ -179,14 +180,14 @@ describe("assignmentStudent.cy.js", () => {
|
|||
"Teilaufgabe 2: Kundensituation und Ausgangslage"
|
||||
);
|
||||
// Enter text
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallovelo");
|
||||
// wait because of input debounce
|
||||
cy.wait(550);
|
||||
cy.reload();
|
||||
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]').should(
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]").should(
|
||||
"have.value",
|
||||
"Hallovelo"
|
||||
);
|
||||
|
|
@ -200,7 +201,7 @@ describe("assignmentStudent.cy.js", () => {
|
|||
});
|
||||
|
||||
it("can visit sub step by clicking navigation bar", () => {
|
||||
cy.get('[data-cy="nav-progress-step-4"]').click();
|
||||
cy.get("[data-cy=\"nav-progress-step-4\"]").click();
|
||||
cy.testLearningContentTitle("Teilaufgabe 4: Deine Empfehlungen");
|
||||
});
|
||||
|
||||
|
|
@ -208,10 +209,10 @@ describe("assignmentStudent.cy.js", () => {
|
|||
cy.visit(
|
||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice?step=7"
|
||||
);
|
||||
cy.get('[data-cy="confirm-submit-results"] label').click();
|
||||
cy.get('[data-cy="confirm-submit-person"]').click();
|
||||
cy.get('[data-cy="submit-assignment"]').click();
|
||||
cy.get('[data-cy="success-text"]').should("exist");
|
||||
cy.get("[data-cy=\"confirm-submit-results\"] label").click();
|
||||
cy.get("[data-cy=\"confirm-submit-person\"]").click();
|
||||
cy.get("[data-cy=\"submit-assignment\"]").click();
|
||||
cy.get("[data-cy=\"success-text\"]").should("exist");
|
||||
|
||||
// Check if trainer received notification
|
||||
cy.clearLocalStorage();
|
||||
|
|
@ -220,8 +221,8 @@ describe("assignmentStudent.cy.js", () => {
|
|||
login("test-trainer1@example.com", "test");
|
||||
cy.visit("/notifications");
|
||||
cy.get(`[data-cy=notification-idx-0]`).within(() => {
|
||||
cy.get('[data-cy="unread"]').should("exist");
|
||||
cy.get('[data-cy="notification-target-idx-0-verb"]').contains(
|
||||
cy.get("[data-cy=\"unread\"]").should("exist");
|
||||
cy.get("[data-cy=\"notification-target-idx-0-verb\"]").contains(
|
||||
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben."
|
||||
);
|
||||
});
|
||||
|
|
@ -233,14 +234,14 @@ describe("assignmentStudent.cy.js", () => {
|
|||
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
|
||||
);
|
||||
// Click confirmation
|
||||
cy.get('[data-cy="it-checkbox-confirmation-1"]').click();
|
||||
cy.get("[data-cy=\"it-checkbox-confirmation-1\"]").click();
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
|
||||
// step 2
|
||||
cy.testLearningContentTitle(
|
||||
"Teilaufgabe 2: Kundensituation und Ausgangslage"
|
||||
);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 2");
|
||||
// wait because of input debounce
|
||||
|
|
@ -261,7 +262,7 @@ describe("assignmentStudent.cy.js", () => {
|
|||
|
||||
// step 3
|
||||
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 3");
|
||||
// wait because of input debounce
|
||||
|
|
@ -270,15 +271,15 @@ describe("assignmentStudent.cy.js", () => {
|
|||
|
||||
// step 4
|
||||
cy.testLearningContentTitle("Teilaufgabe 4: Deine Empfehlungen");
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 4.1");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-2"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-2\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 4.2");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-3"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-3\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 4.3");
|
||||
// wait because of input debounce
|
||||
|
|
@ -287,15 +288,15 @@ describe("assignmentStudent.cy.js", () => {
|
|||
|
||||
// step 5
|
||||
cy.testLearningContentTitle("Teilaufgabe 5: Reflexion");
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 5.1");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-2"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-2\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 5.2");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-3"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-3\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 5.3");
|
||||
// wait because of input debounce
|
||||
|
|
@ -304,40 +305,40 @@ describe("assignmentStudent.cy.js", () => {
|
|||
|
||||
// step 6
|
||||
cy.testLearningContentTitle("Teilaufgabe 6: Learnings");
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-1\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 6.1");
|
||||
cy.wait(550);
|
||||
cy.get('[data-cy="it-textarea-user-text-input-2"]')
|
||||
cy.get("[data-cy=\"it-textarea-user-text-input-2\"]")
|
||||
.clear()
|
||||
.type("Hallo Teilaufgabe 6.2");
|
||||
// wait because of input debounce
|
||||
cy.wait(550);
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
|
||||
cy.get('[data-cy="confirm-submit-person"]').should(
|
||||
cy.get("[data-cy=\"confirm-submit-person\"]").should(
|
||||
"contain",
|
||||
"Ja, die folgende Person soll meine Ergebnisse bewerten."
|
||||
);
|
||||
cy.get('[data-cy="confirm-submit-results"] label').click();
|
||||
cy.get('[data-cy="confirm-submit-person"]').click();
|
||||
cy.get('[data-cy="submit-assignment"]').click();
|
||||
cy.get('[data-cy="success-text"]').should("exist");
|
||||
cy.get("[data-cy=\"confirm-submit-results\"] label").click();
|
||||
cy.get("[data-cy=\"confirm-submit-person\"]").click();
|
||||
cy.get("[data-cy=\"submit-assignment\"]").click();
|
||||
cy.get("[data-cy=\"success-text\"]").should("exist");
|
||||
|
||||
cy.get('[data-cy="confirm-container"]')
|
||||
.find('[data-cy="show-sample-solution"]')
|
||||
cy.get("[data-cy=\"confirm-container\"]")
|
||||
.find("[data-cy=\"show-sample-solution\"]")
|
||||
.then(($elements) => {
|
||||
if ($elements.length > 0) {
|
||||
// Ist die Musterlösung da?
|
||||
cy.get('[data-cy="show-sample-solution"]').should("exist");
|
||||
cy.get('[data-cy="show-sample-solution-button"]').should("exist");
|
||||
cy.get("[data-cy=\"show-sample-solution\"]").should("exist");
|
||||
cy.get("[data-cy=\"show-sample-solution-button\"]").should("exist");
|
||||
}
|
||||
});
|
||||
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/");
|
||||
|
||||
cy.get(
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
|
||||
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox\"]"
|
||||
).should("have.class", "cy-checked");
|
||||
|
||||
//reopening page should get directly to last step
|
||||
|
|
@ -370,10 +371,7 @@ describe("assignmentStudent.cy.js", () => {
|
|||
});
|
||||
|
||||
describe("Praxis Assignment", () => {
|
||||
it("can make complete assignment without expert", () =>
|
||||
it("can make complete assignment with mentor", () =>
|
||||
completePraxisAssignment());
|
||||
|
||||
it("can make complete assignment with expert", () =>
|
||||
completePraxisAssignment(true));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,13 +18,7 @@ from vbv_lernwelt.notify.email.email_services import (
|
|||
)
|
||||
|
||||
|
||||
def main():
|
||||
print("start")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
def send_attendance_course_reminder():
|
||||
csac = CourseSessionAttendanceCourse.objects.get(pk=1)
|
||||
print(csac)
|
||||
print(csac.trainer)
|
||||
|
|
@ -38,3 +32,28 @@ if __name__ == "__main__":
|
|||
fail_silently=False,
|
||||
)
|
||||
print(result)
|
||||
|
||||
|
||||
def send_learning_mentor_invitation():
|
||||
result = send_email(
|
||||
recipient_email="daniel.egger+sendgrid@gmail.com",
|
||||
template=EmailTemplate.LEARNING_MENTOR_INVITATION,
|
||||
template_data={
|
||||
"inviter_name": f"Daniel Egger",
|
||||
"inviter_email": "daniel.egger@example.com",
|
||||
"target_url": f"https://stage.vbv-afa.ch/foobar",
|
||||
},
|
||||
template_language="de",
|
||||
fail_silently=True,
|
||||
)
|
||||
print(result)
|
||||
|
||||
|
||||
def main():
|
||||
print("start")
|
||||
# send_attendance_course_reminder()
|
||||
send_learning_mentor_invitation()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ LOCAL_APPS = [
|
|||
"vbv_lernwelt.edoniq_test",
|
||||
"vbv_lernwelt.course_session_group",
|
||||
"vbv_lernwelt.shop",
|
||||
"vbv_lernwelt.learning_mentor",
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import os
|
|||
|
||||
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||
os.environ["AWS_S3_SECRET_ACCESS_KEY"] = os.environ.get(
|
||||
"AWS_S3_SECRET_ACCESS_KEY",
|
||||
"!!!default_for_quieting_tests_within_pycharm!!!",
|
||||
)
|
||||
|
||||
from .base import * # noqa
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ 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 me_user_view
|
||||
from vbv_lernwelt.api.user import get_cockpit_type, me_user_view
|
||||
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
|
||||
|
|
@ -127,6 +127,11 @@ urlpatterns = [
|
|||
path(r"api/course/completion/<signed_int:course_session_id>/<uuid:user_id>/",
|
||||
request_course_completion_for_user,
|
||||
name="request_course_completion_for_user"),
|
||||
path(r"api/course/<signed_int:course_id>/cockpit/",
|
||||
get_cockpit_type,
|
||||
name="get_cockpit_type"),
|
||||
|
||||
path("api/mentor/<signed_int:course_session_id>/", include("vbv_lernwelt.learning_mentor.urls")),
|
||||
|
||||
# assignment
|
||||
path(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
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.learning_mentor.models import LearningMentor
|
||||
|
||||
|
||||
class MeUserViewTest(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.course, _ = create_course("Test Course")
|
||||
self.user = create_user("tester")
|
||||
self.url = reverse("get_cockpit_type", kwargs={"course_id": self.course.id})
|
||||
|
||||
def test_no_cockpit(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data["type"], None)
|
||||
|
||||
def test_mentor_cockpit(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.user)
|
||||
LearningMentor.objects.create(mentor=self.user, course=self.course)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data["type"], "mentor")
|
||||
|
||||
def test_trainer_cockpit(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.user)
|
||||
course_session = create_course_session(course=self.course, title="Test Session")
|
||||
|
||||
CourseSessionUser.objects.create(
|
||||
user=self.user,
|
||||
course_session=course_session,
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data["type"], "expert")
|
||||
|
||||
def test_supervisor_cockpit(self):
|
||||
# GIVEN
|
||||
self.client.force_login(self.user)
|
||||
course_session = create_course_session(course=self.course, title="Test Session")
|
||||
|
||||
csg = CourseSessionGroup.objects.create(
|
||||
name="Test Group",
|
||||
course=course_session.course,
|
||||
)
|
||||
|
||||
csg.course_session.add(course_session)
|
||||
csg.supervisor.add(self.user)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.data["type"], "expert")
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
from rest_framework.decorators import api_view
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
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.learning_mentor.models import LearningMentor
|
||||
|
||||
|
||||
@api_view(["GET", "PUT"])
|
||||
def me_user_view(request):
|
||||
|
|
@ -23,3 +30,32 @@ def me_user_view(request):
|
|||
return Response(UserSerializer(request.user).data)
|
||||
|
||||
return Response(status=400)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_cockpit_type(request, course_id: int):
|
||||
course = get_object_or_404(Course, id=course_id)
|
||||
|
||||
is_mentor = LearningMentor.objects.filter(
|
||||
mentor=request.user, course=course
|
||||
).exists()
|
||||
|
||||
is_expert = CourseSessionUser.objects.filter(
|
||||
user=request.user,
|
||||
course_session__course=course,
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
).exists()
|
||||
|
||||
is_supervisor = CourseSessionGroup.objects.filter(
|
||||
course_session__course=course, supervisor=request.user
|
||||
).exists()
|
||||
|
||||
if is_mentor:
|
||||
cockpit_type = "mentor"
|
||||
elif is_expert or is_supervisor:
|
||||
cockpit_type = "expert"
|
||||
else:
|
||||
cockpit_type = None
|
||||
|
||||
return Response({"type": cockpit_type})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletionStatu
|
|||
from vbv_lernwelt.assignment.services import update_assignment_completion
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
|
||||
from vbv_lernwelt.iam.permissions import can_evaluate_assignments, has_course_access
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
|
@ -31,6 +31,7 @@ class AssignmentCompletionMutation(graphene.Mutation):
|
|||
|
||||
evaluation_points = graphene.Float()
|
||||
evaluation_passed = graphene.Boolean()
|
||||
evaluation_user_id = graphene.ID(required=False)
|
||||
initialize_completion = graphene.Boolean(required=False)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -46,6 +47,7 @@ class AssignmentCompletionMutation(graphene.Mutation):
|
|||
completion_data_string="{}",
|
||||
evaluation_points=None,
|
||||
evaluation_passed=None,
|
||||
evaluation_user_id=None,
|
||||
initialize_completion=False,
|
||||
):
|
||||
if assignment_user_id is None:
|
||||
|
|
@ -78,13 +80,18 @@ class AssignmentCompletionMutation(graphene.Mutation):
|
|||
}
|
||||
|
||||
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
||||
# TODO: determine proper way to assign evaluation user
|
||||
experts = CourseSessionUser.objects.filter(
|
||||
course_session=course_session, role="EXPERT"
|
||||
)
|
||||
if not experts:
|
||||
raise PermissionDenied()
|
||||
assignment_data["evaluation_user"] = experts[0].user
|
||||
if evaluation_user_id:
|
||||
assignment_data["evaluation_user"] = User.objects.get(
|
||||
id=evaluation_user_id
|
||||
)
|
||||
else:
|
||||
# TODO: determine proper way to assign evaluation user
|
||||
experts = CourseSessionUser.objects.filter(
|
||||
course_session=course_session, role="EXPERT"
|
||||
)
|
||||
if not experts:
|
||||
raise PermissionDenied()
|
||||
assignment_data["evaluation_user"] = experts[0].user
|
||||
|
||||
evaluation_data = {}
|
||||
|
||||
|
|
@ -92,7 +99,7 @@ class AssignmentCompletionMutation(graphene.Mutation):
|
|||
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
|
||||
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
|
||||
):
|
||||
if not is_course_session_expert(info.context.user, course_session_id):
|
||||
if not can_evaluate_assignments(info.context.user, course_session_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
evaluation_data = {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
|||
from vbv_lernwelt.core.graphql.types import JSONStreamField
|
||||
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
|
||||
from vbv_lernwelt.iam.permissions import can_evaluate_assignments, has_course_access
|
||||
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
||||
from vbv_lernwelt.media_files.graphql.types import ContentDocumentObjectType
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ def resolve_assignment_completion(
|
|||
if assignment_user_id is None:
|
||||
assignment_user_id = info.context.user.id
|
||||
|
||||
if str(assignment_user_id) == str(info.context.user.id) or is_course_session_expert(
|
||||
if str(assignment_user_id) == str(info.context.user.id) or can_evaluate_assignments(
|
||||
info.context.user, course_session_id
|
||||
):
|
||||
course_id = CourseSession.objects.get(id=course_session_id).course_id
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ def update_assignment_completion(
|
|||
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
||||
ac.submitted_at = timezone.now()
|
||||
if evaluation_user:
|
||||
ac.evaluation_user = evaluation_user
|
||||
NotificationService.send_assignment_submitted_notification(
|
||||
recipient=evaluation_user,
|
||||
sender=ac.assignment_user,
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
user_text_input0 = subtasks[0]
|
||||
user_text_input1 = subtasks[1]
|
||||
|
||||
ac = AssignmentCompletion.objects.create(
|
||||
AssignmentCompletion.objects.create(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
|
|
@ -163,6 +163,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_status=AssignmentCompletionStatus.SUBMITTED,
|
||||
evaluation_user=self.trainer,
|
||||
)
|
||||
|
||||
ac = AssignmentCompletion.objects.get(
|
||||
|
|
@ -173,6 +174,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
|
||||
self.assertEqual(ac.completion_status, "SUBMITTED")
|
||||
self.assertEqual(ac.submitted_at.date(), date.today())
|
||||
self.assertEqual(ac.evaluation_user, self.trainer)
|
||||
|
||||
# will create AssignmentCompletionAuditLog entry
|
||||
acl = AssignmentCompletionAuditLog.objects.get(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from rest_framework.exceptions import PermissionDenied
|
|||
from rest_framework.response import Response
|
||||
|
||||
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
||||
from vbv_lernwelt.iam.permissions import is_course_session_expert
|
||||
from vbv_lernwelt.iam.permissions import can_evaluate_assignments
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ logger = structlog.get_logger(__name__)
|
|||
@api_view(["GET"])
|
||||
def request_assignment_completion_status(request, assignment_id, course_session_id):
|
||||
# TODO quickfix before GraphQL...
|
||||
if is_course_session_expert(request.user, course_session_id):
|
||||
if can_evaluate_assignments(request.user, course_session_id):
|
||||
qs = AssignmentCompletion.objects.filter(
|
||||
course_session_id=course_session_id,
|
||||
assignment_id=assignment_id,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
|
|||
TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
||||
TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
||||
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
||||
TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
|
||||
|
||||
TEST_COURSE_SESSION_BERN_ID = -1
|
||||
TEST_COURSE_SESSION_ZURICH_ID = -2
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ env.read_env()
|
|||
|
||||
from vbv_lernwelt.core.constants import (
|
||||
ADMIN_USER_ID,
|
||||
TEST_MENTOR1_USER_ID,
|
||||
TEST_STUDENT1_USER_ID,
|
||||
TEST_STUDENT2_USER_ID,
|
||||
TEST_STUDENT3_USER_ID,
|
||||
|
|
@ -342,6 +343,15 @@ def create_default_users(default_password="test"):
|
|||
language="de",
|
||||
avatar_url="",
|
||||
)
|
||||
_create_user(
|
||||
_id=TEST_MENTOR1_USER_ID,
|
||||
email="test-mentor1@example.com",
|
||||
first_name="[Mentor]",
|
||||
last_name="Mentor",
|
||||
password=default_password,
|
||||
language="de",
|
||||
avatar_url="",
|
||||
)
|
||||
|
||||
|
||||
def _get_or_create_user(user_model, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
|||
from vbv_lernwelt.competence.models import PerformanceCriteria
|
||||
from vbv_lernwelt.core.constants import (
|
||||
TEST_COURSE_SESSION_BERN_ID,
|
||||
TEST_MENTOR1_USER_ID,
|
||||
TEST_STUDENT1_USER_ID,
|
||||
TEST_STUDENT2_USER_ID,
|
||||
TEST_STUDENT3_USER_ID,
|
||||
|
|
@ -25,11 +26,13 @@ from vbv_lernwelt.course.models import (
|
|||
CourseCompletion,
|
||||
CourseCompletionStatus,
|
||||
CourseSession,
|
||||
CourseSessionUser,
|
||||
)
|
||||
from vbv_lernwelt.course.services import mark_course_completion
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
LearningContentAttendanceCourse,
|
||||
LearningContentFeedbackUK,
|
||||
|
|
@ -81,6 +84,11 @@ from vbv_lernwelt.notify.models import Notification
|
|||
default=True,
|
||||
help="will enable circle documents for test course",
|
||||
)
|
||||
@click.option(
|
||||
"--create-learning-mentor/--no--create-learning-mentor",
|
||||
default=False,
|
||||
help="Will create a learning mentor for test user",
|
||||
)
|
||||
def command(
|
||||
create_assignment_completion,
|
||||
create_assignment_evaluation,
|
||||
|
|
@ -90,6 +98,7 @@ def command(
|
|||
create_course_completion_performance_criteria,
|
||||
create_attendance_days,
|
||||
enable_circle_documents,
|
||||
create_learning_mentor,
|
||||
):
|
||||
print("cypress reset data")
|
||||
CourseCompletion.objects.all().delete()
|
||||
|
|
@ -98,6 +107,7 @@ def command(
|
|||
FeedbackResponse.objects.all().delete()
|
||||
CourseSessionAttendanceCourse.objects.all().update(attendance_user_list=[])
|
||||
|
||||
LearningMentor.objects.all().delete()
|
||||
User.objects.all().update(language="de")
|
||||
User.objects.all().update(additional_json_data={})
|
||||
|
||||
|
|
@ -320,6 +330,18 @@ def command(
|
|||
)
|
||||
attendance_course.save()
|
||||
|
||||
if create_learning_mentor:
|
||||
print("Create learning mentor")
|
||||
mentor = LearningMentor.objects.create(
|
||||
course=Course.objects.get(id=COURSE_TEST_ID),
|
||||
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
|
||||
)
|
||||
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
|
||||
csu = CourseSessionUser.objects.get(
|
||||
user__id=TEST_STUDENT1_USER_ID, course_session=course_session
|
||||
)
|
||||
mentor.participants.add(csu)
|
||||
|
||||
course = Course.objects.get(id=COURSE_TEST_ID)
|
||||
course.enable_circle_documents = enable_circle_documents
|
||||
course.save()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from vbv_lernwelt.course_session.models import (
|
|||
)
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.duedate.models import DueDate
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
Circle,
|
||||
LearningContentAssignment,
|
||||
|
|
@ -94,6 +95,14 @@ def create_course_session(
|
|||
)
|
||||
|
||||
|
||||
def add_learning_mentor(
|
||||
course: Course, mentor: User, mentee: CourseSessionUser
|
||||
) -> LearningMentor:
|
||||
learning_mentor = LearningMentor.objects.create(course=course, mentor=mentor)
|
||||
learning_mentor.participants.add(mentee)
|
||||
return learning_mentor
|
||||
|
||||
|
||||
def add_course_session_user(
|
||||
course_session: CourseSession, user: User, role: CourseSessionUser.Role
|
||||
) -> CourseSessionUser:
|
||||
|
|
|
|||
|
|
@ -94,7 +94,14 @@ class CourseObjectType(DjangoObjectType):
|
|||
|
||||
class Meta:
|
||||
model = Course
|
||||
fields = ("id", "title", "category_name", "slug", "enable_circle_documents")
|
||||
fields = (
|
||||
"id",
|
||||
"title",
|
||||
"category_name",
|
||||
"slug",
|
||||
"enable_circle_documents",
|
||||
"circle_contact_type",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def resolve_learning_path(root: Course, info):
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from vbv_lernwelt.competence.create_vv_new_competence_profile import (
|
|||
create_vv_new_competence_profile,
|
||||
)
|
||||
from vbv_lernwelt.competence.models import PerformanceCriteria
|
||||
from vbv_lernwelt.core.constants import TEST_MENTOR1_USER_ID
|
||||
from vbv_lernwelt.core.create_default_users import default_users
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.consts import (
|
||||
|
|
@ -91,6 +92,7 @@ from vbv_lernwelt.importer.services import (
|
|||
import_students_from_excel,
|
||||
import_trainers_from_excel_for_training,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
||||
create_vv_new_learning_path,
|
||||
)
|
||||
|
|
@ -213,6 +215,13 @@ def create_versicherungsvermittlerin_course(
|
|||
|
||||
cs = CourseSession.objects.create(course_id=course_id, title=names[language])
|
||||
|
||||
for assignment in Assignment.objects.all():
|
||||
if assignment.get_course().id == course_id:
|
||||
CourseSessionAssignment.objects.get_or_create(
|
||||
course_session=cs,
|
||||
learning_content=assignment.find_attached_learning_content(),
|
||||
)
|
||||
|
||||
if language == "de":
|
||||
for user_data in default_users:
|
||||
CourseSessionUser.objects.create(
|
||||
|
|
@ -242,6 +251,13 @@ def create_versicherungsvermittlerin_course(
|
|||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
|
||||
lemme = LearningMentor.objects.create(
|
||||
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
|
||||
course=cs.course,
|
||||
)
|
||||
|
||||
lemme.participants.add(csu)
|
||||
|
||||
experts = [expert1, expert2, expert3]
|
||||
|
||||
circles = Circle.objects.filter(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-21 13:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("course", "0005_course_enable_circle_documents"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="course",
|
||||
name="circle_contact_type",
|
||||
field=models.CharField(
|
||||
choices=[("EXPERT", "EXPERT"), ("LEARNING_MENTOR", "LEARNING_MENTOR")],
|
||||
default="EXPERT",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="coursesessionuser",
|
||||
name="role",
|
||||
field=models.CharField(
|
||||
choices=[("MEMBER", "Teilnehmer"), ("EXPERT", "Experte/Trainer")],
|
||||
default="MEMBER",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import enum
|
||||
import uuid
|
||||
from enum import Enum
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import UniqueConstraint, Value
|
||||
|
|
@ -15,6 +15,11 @@ from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class
|
|||
from vbv_lernwelt.files.models import UploadFile
|
||||
|
||||
|
||||
class CircleContactType(Enum):
|
||||
EXPERT = "EXPERT"
|
||||
LEARNING_MENTOR = "LEARNING_MENTOR"
|
||||
|
||||
|
||||
class Course(models.Model):
|
||||
title = models.CharField(_("Titel"), max_length=255)
|
||||
category_name = models.CharField(
|
||||
|
|
@ -26,6 +31,11 @@ class Course(models.Model):
|
|||
enable_circle_documents = models.BooleanField(
|
||||
_("Trainer Dokumente in Circles"), default=True
|
||||
)
|
||||
circle_contact_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=[(cct.value, cct.value) for cct in CircleContactType],
|
||||
default=CircleContactType.EXPERT.value,
|
||||
)
|
||||
|
||||
def get_course_url(self):
|
||||
return f"/course/{self.slug}"
|
||||
|
|
@ -184,7 +194,7 @@ class CoursePage(CourseBasePage):
|
|||
return f"{self.title}"
|
||||
|
||||
|
||||
class CourseCompletionStatus(enum.Enum):
|
||||
class CourseCompletionStatus(Enum):
|
||||
SUCCESS = "SUCCESS"
|
||||
FAIL = "FAIL"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
|
@ -269,7 +279,6 @@ class CourseSessionUser(models.Model):
|
|||
class Role(models.TextChoices):
|
||||
MEMBER = "MEMBER", _("Teilnehmer")
|
||||
EXPERT = "EXPERT", _("Experte/Trainer")
|
||||
TUTOR = "TUTOR", _("Lernbegleitung")
|
||||
|
||||
role = models.CharField(choices=Role.choices, max_length=255, default=Role.MEMBER)
|
||||
|
||||
|
|
@ -289,6 +298,9 @@ class CourseSessionUser(models.Model):
|
|||
]
|
||||
ordering = ["user__last_name", "user__first_name", "user__email"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} ({self.course_session.title})"
|
||||
|
||||
|
||||
class CircleDocument(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from vbv_lernwelt.iam.permissions import (
|
|||
is_circle_expert,
|
||||
is_course_session_expert,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
|
@ -142,8 +143,17 @@ def get_course_sessions(request):
|
|||
).values_list("course_session", flat=True)
|
||||
).prefetch_related("course")
|
||||
|
||||
# enrich with mentor course sessions
|
||||
mentor_course_sessions = CourseSession.objects.filter(
|
||||
course__in=LearningMentor.objects.filter(mentor=request.user).values_list(
|
||||
"course", flat=True
|
||||
)
|
||||
).prefetch_related("course")
|
||||
|
||||
all_to_serialize = (
|
||||
regular_course_sessions | supervisor_course_sessions
|
||||
regular_course_sessions
|
||||
| supervisor_course_sessions
|
||||
| mentor_course_sessions
|
||||
).distinct()
|
||||
|
||||
return Response(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from vbv_lernwelt.iam.permissions import (
|
|||
can_view_course_session_group_statistics,
|
||||
can_view_course_session_progress,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
|
||||
|
||||
class DashboardQuery(graphene.ObjectType):
|
||||
|
|
@ -86,7 +87,13 @@ class DashboardQuery(graphene.ObjectType):
|
|||
user=user, exclude_course_ids=statistics_dashboard_course_ids
|
||||
)
|
||||
|
||||
return statistic_dashboards + course_session_dashboards
|
||||
learning_mentor_dashboards = get_learning_mentor_dashboards(user=user)
|
||||
|
||||
return (
|
||||
statistic_dashboards
|
||||
+ course_session_dashboards
|
||||
+ learning_mentor_dashboards
|
||||
)
|
||||
|
||||
def resolve_course_progress(root, info, course_id: str): # noqa
|
||||
"""
|
||||
|
|
@ -174,6 +181,24 @@ def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Se
|
|||
return dashboards, course_index
|
||||
|
||||
|
||||
def get_learning_mentor_dashboards(user: User) -> List[Dict[str, str]]:
|
||||
learning_mentor = LearningMentor.objects.filter(mentor=user)
|
||||
dashboards = []
|
||||
|
||||
for mentor in learning_mentor:
|
||||
course = mentor.course
|
||||
dashboards.append(
|
||||
{
|
||||
"id": str(course.id),
|
||||
"name": course.title,
|
||||
"slug": course.slug,
|
||||
"dashboard_type": DashboardType.SIMPLE_DASHBOARD,
|
||||
}
|
||||
)
|
||||
|
||||
return dashboards
|
||||
|
||||
|
||||
def get_user_course_session_dashboards(
|
||||
user: User, exclude_course_ids: Set[int]
|
||||
) -> List[Dict[str, str]]:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from vbv_lernwelt.assignment.models import AssignmentType
|
|||
from vbv_lernwelt.course.creators.test_utils import (
|
||||
add_course_session_group_supervisor,
|
||||
add_course_session_user,
|
||||
add_learning_mentor,
|
||||
create_assignment,
|
||||
create_assignment_completion,
|
||||
create_circle,
|
||||
|
|
@ -213,6 +214,44 @@ class DashboardTestCase(GraphQLTestCase):
|
|||
self.assertEqual(course_3_config["slug"], course_3.slug)
|
||||
self.assertEqual(course_3_config["dashboard_type"], "SIMPLE_DASHBOARD")
|
||||
|
||||
def test_dashboard_config_mentor(self):
|
||||
# GIVEN
|
||||
course_1, _ = create_course("Test Course 1")
|
||||
cs_1 = create_course_session(course=course_1, title="Test Course 1 Session")
|
||||
|
||||
mentor = create_user("learning mentor")
|
||||
csu = add_course_session_user(
|
||||
course_session=cs_1,
|
||||
user=create_user("csu"),
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
add_learning_mentor(course=course_1, mentor=mentor, mentee=csu)
|
||||
|
||||
self.client.force_login(mentor)
|
||||
|
||||
# WHEN
|
||||
query = """query {
|
||||
dashboard_config {
|
||||
id
|
||||
name
|
||||
slug
|
||||
dashboard_type
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
response = self.query(query)
|
||||
|
||||
# THEN
|
||||
self.assertResponseNoErrors(response)
|
||||
|
||||
self.assertEqual(len(response.json()["data"]["dashboard_config"]), 1)
|
||||
self.assertEqual(
|
||||
response.json()["data"]["dashboard_config"][0]["dashboard_type"],
|
||||
"SIMPLE_DASHBOARD",
|
||||
)
|
||||
|
||||
def test_course_statistics_deny_not_allowed_user(self):
|
||||
# GIVEN
|
||||
disallowed_user = create_user("1337_hacker_schorsch")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.models import LearningSequence
|
||||
|
||||
|
||||
|
|
@ -48,6 +49,34 @@ def is_course_session_expert(user, course_session_id: int):
|
|||
return is_supervisor or is_expert
|
||||
|
||||
|
||||
def is_course_session_member(user, course_session_id: int | None = None):
|
||||
if course_session_id is None:
|
||||
return False
|
||||
|
||||
return CourseSessionUser.objects.filter(
|
||||
course_session_id=course_session_id,
|
||||
user=user,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
).exists()
|
||||
|
||||
|
||||
def can_evaluate_assignments(user, course_session_id: int):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
is_supervisor = CourseSessionGroup.objects.filter(
|
||||
supervisor=user, course_session__id=course_session_id
|
||||
).exists()
|
||||
|
||||
is_expert = CourseSessionUser.objects.filter(
|
||||
course_session_id=course_session_id,
|
||||
user=user,
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
).exists()
|
||||
|
||||
return is_supervisor or is_expert
|
||||
|
||||
|
||||
def course_sessions_for_user_qs(user):
|
||||
if user.is_superuser:
|
||||
return CourseSession.objects.all()
|
||||
|
|
@ -106,3 +135,18 @@ def can_view_course_session(user: User, course_session: CourseSession) -> bool:
|
|||
course_session=course_session,
|
||||
user=user,
|
||||
).exists()
|
||||
|
||||
|
||||
def has_role_in_course(user: User, course: Course) -> bool:
|
||||
if CourseSessionUser.objects.filter(
|
||||
course_session__course=course, user=user
|
||||
).exists():
|
||||
return True
|
||||
|
||||
if LearningMentor.objects.filter(course=course, mentor=user).exists():
|
||||
return True
|
||||
|
||||
if CourseSessionGroup.objects.filter(course=course, supervisor=user).exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
from django.test import TestCase
|
||||
|
||||
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
|
||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||
from vbv_lernwelt.iam.permissions import has_role_in_course
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
|
||||
|
||||
class RoleTestCase(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_has_role_regional(self):
|
||||
# GIVEN
|
||||
csg = CourseSessionGroup.objects.create(name="Test Group", course=self.course)
|
||||
csg.supervisor.add(self.user)
|
||||
|
||||
# WHEN
|
||||
has_role = has_role_in_course(user=self.user, course=self.course)
|
||||
|
||||
# THEN
|
||||
self.assertTrue(has_role)
|
||||
|
||||
def test_has_role_course_session(self):
|
||||
# GIVEN
|
||||
add_course_session_user(
|
||||
self.course_session,
|
||||
self.user,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
# WHEN
|
||||
has_role = has_role_in_course(user=self.user, course=self.course)
|
||||
|
||||
# THEN
|
||||
self.assertTrue(has_role)
|
||||
|
||||
def test_has_role_mentor(self):
|
||||
# GIVEN
|
||||
LearningMentor.objects.create(
|
||||
mentor=self.user,
|
||||
course=self.course,
|
||||
)
|
||||
|
||||
# WHEN
|
||||
has_role = has_role_in_course(user=self.user, course=self.course)
|
||||
|
||||
# THEN
|
||||
self.assertTrue(has_role)
|
||||
|
||||
def test_no_role(self):
|
||||
# GIVEN
|
||||
other_course, _ = create_course("Other Test Course")
|
||||
other_course_session = create_course_session(
|
||||
course=other_course, title="Other Test Session"
|
||||
)
|
||||
add_course_session_user(
|
||||
other_course_session,
|
||||
self.user,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
# WHEN
|
||||
has_role = has_role_in_course(user=self.user, course=self.course)
|
||||
|
||||
# THEN
|
||||
self.assertFalse(has_role)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
||||
|
||||
|
||||
@admin.register(LearningMentor)
|
||||
class LearningMentorAdmin(admin.ModelAdmin):
|
||||
def participant_count(self, obj):
|
||||
return obj.participants.count()
|
||||
|
||||
participant_count.short_description = "Participants"
|
||||
|
||||
list_display = ["mentor", "course", "participant_count"]
|
||||
|
||||
|
||||
@admin.register(MentorInvitation)
|
||||
class MentorInvitationAdmin(admin.ModelAdmin):
|
||||
list_display = ["id", "email", "participant", "created"]
|
||||
readonly_fields = ["id", "created"]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LearningMentorConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "vbv_lernwelt.learning_mentor"
|
||||
|
||||
def ready(self):
|
||||
import vbv_lernwelt.learning_mentor.signals # noqa F401
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
from typing import List, Set, Tuple
|
||||
|
||||
from vbv_lernwelt.assignment.models import (
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
AssignmentCompletionStatus,
|
||||
AssignmentType,
|
||||
)
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAssignment
|
||||
from vbv_lernwelt.learning_mentor.entities import (
|
||||
CompletionStatus,
|
||||
PraxisAssignmentCompletion,
|
||||
PraxisAssignmentStatus,
|
||||
)
|
||||
|
||||
|
||||
def get_assignment_completions(
|
||||
course_session: CourseSession,
|
||||
assignment: Assignment,
|
||||
participants: List[User],
|
||||
evaluation_user: User,
|
||||
) -> List[PraxisAssignmentCompletion]:
|
||||
evaluation_results = AssignmentCompletion.objects.filter(
|
||||
assignment_user__in=participants,
|
||||
course_session=course_session,
|
||||
assignment=assignment,
|
||||
evaluation_user=evaluation_user,
|
||||
).values("completion_status", "assignment_user__last_name", "assignment_user")
|
||||
|
||||
user_status_map = {}
|
||||
for result in evaluation_results:
|
||||
completion_status = result["completion_status"]
|
||||
|
||||
if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED.value:
|
||||
status = CompletionStatus.EVALUATED
|
||||
elif completion_status in [
|
||||
AssignmentCompletionStatus.SUBMITTED.value,
|
||||
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS.value,
|
||||
]:
|
||||
status = CompletionStatus.SUBMITTED
|
||||
else:
|
||||
status = CompletionStatus.UNKNOWN
|
||||
|
||||
user_status_map[result["assignment_user"]] = (
|
||||
status,
|
||||
result["assignment_user__last_name"],
|
||||
)
|
||||
|
||||
status_priority = {
|
||||
CompletionStatus.SUBMITTED: 1,
|
||||
CompletionStatus.EVALUATED: 2,
|
||||
CompletionStatus.UNKNOWN: 3,
|
||||
}
|
||||
|
||||
sorted_participants = sorted(
|
||||
participants,
|
||||
key=lambda u: (
|
||||
status_priority.get(
|
||||
user_status_map.get(u.id, (CompletionStatus.UNKNOWN, ""))[0]
|
||||
),
|
||||
user_status_map.get(u.id, ("", u.last_name))[1],
|
||||
),
|
||||
)
|
||||
|
||||
return [
|
||||
PraxisAssignmentCompletion(
|
||||
status=user_status_map.get(
|
||||
user.id, (CompletionStatus.UNKNOWN, user.last_name)
|
||||
)[0],
|
||||
user_id=user.id,
|
||||
last_name=user.last_name,
|
||||
)
|
||||
for user in sorted_participants
|
||||
]
|
||||
|
||||
|
||||
def get_praxis_assignments(
|
||||
course_session: CourseSession, participants: List[User], evaluation_user: User
|
||||
) -> Tuple[List[PraxisAssignmentStatus], Set[int]]:
|
||||
records = []
|
||||
circle_ids = set()
|
||||
|
||||
if not participants:
|
||||
return records, circle_ids
|
||||
|
||||
for course_session_assignment in CourseSessionAssignment.objects.filter(
|
||||
course_session=course_session,
|
||||
learning_content__assignment_type__in=[
|
||||
AssignmentType.PRAXIS_ASSIGNMENT.value,
|
||||
],
|
||||
):
|
||||
learning_content = course_session_assignment.learning_content
|
||||
|
||||
completions = get_assignment_completions(
|
||||
course_session=course_session,
|
||||
assignment=learning_content.content_assignment,
|
||||
participants=participants,
|
||||
evaluation_user=evaluation_user,
|
||||
)
|
||||
|
||||
submitted_count = len(
|
||||
[
|
||||
completion
|
||||
for completion in completions
|
||||
if completion.status == CompletionStatus.SUBMITTED
|
||||
]
|
||||
)
|
||||
|
||||
circle_id = learning_content.get_circle().id
|
||||
|
||||
records.append(
|
||||
PraxisAssignmentStatus(
|
||||
id=course_session_assignment.id,
|
||||
title=learning_content.content_assignment.title,
|
||||
circle_id=circle_id,
|
||||
pending_evaluations=submitted_count,
|
||||
completions=completions,
|
||||
)
|
||||
)
|
||||
|
||||
circle_ids.add(circle_id)
|
||||
|
||||
return records, circle_ids
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
|
||||
class CompletionStatus(str, Enum):
|
||||
UNKNOWN = "UNKNOWN"
|
||||
SUBMITTED = "SUBMITTED"
|
||||
EVALUATED = "EVALUATED"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PraxisAssignmentCompletion:
|
||||
status: CompletionStatus
|
||||
user_id: str
|
||||
last_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PraxisAssignmentStatus:
|
||||
id: str
|
||||
title: str
|
||||
circle_id: str
|
||||
pending_evaluations: int
|
||||
completions: List[PraxisAssignmentCompletion]
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-07 08:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("course", "0005_course_enable_circle_documents"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LearningMentor",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"course",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="course.course"
|
||||
),
|
||||
),
|
||||
(
|
||||
"mentor",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"participants",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="participants",
|
||||
to="course.CourseSessionUser",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("mentor", "course")},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-07 13:46
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("course", "0005_course_enable_circle_documents"),
|
||||
("learning_mentor", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="MentorInvitation",
|
||||
fields=[
|
||||
(
|
||||
"created",
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name="created"
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name="modified"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
(
|
||||
"participant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="course.coursesessionuser",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Mentor Invitation",
|
||||
"verbose_name_plural": "Mentor Invitations",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-07 13:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("learning_mentor", "0002_mentorinvitation"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="learningmentor",
|
||||
options={
|
||||
"verbose_name": "Lernbegleiter",
|
||||
"verbose_name_plural": "Lernbegleiter",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="mentorinvitation",
|
||||
options={
|
||||
"verbose_name": "Lernbegleiter Einladung",
|
||||
"verbose_name_plural": "Lernbegleiter Einladungen",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-11 09:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("course", "0005_course_enable_circle_documents"),
|
||||
("learning_mentor", "0003_auto_20231207_1448"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="mentorinvitation",
|
||||
unique_together={("email", "participant")},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseSessionUser
|
||||
|
||||
|
||||
class LearningMentor(models.Model):
|
||||
mentor = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
course = models.ForeignKey("course.Course", on_delete=models.CASCADE)
|
||||
|
||||
participants = models.ManyToManyField(
|
||||
CourseSessionUser,
|
||||
related_name="participants",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = [["mentor", "course"]]
|
||||
verbose_name = "Lernbegleiter"
|
||||
verbose_name_plural = "Lernbegleiter"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.mentor} ({self.course.title})"
|
||||
|
||||
@property
|
||||
def course_sessions(self):
|
||||
return self.participants.values_list("course_session", flat=True).distinct()
|
||||
|
||||
|
||||
class MentorInvitation(TimeStampedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
email = models.EmailField()
|
||||
participant = models.ForeignKey(CourseSessionUser, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.email} ({self.participant})"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Lernbegleiter Einladung"
|
||||
verbose_name_plural = "Lernbegleiter Einladungen"
|
||||
unique_together = [["email", "participant"]]
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from vbv_lernwelt.core.serializers import UserSerializer
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
||||
|
||||
|
||||
class PraxisAssignmentCompletionSerializer(serializers.Serializer):
|
||||
status = serializers.SerializerMethodField()
|
||||
user_id = serializers.CharField()
|
||||
last_name = serializers.CharField()
|
||||
|
||||
@staticmethod
|
||||
def get_status(obj):
|
||||
return obj.status.value
|
||||
|
||||
|
||||
class PraxisAssignmentStatusSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
title = serializers.CharField()
|
||||
circle_id = serializers.CharField()
|
||||
pending_evaluations = serializers.IntegerField()
|
||||
completions = PraxisAssignmentCompletionSerializer(many=True)
|
||||
type = serializers.ReadOnlyField(default="praxis_assignment")
|
||||
|
||||
|
||||
class InvitationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = MentorInvitation
|
||||
fields = ["id", "email"]
|
||||
read_only_fields = ["id"]
|
||||
|
||||
def create(self, validated_data):
|
||||
participant = self.context["course_session_user"]
|
||||
invitation, _ = MentorInvitation.objects.get_or_create(
|
||||
email=validated_data["email"], participant=participant
|
||||
)
|
||||
return invitation
|
||||
|
||||
|
||||
class MentorSerializer(serializers.ModelSerializer):
|
||||
mentor = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = LearningMentor
|
||||
fields = ["id", "mentor"]
|
||||
read_only_fields = ["id"]
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import LearningMentor
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=LearningMentor.participants.through)
|
||||
def validate_student(sender, instance, action, reverse, model, pk_set, **kwargs):
|
||||
if action == "pre_add":
|
||||
participants = model.objects.filter(pk__in=pk_set)
|
||||
for participant in participants:
|
||||
if participant.course_session.course != instance.course:
|
||||
raise ValidationError(
|
||||
"Participant (CourseSessionUser) does not match the course for this mentor."
|
||||
)
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.assignment.models import (
|
||||
AssignmentCompletion,
|
||||
AssignmentCompletionStatus,
|
||||
AssignmentType,
|
||||
)
|
||||
from vbv_lernwelt.course.creators.test_utils import (
|
||||
create_assignment,
|
||||
create_assignment_learning_content,
|
||||
create_circle,
|
||||
create_course,
|
||||
create_course_session,
|
||||
create_course_session_assignment,
|
||||
create_user,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
|
||||
get_assignment_completions,
|
||||
get_praxis_assignments,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.entities import CompletionStatus
|
||||
|
||||
|
||||
class AttendanceServicesTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.mentor = create_user("Mentor")
|
||||
self.user1 = create_user("Alpha")
|
||||
self.user2 = create_user("Beta")
|
||||
self.user3 = create_user("Kappa")
|
||||
self.user4 = create_user("Gamma")
|
||||
|
||||
self.course, self.course_page = create_course("Test Course")
|
||||
self.course_session = create_course_session(course=self.course, title=":)")
|
||||
self.circle, _ = create_circle(title="Circle", course_page=self.course_page)
|
||||
|
||||
self.assignment = create_assignment(
|
||||
course=self.course, assignment_type=AssignmentType.PRAXIS_ASSIGNMENT
|
||||
)
|
||||
|
||||
AssignmentCompletion.objects.create(
|
||||
assignment_user=self.user1,
|
||||
course_session=self.course_session,
|
||||
assignment=self.assignment,
|
||||
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
AssignmentCompletion.objects.create(
|
||||
assignment_user=self.user2,
|
||||
course_session=self.course_session,
|
||||
assignment=self.assignment,
|
||||
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
AssignmentCompletion.objects.create(
|
||||
assignment_user=self.user3,
|
||||
course_session=self.course_session,
|
||||
assignment=self.assignment,
|
||||
completion_status=AssignmentCompletionStatus.IN_PROGRESS.value,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
|
||||
def test_assignment_completions(self):
|
||||
# GIVEN
|
||||
participants = [self.user1, self.user2, self.user3, self.user4]
|
||||
|
||||
# WHEN
|
||||
results = get_assignment_completions(
|
||||
course_session=self.course_session,
|
||||
assignment=self.assignment,
|
||||
participants=participants,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
|
||||
# THEN
|
||||
expected_order = ["Beta", "Alpha", "Gamma", "Kappa"]
|
||||
expected_statuses = {
|
||||
"Alpha": CompletionStatus.EVALUATED, # user1
|
||||
"Beta": CompletionStatus.SUBMITTED, # user2
|
||||
"Gamma": CompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion)
|
||||
"Kappa": CompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING)
|
||||
}
|
||||
|
||||
self.assertEqual(len(results), len(participants))
|
||||
for i, result in enumerate(results):
|
||||
self.assertEqual(result.last_name, expected_order[i])
|
||||
self.assertEqual(result.status, expected_statuses[result.last_name])
|
||||
|
||||
def test_praxis_assignment_status(self):
|
||||
# GIVEN
|
||||
lca = create_assignment_learning_content(self.circle, self.assignment)
|
||||
create_course_session_assignment(
|
||||
course_session=self.course_session, learning_content_assignment=lca
|
||||
)
|
||||
participants = [self.user1, self.user2, self.user3, self.user4]
|
||||
|
||||
# WHEN
|
||||
assignments, circle_ids = get_praxis_assignments(
|
||||
course_session=self.course_session,
|
||||
participants=participants,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
|
||||
# THEN
|
||||
assignment = assignments[0]
|
||||
self.assertEqual(assignment.pending_evaluations, 1)
|
||||
self.assertEqual(assignment.title, "Dummy Assignment (PRAXIS_ASSIGNMENT)")
|
||||
self.assertEqual(assignment.circle_id, self.circle.id)
|
||||
self.assertEqual(list(circle_ids)[0], self.circle.id)
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
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
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
||||
from vbv_lernwelt.notify.email.email_services import EmailTemplate
|
||||
|
||||
|
||||
class LearningMentorInvitationTest(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.course, self.course_page = create_course("Test Course")
|
||||
self.course_session = create_course_session(course=self.course, title="Test VV")
|
||||
|
||||
self.participant = create_user("participant")
|
||||
|
||||
def test_create_invitation_not_member(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.participant)
|
||||
invite_url = reverse(
|
||||
"create_invitation", kwargs={"course_session_id": self.course_session.id}
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.post(invite_url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@patch("vbv_lernwelt.learning_mentor.views.send_email")
|
||||
def test_create_invitation(self, mock_send_mail) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.participant)
|
||||
add_course_session_user(
|
||||
self.course_session,
|
||||
self.participant,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
invite_url = reverse(
|
||||
"create_invitation", kwargs={"course_session_id": self.course_session.id}
|
||||
)
|
||||
email = "test@example.com"
|
||||
|
||||
# WHEN
|
||||
response = self.client.post(invite_url, data={"email": email})
|
||||
|
||||
# THEN
|
||||
invitation_id = response.data["id"]
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
self.assertTrue(
|
||||
MentorInvitation.objects.filter(
|
||||
id=invitation_id,
|
||||
).exists()
|
||||
)
|
||||
|
||||
mock_send_mail.assert_called_once_with(
|
||||
recipient_email=email,
|
||||
template=EmailTemplate.LEARNING_MENTOR_INVITATION,
|
||||
template_data={
|
||||
"inviter_name": f"{self.participant.first_name} {self.participant.last_name}",
|
||||
"inviter_email": self.participant.email,
|
||||
"target_url": f"https://my.vbv-afa.ch/lernbegleitung/{self.course_session.id}/invitation/{invitation_id}",
|
||||
},
|
||||
template_language=self.participant.language,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
def test_list_invitations(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.participant)
|
||||
participant_cs_user = add_course_session_user(
|
||||
self.course_session,
|
||||
self.participant,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
email = "test@example.com"
|
||||
|
||||
MentorInvitation.objects.create(participant=participant_cs_user, email=email)
|
||||
|
||||
list_url = reverse(
|
||||
"list_invitations", kwargs={"course_session_id": self.course_session.id}
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(list_url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
[{"id": str(MentorInvitation.objects.get(email=email).id), "email": email}],
|
||||
)
|
||||
|
||||
def test_delete_invitation(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.participant)
|
||||
participant_cs_user = add_course_session_user(
|
||||
self.course_session,
|
||||
self.participant,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
email = "test@example.com"
|
||||
|
||||
invitation = MentorInvitation.objects.create(
|
||||
participant=participant_cs_user, email=email
|
||||
)
|
||||
|
||||
delete_url = reverse(
|
||||
"delete_invitation",
|
||||
kwargs={
|
||||
"course_session_id": self.course_session.id,
|
||||
"invitation_id": invitation.id,
|
||||
},
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.delete(delete_url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertFalse(MentorInvitation.objects.filter(id=invitation.id).exists())
|
||||
|
||||
def test_accept_invitation_invalid_course(self) -> None:
|
||||
# GIVEN
|
||||
other_course, _ = create_course("Other Test Course")
|
||||
other_course_session = create_course_session(
|
||||
course=other_course, title="Other Test Session"
|
||||
)
|
||||
participant_cs_user = add_course_session_user(
|
||||
self.course_session,
|
||||
self.participant,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
invitee = create_user("invitee")
|
||||
invitation = MentorInvitation.objects.create(
|
||||
participant=participant_cs_user, email=invitee.email
|
||||
)
|
||||
self.client.force_login(invitee)
|
||||
|
||||
accept_url = reverse(
|
||||
"accept_invitation", kwargs={"course_session_id": other_course_session.id}
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.post(accept_url, data={"invitation_id": invitation.id})
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{
|
||||
"message": "Invalid invitation",
|
||||
"code": "invalidInvitation",
|
||||
},
|
||||
)
|
||||
|
||||
def test_accept_invitation_role_collision(self) -> None:
|
||||
# GIVEN
|
||||
participant_cs_user = add_course_session_user(
|
||||
self.course_session,
|
||||
self.participant,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
invitee = create_user("invitee")
|
||||
invitation = MentorInvitation.objects.create(
|
||||
participant=participant_cs_user, email=invitee.email
|
||||
)
|
||||
# Make invitee a trainer
|
||||
add_course_session_user(
|
||||
self.course_session,
|
||||
invitee,
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
|
||||
self.client.force_login(invitee)
|
||||
|
||||
accept_url = reverse(
|
||||
"accept_invitation", kwargs={"course_session_id": self.course_session.id}
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.post(accept_url, data={"invitation_id": invitation.id})
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{
|
||||
"message": "User already has a role in this course",
|
||||
"code": "existingRole",
|
||||
},
|
||||
)
|
||||
|
||||
def test_accept_invitation(self) -> None:
|
||||
# GIVEN
|
||||
participant_cs_user = add_course_session_user(
|
||||
self.course_session,
|
||||
self.participant,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
invitee = create_user("invitee")
|
||||
invitation = MentorInvitation.objects.create(
|
||||
participant=participant_cs_user, email=invitee.email
|
||||
)
|
||||
|
||||
self.client.force_login(invitee)
|
||||
|
||||
accept_url = reverse(
|
||||
"accept_invitation", kwargs={"course_session_id": self.course_session.id}
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.post(accept_url, data={"invitation_id": invitation.id})
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertFalse(MentorInvitation.objects.filter(id=invitation.id).exists())
|
||||
self.assertTrue(
|
||||
LearningMentor.objects.filter(
|
||||
mentor=invitee, course=self.course, participants=participant_cs_user
|
||||
).exists()
|
||||
)
|
||||
|
||||
user = response.data["user"]
|
||||
self.assertEqual(user["id"], str(self.participant.id))
|
||||
|
||||
self.assertEqual(response.data["course_slug"], str(self.course.slug))
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from vbv_lernwelt.assignment.models import (
|
||||
AssignmentCompletion,
|
||||
AssignmentCompletionStatus,
|
||||
AssignmentType,
|
||||
)
|
||||
from vbv_lernwelt.course.creators.test_utils import (
|
||||
add_course_session_user,
|
||||
create_assignment,
|
||||
create_assignment_learning_content,
|
||||
create_circle,
|
||||
create_course,
|
||||
create_course_session,
|
||||
create_course_session_assignment,
|
||||
create_user,
|
||||
)
|
||||
from vbv_lernwelt.course.models import CourseSessionUser
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
|
||||
|
||||
class LearningMentorAPITest(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.course, self.course_page = create_course("Test Course")
|
||||
self.course_session = create_course_session(course=self.course, title="Test VV")
|
||||
|
||||
self.circle, _ = create_circle(title="Circle", course_page=self.course_page)
|
||||
|
||||
self.assignment = create_assignment(
|
||||
course=self.course, assignment_type=AssignmentType.PRAXIS_ASSIGNMENT
|
||||
)
|
||||
|
||||
lca = create_assignment_learning_content(self.circle, self.assignment)
|
||||
create_course_session_assignment(
|
||||
course_session=self.course_session, learning_content_assignment=lca
|
||||
)
|
||||
|
||||
self.mentor = create_user("mentor")
|
||||
self.participant_1 = add_course_session_user(
|
||||
self.course_session,
|
||||
create_user("participant_1"),
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
self.participant_2 = add_course_session_user(
|
||||
self.course_session,
|
||||
create_user("participant_2"),
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
self.participant_3 = add_course_session_user(
|
||||
self.course_session,
|
||||
create_user("participant_3"),
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
self.url = reverse(
|
||||
"mentor_summary", kwargs={"course_session_id": self.course_session.id}
|
||||
)
|
||||
|
||||
def test_api_no_mentor(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.mentor)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_api_no_participants(self) -> None:
|
||||
# GIVEN
|
||||
self.client.force_login(self.mentor)
|
||||
LearningMentor.objects.create(
|
||||
mentor=self.mentor, course=self.course_session.course
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["participants"], [])
|
||||
self.assertEqual(response.data["assignments"], [])
|
||||
|
||||
def test_api_participants(self) -> None:
|
||||
# GIVEN
|
||||
participants = [self.participant_1, self.participant_2, self.participant_3]
|
||||
self.client.force_login(self.mentor)
|
||||
mentor = LearningMentor.objects.create(
|
||||
mentor=self.mentor,
|
||||
course=self.course_session.course,
|
||||
)
|
||||
mentor.participants.set(participants)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data["participants"]), len(participants))
|
||||
|
||||
participant_1 = [
|
||||
p
|
||||
for p in response.data["participants"]
|
||||
if p["id"] == str(self.participant_1.user.id)
|
||||
][0]
|
||||
self.assertEqual(participant_1["email"], "participant_1@example.com")
|
||||
self.assertEqual(participant_1["first_name"], "Test")
|
||||
self.assertEqual(participant_1["last_name"], "Participant_1")
|
||||
|
||||
def test_api_praxis_assignments(self) -> None:
|
||||
# GIVEN
|
||||
participants = [self.participant_1, self.participant_2, self.participant_3]
|
||||
self.client.force_login(self.mentor)
|
||||
|
||||
mentor = LearningMentor.objects.create(
|
||||
mentor=self.mentor,
|
||||
course=self.course_session.course,
|
||||
)
|
||||
mentor.participants.set(participants)
|
||||
|
||||
AssignmentCompletion.objects.create(
|
||||
assignment_user=self.participant_1.user,
|
||||
course_session=self.course_session,
|
||||
assignment=self.assignment,
|
||||
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
|
||||
AssignmentCompletion.objects.create(
|
||||
assignment_user=self.participant_3.user,
|
||||
course_session=self.course_session,
|
||||
assignment=self.assignment,
|
||||
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
|
||||
evaluation_user=self.mentor,
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(self.url)
|
||||
|
||||
# THEN
|
||||
assignments = response.data["assignments"]
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(assignments), 1)
|
||||
|
||||
self.assertEqual(
|
||||
response.data["circles"],
|
||||
[{"id": self.circle.id, "title": self.circle.title}],
|
||||
)
|
||||
|
||||
assignment = assignments[0]
|
||||
self.assertEqual(assignment["type"], "praxis_assignment")
|
||||
self.assertEqual(assignment["pending_evaluations"], 1)
|
||||
|
||||
self.assertEqual(
|
||||
assignment["completions"][0]["last_name"], self.participant_3.user.last_name
|
||||
)
|
||||
self.assertEqual(
|
||||
assignment["completions"][1]["last_name"], self.participant_1.user.last_name
|
||||
)
|
||||
self.assertEqual(
|
||||
assignment["completions"][2]["last_name"], self.participant_2.user.last_name
|
||||
)
|
||||
|
||||
def test_list_user_mentors(self) -> None:
|
||||
# GIVEN
|
||||
participant = create_user("participant")
|
||||
self.client.force_login(participant)
|
||||
|
||||
participant_cs_user = add_course_session_user(
|
||||
self.course_session,
|
||||
participant,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
learning_mentor = LearningMentor.objects.create(
|
||||
mentor=self.mentor,
|
||||
course=self.course_session.course,
|
||||
)
|
||||
|
||||
learning_mentor.participants.add(participant_cs_user)
|
||||
|
||||
list_url = reverse(
|
||||
"list_user_mentors", kwargs={"course_session_id": self.course_session.id}
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.get(list_url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
mentor = response.data[0]
|
||||
self.assertEqual(mentor["id"], learning_mentor.id)
|
||||
|
||||
mentor_user = mentor["mentor"]
|
||||
self.assertEqual(mentor_user["email"], self.mentor.email)
|
||||
self.assertEqual(mentor_user["id"], str(self.mentor.id))
|
||||
|
||||
def test_remove_user_mentor(self) -> None:
|
||||
# GIVEN
|
||||
participant = create_user("participant")
|
||||
self.client.force_login(participant)
|
||||
|
||||
participant_cs_user = add_course_session_user(
|
||||
self.course_session,
|
||||
participant,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
)
|
||||
|
||||
learning_mentor = LearningMentor.objects.create(
|
||||
mentor=self.mentor,
|
||||
course=self.course_session.course,
|
||||
)
|
||||
|
||||
learning_mentor.participants.add(participant_cs_user)
|
||||
|
||||
remove_self_url = reverse(
|
||||
"remove_self_from_mentor",
|
||||
kwargs={
|
||||
"course_session_id": self.course_session.id,
|
||||
"mentor_id": learning_mentor.id,
|
||||
},
|
||||
)
|
||||
|
||||
# WHEN
|
||||
response = self.client.delete(remove_self_url)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertFalse(
|
||||
LearningMentor.objects.filter(participants=participant_cs_user).exists()
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("summary", views.mentor_summary, name="mentor_summary"),
|
||||
path("mentors", views.list_user_mentors, name="list_user_mentors"),
|
||||
path(
|
||||
"mentors/<int:mentor_id>/leave",
|
||||
views.remove_self_from_mentor,
|
||||
name="remove_self_from_mentor",
|
||||
),
|
||||
path("invitations", views.list_invitations, name="list_invitations"),
|
||||
path("invitations/create", views.create_invitation, name="create_invitation"),
|
||||
path(
|
||||
"invitations/<uuid:invitation_id>/delete",
|
||||
views.delete_invitation,
|
||||
name="delete_invitation",
|
||||
),
|
||||
path("invitations/accept", views.accept_invitation, name="accept_invitation"),
|
||||
]
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
from uuid import UUID
|
||||
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from vbv_lernwelt.core.serializers import UserSerializer
|
||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.iam.permissions import has_role_in_course, is_course_session_member
|
||||
from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
|
||||
get_praxis_assignments,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
|
||||
from vbv_lernwelt.learning_mentor.serializers import (
|
||||
InvitationSerializer,
|
||||
MentorSerializer,
|
||||
PraxisAssignmentStatusSerializer,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def mentor_summary(request, course_session_id: int):
|
||||
course_session = CourseSession.objects.get(id=course_session_id)
|
||||
|
||||
mentor = get_object_or_404(
|
||||
LearningMentor, mentor=request.user, course=course_session.course
|
||||
)
|
||||
|
||||
participants = mentor.participants.filter(course_session=course_session)
|
||||
users = [p.user for p in participants]
|
||||
|
||||
assignments = []
|
||||
circle_ids = set()
|
||||
|
||||
praxis_assignments, _circle_ids = get_praxis_assignments(
|
||||
course_session=course_session, participants=users, evaluation_user=request.user
|
||||
)
|
||||
assignments.extend(
|
||||
PraxisAssignmentStatusSerializer(praxis_assignments, many=True).data
|
||||
)
|
||||
circle_ids.update(_circle_ids)
|
||||
|
||||
circles = Circle.objects.filter(id__in=circle_ids).values("id", "title")
|
||||
|
||||
assignments.sort(
|
||||
key=lambda x: (-x.get("pending_evaluations", 0), x.get("title", "").lower())
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"participants": [UserSerializer(user).data for user in users],
|
||||
"circles": list(circles),
|
||||
"assignments": assignments,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CourseSessionMember(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return is_course_session_member(
|
||||
request.user, view.kwargs.get("course_session_id")
|
||||
)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated, CourseSessionMember])
|
||||
def list_invitations(request, course_session_id: int):
|
||||
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
||||
course_session_user = get_object_or_404(
|
||||
CourseSessionUser, user=request.user, course_session=course_session
|
||||
)
|
||||
invitations = MentorInvitation.objects.filter(participant=course_session_user)
|
||||
serializer = InvitationSerializer(invitations, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@api_view(["DELETE"])
|
||||
@permission_classes([IsAuthenticated, CourseSessionMember])
|
||||
def delete_invitation(request, course_session_id: int, invitation_id: UUID):
|
||||
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
||||
course_session_user = get_object_or_404(
|
||||
CourseSessionUser, user=request.user, course_session=course_session
|
||||
)
|
||||
get_object_or_404(
|
||||
MentorInvitation, id=invitation_id, participant=course_session_user
|
||||
).delete()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, CourseSessionMember])
|
||||
def create_invitation(request, course_session_id: int):
|
||||
user = request.user
|
||||
|
||||
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
||||
course_session_user = get_object_or_404(
|
||||
CourseSessionUser, user=user, course_session=course_session
|
||||
)
|
||||
|
||||
serializer = InvitationSerializer(
|
||||
data=request.data, context={"course_session_user": course_session_user}
|
||||
)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
invitation = serializer.save()
|
||||
|
||||
target_url = f"/lernbegleitung/{course_session_id}/invitation/{invitation.id}"
|
||||
|
||||
send_email(
|
||||
recipient_email=invitation.email,
|
||||
template=EmailTemplate.LEARNING_MENTOR_INVITATION,
|
||||
template_data={
|
||||
"inviter_name": f"{user.first_name} {user.last_name}",
|
||||
"inviter_email": user.email,
|
||||
"target_url": f"https://my.vbv-afa.ch{target_url}",
|
||||
},
|
||||
template_language=request.user.language,
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated, CourseSessionMember])
|
||||
def list_user_mentors(request, course_session_id: int):
|
||||
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
||||
|
||||
course_session_user = get_object_or_404(
|
||||
CourseSessionUser, user=request.user, course_session=course_session
|
||||
)
|
||||
|
||||
mentors = LearningMentor.objects.filter(
|
||||
course=course_session.course, participants=course_session_user
|
||||
)
|
||||
|
||||
return Response(MentorSerializer(mentors, many=True).data)
|
||||
|
||||
|
||||
@api_view(["DELETE"])
|
||||
@permission_classes([IsAuthenticated, CourseSessionMember])
|
||||
def remove_self_from_mentor(request, course_session_id: int, mentor_id: int):
|
||||
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
||||
course_session_user = get_object_or_404(
|
||||
CourseSessionUser, user=request.user, course_session=course_session
|
||||
)
|
||||
|
||||
mentor = get_object_or_404(LearningMentor, id=mentor_id)
|
||||
|
||||
mentor.participants.remove(course_session_user)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def accept_invitation(request, course_session_id: int):
|
||||
course_session = get_object_or_404(CourseSession, id=course_session_id)
|
||||
invitation = get_object_or_404(
|
||||
MentorInvitation, id=request.data.get("invitation_id")
|
||||
)
|
||||
|
||||
if invitation.participant.course_session != course_session:
|
||||
return Response(
|
||||
data={"message": "Invalid invitation", "code": "invalidInvitation"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if LearningMentor.objects.filter(
|
||||
mentor=request.user, course=course_session.course
|
||||
).exists():
|
||||
mentor = LearningMentor.objects.get(
|
||||
mentor=request.user, course=course_session.course
|
||||
)
|
||||
else:
|
||||
if has_role_in_course(request.user, course_session.course):
|
||||
return Response(
|
||||
data={
|
||||
"message": "User already has a role in this course",
|
||||
"code": "existingRole",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
mentor = LearningMentor.objects.create(
|
||||
mentor=request.user, course=course_session.course
|
||||
)
|
||||
|
||||
mentor.participants.add(invitation.participant)
|
||||
invitation.delete()
|
||||
|
||||
return Response(
|
||||
{
|
||||
"course_slug": course_session.course.slug,
|
||||
"user": UserSerializer(invitation.participant.user).data,
|
||||
}
|
||||
)
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import datetime
|
||||
import os
|
||||
from unittest import skipIf
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
|
|
@ -11,6 +13,10 @@ from vbv_lernwelt.media_files.models import ContentDocument
|
|||
TITLE = "Musterlösung Fahrzeug"
|
||||
|
||||
|
||||
@skipIf(
|
||||
os.environ.get("ENABLE_S3_STORAGE_UNIT_TESTS") is None,
|
||||
"Only enable tests by setting ENABLE_S3_STORAGE_UNIT_TESTS=1",
|
||||
)
|
||||
class TestContentDocumentServing(TestCase):
|
||||
def setUp(self):
|
||||
create_default_users()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
import os
|
||||
from unittest import skipIf
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -11,6 +12,10 @@ from vbv_lernwelt.media_files.models import ContentDocument
|
|||
TITLE = "Musterlösung Fahrzeug"
|
||||
|
||||
|
||||
@skipIf(
|
||||
os.environ.get("ENABLE_S3_STORAGE_UNIT_TESTS") is None,
|
||||
"Only enable tests by setting ENABLE_S3_STORAGE_UNIT_TESTS=1",
|
||||
)
|
||||
class TestContentDocumentStorage(TestCase):
|
||||
@override_settings(FILE_UPLOAD_STORAGE="s3")
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -69,6 +69,13 @@ class EmailTemplate(Enum):
|
|||
"it": "d-0882ec9c92f64312b9f358481a943c9a",
|
||||
}
|
||||
|
||||
# VBV - Lernbegleitung Einladung
|
||||
LEARNING_MENTOR_INVITATION = {
|
||||
"de": "d-8c862afde62748b6b8410887eeee89d8",
|
||||
"fr": "d-7451e3c858954c15a9f410fa9d92dc06",
|
||||
"it": "d-30c6aa9accda4973a940dd25703cb4a9",
|
||||
}
|
||||
|
||||
|
||||
def send_email(
|
||||
recipient_email: str,
|
||||
|
|
|
|||
Loading…
Reference in New Issue