Merge branch 'develop' into feat/588-vv-fremdeinschatzung
This commit is contained in:
commit
95a5d1b671
|
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex-none border-r bg-white p-4 lg:p-8">
|
||||||
|
<slot name="side"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 p-4 lg:p-8">
|
||||||
|
<slot name="main"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -17,7 +17,7 @@ const 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": 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 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 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 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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\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 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 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 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 feedback_user\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 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 feedback_user\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,
|
||||||
|
|
@ -60,7 +60,7 @@ export function graphql(source: "\n query attendanceCheckQuery($courseSessionId
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* 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 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"): (typeof documents)["\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"];
|
export function graphql(source: "\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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\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"): (typeof documents)["\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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\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"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* 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
|
|
@ -66,8 +66,13 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
|
||||||
evaluation_submitted_at
|
evaluation_submitted_at
|
||||||
evaluation_user {
|
evaluation_user {
|
||||||
id
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
}
|
}
|
||||||
assignment_user {
|
assignment_user {
|
||||||
|
avatar_url
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
evaluation_points
|
evaluation_points
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import * as log from "loglevel";
|
|
||||||
import { computed, onMounted } from "vue";
|
|
||||||
|
|
||||||
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
|
|
||||||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
|
||||||
import {
|
|
||||||
useCourseDataWithCompletion,
|
|
||||||
useCourseSessionDetailQuery,
|
|
||||||
} from "@/composables";
|
|
||||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
userId: string;
|
|
||||||
courseSlug: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
log.debug("CockpitUserProfilePage created", props.userId);
|
|
||||||
|
|
||||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
|
||||||
|
|
||||||
const courseCompletionData = useCourseDataWithCompletion(
|
|
||||||
props.courseSlug,
|
|
||||||
props.userId
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
log.debug("CockpitUserProfilePage mounted");
|
|
||||||
});
|
|
||||||
|
|
||||||
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug, props.userId);
|
|
||||||
|
|
||||||
const learningPath = computed(() => {
|
|
||||||
return lpQueryResult.learningPath.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { findUser } = useCourseSessionDetailQuery();
|
|
||||||
|
|
||||||
const user = computed(() => {
|
|
||||||
return findUser(props.userId);
|
|
||||||
});
|
|
||||||
|
|
||||||
function setActiveClasses(isActive: boolean) {
|
|
||||||
return isActive ? ["border-blue-900", "border-b-2"] : ["text-bg-900"];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="!loading" class="bg-gray-200">
|
|
||||||
<div v-if="user" class="container-large">
|
|
||||||
<nav class="py-4 pb-4">
|
|
||||||
<router-link
|
|
||||||
class="btn-text inline-flex items-center pl-0"
|
|
||||||
:to="`/course/${props.courseSlug}/cockpit`"
|
|
||||||
>
|
|
||||||
<it-icon-arrow-left />
|
|
||||||
<span>{{ $t("general.back") }}</span>
|
|
||||||
</router-link>
|
|
||||||
</nav>
|
|
||||||
<header class="mb-12 flex flex-row items-center">
|
|
||||||
<img class="mr-8 h-44 w-44 rounded-full" :src="user.avatar_url" />
|
|
||||||
<div>
|
|
||||||
<h1 class="mb-2">{{ user.first_name }} {{ user.last_name }}</h1>
|
|
||||||
<p class="mb-2">{{ user.email }}</p>
|
|
||||||
<p class="bg-message bg-[center_left_-4px] bg-no-repeat pl-6">
|
|
||||||
{{ $t("messages.sendMessage") }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div v-if="learningPath" class="mb-8 w-full bg-white pb-1 pt-2">
|
|
||||||
<!-- TODO: rework this section with next redesign -->
|
|
||||||
<LearningPathPathView
|
|
||||||
:use-mobile-layout="false"
|
|
||||||
:hide-buttons="true"
|
|
||||||
:learning-path="learningPath"
|
|
||||||
:next-learning-content="undefined"
|
|
||||||
:override-circle-url-base="`/course/${props.courseSlug}/cockpit/profile/${props.userId}`"
|
|
||||||
></LearningPathPathView>
|
|
||||||
</div>
|
|
||||||
<ul class="mb-5 flex flex-row border-b-2">
|
|
||||||
<li class="relative top-px mr-12 pb-3" :class="setActiveClasses(true)">
|
|
||||||
<button>{{ $t("competences.competences") }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="mr-12">
|
|
||||||
<button>{{ $t("general.transferTask_other") }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="mr-12">
|
|
||||||
<button>{{ $t("general.exam_other") }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="mr-12">
|
|
||||||
<button>{{ $t("general.certificate_other") }}</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<ul class="bg-white px-8">
|
|
||||||
<li
|
|
||||||
v-for="competence in courseCompletionData.actionCompetences.value ?? []"
|
|
||||||
:key="competence.id"
|
|
||||||
class="border-b border-gray-500 p-8 last:border-0"
|
|
||||||
>
|
|
||||||
<CompetenceDetail
|
|
||||||
:competence="competence"
|
|
||||||
:course-slug="props.courseSlug"
|
|
||||||
:show-assess-again="false"
|
|
||||||
:is-inline="true"
|
|
||||||
></CompetenceDetail>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
|
import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
|
||||||
import EvaluationContainer from "@/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue";
|
import EvaluationContainer from "@/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue";
|
||||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||||
import type { Assignment, AssignmentCompletion } from "@/types";
|
import type { Assignment, AssignmentCompletion, CourseSessionUser } from "@/types";
|
||||||
import { useQuery } from "@urql/vue";
|
import { useQuery } from "@urql/vue";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed, onMounted } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { getPreviousRoute } from "@/router/history";
|
import { getPreviousRoute } from "@/router/history";
|
||||||
import { getAssignmentTypeTitle } from "../../../utils/utils";
|
import { getAssignmentTypeTitle } from "../../../utils/utils";
|
||||||
import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
@ -20,7 +19,6 @@ const props = defineProps<{
|
||||||
|
|
||||||
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId);
|
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId);
|
||||||
|
|
||||||
const { loading } = useExpertCockpitPageData(props.courseSlug);
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
@ -38,9 +36,6 @@ onMounted(async () => {
|
||||||
log.debug("AssignmentView mounted", props.assignmentId, props.userId);
|
log.debug("AssignmentView mounted", props.assignmentId, props.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
|
||||||
const assignmentUser = computed(() => courseSessionDetailResult.findUser(props.userId));
|
|
||||||
|
|
||||||
const previousRoute = getPreviousRoute();
|
const previousRoute = getPreviousRoute();
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
|
@ -58,13 +53,17 @@ const assignmentCompletion = computed(
|
||||||
queryResult.data.value?.assignment_completion as AssignmentCompletion | undefined
|
queryResult.data.value?.assignment_completion as AssignmentCompletion | undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const assignmentUser = computed(
|
||||||
|
() => assignmentCompletion.value?.assignment_user as CourseSessionUser | undefined
|
||||||
|
);
|
||||||
|
|
||||||
const assignment = computed(
|
const assignment = computed(
|
||||||
() => queryResult.data.value?.assignment as Assignment | undefined
|
() => queryResult.data.value?.assignment as Assignment | undefined
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!loading" class="absolute bottom-0 top-0 z-10 w-full bg-white">
|
<div class="absolute bottom-0 top-0 z-10 w-full bg-white">
|
||||||
<div v-if="queryResult.fetching.value"></div>
|
<div v-if="queryResult.fetching.value"></div>
|
||||||
<div v-else-if="queryResult.error.value">{{ queryResult.error.value }}</div>
|
<div v-else-if="queryResult.error.value">{{ queryResult.error.value }}</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
|
@ -101,12 +100,12 @@ const assignment = computed(
|
||||||
|
|
||||||
<div class="my-6 flex items-center">
|
<div class="my-6 flex items-center">
|
||||||
<img
|
<img
|
||||||
:src="assignmentUser?.avatar_url"
|
:src="assignmentUser.avatar_url"
|
||||||
class="mr-4 h-11 w-11 rounded-full"
|
class="mr-4 h-11 w-11 rounded-full"
|
||||||
/>
|
/>
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
{{ assignmentUser?.first_name }}
|
{{ assignmentUser.first_name }}
|
||||||
{{ assignmentUser?.last_name }}
|
{{ assignmentUser.last_name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AssignmentSubmissionResponses
|
<AssignmentSubmissionResponses
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,9 @@ const assignmentDetail = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const dueDate = computed(() =>
|
const dueDate = computed(() =>
|
||||||
dayjs(assignmentDetail.value?.evaluation_deadline?.start)
|
assignmentDetail.value?.evaluation_deadline?.start
|
||||||
|
? dayjs(assignmentDetail.value?.evaluation_deadline?.start)
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const inEvaluationTask = computed(
|
const inEvaluationTask = computed(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const props = defineProps<{
|
||||||
assignmentUser: CourseSessionUser;
|
assignmentUser: CourseSessionUser;
|
||||||
assignment: Assignment;
|
assignment: Assignment;
|
||||||
assignmentCompletion: AssignmentCompletion;
|
assignmentCompletion: AssignmentCompletion;
|
||||||
dueDate?: Dayjs;
|
dueDate?: Dayjs | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(["startEvaluation"]);
|
const emit = defineEmits(["startEvaluation"]);
|
||||||
|
|
@ -58,7 +58,7 @@ async function startEvaluation() {
|
||||||
upsertAssignmentCompletionMutation.executeMutation({
|
upsertAssignmentCompletionMutation.executeMutation({
|
||||||
assignmentId: props.assignment.id,
|
assignmentId: props.assignment.id,
|
||||||
courseSessionId: courseSession.value.id,
|
courseSessionId: courseSession.value.id,
|
||||||
assignmentUserId: props.assignmentUser.user_id,
|
assignmentUserId: props.assignmentUser.id,
|
||||||
completionStatus: "EVALUATION_IN_PROGRESS",
|
completionStatus: "EVALUATION_IN_PROGRESS",
|
||||||
completionDataString: JSON.stringify({}),
|
completionDataString: JSON.stringify({}),
|
||||||
// next line used for urql
|
// next line used for urql
|
||||||
|
|
@ -101,7 +101,13 @@ async function startEvaluation() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="my-4" data-cy="instruction">
|
<p class="my-4" data-cy="instruction">
|
||||||
{{ $t(text.evaluationInstruction) }}
|
{{
|
||||||
|
$t(text.evaluationInstruction, {
|
||||||
|
name: `${
|
||||||
|
props.assignmentUser.first_name + " " + props.assignmentUser.last_name
|
||||||
|
}`,
|
||||||
|
})
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-if="props.assignment.assignment_type === 'CASEWORK'" class="my-4">
|
<p v-if="props.assignment.assignment_type === 'CASEWORK'" class="my-4">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||||
import {
|
import {
|
||||||
maxAssignmentPoints,
|
maxAssignmentPoints,
|
||||||
|
|
@ -75,7 +75,7 @@ async function submitEvaluation() {
|
||||||
upsertAssignmentCompletionMutation.executeMutation({
|
upsertAssignmentCompletionMutation.executeMutation({
|
||||||
assignmentId: props.assignment.id,
|
assignmentId: props.assignment.id,
|
||||||
courseSessionId: courseSession.value.id,
|
courseSessionId: courseSession.value.id,
|
||||||
assignmentUserId: props.assignmentUser.user_id,
|
assignmentUserId: props.assignmentUser.id,
|
||||||
completionStatus: "EVALUATION_SUBMITTED",
|
completionStatus: "EVALUATION_SUBMITTED",
|
||||||
completionDataString: JSON.stringify({}),
|
completionDataString: JSON.stringify({}),
|
||||||
evaluationPoints: userPoints.value,
|
evaluationPoints: userPoints.value,
|
||||||
|
|
@ -110,26 +110,19 @@ const maxPoints = computed(() => maxAssignmentPoints(props.assignment));
|
||||||
const userPoints = computed(() =>
|
const userPoints = computed(() =>
|
||||||
userAssignmentPoints(props.assignment, props.assignmentCompletion)
|
userAssignmentPoints(props.assignment, props.assignmentCompletion)
|
||||||
);
|
);
|
||||||
|
|
||||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
|
||||||
const evaluationUser = computed(() => {
|
|
||||||
if (props.assignmentCompletion.evaluation_user) {
|
|
||||||
return courseSessionDetailResult.findUser(
|
|
||||||
props.assignmentCompletion.evaluation_user?.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<div>
|
<div>
|
||||||
<h3 v-if="evaluationUser && props.showEvaluationUser" class="mb-6">
|
<h3
|
||||||
|
v-if="assignmentCompletion.evaluation_user && props.showEvaluationUser"
|
||||||
|
class="mb-6"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
$t(text.evaluationFromUser, {
|
$t(text.evaluationFromUser, {
|
||||||
x: evaluationUser?.first_name,
|
x: assignmentCompletion.evaluation_user.first_name,
|
||||||
y: evaluationUser?.last_name,
|
y: assignmentCompletion.evaluation_user.last_name,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ async function evaluateAssignmentCompletion(completionData: AssignmentCompletion
|
||||||
upsertAssignmentCompletionMutation.executeMutation({
|
upsertAssignmentCompletionMutation.executeMutation({
|
||||||
assignmentId: props.assignment.id,
|
assignmentId: props.assignment.id,
|
||||||
courseSessionId: courseSession.value.id,
|
courseSessionId: courseSession.value.id,
|
||||||
assignmentUserId: props.assignmentUser.user_id,
|
assignmentUserId: props.assignmentUser.id,
|
||||||
completionStatus: "EVALUATION_IN_PROGRESS",
|
completionStatus: "EVALUATION_IN_PROGRESS",
|
||||||
completionDataString: JSON.stringify(completionData),
|
completionDataString: JSON.stringify(completionData),
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,12 @@ const { summary } = useMentorCockpit(courseSession.value.id);
|
||||||
{{ participant.email }}
|
{{ participant.email }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <router-link :to="{name: 'cockpitUserProfile', params: {userId: participant.id}}" class="underline">-->
|
<router-link
|
||||||
<!-- {{ $t("a.Profil anzeigen") }}-->
|
:to="{ name: 'cockpitUserProfile', params: { userId: participant.id } }"
|
||||||
<!-- </router-link>-->
|
class="underline"
|
||||||
|
>
|
||||||
|
{{ $t("cockpit.profileLink") }}
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Assignment, Participant } from "@/services/mentorCockpit";
|
import type { Assignment, Participant } from "@/services/mentorCockpit";
|
||||||
import { useMentorCockpit } from "@/services/mentorCockpit";
|
import { useMentorCockpit } from "@/services/mentorCockpit";
|
||||||
import { computed, type Ref } from "vue";
|
import { computed, onMounted, type Ref } from "vue";
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
import log from "loglevel";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
praxisAssignmentId: string;
|
praxisAssignmentId: string;
|
||||||
|
|
@ -10,20 +11,19 @@ const props = defineProps<{
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
|
const mentorCockpitStore = useMentorCockpit(courseSession.value.id);
|
||||||
|
const participants = computed(() => mentorCockpitStore.summary.value?.participants);
|
||||||
const praxisAssignment: Ref<Assignment | null> = computed(() =>
|
const praxisAssignment: Ref<Assignment | null> = computed(() =>
|
||||||
mentorCockpitStore.getAssignmentById(props.praxisAssignmentId)
|
mentorCockpitStore.getAssignmentById(props.praxisAssignmentId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const getParticipantById = (id: string): Participant | null => {
|
const getParticipantById = (id: string): Participant | null => {
|
||||||
if (mentorCockpitStore.summary.value?.participants) {
|
return participants.value?.find((participant) => participant.id === id) || null;
|
||||||
const found = mentorCockpitStore.summary.value.participants.find(
|
|
||||||
(item) => item.id === id
|
|
||||||
);
|
|
||||||
return found || null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
log.debug("MentorPraxisAssignment mounted");
|
||||||
|
mentorCockpitStore.fetchData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -96,7 +96,22 @@ const getParticipantById = (id: string): Participant | null => {
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<!-- Right -->
|
<!-- Right -->
|
||||||
<div></div>
|
<div>
|
||||||
|
<router-link
|
||||||
|
v-if="item.status == 'SUBMITTED'"
|
||||||
|
class="btn-primary"
|
||||||
|
:to="item.url"
|
||||||
|
>
|
||||||
|
{{ $t("a.Ergebnis bewerten") }}
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-else-if="item.status == 'EVALUATED'"
|
||||||
|
class="underline"
|
||||||
|
:to="item.url"
|
||||||
|
>
|
||||||
|
{{ $t("a.Bewertung ansehen") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import { useFetch } from "@vueuse/core";
|
||||||
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userId: string;
|
||||||
|
courseSlug: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const pages = ref([
|
||||||
|
{ label: t("general.learningPath"), route: "cockpitProfileLearningPath" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const courseSession = useCurrentCourseSession();
|
||||||
|
const { data: user } = useFetch(
|
||||||
|
`/api/core/profile/${courseSession.value.id}/${props.userId}`
|
||||||
|
).json();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// if current route name not in pages, redirect to first page
|
||||||
|
if (route.name && !pages.value.find((page) => page.route === route.name)) {
|
||||||
|
router.push({
|
||||||
|
name: pages.value[0].route,
|
||||||
|
params: { userId: props.userId, courseSlug: props.courseSlug },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="user" class="flex flex-col bg-gray-200">
|
||||||
|
<div class="relative border-b bg-white shadow-md">
|
||||||
|
<div class="container-large pb-0">
|
||||||
|
<router-link
|
||||||
|
class="btn-text inline-flex items-center pl-0"
|
||||||
|
:to="`/course/${props.courseSlug}/cockpit`"
|
||||||
|
>
|
||||||
|
<it-icon-arrow-left />
|
||||||
|
<span>{{ $t("general.back") }}</span>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<div class="mb-12 mt-2 flex items-center">
|
||||||
|
<img class="mr-8 h-48 w-48 rounded-full" :src="user.avatar_url" />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h2 class="mb-2">{{ user.first_name }} {{ user.last_name }}</h2>
|
||||||
|
<p class="mb-2">{{ user.email }}</p>
|
||||||
|
<p class="text-gray-800">{{ $t("a.Teilnehmer") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-row px-4 lg:px-8">
|
||||||
|
<li
|
||||||
|
v-for="page in pages"
|
||||||
|
:key="page.route"
|
||||||
|
class="relative top-px mr-12 pb-3"
|
||||||
|
:class="[route.name === page.route ? 'border-b-2 border-blue-900 pb-3' : '']"
|
||||||
|
>
|
||||||
|
<router-link :to="{ name: page.route }">
|
||||||
|
{{ page.label }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<router-view class="flex flex-grow py-0" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||||
|
import { useCourseDataWithCompletion } from "@/composables";
|
||||||
|
import CockpitProfileContent from "@/components/cockpit/profile/CockpitProfileContent.vue";
|
||||||
|
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||||
|
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import type { CircleType } from "@/types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userId: string;
|
||||||
|
courseSlug: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedCircle = ref();
|
||||||
|
|
||||||
|
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug, props.userId);
|
||||||
|
|
||||||
|
function selectCircle(circle: CircleType) {
|
||||||
|
selectedCircle.value = circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(lpQueryResult.learningPath, () => {
|
||||||
|
if (lpQueryResult.learningPath?.value?.topics?.length) {
|
||||||
|
selectCircle(lpQueryResult.learningPath.value.topics[0].circles[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CockpitProfileContent>
|
||||||
|
<template #side>
|
||||||
|
<div
|
||||||
|
v-for="topic in lpQueryResult.learningPath?.value?.topics ?? []"
|
||||||
|
:key="topic.id"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<h4 class="mb-1 font-semibold text-gray-800">
|
||||||
|
{{ topic.title }}
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
v-for="circle in topic.circles"
|
||||||
|
:key="circle.id"
|
||||||
|
class="flex w-full items-center space-x-2 p-2 pr-4 hover:bg-gray-200 lg:pr-8"
|
||||||
|
:class="{ 'bg-gray-200': selectedCircle === circle }"
|
||||||
|
@click="selectCircle(circle)"
|
||||||
|
>
|
||||||
|
<LearningPathCircle
|
||||||
|
:sectors="calculateCircleSectorData(circle)"
|
||||||
|
class="h-10 w-10 snap-center rounded-full bg-white p-0.5"
|
||||||
|
></LearningPathCircle>
|
||||||
|
<span>{{ circle.title }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #main>
|
||||||
|
<ol v-if="selectedCircle" class="flex-auto bg-gray-200 px-6 py-4 lg:px-16">
|
||||||
|
<li
|
||||||
|
v-for="learningSequence in selectedCircle.learning_sequences ?? []"
|
||||||
|
:key="learningSequence.id"
|
||||||
|
>
|
||||||
|
<LearningSequence
|
||||||
|
:course-slug="props.courseSlug"
|
||||||
|
:circle="selectedCircle"
|
||||||
|
:learning-sequence="learningSequence"
|
||||||
|
readonly
|
||||||
|
hide-links
|
||||||
|
></LearningSequence>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</template>
|
||||||
|
</CockpitProfileContent>
|
||||||
|
</template>
|
||||||
|
|
@ -32,6 +32,7 @@ type Props = {
|
||||||
learningSequence: LearningSequence;
|
learningSequence: LearningSequence;
|
||||||
circle: CircleType;
|
circle: CircleType;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
hideLinks?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -229,7 +230,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="belongsToCompetenceCertificate(learningContent)"
|
v-if="belongsToCompetenceCertificate(learningContent) && !hideLinks"
|
||||||
class="ml-16 text-sm text-gray-800"
|
class="ml-16 text-sm text-gray-800"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
|
|
@ -298,11 +299,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
||||||
<div>{{ $t("a.Selbsteinschätzung") }}</div>
|
<div>{{ $t("a.Selbsteinschätzung") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />-->
|
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,8 @@ function handleFinishedLearningContent() {
|
||||||
props.learningContent,
|
props.learningContent,
|
||||||
props.circle,
|
props.circle,
|
||||||
previousRoute,
|
previousRoute,
|
||||||
(lc: LearningContentWithCompletion) => {
|
async (lc: LearningContentWithCompletion) => {
|
||||||
courseCompletionData.markCompletion(lc, "SUCCESS");
|
await courseCompletionData.markCompletion(lc, "SUCCESS");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ import { useRouteQuery } from "@vueuse/router";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { computed, onMounted, ref, watchEffect } from "vue";
|
import { computed, onMounted, ref, watchEffect } from "vue";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import { bustItGetCache } from "@/fetchHelpers";
|
|
||||||
import { learningContentTypeData } from "@/utils/typeMaps";
|
import { learningContentTypeData } from "@/utils/typeMaps";
|
||||||
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
|
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
|
||||||
|
import { bustItGetCache } from "@/fetchHelpers";
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useUserStore } from "@/stores/user";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import { profileNextRoute, useEntities } from "@/services/onboarding";
|
import { profileNextRoute, useEntities } from "@/services/onboarding";
|
||||||
|
import AvatarImage from "@/components/ui/AvatarImage.vue";
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
@ -36,17 +37,23 @@ const validOrganisation = computed(() => {
|
||||||
return selectedOrganisation.value.id !== 0;
|
return selectedOrganisation.value.id !== 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
/* TODO: We do this later (not in the first release)
|
const avatarError = ref(false);
|
||||||
const {
|
const avatarLoading = ref(false);
|
||||||
upload: avatarUpload,
|
|
||||||
loading: avatarLoading,
|
|
||||||
error: avatarError,
|
|
||||||
fileInfo: avatarFileInfo,
|
|
||||||
} = useFileUpload();
|
|
||||||
|
|
||||||
watch(avatarFileInfo, (info) => {
|
async function avatarUpload(e: Event) {
|
||||||
console.log("fileInfo changed", info);
|
const { files } = e.target as HTMLInputElement;
|
||||||
})*/
|
if (!files?.length) return;
|
||||||
|
avatarLoading.value = true;
|
||||||
|
avatarError.value = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await user.setUserAvatar(files[0]);
|
||||||
|
} catch (e) {
|
||||||
|
avatarError.value = true;
|
||||||
|
} finally {
|
||||||
|
avatarLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(selectedOrganisation, async (organisation) => {
|
watch(selectedOrganisation, async (organisation) => {
|
||||||
await user.setUserOrganisation(organisation.id);
|
await user.setUserOrganisation(organisation.id);
|
||||||
|
|
@ -74,7 +81,6 @@ const nextRoute = computed(() => {
|
||||||
|
|
||||||
<ItDropdownSelect v-model="selectedOrganisation" :items="organisations" />
|
<ItDropdownSelect v-model="selectedOrganisation" :items="organisations" />
|
||||||
|
|
||||||
<!--- TODO: We do this later (not in the first release)
|
|
||||||
<div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24">
|
<div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-3">{{ $t("a.Profilbild") }}</h3>
|
<h3 class="mb-3">{{ $t("a.Profilbild") }}</h3>
|
||||||
|
|
@ -103,7 +109,6 @@ const nextRoute = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
<AvatarImage :loading="avatarLoading" :image-url="user.avatar_url" />
|
<AvatarImage :loading="avatarLoading" :image-url="user.avatar_url" />
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|
|
||||||
|
|
@ -243,9 +243,19 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "profile/:userId",
|
path: "profile/:userId",
|
||||||
component: () => import("@/pages/cockpit/CockpitUserProfilePage.vue"),
|
component: () =>
|
||||||
|
import("@/pages/cockpit/profilePage/CockpitUserProfilePage.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
name: "cockpitUserProfile",
|
name: "cockpitUserProfile",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "learning-path",
|
||||||
|
component: () =>
|
||||||
|
import("@/pages/cockpit/profilePage/LearningPathProfilePage.vue"),
|
||||||
|
props: true,
|
||||||
|
name: "cockpitProfileLearningPath",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "profile/:userId/:circleSlug",
|
path: "profile/:userId/:circleSlug",
|
||||||
|
|
|
||||||
|
|
@ -24,27 +24,30 @@ export async function uploadFile(fileData: FileData, file: File) {
|
||||||
if (fileData.fields) {
|
if (fileData.fields) {
|
||||||
return s3Upload(fileData, file);
|
return s3Upload(fileData, file);
|
||||||
} else {
|
} else {
|
||||||
return directUpload(fileData, file);
|
return directUpload(fileData.url, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function directUpload(fileData: FileData, file: File) {
|
export function directUpload(url: string, file: File) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
|
||||||
const headers = {
|
const headers: HeadersInit = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
} as HeadersInit;
|
};
|
||||||
|
|
||||||
|
const csrfToken = getCookieValue("csrftoken");
|
||||||
|
if (csrfToken) {
|
||||||
|
headers["X-CSRFToken"] = csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: formData,
|
body: formData,
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
|
||||||
options.headers["X-CSRFToken"] = getCookieValue("csrftoken");
|
|
||||||
|
|
||||||
return handleUpload(fileData.url, options);
|
return handleUpload(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function s3Upload(fileData: FileData, file: File) {
|
function s3Upload(fileData: FileData, file: File) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { itGetCached } from "@/fetchHelpers";
|
import { itGet } from "@/fetchHelpers";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { ref, watchEffect } from "vue";
|
import { ref, watchEffect } from "vue";
|
||||||
|
|
||||||
|
|
@ -27,6 +27,7 @@ interface Completion {
|
||||||
status: CompletionStatus;
|
status: CompletionStatus;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Assignment {
|
export interface Assignment {
|
||||||
|
|
@ -73,7 +74,7 @@ export const useMentorCockpit = (
|
||||||
summary.value = null;
|
summary.value = null;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
itGetCached(`/api/mentor/${courseSessionId}/summary`)
|
itGet(`/api/mentor/${courseSessionId}/summary`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
summary.value = response;
|
summary.value = response;
|
||||||
})
|
})
|
||||||
|
|
@ -92,6 +93,7 @@ export const useMentorCockpit = (
|
||||||
summary,
|
summary,
|
||||||
error,
|
error,
|
||||||
getCircleTitleById,
|
getCircleTitleById,
|
||||||
|
fetchData,
|
||||||
getAssignmentById,
|
getAssignmentById,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -73,16 +73,18 @@ export const useCircleStore = defineStore({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
continueFromLearningContent(
|
async continueFromLearningContent(
|
||||||
currentLearningContent: LearningContentWithCompletion,
|
currentLearningContent: LearningContentWithCompletion,
|
||||||
circle: CircleType,
|
circle: CircleType,
|
||||||
returnRoute?: RouteLocationNormalized,
|
returnRoute?: RouteLocationNormalized,
|
||||||
markCompletionFn?: (learningContent: LearningContentWithCompletion) => void
|
markCompletionFn?: (
|
||||||
|
learningContent: LearningContentWithCompletion
|
||||||
|
) => Promise<void>
|
||||||
) {
|
) {
|
||||||
if (currentLearningContent) {
|
if (currentLearningContent) {
|
||||||
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
||||||
if (markCompletionFn) {
|
if (markCompletionFn) {
|
||||||
markCompletionFn(currentLearningContent);
|
await markCompletionFn(currentLearningContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.closeLearningContent(currentLearningContent, circle, returnRoute);
|
this.closeLearningContent(currentLearningContent, circle, returnRoute);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import log from "loglevel";
|
||||||
|
|
||||||
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
|
||||||
import { setI18nLanguage } from "@/i18nextWrapper";
|
import { setI18nLanguage } from "@/i18nextWrapper";
|
||||||
|
import { directUpload } from "@/services/files";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
|
@ -150,5 +151,9 @@ export const useUserStore = defineStore({
|
||||||
this.$state.organisation = organisation;
|
this.$state.organisation = organisation;
|
||||||
await itPost("/api/core/me/", { organisation }, { method: "PUT" });
|
await itPost("/api/core/me/", { organisation }, { method: "PUT" });
|
||||||
},
|
},
|
||||||
|
async setUserAvatar(file: File) {
|
||||||
|
const r = await directUpload("/api/core/avatar/", file);
|
||||||
|
this.$state.avatar_url = r.url;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,8 @@ fi
|
||||||
# sed -i "s|command=/usr/local/bin/supercronic /app/supercronic_crontab|command=/usr/local/bin/supercronic /app/supercronic_crontab -sentry-dsn '$IT_SENTRY_DSN'|" /app/supervisord.conf
|
# sed -i "s|command=/usr/local/bin/supercronic /app/supercronic_crontab|command=/usr/local/bin/supercronic /app/supercronic_crontab -sentry-dsn '$IT_SENTRY_DSN'|" /app/supervisord.conf
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
|
# Create Prüfungslehrgang
|
||||||
|
python /app/manage.py create_vermittler_pruefung
|
||||||
|
|
||||||
# Set the command to run supervisord
|
# Set the command to run supervisord
|
||||||
/home/django/.local/bin/supervisord -c /app/supervisord.conf
|
/home/django/.local/bin/supervisord -c /app/supervisord.conf
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,10 @@ describe("assignmentTrainer.cy.js", () => {
|
||||||
|
|
||||||
cy.get('[data-cy="title"]').should("contain", "Feedback");
|
cy.get('[data-cy="title"]').should("contain", "Feedback");
|
||||||
cy.get('[data-cy="evaluation-duedate]"').should("not.exist");
|
cy.get('[data-cy="evaluation-duedate]"').should("not.exist");
|
||||||
cy.get('[data-cy="instruction"]').should("contain", "Intro für Feedback");
|
cy.get('[data-cy="instruction"]').should(
|
||||||
|
"contain",
|
||||||
|
"Bitte unterstütze Test Student1 und gib Feedback zum Auftrag."
|
||||||
|
);
|
||||||
cy.get('[data-cy="start-evaluation"]').click();
|
cy.get('[data-cy="start-evaluation"]').click();
|
||||||
cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 1 / 5");
|
cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 1 / 5");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,71 +9,71 @@ describe("circle.cy.js", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open circle page", () => {
|
it("can open circle page", () => {
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can toggle learning content", () => {
|
it("can toggle learning content", () => {
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).should("have.class", "cy-unchecked");
|
).should("have.class", "cy-unchecked");
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).click();
|
).click();
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
|
|
||||||
// completion data should still be there after reload
|
// completion data should still be there after reload
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open learning contents and complete them by continuing", () => {
|
it("can open learning contents and complete them by continuing", () => {
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick\"]"
|
||||||
).click();
|
).click();
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Verschaffe dir einen Überblick"
|
"Verschaffe dir einen Überblick"
|
||||||
);
|
);
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
|
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get("[data-cy=\"ls-continue-button\"]").click({ force: true });
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox\"]"
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
|
"[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("continue button works", () => {
|
it("continue button works", () => {
|
||||||
cy.get('[data-cy="ls-continue-button"]').should("contain", "Los geht's");
|
cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Los geht's");
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
||||||
|
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Verschaffe dir einen Überblick"
|
"Verschaffe dir einen Überblick"
|
||||||
);
|
);
|
||||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
|
||||||
|
|
||||||
cy.get('[data-cy="ls-continue-button"]').should("contain", "Weiter geht's");
|
cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Weiter geht's");
|
||||||
cy.get('[data-cy="ls-continue-button"]').click();
|
cy.get("[data-cy=\"ls-continue-button\"]").click();
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
|
|
@ -81,43 +81,43 @@ describe("circle.cy.js", () => {
|
||||||
|
|
||||||
it("can open learning content by url", () => {
|
it("can open learning content by url", () => {
|
||||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
|
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
|
||||||
cy.get('[data-cy="lc-title"]').should(
|
cy.get("[data-cy=\"lc-title\"]").should(
|
||||||
"contain",
|
"contain",
|
||||||
"Handlungsfeld «Fahrzeug»"
|
"Handlungsfeld «Fahrzeug»"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('[data-cy="close-learning-content"]').click();
|
cy.get("[data-cy=\"close-learning-content\"]").click();
|
||||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checks number of sequences and contents", () => {
|
it("checks number of sequences and contents", () => {
|
||||||
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
|
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
|
||||||
cy.get('[data-cy="lp-learning-sequence"]')
|
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||||
.first()
|
.first()
|
||||||
.should("contain", "Vorbereitung");
|
.should("contain", "Vorbereitung");
|
||||||
cy.get('[data-cy="lp-learning-sequence"]')
|
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.should("contain", "Training");
|
.should("contain", "Training");
|
||||||
cy.get('[data-cy="lp-learning-sequence"]')
|
cy.get("[data-cy=\"lp-learning-sequence\"]")
|
||||||
.last()
|
.last()
|
||||||
.should("contain", "Transfer");
|
.should("contain", "Transfer");
|
||||||
|
|
||||||
cy.get('[data-cy="lp-learning-content"]').should("have.length", 10);
|
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 10);
|
||||||
cy.get('[data-cy="lp-learning-content"]')
|
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||||
.first()
|
.first()
|
||||||
.should("contain", "Verschaffe dir einen Überblick");
|
.should("contain", "Verschaffe dir einen Überblick");
|
||||||
cy.get('[data-cy="lp-learning-content"]')
|
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||||
.eq(4)
|
.eq(4)
|
||||||
.should("contain", "Präsenzkurs Fahrzeug");
|
.should("contain", "Präsenzkurs Fahrzeug");
|
||||||
cy.get('[data-cy="lp-learning-content"]')
|
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||||
.eq(7)
|
.eq(7)
|
||||||
.should("contain", "Reflexion");
|
.should("contain", "Reflexion");
|
||||||
cy.get('[data-cy="lp-learning-content"]')
|
cy.get("[data-cy=\"lp-learning-content\"]")
|
||||||
.last()
|
.last()
|
||||||
.should("contain", "Feedback");
|
.should("contain", "Feedback");
|
||||||
|
|
||||||
cy.visit("/course/test-lehrgang/learn/reisen");
|
cy.visit("/course/test-lehrgang/learn/reisen");
|
||||||
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
|
cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
|
||||||
cy.get('[data-cy="lp-learning-content"]').should("have.length", 9);
|
cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 9);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,12 @@ from django_ratelimit.exceptions import Ratelimited
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
from vbv_lernwelt.api.directory import list_entities
|
from vbv_lernwelt.api.directory import list_entities
|
||||||
from vbv_lernwelt.api.user import get_cockpit_type, me_user_view
|
from vbv_lernwelt.api.user import (
|
||||||
|
get_cockpit_type,
|
||||||
|
get_profile,
|
||||||
|
me_user_view,
|
||||||
|
post_avatar,
|
||||||
|
)
|
||||||
from vbv_lernwelt.assignment.views import request_assignment_completion_status
|
from vbv_lernwelt.assignment.views import request_assignment_completion_status
|
||||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||||
from vbv_lernwelt.core.schema import schema
|
from vbv_lernwelt.core.schema import schema
|
||||||
|
|
@ -54,6 +59,7 @@ from vbv_lernwelt.importer.views import (
|
||||||
coursesessions_trainers_import,
|
coursesessions_trainers_import,
|
||||||
t2l_sync,
|
t2l_sync,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.media_files.views import user_image
|
||||||
from vbv_lernwelt.notify.views import email_notification_settings
|
from vbv_lernwelt.notify.views import email_notification_settings
|
||||||
from wagtail import urls as wagtail_urls
|
from wagtail import urls as wagtail_urls
|
||||||
from wagtail.admin import urls as wagtailadmin_urls
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
|
|
@ -99,7 +105,9 @@ urlpatterns = [
|
||||||
# user management
|
# user management
|
||||||
path("sso/", include("vbv_lernwelt.sso.urls")),
|
path("sso/", include("vbv_lernwelt.sso.urls")),
|
||||||
re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
|
re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
|
||||||
|
re_path(r'api/core/avatar/$', post_avatar, name='post_avatar'),
|
||||||
re_path(r'api/core/entities/$', list_entities, name='list_entities'),
|
re_path(r'api/core/entities/$', list_entities, name='list_entities'),
|
||||||
|
path(r'api/core/profile/<signed_int:course_session_id>/<uuid:user_id>', get_profile, name='get_profile_view'),
|
||||||
|
|
||||||
re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login),
|
re_path(r'api/core/login/$', django_view_authentication_exempt(vue_login),
|
||||||
name='vue_login'),
|
name='vue_login'),
|
||||||
|
|
@ -143,6 +151,8 @@ urlpatterns = [
|
||||||
name="request_assignment_completion_status"),
|
name="request_assignment_completion_status"),
|
||||||
|
|
||||||
# documents
|
# documents
|
||||||
|
path("api/core/userimage/<int:image_id>", user_image, name="user_image"),
|
||||||
|
|
||||||
# TODO: remfactor to files app
|
# TODO: remfactor to files app
|
||||||
path(r'api/core/document/start/', document_upload_start,
|
path(r'api/core/document/start/', document_upload_start,
|
||||||
name='file_upload_start'),
|
name='file_upload_start'),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
add_course_session_user,
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileViewTest(APITestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.course, _ = create_course("Test Course")
|
||||||
|
self.course_session = create_course_session(
|
||||||
|
course=self.course, title="Test Session"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user = create_user("user")
|
||||||
|
add_course_session_user(
|
||||||
|
self.course_session,
|
||||||
|
self.user,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_user_profile(self) -> None:
|
||||||
|
# GIVEN
|
||||||
|
url = reverse(
|
||||||
|
"get_profile_view",
|
||||||
|
kwargs={
|
||||||
|
"course_session_id": self.course_session.id,
|
||||||
|
"user_id": self.user.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
profile = response.data
|
||||||
|
self.assertEqual(
|
||||||
|
profile,
|
||||||
|
{
|
||||||
|
"id": str(self.user.id),
|
||||||
|
"first_name": self.user.first_name,
|
||||||
|
"last_name": self.user.last_name,
|
||||||
|
"email": self.user.email,
|
||||||
|
"username": self.user.username,
|
||||||
|
"avatar_url": "/static/avatars/myvbv-default-avatar.png",
|
||||||
|
"organisation": None,
|
||||||
|
"is_superuser": False,
|
||||||
|
"course_session_experts": [],
|
||||||
|
"language": "de",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -5,10 +5,11 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from vbv_lernwelt.core.serializers import UserSerializer
|
from vbv_lernwelt.core.serializers import UserSerializer
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import Course, CourseSessionUser
|
from vbv_lernwelt.course.models import Course, CourseSessionUser
|
||||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.iam.permissions import can_view_profile
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
|
from vbv_lernwelt.media_files.models import UserImage
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET", "PUT"])
|
@api_view(["GET", "PUT"])
|
||||||
|
|
@ -59,3 +60,30 @@ def get_cockpit_type(request, course_id: int):
|
||||||
cockpit_type = None
|
cockpit_type = None
|
||||||
|
|
||||||
return Response({"type": cockpit_type})
|
return Response({"type": cockpit_type})
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET"])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def get_profile(request, course_session_id: int, user_id: str):
|
||||||
|
course_session_user = get_object_or_404(
|
||||||
|
CourseSessionUser, course_session_id=course_session_id, user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not can_view_profile(request.user, course_session_user):
|
||||||
|
return Response(status=403)
|
||||||
|
|
||||||
|
return Response(UserSerializer(course_session_user.user).data)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def post_avatar(request):
|
||||||
|
if "file" not in request.FILES:
|
||||||
|
return Response(status=400)
|
||||||
|
|
||||||
|
request.user.avatar = UserImage.objects.create(
|
||||||
|
file=request.FILES["file"],
|
||||||
|
)
|
||||||
|
request.user.save()
|
||||||
|
|
||||||
|
return Response({"url": request.user.avatar_url})
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,11 @@ class AssignmentCompletionMutation(graphene.Mutation):
|
||||||
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
|
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
|
||||||
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
|
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
|
||||||
):
|
):
|
||||||
if not can_evaluate_assignments(info.context.user, course_session_id):
|
if not can_evaluate_assignments(
|
||||||
|
evaluation_user=info.context.user,
|
||||||
|
assignment_user_id=assignment_user_id,
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
evaluation_data = {
|
evaluation_data = {
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,9 @@ def resolve_assignment_completion(
|
||||||
assignment_user_id = info.context.user.id
|
assignment_user_id = info.context.user.id
|
||||||
|
|
||||||
if str(assignment_user_id) == str(info.context.user.id) or can_evaluate_assignments(
|
if str(assignment_user_id) == str(info.context.user.id) or can_evaluate_assignments(
|
||||||
info.context.user, course_session_id
|
evaluation_user=info.context.user,
|
||||||
|
assignment_user_id=assignment_user_id,
|
||||||
|
course_session_id=course_session_id,
|
||||||
):
|
):
|
||||||
course_id = CourseSession.objects.get(id=course_session_id).course_id
|
course_id = CourseSession.objects.get(id=course_session_id).course_id
|
||||||
if has_course_access(info.context.user, course_id):
|
if has_course_access(info.context.user, course_id):
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class UserAdmin(auth_admin.UserAdmin):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||||
(_("Profile"), {"fields": ("organisation", "language")}),
|
(_("Profile"), {"fields": ("organisation", "language", "avatar")}),
|
||||||
(_("Additional data"), {"fields": ("additional_json_data",)}),
|
(_("Additional data"), {"fields": ("additional_json_data",)}),
|
||||||
)
|
)
|
||||||
list_display = [
|
list_display = [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.contrib.auth.models import Group, Permission
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.core.files import File
|
||||||
from environs import Env
|
from environs import Env
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.models import UserImage
|
||||||
|
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
|
|
||||||
|
|
@ -24,52 +28,54 @@ default_users = [
|
||||||
"email": "student",
|
"email": "student",
|
||||||
"first_name": "Student",
|
"first_name": "Student",
|
||||||
"last_name": "Meier",
|
"last_name": "Meier",
|
||||||
"avatar_url": "/static/avatars/avatar_iterativ.png",
|
"avatar_image": "avatar_iterativ.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": "daniel.egger@iterativ.ch",
|
"email": "daniel.egger@iterativ.ch",
|
||||||
"first_name": "Daniel",
|
"first_name": "Daniel",
|
||||||
"last_name": "Egger",
|
"last_name": "Egger",
|
||||||
"avatar_url": "/static/avatars/avatar_iterativ.png",
|
"avatar_image": "avatar_iterativ.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": "axel.manderbach@lernetz.ch",
|
"email": "axel.manderbach@lernetz.ch",
|
||||||
"first_name": "Axel",
|
"first_name": "Axel",
|
||||||
"last_name": "Manderbach",
|
"last_name": "Manderbach",
|
||||||
"avatar_url": "/static/avatars/avatar_axel.jpg",
|
"avatar_image": "avatar_axel.jpg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": "christoph.bosshard@vbv-afa.ch",
|
"email": "christoph.bosshard@vbv-afa.ch",
|
||||||
"first_name": "Christoph",
|
"first_name": "Christoph",
|
||||||
"last_name": "Bosshard",
|
"last_name": "Bosshard",
|
||||||
"avatar_url": "/static/avatars/avatar_christoph.png",
|
"avatar_image": "avatar_christoph.png",
|
||||||
"password": "myvbv1234",
|
"password": "myvbv1234",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": "alexandra.vangelista@lernetz.ch",
|
"email": "alexandra.vangelista@lernetz.ch",
|
||||||
"first_name": "Alexandra",
|
"first_name": "Alexandra",
|
||||||
"last_name": "Vangelista",
|
"last_name": "Vangelista",
|
||||||
"avatar_url": "/static/avatars/avatar_alexandra.png",
|
"avatar_image": "avatar_alexandra.png",
|
||||||
"password": "myvbv1234",
|
"password": "myvbv1234",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": "chantal.rosenberg@vbv-afa.ch",
|
"email": "chantal.rosenberg@vbv-afa.ch",
|
||||||
"first_name": "Chantal",
|
"first_name": "Chantal",
|
||||||
"last_name": "Rosenberg",
|
"last_name": "Rosenberg",
|
||||||
"avatar_url": "/static/avatars/avatar_chantal.png",
|
"avatar_image": "avatar_chantal.png",
|
||||||
"password": "myvbv1234",
|
"password": "myvbv1234",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": "bianca.muster@eiger-versicherungen.ch",
|
"email": "bianca.muster@eiger-versicherungen.ch",
|
||||||
"first_name": "Bianca",
|
"first_name": "Bianca",
|
||||||
"last_name": "Muster",
|
"last_name": "Muster",
|
||||||
"avatar_url": "/static/avatars/avatar_bianca.png",
|
"avatar_image": "avatar_bianca.png",
|
||||||
"password": "myvbv1234",
|
"password": "myvbv1234",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
AVATAR_DIR = settings.APPS_DIR / "static" / "avatars"
|
||||||
|
|
||||||
def create_default_users(default_password="test"):
|
|
||||||
|
def create_default_users(default_password="test", set_avatar=False):
|
||||||
admin_group, created = Group.objects.get_or_create(name="admin_group")
|
admin_group, created = Group.objects.get_or_create(name="admin_group")
|
||||||
_content_creator_grop, _created = Group.objects.get_or_create(
|
_content_creator_grop, _created = Group.objects.get_or_create(
|
||||||
name="content_creator_grop"
|
name="content_creator_grop"
|
||||||
|
|
@ -81,9 +87,9 @@ def create_default_users(default_password="test"):
|
||||||
email,
|
email,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
avatar_url,
|
|
||||||
language,
|
language,
|
||||||
password,
|
password,
|
||||||
|
avatar_image: str = None,
|
||||||
):
|
):
|
||||||
user, _ = User.objects.get_or_create(
|
user, _ = User.objects.get_or_create(
|
||||||
id=_id,
|
id=_id,
|
||||||
|
|
@ -92,25 +98,33 @@ def create_default_users(default_password="test"):
|
||||||
language=language,
|
language=language,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
avatar_url=avatar_url,
|
|
||||||
password=make_password(password),
|
password=make_password(password),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if avatar_image and set_avatar:
|
||||||
|
with open(AVATAR_DIR / avatar_image, "rb") as f:
|
||||||
|
image, _ = UserImage.objects.get_or_create(
|
||||||
|
file=File(f),
|
||||||
|
)
|
||||||
|
user.avatar = image
|
||||||
|
user.save()
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def _create_student_user(
|
def _create_student_user(
|
||||||
email,
|
email,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
avatar_url="",
|
|
||||||
password=default_password,
|
password=default_password,
|
||||||
language="de",
|
language="de",
|
||||||
|
avatar_image=None,
|
||||||
id=None,
|
id=None,
|
||||||
):
|
):
|
||||||
student_user = _create_user(
|
student_user = _create_user(
|
||||||
email=email,
|
email=email,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
avatar_url=avatar_url,
|
avatar_image=avatar_image,
|
||||||
language=language,
|
language=language,
|
||||||
password=password,
|
password=password,
|
||||||
_id=id,
|
_id=id,
|
||||||
|
|
@ -120,13 +134,18 @@ def create_default_users(default_password="test"):
|
||||||
student_user.save()
|
student_user.save()
|
||||||
|
|
||||||
def _create_admin_user(
|
def _create_admin_user(
|
||||||
email, first_name, last_name, avatar_url="", id=None, password=default_password
|
email,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
avatar_image=None,
|
||||||
|
id=None,
|
||||||
|
password=default_password,
|
||||||
):
|
):
|
||||||
admin_user = _create_user(
|
admin_user = _create_user(
|
||||||
email=email,
|
email=email,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
avatar_url=avatar_url,
|
avatar_image=avatar_image,
|
||||||
password=password,
|
password=password,
|
||||||
language="de",
|
language="de",
|
||||||
_id=id,
|
_id=id,
|
||||||
|
|
@ -145,7 +164,7 @@ def create_default_users(default_password="test"):
|
||||||
email=email,
|
email=email,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
avatar_url="",
|
avatar_image=None,
|
||||||
language="de",
|
language="de",
|
||||||
password=password,
|
password=password,
|
||||||
)
|
)
|
||||||
|
|
@ -158,7 +177,7 @@ def create_default_users(default_password="test"):
|
||||||
email="info@iterativ.ch",
|
email="info@iterativ.ch",
|
||||||
first_name="Info",
|
first_name="Info",
|
||||||
last_name="Iterativ",
|
last_name="Iterativ",
|
||||||
avatar_url="/static/avatars/avatar_iterativ.png",
|
avatar_image="avatar_iterativ.png",
|
||||||
password=env("IT_DEFAULT_ADMIN_PASSWORD", default_password),
|
password=env("IT_DEFAULT_ADMIN_PASSWORD", default_password),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -166,7 +185,7 @@ def create_default_users(default_password="test"):
|
||||||
email="admin",
|
email="admin",
|
||||||
first_name="Peter",
|
first_name="Peter",
|
||||||
last_name="Adminson",
|
last_name="Adminson",
|
||||||
avatar_url="/static/avatars/avatar_iterativ.png",
|
avatar_image="avatar_iterativ.png",
|
||||||
id=ADMIN_USER_ID,
|
id=ADMIN_USER_ID,
|
||||||
password=env("IT_DEFAULT_ADMIN_PASSWORD", default_password),
|
password=env("IT_DEFAULT_ADMIN_PASSWORD", default_password),
|
||||||
)
|
)
|
||||||
|
|
@ -183,7 +202,7 @@ def create_default_users(default_password="test"):
|
||||||
email="expert-vv.expert2@eiger-versicherungen.ch",
|
email="expert-vv.expert2@eiger-versicherungen.ch",
|
||||||
first_name="Christa",
|
first_name="Christa",
|
||||||
last_name="von Allmen",
|
last_name="von Allmen",
|
||||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
avatar_image="uk1.patrizia.huggel.jpg",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="expert-vv.expert3@eiger-versicherungen.ch",
|
email="expert-vv.expert3@eiger-versicherungen.ch",
|
||||||
|
|
@ -199,53 +218,53 @@ def create_default_users(default_password="test"):
|
||||||
email="patrizia.huggel@eiger-versicherungen.ch",
|
email="patrizia.huggel@eiger-versicherungen.ch",
|
||||||
first_name="Patrizia",
|
first_name="Patrizia",
|
||||||
last_name="Huggel",
|
last_name="Huggel",
|
||||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
avatar_image="uk1.patrizia.huggel.jpg",
|
||||||
password="myvbv1234",
|
password="myvbv1234",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="andreas.feuz@eiger-versicherungen.ch",
|
email="andreas.feuz@eiger-versicherungen.ch",
|
||||||
first_name="Andreas",
|
first_name="Andreas",
|
||||||
last_name="Feuz",
|
last_name="Feuz",
|
||||||
avatar_url="/static/avatars/uk1.daniel.tanaka.jpg",
|
avatar_image="uk1.daniel.tanaka.jpg",
|
||||||
password="myvbv1234",
|
password="myvbv1234",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="daniel.tanaka@eiger-versicherung.ch",
|
email="daniel.tanaka@eiger-versicherung.ch",
|
||||||
first_name="Daniel",
|
first_name="Daniel",
|
||||||
last_name="Tanaka",
|
last_name="Tanaka",
|
||||||
avatar_url="/static/avatars/uk1.daniel.tanaka.jpg",
|
avatar_image="uk1.daniel.tanaka.jpg",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="maria.spini@eiger-versicherung.ch",
|
email="maria.spini@eiger-versicherung.ch",
|
||||||
first_name="Maria",
|
first_name="Maria",
|
||||||
last_name="Spini",
|
last_name="Spini",
|
||||||
avatar_url="/static/avatars/uk1.maria.spini.jpg",
|
avatar_image="uk1.maria.spini.jpg",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="christian.koller@eiger-versicherung.ch",
|
email="christian.koller@eiger-versicherung.ch",
|
||||||
first_name="Christian",
|
first_name="Christian",
|
||||||
last_name="Koller",
|
last_name="Koller",
|
||||||
avatar_url="/static/avatars/uk1.christian.koller.jpg",
|
avatar_image="uk1.christian.koller.jpg",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="michael.meier@example.com",
|
email="michael.meier@example.com",
|
||||||
first_name="Michael",
|
first_name="Michael",
|
||||||
last_name="Meier",
|
last_name="Meier",
|
||||||
avatar_url="/static/avatars/uk1.michael.meier.jpg",
|
avatar_image="uk1.michael.meier.jpg",
|
||||||
password="myvbv1234",
|
password="myvbv1234",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="lina.egger@example.com",
|
email="lina.egger@example.com",
|
||||||
first_name="Lina",
|
first_name="Lina",
|
||||||
last_name="Egger",
|
last_name="Egger",
|
||||||
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
avatar_image="uk1.lina.egger.jpg",
|
||||||
password="myvbv1234",
|
password="myvbv1234",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="evelyn.schmid@example.com",
|
email="evelyn.schmid@example.com",
|
||||||
first_name="Evelyn",
|
first_name="Evelyn",
|
||||||
last_name="Schmid",
|
last_name="Schmid",
|
||||||
avatar_url="/static/avatars/uk1.evelyn.schmid.jpg",
|
avatar_image="uk1.evelyn.schmid.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
|
|
@ -272,7 +291,7 @@ def create_default_users(default_password="test"):
|
||||||
email="luca.dupont@assurance.ch",
|
email="luca.dupont@assurance.ch",
|
||||||
first_name="Luca",
|
first_name="Luca",
|
||||||
last_name="Dupont",
|
last_name="Dupont",
|
||||||
avatar_url="/static/avatars/uk1.michael.meier.jpg",
|
avatar_image="uk1.michael.meier.jpg",
|
||||||
password="myafa1234",
|
password="myafa1234",
|
||||||
language="fr",
|
language="fr",
|
||||||
)
|
)
|
||||||
|
|
@ -280,7 +299,7 @@ def create_default_users(default_password="test"):
|
||||||
email="patrick.muster@eiger-versicherungen.ch",
|
email="patrick.muster@eiger-versicherungen.ch",
|
||||||
first_name="Patrick",
|
first_name="Patrick",
|
||||||
last_name="Muster",
|
last_name="Muster",
|
||||||
avatar_url="/static/avatars/uk1.michael.meier.jpg",
|
avatar_image="uk1.michael.meier.jpg",
|
||||||
password="myvbv1234",
|
password="myvbv1234",
|
||||||
language="de",
|
language="de",
|
||||||
)
|
)
|
||||||
|
|
@ -288,7 +307,7 @@ def create_default_users(default_password="test"):
|
||||||
email="geraldine.kolly@assurance.ch",
|
email="geraldine.kolly@assurance.ch",
|
||||||
first_name="Géraldine",
|
first_name="Géraldine",
|
||||||
last_name="Kolly",
|
last_name="Kolly",
|
||||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
avatar_image="uk1.patrizia.huggel.jpg",
|
||||||
password="myafa1234",
|
password="myafa1234",
|
||||||
language="fr",
|
language="fr",
|
||||||
)
|
)
|
||||||
|
|
@ -299,35 +318,35 @@ def create_default_users(default_password="test"):
|
||||||
email="test-trainer1@example.com",
|
email="test-trainer1@example.com",
|
||||||
first_name="Test",
|
first_name="Test",
|
||||||
last_name="Trainer1",
|
last_name="Trainer1",
|
||||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
avatar_image="uk1.patrizia.huggel.jpg",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
id=TEST_TRAINER2_USER_ID,
|
id=TEST_TRAINER2_USER_ID,
|
||||||
email="test-trainer2@example.com",
|
email="test-trainer2@example.com",
|
||||||
first_name="Test",
|
first_name="Test",
|
||||||
last_name="Trainer2",
|
last_name="Trainer2",
|
||||||
avatar_url="/static/avatars/uk1.christian.koller.jpg",
|
avatar_image="uk1.christian.koller.jpg",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
id=TEST_STUDENT1_USER_ID,
|
id=TEST_STUDENT1_USER_ID,
|
||||||
email="test-student1@example.com",
|
email="test-student1@example.com",
|
||||||
first_name="Test",
|
first_name="Test",
|
||||||
last_name="Student1",
|
last_name="Student1",
|
||||||
avatar_url="/static/avatars/uk1.michael.meier.jpg",
|
avatar_image="uk1.michael.meier.jpg",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
id=TEST_STUDENT2_USER_ID,
|
id=TEST_STUDENT2_USER_ID,
|
||||||
email="test-student2@example.com",
|
email="test-student2@example.com",
|
||||||
first_name="Test",
|
first_name="Test",
|
||||||
last_name="Student2",
|
last_name="Student2",
|
||||||
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
avatar_image="uk1.lina.egger.jpg",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
id=TEST_STUDENT3_USER_ID,
|
id=TEST_STUDENT3_USER_ID,
|
||||||
email="test-student3@example.com",
|
email="test-student3@example.com",
|
||||||
first_name="Test",
|
first_name="Test",
|
||||||
last_name="Student3",
|
last_name="Student3",
|
||||||
avatar_url="/static/avatars/uk1.christian.koller.jpg",
|
avatar_image="uk1.christian.koller.jpg",
|
||||||
)
|
)
|
||||||
_create_staff_user(
|
_create_staff_user(
|
||||||
email="matthias.wirth@vbv-afa.ch",
|
email="matthias.wirth@vbv-afa.ch",
|
||||||
|
|
@ -341,7 +360,6 @@ def create_default_users(default_password="test"):
|
||||||
last_name="Regionalleiter",
|
last_name="Regionalleiter",
|
||||||
password=default_password,
|
password=default_password,
|
||||||
language="de",
|
language="de",
|
||||||
avatar_url="",
|
|
||||||
)
|
)
|
||||||
_create_user(
|
_create_user(
|
||||||
_id=TEST_MENTOR1_USER_ID,
|
_id=TEST_MENTOR1_USER_ID,
|
||||||
|
|
@ -350,7 +368,7 @@ def create_default_users(default_password="test"):
|
||||||
last_name="Weber-Mentor",
|
last_name="Weber-Mentor",
|
||||||
password=default_password,
|
password=default_password,
|
||||||
language="de",
|
language="de",
|
||||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
avatar_image="uk1.patrizia.huggel.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from graphene import String
|
||||||
from graphene.types.generic import GenericScalar
|
from graphene.types.generic import GenericScalar
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
|
|
@ -7,6 +8,8 @@ from vbv_lernwelt.core.models import User
|
||||||
|
|
||||||
|
|
||||||
class UserObjectType(DjangoObjectType):
|
class UserObjectType(DjangoObjectType):
|
||||||
|
avatar_url = String(source="avatar_url")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = (
|
fields = (
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@ from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
@click.command()
|
@click.command()
|
||||||
def command():
|
def command():
|
||||||
print("Creating default users.")
|
print("Creating default users.")
|
||||||
create_default_users()
|
create_default_users(set_avatar=True)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.model_utils import migrate_avatars
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("media_files", "0001_initial"),
|
||||||
|
("core", "0003_organisations"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="avatar",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Avatar image for the user",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="media_files.userimage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_avatars),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="user",
|
||||||
|
name="avatar_url",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files import File
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -144,3 +146,39 @@ def remove_organisations(apps=None, schema_editor=None):
|
||||||
Organisation.objects.filter(
|
Organisation.objects.filter(
|
||||||
organisation_id=org_id,
|
organisation_id=org_id,
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_avatars(apps=None, schema_editor=None):
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
|
if apps is None:
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.media_files.models import UserImage
|
||||||
|
else:
|
||||||
|
User = apps.get_model("core", "User")
|
||||||
|
UserImage = apps.get_model("media_files", "UserImage")
|
||||||
|
|
||||||
|
# Models created by Django migration don't contain methods of the parent model.
|
||||||
|
# We need to add them manually.
|
||||||
|
from wagtail.images.models import AbstractImage
|
||||||
|
|
||||||
|
UserImage.get_upload_to = AbstractImage.get_upload_to
|
||||||
|
|
||||||
|
avatar_dir = settings.APPS_DIR / "static" / "avatars"
|
||||||
|
|
||||||
|
for user in User.objects.all().exclude(
|
||||||
|
avatar_url="/static/avatars/myvbv-default-avatar.png"
|
||||||
|
):
|
||||||
|
if not user.avatar_url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
avatar_file = user.avatar_url.split("/")[-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(avatar_dir / avatar_file, "rb") as f:
|
||||||
|
image = UserImage.objects.create(
|
||||||
|
file=File(f),
|
||||||
|
)
|
||||||
|
user.avatar = image
|
||||||
|
user.save()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import uuid
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import JSONField
|
from django.db.models import JSONField
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
class Organisation(models.Model):
|
class Organisation(models.Model):
|
||||||
|
|
@ -34,9 +35,14 @@ class User(AbstractUser):
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
|
||||||
avatar_url = models.CharField(
|
avatar = models.ForeignKey(
|
||||||
max_length=254, blank=True, default="/static/avatars/myvbv-default-avatar.png"
|
"media_files.UserImage",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
help_text="Avatar image for the user",
|
||||||
)
|
)
|
||||||
|
|
||||||
email = models.EmailField("email address", unique=True)
|
email = models.EmailField("email address", unique=True)
|
||||||
sso_id = models.UUIDField(
|
sso_id = models.UUIDField(
|
||||||
"SSO subscriber ID", unique=True, null=True, blank=True, default=None
|
"SSO subscriber ID", unique=True, null=True, blank=True, default=None
|
||||||
|
|
@ -48,6 +54,16 @@ class User(AbstractUser):
|
||||||
Organisation, on_delete=models.SET_NULL, null=True, blank=True
|
Organisation, on_delete=models.SET_NULL, null=True, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar_url(self):
|
||||||
|
if self.avatar:
|
||||||
|
filter_spec = "fill-400x400"
|
||||||
|
self.avatar.get_rendition(filter_spec)
|
||||||
|
url = reverse("user_image", kwargs={"image_id": self.avatar.id})
|
||||||
|
return f"{url}?filter={filter_spec}"
|
||||||
|
else:
|
||||||
|
return "/static/avatars/myvbv-default-avatar.png"
|
||||||
|
|
||||||
|
|
||||||
class SecurityRequestResponseLog(models.Model):
|
class SecurityRequestResponseLog(models.Model):
|
||||||
label = models.CharField(max_length=255, blank=True, default="")
|
label = models.CharField(max_length=255, blank=True, default="")
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,4 @@ COURSE_UK_IT = -8
|
||||||
COURSE_UK_TRAINING_IT = -9
|
COURSE_UK_TRAINING_IT = -9
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10
|
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11
|
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID = -12
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||||
from vbv_lernwelt.course.factories import CoursePageFactory
|
from vbv_lernwelt.course.factories import CoursePageFactory
|
||||||
|
from vbv_lernwelt.course.models import CircleContactType
|
||||||
from vbv_lernwelt.course.utils import get_wagtail_default_site
|
from vbv_lernwelt.course.utils import get_wagtail_default_site
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -20,6 +21,8 @@ def create_versicherungsvermittlerin_with_categories(
|
||||||
id=course_id,
|
id=course_id,
|
||||||
title=title,
|
title=title,
|
||||||
category_name="Handlungsfeld",
|
category_name="Handlungsfeld",
|
||||||
|
enable_circle_documents=False,
|
||||||
|
circle_contact_type=CircleContactType.LEARNING_MENTOR.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
CourseCategory.objects.get_or_create(course=course, title="Allgemein", general=True)
|
CourseCategory.objects.get_or_create(course=course, title="Allgemein", general=True)
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ from vbv_lernwelt.course.consts import (
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.creators.test_course import (
|
from vbv_lernwelt.course.creators.test_course import (
|
||||||
create_edoniq_test_assignment,
|
create_edoniq_test_assignment,
|
||||||
|
|
@ -95,6 +96,7 @@ from vbv_lernwelt.importer.services import (
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
||||||
create_vv_new_learning_path,
|
create_vv_new_learning_path,
|
||||||
|
create_vv_pruefung_learning_path,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.learnpath.models import (
|
from vbv_lernwelt.learnpath.models import (
|
||||||
Circle,
|
Circle,
|
||||||
|
|
@ -153,6 +155,9 @@ def command(course):
|
||||||
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID, language="it"
|
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID, language="it"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID in course:
|
||||||
|
create_versicherungsvermittlerin_pruefung_course()
|
||||||
|
|
||||||
if COURSE_UK in course:
|
if COURSE_UK in course:
|
||||||
create_course_uk_de()
|
create_course_uk_de()
|
||||||
create_course_uk_de_course_sessions()
|
create_course_uk_de_course_sessions()
|
||||||
|
|
@ -276,6 +281,34 @@ def create_versicherungsvermittlerin_course(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_versicherungsvermittlerin_pruefung_course(
|
||||||
|
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID, language="de"
|
||||||
|
):
|
||||||
|
names = {
|
||||||
|
"de": "Versicherungsvermittler/-in VBV Prüfung",
|
||||||
|
"fr": "Intermédiaire d’assurance AFA Examen",
|
||||||
|
"it": "Intermediario/a assicurativo/a AFA Esame",
|
||||||
|
}
|
||||||
|
# Versicherungsvermittler/in mit neuen Circles
|
||||||
|
course = create_versicherungsvermittlerin_with_categories(
|
||||||
|
course_id=course_id,
|
||||||
|
title=names[language],
|
||||||
|
)
|
||||||
|
|
||||||
|
# assignments create assignments parent page
|
||||||
|
_assignment_list_page = AssignmentListPageFactory(
|
||||||
|
parent=course.coursepage,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_vv_new_competence_profile(course_id=course_id)
|
||||||
|
create_default_media_library(course_id=course_id)
|
||||||
|
create_vv_reflection(course_id=course_id)
|
||||||
|
|
||||||
|
CourseSession.objects.create(course_id=course_id, title=names[language])
|
||||||
|
|
||||||
|
create_vv_pruefung_learning_path(course_id=course_id)
|
||||||
|
|
||||||
|
|
||||||
def create_course_uk_de(course_id=COURSE_UK, lang="de"):
|
def create_course_uk_de(course_id=COURSE_UK, lang="de"):
|
||||||
names = {
|
names = {
|
||||||
"de": "Überbetriebliche Kurse",
|
"de": "Überbetriebliche Kurse",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import djclick as click
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID
|
||||||
|
from vbv_lernwelt.course.management.commands.create_default_courses import (
|
||||||
|
create_versicherungsvermittlerin_pruefung_course,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import Course
|
||||||
|
|
||||||
|
ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def command():
|
||||||
|
print(
|
||||||
|
"Creating Vermittler Prüfung course",
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if Course.objects.filter(id=COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID).exists():
|
||||||
|
print("Course already exists, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
create_versicherungsvermittlerin_pruefung_course()
|
||||||
|
|
@ -41,7 +41,7 @@ class Course(models.Model):
|
||||||
return f"/course/{self.slug}"
|
return f"/course/{self.slug}"
|
||||||
|
|
||||||
def get_cockpit_url(self):
|
def get_cockpit_url(self):
|
||||||
return f"/{self.get_course_url()}/cockpit"
|
return f"{self.get_course_url()}/cockpit"
|
||||||
|
|
||||||
def get_learning_path(self):
|
def get_learning_path(self):
|
||||||
from vbv_lernwelt.learnpath.models import LearningPath
|
from vbv_lernwelt.learnpath.models import LearningPath
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
from vbv_lernwelt.files.models import UploadFile
|
from vbv_lernwelt.files.models import UploadFile
|
||||||
from vbv_lernwelt.files.services import FileDirectUploadService
|
from vbv_lernwelt.files.services import FileDirectUploadService
|
||||||
from vbv_lernwelt.iam.permissions import (
|
from vbv_lernwelt.iam.permissions import (
|
||||||
|
can_view_course_completions,
|
||||||
course_sessions_for_user_qs,
|
course_sessions_for_user_qs,
|
||||||
has_course_access,
|
has_course_access,
|
||||||
has_course_access_by_page_request,
|
has_course_access_by_page_request,
|
||||||
is_circle_expert,
|
is_circle_expert,
|
||||||
is_course_session_expert,
|
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
|
|
||||||
|
|
@ -76,8 +76,8 @@ def request_course_completion(request, course_session_id):
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def request_course_completion_for_user(request, course_session_id, user_id):
|
def request_course_completion_for_user(request, course_session_id, user_id):
|
||||||
if request.user.id == user_id or is_course_session_expert(
|
if can_view_course_completions(
|
||||||
request.user, course_session_id
|
user=request.user, course_session_id=course_session_id, target_user_id=user_id
|
||||||
):
|
):
|
||||||
return _request_course_completion(course_session_id, user_id)
|
return _request_course_completion(course_session_id, user_id)
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ def has_course_access(user, course_id):
|
||||||
).exists():
|
).exists():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if LearningMentor.objects.filter(course_id=course_id, mentor=user).exists():
|
||||||
|
return True
|
||||||
|
|
||||||
return CourseSessionUser.objects.filter(
|
return CourseSessionUser.objects.filter(
|
||||||
course_session__course_id=course_id, user=user
|
course_session__course_id=course_id, user=user
|
||||||
).exists()
|
).exists()
|
||||||
|
|
@ -32,16 +35,31 @@ def has_course_session_access(user, course_session_id: int):
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def is_user_mentor(mentor: User, participant_user_id: str, course_session_id: int):
|
||||||
|
csu = CourseSessionUser.objects.filter(
|
||||||
|
course_session_id=course_session_id, user_id=participant_user_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if csu is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return LearningMentor.objects.filter(
|
||||||
|
course_id=csu.course_session.course_id, mentor=mentor, participants=csu
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
def is_course_session_expert(user, course_session_id: int):
|
def is_course_session_expert(user, course_session_id: int):
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
course_session = CourseSession.objects.get(id=course_session_id)
|
||||||
|
|
||||||
is_supervisor = CourseSessionGroup.objects.filter(
|
is_supervisor = CourseSessionGroup.objects.filter(
|
||||||
supervisor=user, course_session__id=course_session_id
|
supervisor=user, course_session=course_session
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
is_expert = CourseSessionUser.objects.filter(
|
is_expert = CourseSessionUser.objects.filter(
|
||||||
course_session_id=course_session_id,
|
course_session=course_session,
|
||||||
user=user,
|
user=user,
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
@ -60,21 +78,29 @@ def is_course_session_member(user, course_session_id: int | None = None):
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
def can_evaluate_assignments(user, course_session_id: int):
|
def can_evaluate_assignments(
|
||||||
if user.is_superuser:
|
evaluation_user: User, course_session_id: int, assignment_user_id: str | None = None
|
||||||
|
):
|
||||||
|
if evaluation_user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
is_supervisor = CourseSessionGroup.objects.filter(
|
is_supervisor = CourseSessionGroup.objects.filter(
|
||||||
supervisor=user, course_session__id=course_session_id
|
supervisor=evaluation_user, course_session__id=course_session_id
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
is_expert = CourseSessionUser.objects.filter(
|
is_expert = CourseSessionUser.objects.filter(
|
||||||
course_session_id=course_session_id,
|
course_session_id=course_session_id,
|
||||||
user=user,
|
user=evaluation_user,
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
return is_supervisor or is_expert
|
is_mentor = is_user_mentor(
|
||||||
|
mentor=evaluation_user,
|
||||||
|
participant_user_id=assignment_user_id,
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return is_supervisor or is_expert or is_mentor
|
||||||
|
|
||||||
|
|
||||||
def course_sessions_for_user_qs(user):
|
def course_sessions_for_user_qs(user):
|
||||||
|
|
@ -150,3 +176,34 @@ def has_role_in_course(user: User, course: Course) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool:
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if user == profile_user.user:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if is_course_session_expert(user, profile_user.course_session.id) or is_user_mentor(
|
||||||
|
mentor=user,
|
||||||
|
participant_user_id=profile_user.user.id,
|
||||||
|
course_session_id=profile_user.course_session.id,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def can_view_course_completions(
|
||||||
|
user: User, course_session_id: int, target_user_id: str
|
||||||
|
) -> bool:
|
||||||
|
return (
|
||||||
|
user.id == target_user_id
|
||||||
|
or is_course_session_expert(user=user, course_session_id=course_session_id)
|
||||||
|
or is_user_mentor(
|
||||||
|
mentor=user,
|
||||||
|
participant_user_id=target_user_id,
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import (
|
||||||
|
create_course,
|
||||||
|
create_course_session,
|
||||||
|
create_user,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.iam.permissions import is_course_session_expert
|
||||||
|
|
||||||
|
|
||||||
|
class ExpertTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.course, _ = create_course("Test Course")
|
||||||
|
self.course_session = create_course_session(
|
||||||
|
course=self.course, title="Test Session"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user = create_user("user")
|
||||||
|
|
||||||
|
def test_member(self):
|
||||||
|
# GIVEN
|
||||||
|
csu = CourseSessionUser.objects.create(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=self.user,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
is_expert = is_course_session_expert(
|
||||||
|
user=csu.user, course_session_id=self.course_session.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertFalse(is_expert)
|
||||||
|
|
||||||
|
def test_supervisor(self):
|
||||||
|
# GIVEN
|
||||||
|
csg = CourseSessionGroup.objects.create(name="Test Group", course=self.course)
|
||||||
|
csg.course_session.add(self.course_session)
|
||||||
|
csg.supervisor.add(self.user)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
is_expert = is_course_session_expert(
|
||||||
|
user=self.user, course_session_id=self.course_session.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(is_expert)
|
||||||
|
|
||||||
|
def test_expert(self):
|
||||||
|
# GIVEN
|
||||||
|
csu = CourseSessionUser.objects.create(
|
||||||
|
course_session=self.course_session,
|
||||||
|
user=self.user,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
is_expert = is_course_session_expert(
|
||||||
|
user=csu.user, course_session_id=self.course_session.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(is_expert)
|
||||||
|
|
@ -8,7 +8,7 @@ from vbv_lernwelt.course.creators.test_utils import (
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
from vbv_lernwelt.iam.permissions import has_role_in_course
|
from vbv_lernwelt.iam.permissions import has_role_in_course, is_user_mentor
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -59,6 +59,62 @@ class RoleTestCase(TestCase):
|
||||||
# THEN
|
# THEN
|
||||||
self.assertTrue(has_role)
|
self.assertTrue(has_role)
|
||||||
|
|
||||||
|
def test_is_user_mentor(self):
|
||||||
|
# GIVEN
|
||||||
|
member = create_user("member")
|
||||||
|
mentor = create_user("mentor")
|
||||||
|
|
||||||
|
course, course_page = create_course("Test Course")
|
||||||
|
course_session = create_course_session(course=course, title=":)")
|
||||||
|
|
||||||
|
member_csu = add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=member,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
learning_mentor = LearningMentor.objects.create(
|
||||||
|
mentor=mentor,
|
||||||
|
course=course,
|
||||||
|
)
|
||||||
|
|
||||||
|
learning_mentor.participants.add(member_csu)
|
||||||
|
learning_mentor.save()
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
is_mentor = is_user_mentor(
|
||||||
|
mentor=mentor,
|
||||||
|
participant_user_id=member.id,
|
||||||
|
course_session_id=course_session.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertTrue(is_mentor)
|
||||||
|
|
||||||
|
def test_not_is_user_mentor(self):
|
||||||
|
# GIVEN
|
||||||
|
member = create_user("member")
|
||||||
|
wanna_be_mentor = create_user("wanna_be_mentor")
|
||||||
|
|
||||||
|
course, course_page = create_course("Test Course")
|
||||||
|
course_session = create_course_session(course=course, title=":)")
|
||||||
|
|
||||||
|
add_course_session_user(
|
||||||
|
course_session=course_session,
|
||||||
|
user=member,
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
is_mentor = is_user_mentor(
|
||||||
|
mentor=wanna_be_mentor,
|
||||||
|
participant_user_id=member.id,
|
||||||
|
course_session_id=course_session.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertFalse(is_mentor)
|
||||||
|
|
||||||
def test_no_role(self):
|
def test_no_role(self):
|
||||||
# GIVEN
|
# GIVEN
|
||||||
other_course, _ = create_course("Other Test Course")
|
other_course, _ = create_course("Other Test Course")
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ def get_assignment_completions(
|
||||||
)[0],
|
)[0],
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
|
url=f"/course/{course_session.course.slug}/cockpit/assignment/{assignment.id}/{user.id}",
|
||||||
)
|
)
|
||||||
for user in sorted_participants
|
for user in sorted_participants
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class MentorAssignmentCompletion:
|
||||||
status: MentorCompletionStatus
|
status: MentorCompletionStatus
|
||||||
user_id: str
|
user_id: str
|
||||||
last_name: str
|
last_name: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ class MentorAssignmentCompletionSerializer(serializers.Serializer):
|
||||||
status = serializers.SerializerMethodField()
|
status = serializers.SerializerMethodField()
|
||||||
user_id = serializers.CharField()
|
user_id = serializers.CharField()
|
||||||
last_name = serializers.CharField()
|
last_name = serializers.CharField()
|
||||||
|
url = serializers.CharField()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_status(obj):
|
def get_status(obj):
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,10 @@ class AttendanceServicesTestCase(TestCase):
|
||||||
for i, result in enumerate(results):
|
for i, result in enumerate(results):
|
||||||
self.assertEqual(result.last_name, expected_order[i])
|
self.assertEqual(result.last_name, expected_order[i])
|
||||||
self.assertEqual(result.status, expected_statuses[result.last_name])
|
self.assertEqual(result.status, expected_statuses[result.last_name])
|
||||||
|
self.assertEqual(
|
||||||
|
result.url,
|
||||||
|
f"/course/test-lehrgang/cockpit/assignment/{self.assignment.id}/{result.user_id}",
|
||||||
|
)
|
||||||
|
|
||||||
def test_praxis_assignment_status(self):
|
def test_praxis_assignment_status(self):
|
||||||
# GIVEN
|
# GIVEN
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,25 @@ def create_vv_new_learning_path(
|
||||||
# all pages belong to 'admin' by default
|
# all pages belong to 'admin' by default
|
||||||
Page.objects.update(owner=user)
|
Page.objects.update(owner=user)
|
||||||
|
|
||||||
|
def create_vv_pruefung_learning_path(
|
||||||
|
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, user=None
|
||||||
|
):
|
||||||
|
if user is None:
|
||||||
|
user = User.objects.get(username="info@iterativ.ch")
|
||||||
|
|
||||||
|
course_page = CoursePage.objects.get(course_id=course_id)
|
||||||
|
lp = LearningPathFactory(
|
||||||
|
title="Lernpfad",
|
||||||
|
parent=course_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
TopicFactory(title="Prüfung", parent=lp)
|
||||||
|
create_circle_pruefungsvorbereitung(lp)
|
||||||
|
create_circle_pruefung(lp)
|
||||||
|
|
||||||
|
# all pages belong to 'admin' by default
|
||||||
|
Page.objects.update(owner=user)
|
||||||
|
|
||||||
|
|
||||||
def create_circle_basis(lp, title="Basis", course_page=None):
|
def create_circle_basis(lp, title="Basis", course_page=None):
|
||||||
circle = CircleFactory(
|
circle = CircleFactory(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import skipIf
|
||||||
|
|
||||||
|
from django.core.files import File
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_utils import create_user
|
||||||
|
from vbv_lernwelt.media_files.models import UserImage
|
||||||
|
|
||||||
|
TEST_IMAGE = Path(__file__).parent / "test_images" / "user1_profile.jpg"
|
||||||
|
|
||||||
|
|
||||||
|
@skipIf(
|
||||||
|
os.environ.get("ENABLE_S3_STORAGE_UNIT_TESTS") is None,
|
||||||
|
"Only enable tests by setting ENABLE_S3_STORAGE_UNIT_TESTS=1",
|
||||||
|
)
|
||||||
|
class UserImageViewTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.user = create_user("supervisor")
|
||||||
|
|
||||||
|
with open(TEST_IMAGE, "rb") as f:
|
||||||
|
self.user_image, _ = UserImage.objects.get_or_create(
|
||||||
|
file=File(f, name=TEST_IMAGE.name),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_image(self):
|
||||||
|
# GIVEN
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self._get_image(self.user_image.id, "fill-300x150")
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response["Content-Type"], "image/jpeg")
|
||||||
|
|
||||||
|
def test_invalid_id(self):
|
||||||
|
# GIVEN
|
||||||
|
image_id = 123456789
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self._get_image(image_id)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_invalid_filter(self):
|
||||||
|
# GIVEN
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
filter_spec = "invalid-filter"
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
response = self._get_image(self.user_image.id, filter_spec)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(
|
||||||
|
response.content, f"Invalid filter spec: {filter_spec}".encode()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_image(self, image_id, filter_spec=None):
|
||||||
|
url = reverse("user_image", kwargs={"image_id": image_id})
|
||||||
|
if filter_spec:
|
||||||
|
url += f"?filter={filter_spec}"
|
||||||
|
|
||||||
|
return self.client.get(url)
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import imghdr
|
||||||
|
from wsgiref.util import FileWrapper
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponse, StreamingHttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from wagtail.images.exceptions import InvalidFilterSpecError
|
||||||
|
from wagtail.images.models import SourceImageIOError
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.models import UserImage
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def user_image(request, image_id):
|
||||||
|
image = get_object_or_404(UserImage, id=image_id)
|
||||||
|
|
||||||
|
filter_spec = request.GET.get("filter", "original")
|
||||||
|
|
||||||
|
try:
|
||||||
|
rendition = image.get_rendition(filter_spec)
|
||||||
|
except SourceImageIOError:
|
||||||
|
return HttpResponse(
|
||||||
|
"Source image file not found", content_type="text/plain", status=410
|
||||||
|
)
|
||||||
|
except InvalidFilterSpecError:
|
||||||
|
return HttpResponse(
|
||||||
|
"Invalid filter spec: " + filter_spec,
|
||||||
|
content_type="text/plain",
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
rendition.file.open("rb")
|
||||||
|
image_format = imghdr.what(rendition.file)
|
||||||
|
return StreamingHttpResponse(
|
||||||
|
FileWrapper(rendition.file), content_type="image/" + image_format
|
||||||
|
)
|
||||||
|
|
@ -9,6 +9,7 @@ from vbv_lernwelt.course_session.models import (
|
||||||
CourseSessionAssignment,
|
CourseSessionAssignment,
|
||||||
CourseSessionEdoniqTest,
|
CourseSessionEdoniqTest,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
from vbv_lernwelt.notify.services import NotificationService
|
from vbv_lernwelt.notify.services import NotificationService
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
@ -63,9 +64,12 @@ def send_assignment_reminder_notifications():
|
||||||
AssignmentType.CASEWORK.value,
|
AssignmentType.CASEWORK.value,
|
||||||
],
|
],
|
||||||
):
|
):
|
||||||
|
circle_page = assignment.learning_content.get_parent_circle()
|
||||||
|
circle = Circle.objects.get(page_ptr=circle_page.id)
|
||||||
for expert in CourseSessionUser.objects.filter(
|
for expert in CourseSessionUser.objects.filter(
|
||||||
course_session_id=assignment.course_session.id,
|
course_session_id=assignment.course_session.id,
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
expert=circle,
|
||||||
):
|
):
|
||||||
sent.append(
|
sent.append(
|
||||||
NotificationService.send_casework_expert_evaluation_reminder(
|
NotificationService.send_casework_expert_evaluation_reminder(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import validate_email
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from notifications.signals import notify
|
from notifications.signals import notify
|
||||||
|
|
||||||
|
|
@ -38,11 +40,22 @@ class NotificationService:
|
||||||
def send_assignment_submitted_notification(
|
def send_assignment_submitted_notification(
|
||||||
cls, recipient: User, sender: User, assignment_completion: AssignmentCompletion
|
cls, recipient: User, sender: User, assignment_completion: AssignmentCompletion
|
||||||
):
|
):
|
||||||
texts = {
|
if (
|
||||||
"de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben.",
|
assignment_completion.assignment.assignment_type
|
||||||
"fr": "%(sender)s a soumis l'étude de cas dirigée «%(assignment_title)s».",
|
== AssignmentType.PRAXIS_ASSIGNMENT.value
|
||||||
"it": "%(sender)s ha consegnato il caso di studio guidato «%(assignment_title)s».",
|
):
|
||||||
}
|
texts = {
|
||||||
|
"de": "%(sender)s hat den Praxisauftrag «%(assignment_title)s» abgegeben.",
|
||||||
|
"fr": "%(sender)s a soumis la mission pratique «%(assignment_title)s».",
|
||||||
|
"it": "%(sender)s ha consegnato l'incarico pratico «%(assignment_title)s».",
|
||||||
|
}
|
||||||
|
# this was the default case before the praxis assignment was introduced
|
||||||
|
else:
|
||||||
|
texts = {
|
||||||
|
"de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben.",
|
||||||
|
"fr": "%(sender)s a soumis l'étude de cas dirigée «%(assignment_title)s».",
|
||||||
|
"it": "%(sender)s ha consegnato il caso di studio guidato «%(assignment_title)s».",
|
||||||
|
}
|
||||||
verb = texts.get(recipient.language, "de") % {
|
verb = texts.get(recipient.language, "de") % {
|
||||||
"sender": sender.get_full_name(),
|
"sender": sender.get_full_name(),
|
||||||
"assignment_title": assignment_completion.assignment.title,
|
"assignment_title": assignment_completion.assignment.title,
|
||||||
|
|
@ -68,11 +81,22 @@ class NotificationService:
|
||||||
assignment_completion: AssignmentCompletion,
|
assignment_completion: AssignmentCompletion,
|
||||||
target_url: str,
|
target_url: str,
|
||||||
):
|
):
|
||||||
texts = {
|
if (
|
||||||
"de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet.",
|
assignment_completion.assignment.assignment_type
|
||||||
"fr": "%(sender)s a évalué l'étude de cas dirigée «%(assignment_title)s».",
|
== AssignmentType.PRAXIS_ASSIGNMENT.value
|
||||||
"it": "%(sender)s ha valutato il caso di studio guidato «%(assignment_title)s».",
|
):
|
||||||
}
|
texts = {
|
||||||
|
"de": "%(sender)s hat den Praxisauftrag «%(assignment_title)s» bewertet.",
|
||||||
|
"fr": "%(sender)s a évalué la mission pratique «%(assignment_title)s».",
|
||||||
|
"it": "%(sender)s ha valutato l'incarico pratico «%(assignment_title)s».",
|
||||||
|
}
|
||||||
|
# this was the default case before the praxis assignment was introduced
|
||||||
|
else:
|
||||||
|
texts = {
|
||||||
|
"de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet.",
|
||||||
|
"fr": "%(sender)s a évalué l'étude de cas dirigée «%(assignment_title)s».",
|
||||||
|
"it": "%(sender)s ha valutato il caso di studio guidato «%(assignment_title)s».",
|
||||||
|
}
|
||||||
verb = texts.get(recipient.language, "de") % {
|
verb = texts.get(recipient.language, "de") % {
|
||||||
"sender": sender.get_full_name(),
|
"sender": sender.get_full_name(),
|
||||||
"assignment_title": assignment_completion.assignment.title,
|
"assignment_title": assignment_completion.assignment.title,
|
||||||
|
|
@ -281,6 +305,13 @@ class NotificationService:
|
||||||
template_data=template_data,
|
template_data=template_data,
|
||||||
)
|
)
|
||||||
emailed = False
|
emailed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_email(recipient.email)
|
||||||
|
except ValidationError:
|
||||||
|
log.info("Recipient email is invalid")
|
||||||
|
return f"{notification_identifier}_invalid_email"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
notification = NotificationService._find_duplicate_notification(
|
notification = NotificationService._find_duplicate_notification(
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@ from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import AssignmentType
|
from vbv_lernwelt.assignment.models import (
|
||||||
|
Assignment,
|
||||||
|
AssignmentCompletion,
|
||||||
|
AssignmentCompletionStatus,
|
||||||
|
AssignmentType,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
from vbv_lernwelt.core.constants import TEST_COURSE_SESSION_BERN_ID
|
from vbv_lernwelt.core.constants import TEST_COURSE_SESSION_BERN_ID
|
||||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||||
|
|
@ -24,6 +30,7 @@ from vbv_lernwelt.notify.email.reminders.assigment import (
|
||||||
send_assignment_reminder_notifications,
|
send_assignment_reminder_notifications,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.notify.models import Notification
|
from vbv_lernwelt.notify.models import Notification
|
||||||
|
from vbv_lernwelt.notify.services import NotificationService
|
||||||
|
|
||||||
EXPECTED_MEMBER_VERB = "Erinnerung: Bald ist ein Abgabetermin"
|
EXPECTED_MEMBER_VERB = "Erinnerung: Bald ist ein Abgabetermin"
|
||||||
EXPECTED_EXPERT_VERB = "Erinnerung: Bald ist ein Bewertungstermin"
|
EXPECTED_EXPERT_VERB = "Erinnerung: Bald ist ein Bewertungstermin"
|
||||||
|
|
@ -40,6 +47,7 @@ ASSIGNMENT_TYPE_LEARNING_CONTENT_LOOKUP: Dict[AssignmentType, str] = {
|
||||||
AssignmentType.PREP_ASSIGNMENT: "test-lehrgang-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto",
|
AssignmentType.PREP_ASSIGNMENT: "test-lehrgang-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto",
|
||||||
AssignmentType.REFLECTION: "test-lehrgang-lp-circle-fahrzeug-lc-reflexion",
|
AssignmentType.REFLECTION: "test-lehrgang-lp-circle-fahrzeug-lc-reflexion",
|
||||||
AssignmentType.CASEWORK: "test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice",
|
AssignmentType.CASEWORK: "test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice",
|
||||||
|
AssignmentType.PRAXIS_ASSIGNMENT: "test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -284,3 +292,164 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
|
|
||||||
with self.assertRaises(Notification.DoesNotExist):
|
with self.assertRaises(Notification.DoesNotExist):
|
||||||
Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAssignmentCourseUpdateTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
create_default_users()
|
||||||
|
create_test_course(with_sessions=True)
|
||||||
|
|
||||||
|
CourseSessionAssignment.objects.all().delete()
|
||||||
|
Notification.objects.all().delete()
|
||||||
|
|
||||||
|
self.student = User.objects.get(email=RECIPIENT_STUDENTS[0])
|
||||||
|
self.trainer = User.objects.get(email=RECIPIENT_TRAINER)
|
||||||
|
|
||||||
|
@freeze_time("2023-01-01")
|
||||||
|
def test_notification_title_casework_for_experts(self):
|
||||||
|
# GIVEN
|
||||||
|
casework = create_assignment(
|
||||||
|
assignment_type=AssignmentType.CASEWORK,
|
||||||
|
submission_deadline=timezone.make_aware(datetime(2022, 12, 12)),
|
||||||
|
evaluation_deadline=timezone.make_aware(datetime(2023, 1, 2)),
|
||||||
|
)
|
||||||
|
assignment = Assignment.objects.get(
|
||||||
|
slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
||||||
|
)
|
||||||
|
|
||||||
|
ac = AssignmentCompletion.objects.create(
|
||||||
|
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
|
||||||
|
assignment_user=self.student,
|
||||||
|
assignment=assignment,
|
||||||
|
evaluation_passed=True,
|
||||||
|
course_session=casework.course_session,
|
||||||
|
completion_data={},
|
||||||
|
evaluation_max_points=10,
|
||||||
|
evaluation_points=10,
|
||||||
|
evaluation_user=self.trainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
NotificationService.send_assignment_submitted_notification(
|
||||||
|
recipient=self.trainer,
|
||||||
|
sender=ac.assignment_user,
|
||||||
|
assignment_completion=ac,
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(1, len(Notification.objects.all()))
|
||||||
|
|
||||||
|
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
||||||
|
self.assertEqual("USER_INTERACTION", notification.notification_category)
|
||||||
|
self.assertIn("hat die geleitete Fallarbeit", notification.verb)
|
||||||
|
|
||||||
|
def test_notification_title_praxis_assignment_for_experts(self):
|
||||||
|
# GIVEN.
|
||||||
|
casework = create_assignment(
|
||||||
|
assignment_type=AssignmentType.PRAXIS_ASSIGNMENT,
|
||||||
|
)
|
||||||
|
assignment = Assignment.objects.get(
|
||||||
|
slug="test-lehrgang-assignment-mein-kundenstamm"
|
||||||
|
)
|
||||||
|
|
||||||
|
ac = AssignmentCompletion.objects.create(
|
||||||
|
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
|
||||||
|
assignment_user=self.student,
|
||||||
|
assignment=assignment,
|
||||||
|
evaluation_passed=True,
|
||||||
|
course_session=casework.course_session,
|
||||||
|
completion_data={},
|
||||||
|
evaluation_max_points=10,
|
||||||
|
evaluation_points=10,
|
||||||
|
evaluation_user=self.trainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
NotificationService.send_assignment_submitted_notification(
|
||||||
|
recipient=self.trainer,
|
||||||
|
sender=ac.assignment_user,
|
||||||
|
assignment_completion=ac,
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(1, len(Notification.objects.all()))
|
||||||
|
|
||||||
|
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
||||||
|
self.assertEqual("USER_INTERACTION", notification.notification_category)
|
||||||
|
self.assertIn("hat den Praxisauftrag", notification.verb)
|
||||||
|
|
||||||
|
@freeze_time("2023-01-01")
|
||||||
|
def test_notification_title_casework_for_student(self):
|
||||||
|
# GIVEN
|
||||||
|
casework = create_assignment(
|
||||||
|
assignment_type=AssignmentType.CASEWORK,
|
||||||
|
submission_deadline=timezone.make_aware(datetime(2022, 12, 12)),
|
||||||
|
evaluation_deadline=timezone.make_aware(datetime(2023, 1, 2)),
|
||||||
|
)
|
||||||
|
assignment = Assignment.objects.get(
|
||||||
|
slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
||||||
|
)
|
||||||
|
|
||||||
|
ac = AssignmentCompletion.objects.create(
|
||||||
|
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
||||||
|
assignment_user=self.student,
|
||||||
|
assignment=assignment,
|
||||||
|
evaluation_passed=True,
|
||||||
|
course_session=casework.course_session,
|
||||||
|
completion_data={},
|
||||||
|
evaluation_max_points=10,
|
||||||
|
evaluation_points=10,
|
||||||
|
evaluation_user=self.trainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
NotificationService.send_assignment_evaluated_notification(
|
||||||
|
recipient=ac.assignment_user,
|
||||||
|
sender=self.trainer,
|
||||||
|
assignment_completion=ac,
|
||||||
|
target_url="/some/url",
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(1, len(Notification.objects.all()))
|
||||||
|
|
||||||
|
notification = Notification.objects.get(recipient__username=self.student.email)
|
||||||
|
self.assertEqual("USER_INTERACTION", notification.notification_category)
|
||||||
|
self.assertIn("hat die geleitete Fallarbeit", notification.verb)
|
||||||
|
|
||||||
|
@freeze_time("2023-01-01")
|
||||||
|
def test_notification_title_praxis_assignment_for_student(self):
|
||||||
|
# GIVEN
|
||||||
|
casework = create_assignment(
|
||||||
|
assignment_type=AssignmentType.PRAXIS_ASSIGNMENT,
|
||||||
|
)
|
||||||
|
assignment = Assignment.objects.get(
|
||||||
|
slug="test-lehrgang-assignment-mein-kundenstamm"
|
||||||
|
)
|
||||||
|
|
||||||
|
ac = AssignmentCompletion.objects.create(
|
||||||
|
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
||||||
|
assignment_user=self.student,
|
||||||
|
assignment=assignment,
|
||||||
|
evaluation_passed=True,
|
||||||
|
course_session=casework.course_session,
|
||||||
|
completion_data={},
|
||||||
|
evaluation_max_points=10,
|
||||||
|
evaluation_points=10,
|
||||||
|
evaluation_user=self.trainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# WHEN
|
||||||
|
NotificationService.send_assignment_evaluated_notification(
|
||||||
|
recipient=ac.assignment_user,
|
||||||
|
sender=self.trainer,
|
||||||
|
assignment_completion=ac,
|
||||||
|
target_url="/some/url",
|
||||||
|
)
|
||||||
|
|
||||||
|
# THEN
|
||||||
|
self.assertEqual(1, len(Notification.objects.all()))
|
||||||
|
|
||||||
|
notification = Notification.objects.get(recipient__username=self.student.email)
|
||||||
|
self.assertEqual("USER_INTERACTION", notification.notification_category)
|
||||||
|
self.assertIn("hat den Praxisauftrag", notification.verb)
|
||||||
Loading…
Reference in New Issue