Merged in feature/VBV-321-KN-frontend-trainer (pull request #72)
Feature/VBV-321 KN frontend trainer Approved-by: Elia Bieri
This commit is contained in:
commit
ef37ac0db9
|
|
@ -111,12 +111,10 @@ pipelines:
|
|||
script:
|
||||
- echo "Release ready!"
|
||||
- step:
|
||||
<<: *deploy
|
||||
name: deploy prod
|
||||
deployment: prod
|
||||
trigger: manual
|
||||
script:
|
||||
- source ./env/bitbucket/prepare_for_deployment.sh
|
||||
- ./caprover_deploy.sh myvbv
|
||||
custom:
|
||||
deploy-stage:
|
||||
- step:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"@sentry/vue": "^7.20.0",
|
||||
"@urql/vue": "^1.0.2",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"@vueuse/router": "^10.1.2",
|
||||
"cypress": "^12.9.0",
|
||||
"d3": "^7.6.1",
|
||||
"dayjs": "^1.11.7",
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
"vue": "^3.2.38",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-i18n-extract": "^2.0.7",
|
||||
"vue-router": "^4.1.5"
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.13.12",
|
||||
|
|
@ -7489,6 +7490,32 @@
|
|||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/router": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/router/-/router-10.1.2.tgz",
|
||||
"integrity": "sha512-99KhTBZliU5gRPHPhi7UO97vArgWIYLomLeCPYJQvbg1gYYa3BVX/uFDcdOaVYhdz5rWDeY/DaeW5CeNYR7zzQ==",
|
||||
"dependencies": {
|
||||
"@vueuse/shared": "10.1.2",
|
||||
"vue-demi": ">=0.14.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue-router": ">=4.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/router/node_modules/@vueuse/shared": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.1.2.tgz",
|
||||
"integrity": "sha512-1uoUTPBlgyscK9v6ScGeVYDDzlPSFXBlxuK7SfrDGyUTBiznb3mNceqhwvZHjtDRELZEN79V5uWPTF1VDV8svA==",
|
||||
"dependencies": {
|
||||
"vue-demi": ">=0.14.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||
|
|
@ -18979,9 +19006,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-demi": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.0.tgz",
|
||||
"integrity": "sha512-gt58r2ogsNQeVoQ3EhoUAvUsH9xviydl0dWJj7dabBC/2L4uBId7ujtCwDRD0JhkGsV1i0CtfLAeyYKBht9oWg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
|
|
@ -25294,6 +25321,25 @@
|
|||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
||||
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ=="
|
||||
},
|
||||
"@vueuse/router": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/router/-/router-10.1.2.tgz",
|
||||
"integrity": "sha512-99KhTBZliU5gRPHPhi7UO97vArgWIYLomLeCPYJQvbg1gYYa3BVX/uFDcdOaVYhdz5rWDeY/DaeW5CeNYR7zzQ==",
|
||||
"requires": {
|
||||
"@vueuse/shared": "10.1.2",
|
||||
"vue-demi": ">=0.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/shared": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.1.2.tgz",
|
||||
"integrity": "sha512-1uoUTPBlgyscK9v6ScGeVYDDzlPSFXBlxuK7SfrDGyUTBiznb3mNceqhwvZHjtDRELZEN79V5uWPTF1VDV8svA==",
|
||||
"requires": {
|
||||
"vue-demi": ">=0.14.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vueuse/shared": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||
|
|
@ -33879,9 +33925,9 @@
|
|||
}
|
||||
},
|
||||
"vue-demi": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A=="
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.0.tgz",
|
||||
"integrity": "sha512-gt58r2ogsNQeVoQ3EhoUAvUsH9xviydl0dWJj7dabBC/2L4uBId7ujtCwDRD0JhkGsV1i0CtfLAeyYKBht9oWg=="
|
||||
},
|
||||
"vue-docgen-api": {
|
||||
"version": "4.64.1",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"@sentry/vue": "^7.20.0",
|
||||
"@urql/vue": "^1.0.2",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"@vueuse/router": "^10.1.2",
|
||||
"cypress": "^12.9.0",
|
||||
"d3": "^7.6.1",
|
||||
"dayjs": "^1.11.7",
|
||||
|
|
@ -36,7 +37,7 @@
|
|||
"vue": "^3.2.38",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-i18n-extract": "^2.0.7",
|
||||
"vue-router": "^4.1.5"
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.13.12",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<div class="mb-4 bg-white px-6 py-5">
|
||||
<h2 class="heading-3 mb-4 bg-feedback bg-60 bg-no-repeat pl-[68px] leading-[60px]">
|
||||
{{ $t("general.feedback", 2) }}
|
||||
</h2>
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
<it-icon-feedback-large class="h-16 w-16"></it-icon-feedback-large>
|
||||
<div>{{ $t("general.feedback", 2) }}</div>
|
||||
</h3>
|
||||
<ol v-if="feedbackSummary.length > 0">
|
||||
<ItRow v-for="feedbacks in feedbackSummary" :key="feedbacks.circle_id">
|
||||
<template #firstRow>
|
||||
|
|
|
|||
|
|
@ -55,12 +55,12 @@ const input = (e: Event) => {
|
|||
@input="input"
|
||||
/>
|
||||
<div class="ml-4 flex-col">
|
||||
<div v-if="checkboxItem.label">
|
||||
{{ checkboxItem.label }}
|
||||
</div>
|
||||
<div v-if="checkboxItem.subtitle" class="text-gray-900">
|
||||
{{ checkboxItem.subtitle }}
|
||||
</div>
|
||||
<div v-if="checkboxItem.label" v-html="checkboxItem.label"></div>
|
||||
<div
|
||||
v-if="checkboxItem.subtitle"
|
||||
class="text-gray-900"
|
||||
v-html="checkboxItem.subtitle"
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,15 @@ export interface Props {
|
|||
currentStep: number;
|
||||
startBadgeText?: string;
|
||||
endBadgeText?: string;
|
||||
baseUrl?: string;
|
||||
queryParam?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
startBadgeText: undefined,
|
||||
endBadgeText: undefined,
|
||||
baseUrl: undefined,
|
||||
queryParam: "page",
|
||||
});
|
||||
|
||||
const hasStartBadge = computed(() => typeof props.startBadgeText !== "undefined");
|
||||
|
|
@ -46,6 +50,13 @@ const endBadgeClasses = computed(() => {
|
|||
}
|
||||
return "border";
|
||||
});
|
||||
|
||||
function calcStepIndex(step: number) {
|
||||
if (props.startBadgeText) {
|
||||
return step + 1;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -54,8 +65,12 @@ const endBadgeClasses = computed(() => {
|
|||
v-if="props.startBadgeText"
|
||||
class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-3xl px-3 text-sm"
|
||||
:class="startBadgeClasses"
|
||||
data-cy="nav-progress-step-start"
|
||||
>
|
||||
{{ props.startBadgeText }}
|
||||
<router-link v-if="props.baseUrl" :to="`${props.baseUrl}?${props.queryParam}=0`">
|
||||
{{ props.startBadgeText }}
|
||||
</router-link>
|
||||
<span v-else>{{ props.startBadgeText }}</span>
|
||||
</div>
|
||||
<div v-for="(_, step) in numNumberSteps" :key="step" class="flex flex-row">
|
||||
<hr
|
||||
|
|
@ -65,8 +80,15 @@ const endBadgeClasses = computed(() => {
|
|||
<div
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded-full px-3 py-1 text-sm"
|
||||
:class="getPillClasses(step)"
|
||||
:data-cy="`nav-progress-step-${calcStepIndex(step)}`"
|
||||
>
|
||||
{{ step + 1 }}
|
||||
<router-link
|
||||
v-if="props.baseUrl"
|
||||
:to="`${props.baseUrl}?${props.queryParam}=${calcStepIndex(step)}`"
|
||||
>
|
||||
{{ step + 1 }}
|
||||
</router-link>
|
||||
<span v-else>{{ step + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr v-if="hasEndBadge" class="w-8 self-center border border-gray-400" />
|
||||
|
|
@ -74,8 +96,15 @@ const endBadgeClasses = computed(() => {
|
|||
v-if="endBadgeText"
|
||||
class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-3xl px-3 text-sm"
|
||||
:class="endBadgeClasses"
|
||||
data-cy="nav-progress-end"
|
||||
>
|
||||
{{ props.endBadgeText }}
|
||||
<router-link
|
||||
v-if="props.baseUrl"
|
||||
:to="`${props.baseUrl}?${props.queryParam}=${steps - 1}`"
|
||||
>
|
||||
{{ props.endBadgeText }}
|
||||
</router-link>
|
||||
<span v-else>{{ props.endBadgeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
export type StatusCountKey = "fail" | "success" | "unknown";
|
||||
export type StatusCount = Record<StatusCountKey, number>;
|
||||
|
||||
const props = defineProps<{
|
||||
statusCount?: {
|
||||
fail: number;
|
||||
success: number;
|
||||
unknown: number;
|
||||
};
|
||||
statusCount?: StatusCount;
|
||||
}>();
|
||||
|
||||
const total = computed(() => {
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2 v-if="label" class="heading-1 mb-8 block">{{ label }}</h2>
|
||||
<div v-if="label" class="mb-2 block">{{ label }}</div>
|
||||
<textarea
|
||||
:value="modelValue"
|
||||
class="h-40 w-full border-gray-500"
|
||||
:data-cy="`it-textarea-${cyKey}`"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
export interface Props {
|
||||
modelValue: string;
|
||||
label: string | undefined;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
cyKey?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
label: undefined,
|
||||
cyKey: "",
|
||||
placeholder: "",
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import * as log from "loglevel";
|
||||
|
|
@ -15,21 +16,25 @@ const props = defineProps<{
|
|||
const cockpitStore = useCockpitStore();
|
||||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("CockpitParentPage mounted", props.courseSlug);
|
||||
|
||||
try {
|
||||
await cockpitStore.loadCourseSessionUsers(props.courseSlug);
|
||||
cockpitStore.courseSessionUsers?.forEach((csu) => {
|
||||
competenceStore.loadCompetenceProfilePage(
|
||||
props.courseSlug + "-competence",
|
||||
csu.user_id
|
||||
);
|
||||
const currentCourseSession = courseSessionsStore.currentCourseSession;
|
||||
if (currentCourseSession?.id) {
|
||||
await cockpitStore.loadCourseSessionUsers(currentCourseSession.id);
|
||||
cockpitStore.courseSessionUsers?.forEach((csu) => {
|
||||
competenceStore.loadCompetenceProfilePage(
|
||||
props.courseSlug + "-competence",
|
||||
csu.user_id
|
||||
);
|
||||
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
|
||||
});
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", useUserStore().id);
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
|
||||
});
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", useUserStore().id);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ const props = defineProps<{
|
|||
|
||||
log.debug("FeedbackPage created", props.circleId);
|
||||
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const orderedQuestions = [
|
||||
|
|
@ -149,7 +149,7 @@ const feedbackData = reactive<FeedbackData>({ amount: 0, questions: {} });
|
|||
onMounted(async () => {
|
||||
log.debug("FeedbackPage mounted");
|
||||
const data = await itGet(
|
||||
`/api/core/feedback/${courseSessionStore.currentCourseSession?.course.id}/${props.circleId}`
|
||||
`/api/core/feedback/${courseSessionsStore.currentCourseSession?.course.id}/${props.circleId}`
|
||||
);
|
||||
Object.assign(feedbackData, data);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
<script setup lang="ts">
|
||||
import EvaluationContainer from "@/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue";
|
||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type {
|
||||
Assignment,
|
||||
CourseSessionAssignmentDetails,
|
||||
CourseSessionUser,
|
||||
} from "@/types";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
assignmentId: string;
|
||||
userId: string;
|
||||
}>();
|
||||
|
||||
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId);
|
||||
|
||||
interface StateInterface {
|
||||
assignment: Assignment | undefined;
|
||||
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
|
||||
assignmentUser: CourseSessionUser | undefined;
|
||||
}
|
||||
|
||||
const state: StateInterface = reactive({
|
||||
assignment: undefined,
|
||||
courseSessionAssignmentDetails: undefined,
|
||||
assignmentUser: undefined,
|
||||
});
|
||||
|
||||
const assignmentStore = useAssignmentStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("AssignmentView mounted", props.assignmentId, props.userId);
|
||||
|
||||
if (courseSessionsStore.currentCourseSession) {
|
||||
state.assignmentUser = courseSessionsStore.currentCourseSession.users.find(
|
||||
(user) => user.user_id === Number(props.userId)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
state.assignment = await assignmentStore.loadAssignment(Number(props.assignmentId));
|
||||
await assignmentStore.loadAssignmentCompletion(
|
||||
Number(props.assignmentId),
|
||||
courseSessionsStore.currentCourseSession!.id,
|
||||
props.userId
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
router.push({
|
||||
path: `/course/${props.courseSlug}/cockpit/assignment`,
|
||||
});
|
||||
}
|
||||
|
||||
const assignmentCompletion = computed(() => assignmentStore.assignmentCompletion);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute bottom-0 top-0 z-10 w-full bg-white">
|
||||
<div
|
||||
v-if="state.assignment && state.assignmentUser && assignmentCompletion"
|
||||
class="relative"
|
||||
>
|
||||
<header
|
||||
class="relative flex h-12 w-full items-center justify-between border-b border-b-gray-400 bg-white px-4 lg:h-16 lg:px-8"
|
||||
>
|
||||
<div class="flex items-center text-gray-900">
|
||||
<it-icon-assignment class="h-6 w-6"></it-icon-assignment>
|
||||
<div class="ml-2">Geleitete Fallarbeit: {{ state.assignment.title }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-2 top-2 h-8 w-8 cursor-pointer lg:right-4 lg:top-4"
|
||||
data-cy="close-learning-content"
|
||||
@click="close()"
|
||||
>
|
||||
<it-icon-close></it-icon-close>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="h-content flex">
|
||||
<div class="h-full w-1/2 overflow-y-auto bg-white">
|
||||
<!-- Left part content goes here -->
|
||||
<div class="p-10">
|
||||
<h3>Ergebnisse</h3>
|
||||
<div class="my-6 flex items-center">
|
||||
<img
|
||||
:src="state.assignmentUser?.avatar_url"
|
||||
class="mr-4 h-11 w-11 rounded-full"
|
||||
/>
|
||||
<div class="font-bold">
|
||||
{{ state.assignmentUser?.first_name }}
|
||||
{{ state.assignmentUser?.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
<AssignmentSubmissionResponses
|
||||
:assignment="state.assignment"
|
||||
:assignment-completion-data="assignmentCompletion.completion_data"
|
||||
:allow-edit="false"
|
||||
></AssignmentSubmissionResponses>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 overflow-y-auto bg-gray-200">
|
||||
<EvaluationContainer
|
||||
:assignment-completion="assignmentCompletion"
|
||||
:assignment-user="state.assignmentUser"
|
||||
:assignment="state.assignment"
|
||||
@close="close()"
|
||||
></EvaluationContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$nav-height: 64px;
|
||||
|
||||
.h-content {
|
||||
height: calc(100vh - $nav-height);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
<script setup lang="ts">
|
||||
import EvaluationIntro from "@/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue";
|
||||
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
|
||||
import EvaluationTask from "@/pages/cockpit/assignmentEvaluationPage/EvaluationTask.vue";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
AssignmentEvaluationTask,
|
||||
CourseSessionUser,
|
||||
} from "@/types";
|
||||
import dayjs from "dayjs";
|
||||
import { findIndex } from "lodash";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentUser: CourseSessionUser;
|
||||
assignmentCompletion: AssignmentCompletion;
|
||||
assignment: Assignment;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
log.debug("UserEvaluation setup");
|
||||
|
||||
interface StateInterface {
|
||||
// 0 = introduction, 1 - n = tasks, n+1 = submission
|
||||
pageIndex: number;
|
||||
}
|
||||
|
||||
const state: StateInterface = reactive({
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
||||
const numTasks = computed(() => props.assignment.evaluation_tasks?.length ?? 0);
|
||||
const evaluationSubmitted = computed(
|
||||
() => props.assignmentCompletion.completion_status === "evaluation_submitted"
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (evaluationSubmitted.value) {
|
||||
state.pageIndex = props.assignment.evaluation_tasks?.length + 1 ?? 0;
|
||||
} else {
|
||||
state.pageIndex = 0;
|
||||
}
|
||||
});
|
||||
|
||||
function previousPage() {
|
||||
log.debug("previousTask");
|
||||
state.pageIndex = Math.max(0, state.pageIndex - 1);
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
log.debug("nextTask");
|
||||
state.pageIndex = Math.min(numTasks.value + 1, state.pageIndex + 1);
|
||||
}
|
||||
|
||||
function editTask(task: AssignmentEvaluationTask) {
|
||||
log.debug("editTask", task);
|
||||
const taskIndex =
|
||||
findIndex(props.assignment.evaluation_tasks, {
|
||||
id: task.id,
|
||||
}) ?? 0;
|
||||
state.pageIndex = taskIndex + 1;
|
||||
}
|
||||
|
||||
const assignmentDetail = computed(() =>
|
||||
assignmentStore.findAssignmentDetail(props.assignment.id)
|
||||
);
|
||||
|
||||
const dueDate = computed(() =>
|
||||
dayjs(assignmentDetail.value?.evaluationDeadlineDateTimeUtc)
|
||||
);
|
||||
|
||||
const inEvaluationTask = computed(
|
||||
() => state.pageIndex >= 1 && state.pageIndex <= numTasks.value
|
||||
);
|
||||
const taskIndex = computed(() => state.pageIndex - 1);
|
||||
const task = computed(() => props.assignment.evaluation_tasks[taskIndex.value]);
|
||||
|
||||
const taskExpertDataText = computed(() => {
|
||||
let result = "";
|
||||
if (inEvaluationTask.value) {
|
||||
result =
|
||||
assignmentStore.assignmentCompletion?.completion_data?.[task.value.id]
|
||||
?.expert_data?.text ?? "";
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
function nextButtonEnabled() {
|
||||
if (inEvaluationTask.value) {
|
||||
return taskExpertDataText.value ?? false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function finishButtonEnabled() {
|
||||
return props.assignmentCompletion.completion_status === "evaluation_submitted";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-full flex-col">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<section class="p-10">
|
||||
<EvaluationIntro
|
||||
v-if="state.pageIndex === 0"
|
||||
:assignment-user="props.assignmentUser"
|
||||
:assignment="props.assignment"
|
||||
:assignment-completion="props.assignmentCompletion"
|
||||
:due-date="dueDate"
|
||||
@start-evaluation="nextPage"
|
||||
></EvaluationIntro>
|
||||
<EvaluationTask
|
||||
v-else-if="inEvaluationTask"
|
||||
:assignment-user="props.assignmentUser"
|
||||
:assignment="props.assignment"
|
||||
:task-index="state.pageIndex - 1"
|
||||
:allow-edit="
|
||||
props.assignmentCompletion.completion_status !== 'evaluation_submitted'
|
||||
"
|
||||
/>
|
||||
<EvaluationSummary
|
||||
v-else
|
||||
:assignment-user="props.assignmentUser"
|
||||
:assignment="props.assignment"
|
||||
:assignment-completion="props.assignmentCompletion"
|
||||
:due-date="dueDate"
|
||||
@edit-task="editTask"
|
||||
></EvaluationSummary>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<nav v-if="state.pageIndex > 0" class="sticky bottom-0 border-t bg-gray-200 p-6">
|
||||
<div class="relative flex flex-row place-content-end">
|
||||
<button
|
||||
v-if="true"
|
||||
class="btn-secondary mr-2 flex items-center"
|
||||
data-cy="previous-step"
|
||||
@click="previousPage()"
|
||||
>
|
||||
<it-icon-arrow-left class="mr-2 h-6 w-6"></it-icon-arrow-left>
|
||||
{{ $t("general.backCapitalized") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="state.pageIndex <= numTasks"
|
||||
:disabled="!nextButtonEnabled()"
|
||||
class="btn-secondary z-10 flex items-center"
|
||||
data-cy="next-step"
|
||||
@click="nextPage()"
|
||||
>
|
||||
{{ $t("general.next") }}
|
||||
<it-icon-arrow-right class="ml-2 h-6 w-6"></it-icon-arrow-right>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="state.pageIndex > numTasks"
|
||||
:disabled="!finishButtonEnabled()"
|
||||
class="btn-secondary z-10"
|
||||
data-cy="next-step"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
Bewertung abschliessen
|
||||
<it-icon-check class="ml-2 h-6 w-6"></it-icon-check>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<script setup lang="ts">
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { Assignment, AssignmentCompletion, CourseSessionUser } from "@/types";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import * as log from "loglevel";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentUser: CourseSessionUser;
|
||||
assignment: Assignment;
|
||||
assignmentCompletion: AssignmentCompletion;
|
||||
dueDate?: Dayjs;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["startEvaluation"]);
|
||||
|
||||
log.debug("EvaluationIntro setup");
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
||||
async function startEvaluation() {
|
||||
log.debug("startEvaluation");
|
||||
if (props.assignmentCompletion.completion_status !== "evaluation_submitted") {
|
||||
await assignmentStore.evaluateAssignmentCompletion({
|
||||
assignment_user_id: Number(props.assignmentUser.user_id),
|
||||
assignment_id: props.assignment.id,
|
||||
course_session_id: courseSessionsStore.currentCourseSession!.id,
|
||||
completion_data: {},
|
||||
completion_status: "evaluation_in_progress",
|
||||
});
|
||||
emit("startEvaluation");
|
||||
} else {
|
||||
emit("startEvaluation");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
{{ props.assignmentUser.first_name }} {{ props.assignmentUser.last_name }} hat die
|
||||
Ergebnisse am
|
||||
{{ dayjs(props.assignmentCompletion.submitted_at).format("DD.MM.YYYY") }} um
|
||||
{{ dayjs(props.assignmentCompletion.submitted_at).format("HH.mm") }} Uhr
|
||||
abgegeben.
|
||||
</div>
|
||||
|
||||
<h3>Bewertung</h3>
|
||||
|
||||
<p v-if="props.dueDate" class="my-4">
|
||||
Du musst die Bewertung bis am {{ props.dueDate.format("DD.MM.YYYY") }} um
|
||||
{{ props.dueDate.format("HH.mm") }} Uhr abschliessen und freigeben.
|
||||
</p>
|
||||
|
||||
<p class="my-4">
|
||||
Die Gesamtpunktzahl und die daraus resultierende Note wird auf Grund des
|
||||
hinterlegeten Beurteilungsinstrument berechnet.
|
||||
</p>
|
||||
|
||||
<p class="my-4">
|
||||
<a :href="props.assignment.evaluation_document_url" class="link" target="_blank">
|
||||
Beurteilungsinstrument anzeigen
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<button class="btn-primary" @click="startEvaluation()">
|
||||
<span
|
||||
v-if="
|
||||
props.assignmentCompletion.completion_status === 'evaluation_in_progress'
|
||||
"
|
||||
>
|
||||
Bewertung fortsetzen
|
||||
</span>
|
||||
<span
|
||||
v-if="props.assignmentCompletion.completion_status === 'evaluation_submitted'"
|
||||
>
|
||||
Bewertung ansehen
|
||||
</span>
|
||||
<span v-else>Bewertung starten</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
<script setup lang="ts">
|
||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||
import {
|
||||
maxAssignmentPoints,
|
||||
pointsToGrade,
|
||||
userAssignmentPoints,
|
||||
} from "@/services/assignmentService";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
AssignmentEvaluationTask,
|
||||
CourseSessionUser,
|
||||
} from "@/types";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import * as log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentUser: CourseSessionUser;
|
||||
assignment: Assignment;
|
||||
assignmentCompletion: AssignmentCompletion;
|
||||
showEvaluationUser?: boolean;
|
||||
dueDate?: Dayjs;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["editTask"]);
|
||||
|
||||
const state = reactive({
|
||||
showSuccessInfo: false,
|
||||
});
|
||||
|
||||
log.debug("EvaluationSummary setup");
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
||||
async function submitEvaluation() {
|
||||
log.debug("submitEvaluation");
|
||||
await assignmentStore.evaluateAssignmentCompletion({
|
||||
assignment_user_id: Number(props.assignmentUser.user_id),
|
||||
assignment_id: props.assignment.id,
|
||||
course_session_id: courseSessionsStore.currentCourseSession!.id,
|
||||
completion_data: {},
|
||||
completion_status: "evaluation_submitted",
|
||||
evaluation_grade: grade.value ?? undefined,
|
||||
evaluation_points: userPoints.value,
|
||||
});
|
||||
state.showSuccessInfo = true;
|
||||
}
|
||||
|
||||
function subTaskByPoints(task: AssignmentEvaluationTask, points = 0) {
|
||||
return task.value.sub_tasks.find((subTask) => subTask.points === points);
|
||||
}
|
||||
|
||||
function evaluationForTask(task: AssignmentEvaluationTask) {
|
||||
const expertData = props.assignmentCompletion.completion_data[task.id]?.expert_data;
|
||||
if (!expertData) {
|
||||
return {
|
||||
points: 0,
|
||||
text: "",
|
||||
};
|
||||
}
|
||||
|
||||
return expertData;
|
||||
}
|
||||
|
||||
const maxPoints = computed(() => maxAssignmentPoints(props.assignment));
|
||||
const userPoints = computed(() =>
|
||||
userAssignmentPoints(props.assignment, props.assignmentCompletion)
|
||||
);
|
||||
const grade = computed(() => {
|
||||
if (props.assignmentCompletion.completion_status === "evaluation_submitted") {
|
||||
return props.assignmentCompletion.evaluation_grade;
|
||||
}
|
||||
return pointsToGrade(userPoints.value, maxPoints.value);
|
||||
});
|
||||
|
||||
const evaluationUser = computed(() => {
|
||||
if (props.assignmentCompletion.evaluation_user) {
|
||||
return (courseSessionsStore.currentCourseSession?.users ?? []).find(
|
||||
(user) => user.user_id === Number(props.assignmentCompletion.evaluation_user)
|
||||
) as CourseSessionUser;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3 v-if="evaluationUser && props.showEvaluationUser" class="mb-6">
|
||||
Bewertung von {{ evaluationUser.first_name }} {{ evaluationUser.last_name }}
|
||||
</h3>
|
||||
<h3 v-else class="mb-6">Bewertung Freigabe</h3>
|
||||
|
||||
<section class="mb-6 border p-6">
|
||||
<div class="text-lg font-bold">Note: {{ grade }}</div>
|
||||
<div class="text-gray-900">
|
||||
Gesamtpunktezahl {{ userPoints }} / {{ maxPoints }}
|
||||
</div>
|
||||
|
||||
<p class="my-4">
|
||||
Die Gesamtpunktzahl und die daraus resultierende Note wird auf Grund des
|
||||
hinterlegeten Beurteilungsinstrument berechnet.
|
||||
</p>
|
||||
|
||||
<p class="my-4">
|
||||
<a
|
||||
:href="props.assignment.evaluation_document_url"
|
||||
class="link"
|
||||
target="_blank"
|
||||
>
|
||||
Beurteilungsinstrument anzeigen
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="props.assignmentCompletion.completion_status === 'evaluation_submitted'"
|
||||
>
|
||||
Freigabetermin:
|
||||
{{
|
||||
dayjs(props.assignmentCompletion.evaluation_submitted_at).format("DD.MM.YYYY")
|
||||
}}
|
||||
um
|
||||
{{ dayjs(props.assignmentCompletion.evaluation_submitted_at).format("HH.mm") }}
|
||||
Uhr
|
||||
</div>
|
||||
<div v-else>
|
||||
<button class="btn-primary" @click="submitEvaluation()">
|
||||
Bewertung freigeben
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="state.showSuccessInfo" class="mt-4">
|
||||
<ItSuccessAlert
|
||||
:text="`Deine Bewertung für ${props.assignmentUser.first_name} ${props.assignmentUser.last_name} wurde freigegeben.`"
|
||||
></ItSuccessAlert>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div v-for="(task, index) in props.assignment.evaluation_tasks" :key="task.id">
|
||||
<article class="border-t py-4">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="mb-4">
|
||||
Bewertungskriterium {{ index + 1 }}: {{ task.value.title }}
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
props.assignmentCompletion.completion_status !== 'evaluation_submitted'
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="link pl-2text-sm whitespace-nowrap"
|
||||
@click="emit('editTask', task)"
|
||||
>
|
||||
{{ $t("assignment.edit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="default-wagtail-rich-text mb-2 font-bold"
|
||||
v-html="task.value.description"
|
||||
></div>
|
||||
|
||||
<section class="mb-4">
|
||||
<div
|
||||
v-html="subTaskByPoints(task, evaluationForTask(task).points)?.title"
|
||||
></div>
|
||||
<p
|
||||
class="default-wagtail-rich-text"
|
||||
v-html="
|
||||
subTaskByPoints(task, evaluationForTask(task).points)?.description
|
||||
"
|
||||
></p>
|
||||
<div class="text-sm text-gray-800">
|
||||
{{ evaluationForTask(task).points }} Punkte
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<span class="font-bold">Begründung:</span>
|
||||
{{ evaluationForTask(task).text }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<script setup lang="ts">
|
||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletionData,
|
||||
CourseSessionUser,
|
||||
ExpertData,
|
||||
} from "@/types";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import * as log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentUser: CourseSessionUser;
|
||||
assignment: Assignment;
|
||||
taskIndex: number;
|
||||
allowEdit: boolean;
|
||||
}>();
|
||||
|
||||
log.debug("EvaluationTask setup", props.taskIndex);
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
||||
const task = computed(() => props.assignment.evaluation_tasks[props.taskIndex]);
|
||||
|
||||
const expertData = computed(() => {
|
||||
const data = (assignmentStore.assignmentCompletion?.completion_data?.[task.value.id]
|
||||
?.expert_data ?? {
|
||||
points: 0,
|
||||
text: "",
|
||||
}) as ExpertData;
|
||||
return data;
|
||||
});
|
||||
|
||||
function changePoints(points: number) {
|
||||
log.debug("changePoints", points);
|
||||
evaluateAssignmentCompletion({
|
||||
[task.value.id]: {
|
||||
expert_data: { points, text: expertData.value.text },
|
||||
},
|
||||
});
|
||||
}
|
||||
function onUpdateText(value: string) {
|
||||
// log.debug("onUpdateText", value);
|
||||
evaluateAssignmentCompletionDebounced({
|
||||
[task.value.id]: {
|
||||
expert_data: {
|
||||
text: value,
|
||||
points: expertData.value.points,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function evaluateAssignmentCompletion(completionData: AssignmentCompletionData) {
|
||||
log.debug("evaluateAssignmentCompletion", completionData);
|
||||
|
||||
return assignmentStore.evaluateAssignmentCompletion({
|
||||
assignment_user_id: Number(props.assignmentUser.user_id),
|
||||
assignment_id: props.assignment.id,
|
||||
course_session_id: courseSessionsStore.currentCourseSession!.id,
|
||||
completion_data: completionData,
|
||||
completion_status: "evaluation_in_progress",
|
||||
});
|
||||
}
|
||||
|
||||
const evaluateAssignmentCompletionDebounced = useDebounceFn(
|
||||
evaluateAssignmentCompletion,
|
||||
300
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-bold mb-4 text-sm">
|
||||
Beurteilungskriterium {{ taskIndex + 1 }} /
|
||||
{{ props.assignment.evaluation_tasks.length }}
|
||||
{{ task.value.title }}
|
||||
</div>
|
||||
|
||||
<h3 class="default-wagtail-rich-text mb-8" v-html="task.value.description"></h3>
|
||||
|
||||
<fieldset>
|
||||
<div>
|
||||
<div
|
||||
v-for="(subTask, index) in task.value.sub_tasks"
|
||||
:key="index"
|
||||
class="mb-4 flex items-center last:mb-0"
|
||||
>
|
||||
<input
|
||||
:id="String(index)"
|
||||
name="coursesessions"
|
||||
type="radio"
|
||||
:value="subTask.points"
|
||||
:checked="expertData.points === subTask.points"
|
||||
:disabled="!props.allowEdit"
|
||||
class="focus:ring-indigo-900 h-4 w-4 border-gray-300 text-blue-900"
|
||||
@change="changePoints(subTask.points)"
|
||||
/>
|
||||
<label :for="String(index)" class="ml-4 block">
|
||||
<div>{{ subTask.title }}</div>
|
||||
<div
|
||||
v-if="subTask.description"
|
||||
class="default-wagtail-rich-text"
|
||||
v-html="subTask.description"
|
||||
></div>
|
||||
<div class="text-sm text-gray-800">{{ subTask.points }} Punkte</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<ItTextarea
|
||||
class="mt-8"
|
||||
:model-value="expertData.text ?? ''"
|
||||
label="Begründung"
|
||||
:disabled="!props.allowEdit"
|
||||
placeholder="Hier muss zwingend eine Begründung erfasst werden."
|
||||
@update:model-value="onUpdateText($event)"
|
||||
></ItTextarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<script setup lang="ts">
|
||||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||
import type { StatusCount, StatusCountKey } from "@/components/ui/ItProgress.vue";
|
||||
import AssignmentSubmissionProgress from "@/pages/cockpit/assignmentsPage/AssignmentSubmissionProgress.vue";
|
||||
import type { AssignmentLearningContent } from "@/services/assignmentService";
|
||||
import { loadAssignmentCompletionStatusData } from "@/services/assignmentService";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import type { AssignmentCompletionStatus, CourseSession } from "@/types";
|
||||
import dayjs from "dayjs";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
assignment: AssignmentLearningContent;
|
||||
}>();
|
||||
|
||||
log.debug("AssignmentDetails created", props.assignment.assignmentId);
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
||||
const state = reactive({
|
||||
statusByUser: [] as {
|
||||
userStatus: AssignmentCompletionStatus;
|
||||
progressStatus: StatusCountKey;
|
||||
userId: number;
|
||||
grade: number | null;
|
||||
}[],
|
||||
progressStatusCount: {} as StatusCount,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
state.statusByUser = await loadAssignmentCompletionStatusData(
|
||||
props.assignment.assignmentId,
|
||||
props.courseSession.id
|
||||
);
|
||||
});
|
||||
|
||||
function submissionStatusForUser(userId: number) {
|
||||
return state.statusByUser.find((s) => s.userId === userId);
|
||||
}
|
||||
|
||||
const assignmentDetail = computed(() =>
|
||||
assignmentStore.findAssignmentDetail(props.assignment.assignmentId)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="state.statusByUser.length">
|
||||
<div class="text-large font-bold">
|
||||
{{ assignment.title }}
|
||||
</div>
|
||||
<div v-if="assignmentDetail">
|
||||
<span>
|
||||
Abgabetermin:
|
||||
{{ dayjs(assignmentDetail.submissionDeadlineDateTimeUtc).format("DD.MM.YYYY") }}
|
||||
</span>
|
||||
-
|
||||
<span>
|
||||
Freigabetermin:
|
||||
{{ dayjs(assignmentDetail.evaluationDeadlineDateTimeUtc).format("DD.MM.YYYY") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<router-link :to="props.assignment.frontend_url" class="link">
|
||||
Im Circle anzeigen
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<AssignmentSubmissionProgress
|
||||
:course-session="courseSession"
|
||||
:assignment="assignment"
|
||||
:show-title="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="cockpitStore.courseSessionUsers?.length" class="mt-6">
|
||||
<ul>
|
||||
<ItPersonRow
|
||||
v-for="csu in cockpitStore.courseSessionUsers"
|
||||
:key="csu.user_id + csu.session_title"
|
||||
:name="`${csu.first_name} ${csu.last_name}`"
|
||||
:avatar-url="csu.avatar_url"
|
||||
>
|
||||
<template #center>
|
||||
<section class="flex w-full justify-between px-8">
|
||||
<div
|
||||
v-if="
|
||||
['evaluation_submitted'].includes(
|
||||
submissionStatusForUser(csu.user_id)?.userStatus ?? ''
|
||||
)
|
||||
"
|
||||
class="flex items-center"
|
||||
>
|
||||
<div
|
||||
class="relative flex h-7 w-7 items-center justify-center rounded-full border border-green-500 bg-green-500"
|
||||
>
|
||||
<it-icon-check class="h-4/5 w-4/5"></it-icon-check>
|
||||
</div>
|
||||
<div class="ml-2">Bewertung freigegeben</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
['evaluation_in_progress', 'submitted'].includes(
|
||||
submissionStatusForUser(csu.user_id)?.userStatus ?? ''
|
||||
)
|
||||
"
|
||||
class="flex items-center"
|
||||
>
|
||||
<div
|
||||
class="relative flex h-7 w-7 items-center justify-center rounded-full border border-green-500"
|
||||
>
|
||||
<it-icon-check class="h-6 w-6"></it-icon-check>
|
||||
</div>
|
||||
<div class="ml-2">Ergebnisse abgegeben</div>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
|
||||
<div v-if="submissionStatusForUser(csu.user_id)?.grade">
|
||||
Note: {{ submissionStatusForUser(csu.user_id)?.grade }}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<template #link>
|
||||
<router-link
|
||||
v-if="submissionStatusForUser(csu.user_id)?.progressStatus === 'success'"
|
||||
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${assignment.assignmentId}/${csu.user_id}`"
|
||||
class="w-full text-right underline"
|
||||
>
|
||||
Ergebnisse anzeigen
|
||||
</router-link>
|
||||
</template>
|
||||
</ItPersonRow>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<script setup lang="ts">
|
||||
import type { StatusCount, StatusCountKey } from "@/components/ui/ItProgress.vue";
|
||||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import type { AssignmentLearningContent } from "@/services/assignmentService";
|
||||
import { loadAssignmentCompletionStatusData } from "@/services/assignmentService";
|
||||
import type { AssignmentCompletionStatus, CourseSession } from "@/types";
|
||||
import { countBy } from "lodash";
|
||||
import log from "loglevel";
|
||||
import { onMounted, reactive } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
assignment: AssignmentLearningContent;
|
||||
showTitle: boolean;
|
||||
}>();
|
||||
|
||||
log.debug("AssignmentSubmissionProgress created", props.assignment.assignmentId);
|
||||
|
||||
const state = reactive({
|
||||
statusByUser: [] as {
|
||||
userStatus: AssignmentCompletionStatus;
|
||||
progressStatus: StatusCountKey;
|
||||
userId: number;
|
||||
}[],
|
||||
progressStatusCount: {} as StatusCount,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
state.statusByUser = await loadAssignmentCompletionStatusData(
|
||||
props.assignment.assignmentId,
|
||||
props.courseSession.id
|
||||
);
|
||||
|
||||
state.progressStatusCount = countBy(
|
||||
state.statusByUser,
|
||||
"progressStatus"
|
||||
) as StatusCount;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="state.statusByUser.length">
|
||||
<div v-if="showTitle">
|
||||
{{ props.assignment.title }}
|
||||
</div>
|
||||
<div><ItProgress :status-count="state.progressStatusCount" /></div>
|
||||
<div>
|
||||
{{ state.progressStatusCount.success || 0 }} von
|
||||
{{
|
||||
(state.progressStatusCount.success || 0) +
|
||||
(state.progressStatusCount.unknown || 0)
|
||||
}}
|
||||
Lernenden haben ihre Ergebnisse eingereicht.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<script setup lang="ts">
|
||||
import AssignmentDetails from "@/pages/cockpit/assignmentsPage/AssignmentDetails.vue";
|
||||
import { calcAssignmentLearningContents } from "@/services/assignmentService";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
}>();
|
||||
|
||||
log.debug("AssignmentsPage created", props.courseSlug);
|
||||
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("AssignmentsPage mounted");
|
||||
});
|
||||
|
||||
const assignments = computed(() => {
|
||||
// TODO: filter by selected circle
|
||||
if (!courseSessionsStore.currentCourseSession) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return calcAssignmentLearningContents(
|
||||
learningPathStore.learningPathForUser(
|
||||
courseSessionsStore.currentCourseSession.course.slug,
|
||||
userStore.id
|
||||
)
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div v-if="courseSessionsStore.currentCourseSession" 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>
|
||||
<h2 class="heading-2 mb-4 flex items-center gap-2">
|
||||
<it-icon-assignment-large class="h-16 w-16"></it-icon-assignment-large>
|
||||
<div>Geleitete Fallarbeiten</div>
|
||||
</h2>
|
||||
</header>
|
||||
<main>
|
||||
<div v-for="assignment in assignments" :key="assignment.id">
|
||||
<div class="bg-white p-6">
|
||||
<AssignmentDetails
|
||||
:course-session="courseSessionsStore.currentCourseSession"
|
||||
:assignment="assignment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<script setup lang="ts">
|
||||
import AssignmentSubmissionProgress from "@/pages/cockpit/assignmentsPage/AssignmentSubmissionProgress.vue";
|
||||
import { calcAssignmentLearningContents } from "@/services/assignmentService";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
}>();
|
||||
|
||||
log.debug("AssignmentsTile created", props.courseSession.id);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const cockpitStore = useCockpitStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
|
||||
const assignments = computed(() => {
|
||||
// TODO: filter by selected circle
|
||||
return calcAssignmentLearningContents(
|
||||
learningPathStore.learningPathForUser(props.courseSession.course.slug, userStore.id)
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white px-6 py-5">
|
||||
<div v-if="cockpitStore.courseSessionUsers">
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
<it-icon-assignment-large class="h-16 w-16"></it-icon-assignment-large>
|
||||
<div>Geleitete Fallarbeiten</div>
|
||||
</h3>
|
||||
|
||||
<div v-for="assignment in assignments" :key="assignment.id">
|
||||
<AssignmentSubmissionProgress
|
||||
:show-title="true"
|
||||
:course-session="props.courseSession"
|
||||
:assignment="assignment"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<router-link
|
||||
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment`"
|
||||
class="link"
|
||||
>
|
||||
Alle anzeigen
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -5,6 +5,7 @@ import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
|||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
|
||||
import AssignmentsTile from "@/pages/cockpit/cockpitPage/AssignmentsTile.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
|
|
@ -24,7 +25,7 @@ const userStore = useUserStore();
|
|||
const cockpitStore = useCockpitStore();
|
||||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
function userCountStatusForCircle(userId: number, translationKey: string) {
|
||||
const criteria = competenceStore.flatPerformanceCriteria(
|
||||
|
|
@ -92,7 +93,7 @@ function setActiveClasses(translationKey: string) {
|
|||
<button
|
||||
class="mr-4 rounded-full border-2 border-blue-900 px-4 last:mr-0"
|
||||
:class="setActiveClasses(circle.translation_key)"
|
||||
@click="cockpitStore.toggleCourseSelection(circle.translation_key)"
|
||||
@click="cockpitStore.toggleCircleSelection(circle.translation_key)"
|
||||
>
|
||||
{{ circle.title }}
|
||||
</button>
|
||||
|
|
@ -101,23 +102,15 @@ function setActiveClasses(translationKey: string) {
|
|||
</div>
|
||||
<!-- Status -->
|
||||
<div class="mb-4 grid grid-rows-2 gap-4 lg:grid-cols-2 lg:grid-rows-none">
|
||||
<AssignmentsTile
|
||||
v-if="courseSessionsStore.currentCourseSession"
|
||||
:course-session="courseSessionsStore.currentCourseSession"
|
||||
/>
|
||||
<div class="bg-white px-6 py-5">
|
||||
<h1
|
||||
class="heading-3 mb-4 bg-assignment bg-60 bg-no-repeat pl-[68px] leading-[60px]"
|
||||
>
|
||||
{{ $t("general.transferTask", 2) }}
|
||||
</h1>
|
||||
<div class="mb-4">
|
||||
<ItProgress :status-count="data.transferProgress"></ItProgress>
|
||||
</div>
|
||||
<p>{{ $t("cockpit.tasksDone") }}</p>
|
||||
</div>
|
||||
<div class="bg-white px-6 py-5">
|
||||
<h1
|
||||
class="heading-3 mb-4 bg-test bg-60 bg-no-repeat pl-[68px] leading-[60px]"
|
||||
>
|
||||
{{ $t("general.examResult", 2) }}
|
||||
</h1>
|
||||
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||
<it-icon-test-large class="h-16 w-16"></it-icon-test-large>
|
||||
<div>{{ $t("general.examResult", 2) }}</div>
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
<ItProgress :status-count="data.transferProgress"></ItProgress>
|
||||
</div>
|
||||
|
|
@ -131,8 +124,8 @@ function setActiveClasses(translationKey: string) {
|
|||
learningPathStore.learningPathForUser(props.courseSlug, userStore.id)
|
||||
?.circles || []
|
||||
"
|
||||
:course-id="courseSessionStore.currentCourseSession?.course.id || 0"
|
||||
:url="courseSessionStore.currentCourseSession?.course_url || ''"
|
||||
:course-id="courseSessionsStore.currentCourseSession?.course.id || 0"
|
||||
:url="courseSessionsStore.currentCourseSession?.course_url || ''"
|
||||
></FeedbackSummary>
|
||||
<div>
|
||||
<!-- progress -->
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import CircleDiagram from "./CircleDiagram.vue";
|
||||
import CircleOverview from "./CircleOverview.vue";
|
||||
import DocumentSection from "./DocumentSection.vue";
|
||||
import LearningSequence from "./LearningSequence.vue";
|
||||
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { CourseSessionUser } from "@/types";
|
||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||
import sumBy from "lodash/sumBy";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import CircleDiagram from "./CircleDiagram.vue";
|
||||
import CircleOverview from "./CircleOverview.vue";
|
||||
import DocumentSection from "./DocumentSection.vue";
|
||||
import LearningSequence from "./LearningSequence.vue";
|
||||
|
||||
export interface Props {
|
||||
courseSlug: string;
|
||||
|
|
@ -31,9 +29,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
log.debug("CirclePage created", props.readonly, props.profileUser);
|
||||
|
||||
const appStore = useAppStore();
|
||||
appStore.showMainNavigationBar = true;
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const duration = computed(() => {
|
||||
|
|
|
|||
|
|
@ -115,8 +115,7 @@ async function uploadDocument(data: DocumentUploadData) {
|
|||
data,
|
||||
courseSessionsStore.currentCourseSession.id
|
||||
);
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
courseSessionStore.addDocument(newDocument);
|
||||
courseSessionsStore.addDocument(newDocument);
|
||||
showUploadModal.value = false;
|
||||
isUploading.value = false;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ defineEmits(["exit"]);
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div class="h-full"></div>
|
||||
<div class="absolute bottom-0 top-0 w-full bg-white">
|
||||
<div class="h-content overflow-y-auto">
|
||||
<header
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContent from "@/pages/learningPath/learningContentPage/LearningContent.vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningContent as LearningContentType } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
|
@ -16,9 +15,6 @@ const props = defineProps<{
|
|||
|
||||
const state: { learningContent?: LearningContentType } = reactive({});
|
||||
|
||||
const appStore = useAppStore();
|
||||
appStore.showMainNavigationBar = false;
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const loadLearningContent = async () => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
<template>
|
||||
<h3 class="mt-8">{{ $t("assignment.initialSituationTitle") }}</h3>
|
||||
<p class="text-large">{{ props.assignment.starting_position }}</p>
|
||||
<p
|
||||
v-if="props.assignment.starting_position"
|
||||
class="default-wagtail-rich-text text-large"
|
||||
v-html="props.assignment.starting_position"
|
||||
></p>
|
||||
|
||||
<h3 class="mt-8">{{ $t("assignment.taskDefinitionTitle") }}</h3>
|
||||
<p class="text-large">
|
||||
|
|
@ -55,8 +59,16 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
</p>
|
||||
|
||||
<h3 class="mt-8">{{ $t("assignment.assessmentTitle") }}</h3>
|
||||
<p class="text-large">{{ props.assignment.assessment_description }}</p>
|
||||
<a :href="props.assignment.assessment_document_url" class="text-large underline">
|
||||
<p
|
||||
v-if="props.assignment.evaluation_description"
|
||||
class="default-wagtail-rich-text text-large"
|
||||
v-html="props.assignment.evaluation_description"
|
||||
></p>
|
||||
<a
|
||||
:href="props.assignment.evaluation_document_url"
|
||||
target="_blank"
|
||||
class="text-large link"
|
||||
>
|
||||
{{ $t("assignment.showAssessmentDocument") }}
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,50 @@
|
|||
<script setup lang="ts">
|
||||
import type { UserDataText } from "@/stores/assignmentStore";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import type { AssignmentTask } from "@/types";
|
||||
import { computed } from "vue";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletionData,
|
||||
AssignmentTask,
|
||||
UserDataText,
|
||||
} from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
assignmentCompletionData: AssignmentCompletionData;
|
||||
allowEdit: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "editTask", task: AssignmentTask): void;
|
||||
}>();
|
||||
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
||||
const completionData = computed(() => assignmentStore.completion_data);
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-for="task in assignmentStore.assignment?.tasks ?? []"
|
||||
v-for="task in props.assignment.tasks ?? []"
|
||||
:key="task.id"
|
||||
class="mb-6 border-t border-gray-400"
|
||||
>
|
||||
<div class="flex flex-row justify-between pt-8">
|
||||
<p class="text-sm text-gray-900">{{ task.value.title }}</p>
|
||||
<button
|
||||
class="link whitespace-nowrap pl-2 text-sm"
|
||||
v-if="props.allowEdit"
|
||||
class="link pl-2text-sm whitespace-nowrap"
|
||||
@click="emit('editTask', task)"
|
||||
>
|
||||
{{ $t("assignment.edit") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="taskBlock in task.value.content" :key="taskBlock.id">
|
||||
<p class="pt-6 text-base font-bold">{{ taskBlock.value.text }}</p>
|
||||
<p v-if="completionData && taskBlock.id in completionData" class="font-normal">
|
||||
{{ (completionData[taskBlock.id].user_data as UserDataText).text }}
|
||||
<p
|
||||
class="default-wagtail-rich-text pt-6 text-base font-bold"
|
||||
v-html="taskBlock.value.text"
|
||||
></p>
|
||||
<p
|
||||
v-if="
|
||||
props.assignmentCompletionData &&
|
||||
taskBlock.id in props.assignmentCompletionData
|
||||
"
|
||||
class="font-normal"
|
||||
>
|
||||
{{ (assignmentCompletionData[taskBlock.id].user_data as UserDataText).text }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
|||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { Assignment, AssignmentTask } from "@/types";
|
||||
import type { Assignment, AssignmentCompletionData, AssignmentTask } from "@/types";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
|
|
@ -13,6 +13,7 @@ import { useI18n } from "vue-i18n";
|
|||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
assignmentCompletionData: AssignmentCompletionData;
|
||||
courseSessionId: number;
|
||||
dueDate: Dayjs;
|
||||
}>();
|
||||
|
|
@ -22,7 +23,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const assignmentStore = useAssignmentStore();
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
|
|
@ -31,30 +32,34 @@ const state = reactive({
|
|||
});
|
||||
|
||||
const circleExpert = computed(() => {
|
||||
return courseSessionStore.circleExperts[0];
|
||||
return courseSessionsStore.circleExperts[0];
|
||||
});
|
||||
|
||||
const circleExpertName = computed(() => {
|
||||
return `${circleExpert.value?.first_name} ${circleExpert.value?.last_name}`;
|
||||
});
|
||||
|
||||
const completionStatus = computed(() => {
|
||||
return assignmentStore.assignmentCompletion?.completion_status ?? "in_progress";
|
||||
});
|
||||
|
||||
const onEditTask = (task: AssignmentTask) => {
|
||||
emit("editTask", task);
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
const courseSessionId = courseSessionStore.currentCourseSession?.id;
|
||||
const courseSessionId = courseSessionsStore.currentCourseSession?.id;
|
||||
if (!courseSessionId) {
|
||||
log.error("Invalid courseSessionId");
|
||||
return;
|
||||
}
|
||||
await assignmentStore.upsertAssignmentCompletion(
|
||||
props.assignment.id,
|
||||
{},
|
||||
courseSessionId,
|
||||
true
|
||||
);
|
||||
await assignmentStore.upsertAssignmentCompletion({
|
||||
assignment_id: props.assignment.id,
|
||||
course_session_id: courseSessionId,
|
||||
completion_data: {},
|
||||
completion_status: "submitted",
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("Could not submit assignment", error);
|
||||
}
|
||||
|
|
@ -66,7 +71,7 @@ const onSubmit = async () => {
|
|||
{{ $t("assignment.acceptConditionsDisclaimer") }}
|
||||
</h3>
|
||||
|
||||
<div v-if="!assignmentStore.submitted">
|
||||
<div v-if="completionStatus === 'in_progress'">
|
||||
<ItCheckbox
|
||||
class="w-full border-b border-gray-400 py-6"
|
||||
:checkbox-item="{
|
||||
|
|
@ -99,7 +104,7 @@ const onSubmit = async () => {
|
|||
</div>
|
||||
<div class="flex flex-col space-x-2 pt-6 text-base sm:flex-row">
|
||||
<p>{{ $t("assignment.assessmentDocumentDisclaimer") }}</p>
|
||||
<a :href="props.assignment.assessment_document_url" class="underline">
|
||||
<a :href="props.assignment.evaluation_document_url" class="underline">
|
||||
{{ $t("assignment.showAssessmentDocument") }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -126,6 +131,9 @@ const onSubmit = async () => {
|
|||
</div>
|
||||
</div>
|
||||
<AssignmentSubmissionResponses
|
||||
:assignment="props.assignment"
|
||||
:assignment-completion-data="props.assignmentCompletionData"
|
||||
:allow-edit="completionStatus === 'in_progress'"
|
||||
@edit-task="onEditTask"
|
||||
></AssignmentSubmissionResponses>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||
import type {
|
||||
AssignmentCompletionData,
|
||||
BlockId,
|
||||
UserDataConfirmation,
|
||||
UserDataText,
|
||||
} from "@/stores/assignmentStore";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { AssignmentTask } from "@/types";
|
||||
import type {
|
||||
AssignmentCompletionData,
|
||||
AssignmentTask,
|
||||
UserDataConfirmation,
|
||||
UserDataText,
|
||||
} from "@/types";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import dayjs from "dayjs";
|
||||
import { reactive, ref } from "vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentId: number;
|
||||
|
|
@ -22,24 +21,24 @@ const props = defineProps<{
|
|||
const lastSaved = ref(dayjs());
|
||||
const lastSaveUnsuccessful = ref(false);
|
||||
|
||||
const checkboxState = reactive({} as Record<BlockId, boolean>);
|
||||
const checkboxState = reactive({} as Record<string, boolean>);
|
||||
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
||||
async function upsertAssignmentCompletion(completion_data: AssignmentCompletionData) {
|
||||
try {
|
||||
const courseSessionId = courseSessionStore.currentCourseSession?.id;
|
||||
const courseSessionId = courseSessionsStore.currentCourseSession?.id;
|
||||
if (!courseSessionId) {
|
||||
console.error("Invalid courseSessionId");
|
||||
return;
|
||||
}
|
||||
await assignmentStore.upsertAssignmentCompletion(
|
||||
props.assignmentId,
|
||||
completion_data,
|
||||
courseSessionId,
|
||||
false
|
||||
);
|
||||
await assignmentStore.upsertAssignmentCompletion({
|
||||
assignment_id: props.assignmentId,
|
||||
course_session_id: courseSessionId,
|
||||
completion_data: completion_data,
|
||||
completion_status: "in_progress",
|
||||
});
|
||||
lastSaved.value = dayjs();
|
||||
lastSaveUnsuccessful.value = false;
|
||||
console.debug("Saved user input");
|
||||
|
|
@ -54,7 +53,7 @@ const upsertAssignmentCompletionDebounced = useDebounceFn(
|
|||
500
|
||||
);
|
||||
|
||||
const onUpdateText = (id: BlockId, value: string) => {
|
||||
const onUpdateText = (id: string, value: string) => {
|
||||
const data: AssignmentCompletionData = {};
|
||||
data[id] = {
|
||||
user_data: {
|
||||
|
|
@ -64,7 +63,7 @@ const onUpdateText = (id: BlockId, value: string) => {
|
|||
upsertAssignmentCompletionDebounced(data);
|
||||
};
|
||||
|
||||
const onUpdateConfirmation = (id: BlockId, value: boolean) => {
|
||||
const onUpdateConfirmation = (id: string, value: boolean) => {
|
||||
const data: AssignmentCompletionData = {};
|
||||
data[id] = {
|
||||
user_data: {
|
||||
|
|
@ -74,7 +73,7 @@ const onUpdateConfirmation = (id: BlockId, value: boolean) => {
|
|||
upsertAssignmentCompletion(data);
|
||||
};
|
||||
|
||||
const getBlockData = (id: BlockId) => {
|
||||
const getBlockData = (id: string) => {
|
||||
const userData = assignmentStore.getCompletionDataForUserInput(id)?.user_data;
|
||||
if (userData && "text" in userData) {
|
||||
return userData.text;
|
||||
|
|
@ -84,18 +83,23 @@ const getBlockData = (id: BlockId) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const onToggleCheckbox = (id: BlockId) => {
|
||||
const onToggleCheckbox = (id: string) => {
|
||||
checkboxState[id] = !checkboxState[id];
|
||||
onUpdateConfirmation(id, checkboxState[id]);
|
||||
};
|
||||
|
||||
const completionStatus = computed(() => {
|
||||
return assignmentStore.assignmentCompletion?.completion_status ?? "in_progress";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col space-y-10">
|
||||
<div v-for="(block, index) in props.task.value.content" :key="block.id">
|
||||
<div v-if="block.type === 'explanation'">
|
||||
<p class="text-large">{{ block.value.text }}</p>
|
||||
<p class="default-wagtail-rich-text text-large" v-html="block.value.text"></p>
|
||||
</div>
|
||||
|
||||
<div v-if="block.type === 'user_confirmation'">
|
||||
<ItCheckbox
|
||||
:checkbox-item="{
|
||||
|
|
@ -103,16 +107,20 @@ const onToggleCheckbox = (id: BlockId) => {
|
|||
value: `confirmation-${index}`,
|
||||
checked: getBlockData(block.id) as boolean,
|
||||
}"
|
||||
:disabled="assignmentStore.submitted"
|
||||
:disabled="completionStatus !== 'in_progress'"
|
||||
@toggle="onToggleCheckbox(block.id)"
|
||||
></ItCheckbox>
|
||||
</div>
|
||||
<div v-if="block.type === 'user_text_input'">
|
||||
<p class="text-large pb-4">{{ block.value.text }}</p>
|
||||
<p
|
||||
v-if="block.value.text"
|
||||
class="text-large pb-4"
|
||||
v-html="block.value.text"
|
||||
></p>
|
||||
<ItTextarea
|
||||
:model-value="(getBlockData(block.id) as string) ?? ''"
|
||||
:cy-key="`user-text-input-${index}`"
|
||||
:disabled="assignmentStore.submitted"
|
||||
:disabled="completionStatus !== 'in_progress'"
|
||||
label=""
|
||||
@update:model-value="onUpdateText(block.id, $event)"
|
||||
></ItTextarea>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
|
||||
import AssignmentIntroductionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue";
|
||||
import AssignmentSubmissionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue";
|
||||
import AssignmentTaskView from "@/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue";
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import type { AssignmentCompletionData } from "@/stores/assignmentStore";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentTask,
|
||||
CourseSessionAssignmentDetails,
|
||||
CourseSessionUser,
|
||||
LearningContent,
|
||||
} from "@/types";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import dayjs from "dayjs";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const assignmentStore = useAssignmentStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
interface State {
|
||||
assignment: Assignment | undefined;
|
||||
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
|
||||
assignmentCompletionData: AssignmentCompletionData | undefined;
|
||||
pageIndex: number;
|
||||
}
|
||||
|
||||
const state: State = reactive({
|
||||
assignment: undefined,
|
||||
courseSessionAssignmentDetails: undefined,
|
||||
assignmentCompletionData: undefined,
|
||||
// 0 = introduction, 1 - n = tasks, n+1 = submission
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -41,6 +40,14 @@ const props = defineProps<{
|
|||
learningContent: LearningContent;
|
||||
}>();
|
||||
|
||||
// 0 = introduction, 1 - n = tasks, n+1 = submission
|
||||
const pageIndex = useRouteQuery("page", "0", { transform: Number, mode: "push" });
|
||||
|
||||
const assignmentCompletion = computed(() => assignmentStore.assignmentCompletion);
|
||||
const completionStatus = computed(() => {
|
||||
return assignmentCompletion.value?.completion_status ?? "in_progress";
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("AssignmentView mounted", props.assignmentId, props.learningContent);
|
||||
|
||||
|
|
@ -51,11 +58,17 @@ onMounted(async () => {
|
|||
state.courseSessionAssignmentDetails = courseSessionsStore.findAssignmentDetails(
|
||||
props.learningContent.id
|
||||
);
|
||||
state.assignmentCompletionData = await assignmentStore.loadAssignmentCompletion(
|
||||
await assignmentStore.loadAssignmentCompletion(
|
||||
props.assignmentId,
|
||||
courseSessionId.value
|
||||
);
|
||||
log.debug(state.assignment, state.courseSessionAssignmentDetails);
|
||||
|
||||
if (
|
||||
pageIndex.value === 0 &&
|
||||
(completionStatus.value ?? "in_progress") !== "in_progress"
|
||||
) {
|
||||
pageIndex.value = numPages.value - 1;
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
|
@ -63,92 +76,119 @@ onMounted(async () => {
|
|||
|
||||
const numTasks = computed(() => state.assignment?.tasks?.length ?? 0);
|
||||
const numPages = computed(() => numTasks.value + 2);
|
||||
const showPreviousButton = computed(() => state.pageIndex != 0);
|
||||
const showNextButton = computed(() => state.pageIndex + 1 < numPages.value);
|
||||
const showExitButton = computed(() => numPages.value === state.pageIndex + 1);
|
||||
const showPreviousButton = computed(() => pageIndex.value != 0);
|
||||
const showNextButton = computed(() => pageIndex.value + 1 < numPages.value);
|
||||
const showExitButton = computed(() => numPages.value === pageIndex.value + 1);
|
||||
const dueDate = computed(() =>
|
||||
dayjs(state.courseSessionAssignmentDetails?.deadlineDateTimeUtc)
|
||||
dayjs(state.courseSessionAssignmentDetails?.submissionDeadlineDateTimeUtc)
|
||||
);
|
||||
const courseSessionId = computed(
|
||||
() => courseSessionStore.currentCourseSession?.id ?? 0
|
||||
() => courseSessionsStore.currentCourseSession?.id ?? 0
|
||||
);
|
||||
const currentTask = computed(() => {
|
||||
if (state.pageIndex > 0 && state.pageIndex <= numTasks.value) {
|
||||
return state.assignment?.tasks[state.pageIndex - 1];
|
||||
if (pageIndex.value > 0 && pageIndex.value <= numTasks.value) {
|
||||
return state.assignment?.tasks[pageIndex.value - 1];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const handleBack = () => {
|
||||
log.debug("handleBack");
|
||||
if (state.pageIndex > 0) {
|
||||
state.pageIndex -= 1;
|
||||
if (pageIndex.value > 0) {
|
||||
pageIndex.value -= 1;
|
||||
}
|
||||
log.debug(`pageIndex: ${state.pageIndex}`);
|
||||
log.debug(`pageIndex: ${pageIndex.value}`);
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
log.debug("handleContinue");
|
||||
if (state.pageIndex + 1 < numPages.value) {
|
||||
state.pageIndex += 1;
|
||||
if (pageIndex.value + 1 < numPages.value) {
|
||||
pageIndex.value += 1;
|
||||
}
|
||||
log.debug(`pageIndex: ${state.pageIndex}`);
|
||||
log.debug(`pageIndex: ${pageIndex.value}`);
|
||||
};
|
||||
|
||||
const jumpToTask = (task: AssignmentTask) => {
|
||||
log.debug("jumpToTask", task);
|
||||
const index = state.assignment?.tasks.findIndex((t) => t.id === task.id);
|
||||
if (index && index >= 0) {
|
||||
state.pageIndex = index + 1;
|
||||
pageIndex.value = index + 1;
|
||||
}
|
||||
log.debug(`pageIndex: ${state.pageIndex}`);
|
||||
log.debug(`pageIndex: ${pageIndex.value}`);
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
if (0 === state.pageIndex) {
|
||||
if (0 === pageIndex.value) {
|
||||
return t("general.introduction");
|
||||
} else if (state.pageIndex === numPages.value - 1) {
|
||||
} else if (pageIndex.value === numPages.value - 1) {
|
||||
return t("general.submission");
|
||||
}
|
||||
return currentTask?.value?.value.title ?? "Unknown";
|
||||
};
|
||||
|
||||
const assignmentUser = computed(() => {
|
||||
return (courseSessionsStore.currentCourseSession?.users ?? []).find(
|
||||
(user) => user.user_id === Number(userStore.id)
|
||||
) as CourseSessionUser;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LearningContentMultiLayout
|
||||
:current-step="state.pageIndex"
|
||||
:subtitle="state.assignment?.title ?? ''"
|
||||
:title="getTitle()"
|
||||
learning-content-type="assignment"
|
||||
:steps-count="numPages"
|
||||
:show-next-button="showNextButton"
|
||||
:show-exit-button="showExitButton"
|
||||
:show-start-button="false"
|
||||
:show-previous-button="showPreviousButton"
|
||||
start-badge-text="Einleitung"
|
||||
end-badge-text="Abgabe"
|
||||
close-button-variant="close"
|
||||
@previous="handleBack()"
|
||||
@next="handleContinue()"
|
||||
>
|
||||
<div>
|
||||
<AssignmentIntroductionView
|
||||
v-if="state.pageIndex === 0 && state.assignment"
|
||||
:due-date="dueDate"
|
||||
:assignment="state.assignment!"
|
||||
></AssignmentIntroductionView>
|
||||
<AssignmentTaskView
|
||||
v-if="currentTask"
|
||||
:task="currentTask"
|
||||
:assignment-id="props.assignmentId"
|
||||
></AssignmentTaskView>
|
||||
<AssignmentSubmissionView
|
||||
v-if="state.pageIndex + 1 === numPages && state.assignment && courseSessionId"
|
||||
:due-date="dueDate"
|
||||
:assignment="state.assignment!"
|
||||
:course-session-id="courseSessionId!"
|
||||
@edit-task="jumpToTask($event)"
|
||||
></AssignmentSubmissionView>
|
||||
<div v-if="state.assignment">
|
||||
<div class="flex">
|
||||
<LearningContentMultiLayout
|
||||
:current-step="pageIndex"
|
||||
:subtitle="state.assignment?.title ?? ''"
|
||||
:title="getTitle()"
|
||||
learning-content-type="assignment"
|
||||
:steps-count="numPages"
|
||||
:show-next-button="showNextButton"
|
||||
:show-exit-button="showExitButton"
|
||||
:show-start-button="false"
|
||||
:show-previous-button="showPreviousButton"
|
||||
:base-url="props.learningContent.frontend_url"
|
||||
query-param="page"
|
||||
start-badge-text="Einleitung"
|
||||
end-badge-text="Abgabe"
|
||||
close-button-variant="close"
|
||||
@previous="handleBack()"
|
||||
@next="handleContinue()"
|
||||
>
|
||||
<div class="flex">
|
||||
<div>
|
||||
<AssignmentIntroductionView
|
||||
v-if="pageIndex === 0 && state.assignment"
|
||||
:due-date="dueDate"
|
||||
:assignment="state.assignment!"
|
||||
></AssignmentIntroductionView>
|
||||
<AssignmentTaskView
|
||||
v-if="currentTask"
|
||||
:task="currentTask"
|
||||
:assignment-id="props.assignmentId"
|
||||
></AssignmentTaskView>
|
||||
<AssignmentSubmissionView
|
||||
v-if="pageIndex + 1 === numPages && state.assignment && courseSessionId"
|
||||
:due-date="dueDate"
|
||||
:assignment="state.assignment!"
|
||||
:assignment-completion-data="assignmentCompletion?.completion_data ?? {}"
|
||||
:course-session-id="courseSessionId!"
|
||||
@edit-task="jumpToTask($event)"
|
||||
></AssignmentSubmissionView>
|
||||
</div>
|
||||
</div>
|
||||
</LearningContentMultiLayout>
|
||||
<div
|
||||
v-if="assignmentCompletion?.completion_status === 'evaluation_submitted'"
|
||||
class="min-w-2/5 mr-4 bg-gray-200 px-6 py-6"
|
||||
>
|
||||
<EvaluationSummary
|
||||
v-if="state.assignment"
|
||||
:assignment-user="assignmentUser"
|
||||
:assignment="state.assignment"
|
||||
:assignment-completion="assignmentCompletion"
|
||||
:show-evaluation-user="true"
|
||||
></EvaluationSummary>
|
||||
</div>
|
||||
</div>
|
||||
</LearningContentMultiLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ interface Props {
|
|||
startBadgeText?: string;
|
||||
endBadgeText?: string;
|
||||
closeButtonVariant?: ClosingButtonVariant;
|
||||
baseUrl?: string;
|
||||
queryParam?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
@ -55,6 +57,8 @@ const emit = defineEmits(["previous", "next", "exit"]);
|
|||
:start-badge-text="props.startBadgeText"
|
||||
:steps="stepsCount"
|
||||
:end-badge-text="props.endBadgeText"
|
||||
:base-url="props.baseUrl"
|
||||
:query-param="props.queryParam"
|
||||
class="overflow-hidden pb-12"
|
||||
></ItNavigationProgress>
|
||||
<slot></slot>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
import * as log from "loglevel";
|
||||
|
||||
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
||||
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningUnit } from "@/types";
|
||||
import { onMounted, reactive } from "vue";
|
||||
|
|
@ -16,9 +14,6 @@ const props = defineProps<{
|
|||
learningUnitSlug: string;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
appStore.showMainNavigationBar = false;
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const state: { learningUnit?: LearningUnit } = reactive({});
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ const router = createRouter({
|
|||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: () => import("@/pages/cockpit/CockpitIndexPage.vue"),
|
||||
component: () => import("@/pages/cockpit/cockpitPage/CockpitPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -129,6 +129,20 @@ const router = createRouter({
|
|||
component: () => import("@/pages/cockpit/FeedbackPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "assignment",
|
||||
component: () =>
|
||||
import("@/pages/cockpit/assignmentsPage/AssignmentsPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "assignment/:assignmentId/:userId",
|
||||
component: () =>
|
||||
import(
|
||||
"@/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue"
|
||||
),
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -169,11 +183,11 @@ router.beforeEach(updateLoggedIn);
|
|||
router.beforeEach(redirectToLoginIfRequired);
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
if (to.params.courseSlug) {
|
||||
courseSessionStore._currentCourseSlug = to.params.courseSlug as string;
|
||||
courseSessionsStore._currentCourseSlug = to.params.courseSlug as string;
|
||||
} else {
|
||||
courseSessionStore._currentCourseSlug = "";
|
||||
courseSessionsStore._currentCourseSlug = "";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, it } from "vitest";
|
||||
import { pointsToGrade } from "../assignmentService";
|
||||
|
||||
describe("assignmentService", () => {
|
||||
it("pointsToGrade", () => {
|
||||
expect(pointsToGrade(24, 24)).toBe(6);
|
||||
expect(pointsToGrade(23, 24)).toBe(6);
|
||||
expect(pointsToGrade(22, 24)).toBe(5.5);
|
||||
expect(pointsToGrade(21, 24)).toBe(5.5);
|
||||
expect(pointsToGrade(20, 24)).toBe(5);
|
||||
expect(pointsToGrade(19, 24)).toBe(5);
|
||||
expect(pointsToGrade(18, 24)).toBe(5);
|
||||
expect(pointsToGrade(17, 24)).toBe(4.5);
|
||||
expect(pointsToGrade(16, 24)).toBe(4.5);
|
||||
expect(pointsToGrade(15, 24)).toBe(4);
|
||||
expect(pointsToGrade(14, 24)).toBe(4);
|
||||
expect(pointsToGrade(13, 24)).toBe(3.5);
|
||||
expect(pointsToGrade(12, 24)).toBe(3.5);
|
||||
expect(pointsToGrade(11, 24)).toBe(3.5);
|
||||
expect(pointsToGrade(10, 24)).toBe(3);
|
||||
expect(pointsToGrade(9, 24)).toBe(3);
|
||||
expect(pointsToGrade(8, 24)).toBe(2.5);
|
||||
expect(pointsToGrade(7, 24)).toBe(2.5);
|
||||
expect(pointsToGrade(6, 24)).toBe(2.5);
|
||||
expect(pointsToGrade(5, 24)).toBe(2);
|
||||
expect(pointsToGrade(4, 24)).toBe(2);
|
||||
expect(pointsToGrade(3, 24)).toBe(1.5);
|
||||
expect(pointsToGrade(2, 24)).toBe(1.5);
|
||||
expect(pointsToGrade(1, 24)).toBe(1);
|
||||
expect(pointsToGrade(0, 24)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import type { StatusCountKey } from "@/components/ui/ItProgress.vue";
|
||||
import { itGet } from "@/fetchHelpers";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
AssignmentCompletionStatus,
|
||||
CourseSessionUser,
|
||||
LearningContent,
|
||||
UserAssignmentCompletionStatus,
|
||||
} from "@/types";
|
||||
import { sum } from "d3";
|
||||
import pick from "lodash/pick";
|
||||
|
||||
export interface AssignmentLearningContent extends LearningContent {
|
||||
assignmentId: number;
|
||||
}
|
||||
|
||||
export function calcAssignmentLearningContents(learningPath?: LearningPath) {
|
||||
// TODO: filter by circle
|
||||
if (!learningPath) return [];
|
||||
|
||||
return learningPath.circles.flatMap((circle) => {
|
||||
const learningContents = circle.flatLearningContents.filter(
|
||||
(lc) => lc.contents[0].type === "assignment"
|
||||
);
|
||||
return learningContents.map((lc) => {
|
||||
return {
|
||||
...lc,
|
||||
// @ts-ignore
|
||||
assignmentId: lc.contents[0].value.assignment,
|
||||
};
|
||||
});
|
||||
}) as AssignmentLearningContent[];
|
||||
}
|
||||
|
||||
export async function loadAssignmentCompletionStatusData(
|
||||
assignmentId: number,
|
||||
courseSessionId: number
|
||||
) {
|
||||
const cockpitStore = useCockpitStore();
|
||||
|
||||
const assignmentCompletionData = (await itGet(
|
||||
`/api/assignment/${assignmentId}/${courseSessionId}/status/`
|
||||
)) as UserAssignmentCompletionStatus[];
|
||||
|
||||
const courseSessionUsers = await cockpitStore.loadCourseSessionUsers(courseSessionId);
|
||||
|
||||
return calcUserAssignmentCompletionStatus(
|
||||
courseSessionUsers,
|
||||
assignmentCompletionData
|
||||
);
|
||||
}
|
||||
|
||||
export function calcUserAssignmentCompletionStatus(
|
||||
courseSessionUsers: CourseSessionUser[],
|
||||
assignmentCompletionStatusData: UserAssignmentCompletionStatus[]
|
||||
) {
|
||||
return courseSessionUsers.map((u) => {
|
||||
let userStatus = "unknown" as AssignmentCompletionStatus;
|
||||
const userAssignmentStatus = assignmentCompletionStatusData?.find(
|
||||
(s) => s.assignment_user_id === u.user_id
|
||||
);
|
||||
if (userAssignmentStatus) {
|
||||
userStatus = userAssignmentStatus.completion_status;
|
||||
}
|
||||
let progressStatus: StatusCountKey = "unknown";
|
||||
if (
|
||||
["submitted", "evaluation_in_progress", "evaluation_submitted"].includes(
|
||||
userStatus
|
||||
)
|
||||
) {
|
||||
progressStatus = "success";
|
||||
}
|
||||
|
||||
return {
|
||||
userId: u.user_id,
|
||||
userStatus,
|
||||
progressStatus,
|
||||
grade: userAssignmentStatus?.evaluation_grade ?? null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function maxAssignmentPoints(assignment: Assignment) {
|
||||
return sum(assignment.evaluation_tasks.map((task) => task.value.max_points));
|
||||
}
|
||||
|
||||
export function userAssignmentPoints(
|
||||
assignment: Assignment,
|
||||
assignmentCompletion: AssignmentCompletion
|
||||
) {
|
||||
const evaluationTaskIds = assignment.evaluation_tasks.map((task) => {
|
||||
return task.id;
|
||||
});
|
||||
|
||||
return sum(
|
||||
// transform the object of { [expert_id]: { expert_data: { points } } } to an array
|
||||
// of [ [expert_id, { expert_data: { points } }], ... ] so that we can easily sum
|
||||
// the points of the user
|
||||
Object.entries(pick(assignmentCompletion.completion_data, evaluationTaskIds)).map(
|
||||
(entry) => {
|
||||
return entry[1]?.expert_data?.points ?? 0;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function pointsToGrade(points: number, maxPoints: number) {
|
||||
// round to half-grades
|
||||
const grade = Math.round((points / maxPoints) * 10);
|
||||
const halfGrade = grade / 2;
|
||||
return Math.min(halfGrade, 5) + 1;
|
||||
}
|
||||
|
|
@ -3,33 +3,12 @@ import { defineStore } from "pinia";
|
|||
export type AppState = {
|
||||
userLoaded: boolean;
|
||||
routingFinished: boolean;
|
||||
showMainNavigationBar: boolean;
|
||||
currentCourseSlug: string;
|
||||
};
|
||||
|
||||
const showMainNavigationBarInitialState = () => {
|
||||
let path = window.location.pathname;
|
||||
|
||||
// remove dangling slash
|
||||
if (path.endsWith("/")) {
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
|
||||
const numberOfSlashes = (path.match(/\//g) || []).length;
|
||||
|
||||
// it should hide main navigation bar when on learning content page
|
||||
if (path.startsWith("/learn/") && numberOfSlashes >= 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const useAppStore = defineStore({
|
||||
id: "app",
|
||||
state: () =>
|
||||
({
|
||||
showMainNavigationBar: showMainNavigationBarInitialState(),
|
||||
userLoaded: false,
|
||||
routingFinished: false,
|
||||
} as AppState),
|
||||
|
|
|
|||
|
|
@ -1,39 +1,29 @@
|
|||
import { itGet, itPost } from "@/fetchHelpers";
|
||||
import type { Assignment } from "@/types";
|
||||
import { calcAssignmentLearningContents } from "@/services/assignmentService";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
EvaluationCompletionData,
|
||||
UpsertUserAssignmentCompletion,
|
||||
} from "@/types";
|
||||
import { merge } from "lodash";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type AssignmentStoreState = {
|
||||
assignment: Assignment | undefined;
|
||||
completion_data: AssignmentCompletionData;
|
||||
submitted: boolean;
|
||||
assignmentCompletion: AssignmentCompletion | undefined;
|
||||
};
|
||||
|
||||
export type BlockId = string;
|
||||
|
||||
export interface UserDataText {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface UserDataConfirmation {
|
||||
confirmation: boolean;
|
||||
}
|
||||
|
||||
export interface AssignmentCompletionData {
|
||||
// {
|
||||
// "<user_text_input:uuid>": {"user_data": {"text": "some text from user"}},
|
||||
// "<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
|
||||
// }
|
||||
[key: BlockId]: { user_data: UserDataText | UserDataConfirmation };
|
||||
}
|
||||
|
||||
export const useAssignmentStore = defineStore({
|
||||
id: "assignmentStore",
|
||||
state: () => {
|
||||
return {
|
||||
assignment: undefined,
|
||||
completion_data: {},
|
||||
submitted: false,
|
||||
assignmentCompletion: undefined,
|
||||
} as AssignmentStoreState;
|
||||
},
|
||||
getters: {},
|
||||
|
|
@ -48,39 +38,73 @@ export const useAssignmentStore = defineStore({
|
|||
this.assignment = assignmentData;
|
||||
return this.assignment;
|
||||
},
|
||||
async loadAssignmentCompletion(assignmentId: number, courseSessionId: number) {
|
||||
log.debug("load assignment completion", assignmentId, courseSessionId);
|
||||
async loadAssignmentCompletion(
|
||||
assignmentId: number,
|
||||
courseSessionId: number,
|
||||
userId: string | undefined = undefined
|
||||
) {
|
||||
log.debug("load assignment completion", assignmentId, courseSessionId, userId);
|
||||
this.assignmentCompletion = undefined;
|
||||
try {
|
||||
const data = await itGet(`/api/assignment/${assignmentId}/${courseSessionId}/`);
|
||||
this.completion_data = data.completion_data;
|
||||
this.submitted = data.completion_status === "submitted";
|
||||
let url = `/api/assignment/${assignmentId}/${courseSessionId}/`;
|
||||
if (userId) {
|
||||
url += `${userId}/`;
|
||||
}
|
||||
this.assignmentCompletion = await itGet(url);
|
||||
} catch (e) {
|
||||
log.debug("no completion data found ", e);
|
||||
return undefined;
|
||||
}
|
||||
return this.completion_data;
|
||||
return this.assignmentCompletion;
|
||||
},
|
||||
getCompletionDataForUserInput(id: BlockId) {
|
||||
return this.completion_data[id];
|
||||
getCompletionDataForUserInput(id: string) {
|
||||
return this.assignmentCompletion?.completion_data[id];
|
||||
},
|
||||
async upsertAssignmentCompletion(
|
||||
assignmentId: number,
|
||||
completion_data: AssignmentCompletionData,
|
||||
courseSessionId: number,
|
||||
submit: boolean
|
||||
) {
|
||||
const data = {
|
||||
assignment_id: assignmentId,
|
||||
completion_status: submit ? "submitted" : "in_progress",
|
||||
course_session_id: courseSessionId,
|
||||
completion_data: completion_data,
|
||||
};
|
||||
async upsertAssignmentCompletion(data: UpsertUserAssignmentCompletion) {
|
||||
if (this.assignmentCompletion) {
|
||||
merge(this.assignmentCompletion.completion_data, data.completion_data);
|
||||
this.assignmentCompletion.completion_status = data.completion_status;
|
||||
}
|
||||
const responseData = await itPost(`/api/assignment/upsert/`, data);
|
||||
if (responseData) {
|
||||
this.completion_data = responseData.completion_data;
|
||||
this.submitted = responseData.completion_status === "submitted";
|
||||
this.assignmentCompletion = responseData;
|
||||
}
|
||||
return responseData;
|
||||
},
|
||||
async evaluateAssignmentCompletion(data: EvaluationCompletionData) {
|
||||
if (this.assignmentCompletion) {
|
||||
merge(this.assignmentCompletion.completion_data, data.completion_data);
|
||||
this.assignmentCompletion.completion_status = data.completion_status;
|
||||
}
|
||||
const responseData = await itPost(`/api/assignment/evaluate/`, data);
|
||||
if (responseData) {
|
||||
this.assignmentCompletion = responseData;
|
||||
}
|
||||
return responseData;
|
||||
},
|
||||
|
||||
findAssignmentDetail(assignmentId: number) {
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const userStore = useUserStore();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
|
||||
// TODO: filter by selected circle
|
||||
if (!courseSessionsStore.currentCourseSession) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const learningContents = calcAssignmentLearningContents(
|
||||
learningPathStore.learningPathForUser(
|
||||
courseSessionsStore.currentCourseSession.course.slug,
|
||||
userStore.id
|
||||
)
|
||||
);
|
||||
|
||||
const learningContent = learningContents.find(
|
||||
(lc) => lc.assignmentId === assignmentId
|
||||
);
|
||||
|
||||
return courseSessionsStore.findAssignmentDetails(learningContent?.id);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,11 +21,14 @@ export const useCockpitStore = defineStore({
|
|||
} as CockpitStoreState;
|
||||
},
|
||||
actions: {
|
||||
async loadCourseSessionUsers(courseSlug: string, reload = false) {
|
||||
async loadCourseSessionUsers(courseSessionId: number, reload = false) {
|
||||
log.debug("loadCockpitData called");
|
||||
const users = (await itGetCached(`/api/course/sessions/${courseSlug}/users/`, {
|
||||
reload: reload,
|
||||
})) as CourseSessionUser[];
|
||||
const users = (await itGetCached(
|
||||
`/api/course/sessions/${courseSessionId}/users/`,
|
||||
{
|
||||
reload: reload,
|
||||
}
|
||||
)) as CourseSessionUser[];
|
||||
|
||||
this.courseSessionUsers = users.filter((user) => user.role === "MEMBER");
|
||||
|
||||
|
|
@ -45,7 +48,7 @@ export const useCockpitStore = defineStore({
|
|||
}
|
||||
return this.courseSessionUsers;
|
||||
},
|
||||
toggleCourseSelection(translationKey: string) {
|
||||
toggleCircleSelection(translationKey: string) {
|
||||
if (this.selectedCircles.indexOf(translationKey) < 0) {
|
||||
this.selectedCircles.push(translationKey);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -39,12 +39,9 @@ function loadCourseSessionsData(reload = false) {
|
|||
// TODO: refactor after implementing of Klassenkonzept
|
||||
await Promise.all(
|
||||
courseSessions.value.map(async (cs) => {
|
||||
const users = (await itGetCached(
|
||||
`/api/course/sessions/${cs.course.slug}/users/`,
|
||||
{
|
||||
reload: reload,
|
||||
}
|
||||
)) as CourseSessionUser[];
|
||||
const users = (await itGetCached(`/api/course/sessions/${cs.id}/users/`, {
|
||||
reload: reload,
|
||||
})) as CourseSessionUser[];
|
||||
cs.users = users;
|
||||
})
|
||||
);
|
||||
|
|
@ -236,9 +233,9 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
|||
}
|
||||
|
||||
function findAssignmentDetails(
|
||||
contentId: number
|
||||
contentId?: number
|
||||
): CourseSessionAssignmentDetails | undefined {
|
||||
if (currentCourseSession.value) {
|
||||
if (contentId && currentCourseSession.value) {
|
||||
return currentCourseSession.value.assignment_details_list.find(
|
||||
(assignmentDetails) => assignmentDetails.learningContentId === contentId
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ export interface BaseLearningContentBlock {
|
|||
|
||||
export interface AssignmentBlock extends BaseLearningContentBlock {
|
||||
readonly type: "assignment";
|
||||
readonly value: {
|
||||
description: string;
|
||||
url: string;
|
||||
assignment: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BookBlock extends BaseLearningContentBlock {
|
||||
|
|
@ -332,14 +337,32 @@ export interface AssignmentTask {
|
|||
};
|
||||
}
|
||||
|
||||
export interface AssignmentEvaluationSubTask {
|
||||
title: string;
|
||||
description: string;
|
||||
points: number;
|
||||
}
|
||||
|
||||
export interface AssignmentEvaluationTask {
|
||||
readonly type: "task";
|
||||
readonly id: string;
|
||||
readonly value: {
|
||||
title: string;
|
||||
description: string;
|
||||
max_points: number;
|
||||
sub_tasks: AssignmentEvaluationSubTask[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Assignment extends BaseCourseWagtailPage {
|
||||
readonly type: "assignment.Assignment";
|
||||
readonly starting_position: string;
|
||||
readonly effort_required: string;
|
||||
readonly performance_objectives: AssignmentPerformanceObjective[];
|
||||
readonly assessment_description: string;
|
||||
readonly assessment_document_url: string;
|
||||
readonly evaluation_description: string;
|
||||
readonly evaluation_document_url: string;
|
||||
readonly tasks: AssignmentTask[];
|
||||
readonly evaluation_tasks: AssignmentEvaluationTask[];
|
||||
}
|
||||
|
||||
export interface PerformanceCriteria extends BaseCourseWagtailPage {
|
||||
|
|
@ -407,7 +430,8 @@ export interface CourseSessionAttendanceDay {
|
|||
|
||||
export interface CourseSessionAssignmentDetails {
|
||||
learningContentId: number;
|
||||
deadlineDateTimeUtc: string;
|
||||
submissionDeadlineDateTimeUtc: string;
|
||||
evaluationDeadlineDateTimeUtc: string;
|
||||
}
|
||||
|
||||
export interface CourseSession {
|
||||
|
|
@ -479,3 +503,69 @@ export interface Notification {
|
|||
actor_avatar_url: string | null;
|
||||
course: string | null;
|
||||
}
|
||||
|
||||
export type AssignmentCompletionStatus =
|
||||
| "unknwown"
|
||||
| "in_progress"
|
||||
| "submitted"
|
||||
| "evaluation_in_progress"
|
||||
| "evaluation_submitted";
|
||||
|
||||
export interface UserDataText {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface UserDataConfirmation {
|
||||
confirmation: boolean;
|
||||
}
|
||||
|
||||
export interface ExpertData {
|
||||
points?: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface AssignmentCompletionData {
|
||||
// {
|
||||
// "<user_text_input:uuid>": {"user_data": {"text": "some text from user"}},
|
||||
// "<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
|
||||
// }
|
||||
[key: string]: {
|
||||
user_data?: UserDataText | UserDataConfirmation;
|
||||
expert_data?: ExpertData;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AssignmentCompletion {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
submitted_at: string;
|
||||
evaluation_submitted_at: string | null;
|
||||
assignment_user: number;
|
||||
assignment: number;
|
||||
course_session: number;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
evaluation_user: number | null;
|
||||
completion_data: AssignmentCompletionData;
|
||||
evaluation_grade: number | null;
|
||||
}
|
||||
|
||||
export type UpsertUserAssignmentCompletion = {
|
||||
assignment_id: number;
|
||||
course_session_id: number;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
completion_data: AssignmentCompletionData;
|
||||
};
|
||||
|
||||
export type EvaluationCompletionData = UpsertUserAssignmentCompletion & {
|
||||
assignment_user_id: number;
|
||||
evaluation_grade?: number;
|
||||
evaluation_points?: number;
|
||||
};
|
||||
|
||||
export interface UserAssignmentCompletionStatus {
|
||||
id: number;
|
||||
assignment_user_id: number;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
evaluation_grade: number | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,6 @@ module.exports = {
|
|||
"handlungsfelder-overview":
|
||||
"url('/static/icons/icon-handlungsfelder-overview.svg')",
|
||||
"lernmedien-overview": "url('/static/icons/icon-lernmedien-overview.svg')",
|
||||
assignment: "url('/static/icons/icon-lc-assignment.svg')",
|
||||
feedback: "url('/static/icons/icon-feedback.svg')",
|
||||
test: "url('/static/icons/icon-lc-test.svg')",
|
||||
message: "url('/static/icons/icon-message.svg')",
|
||||
},
|
||||
borderColor: (theme) => ({
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ body {
|
|||
hyphens: auto;
|
||||
}
|
||||
|
||||
.default-wagtail-rich-text ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
svg {
|
||||
@apply fill-current;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import { login } from "./helpers";
|
||||
|
||||
const navigateToAssignment = () => {
|
||||
cy.visit(
|
||||
"/course/überbetriebliche-kurse/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||
);
|
||||
};
|
||||
|
||||
describe("assignment completion", () => {
|
||||
describe("student test", () => {
|
||||
beforeEach(() => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
login("admin", "test");
|
||||
navigateToAssignment();
|
||||
login("test-student1@example.com", "test");
|
||||
cy.visit(
|
||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||
);
|
||||
});
|
||||
|
||||
it("can open assignment", () => {
|
||||
|
|
@ -27,10 +23,10 @@ describe("assignment completion", () => {
|
|||
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
|
||||
);
|
||||
|
||||
// 2 Steps forward, 1 step backwards
|
||||
for (let i = 0; i !== 2; i++) {
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
}
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
|
||||
|
||||
cy.learningContentMultiLayoutPreviousStep();
|
||||
|
||||
cy.testLearningContentTitle(
|
||||
|
|
@ -46,10 +42,7 @@ describe("assignment completion", () => {
|
|||
);
|
||||
// Click confirmation
|
||||
cy.get('[data-cy="it-checkbox-confirmation-1"]').click({ force: true });
|
||||
cy.wait(250);
|
||||
cy.reload();
|
||||
// 1 Step forward
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
cy.get('[data-cy="it-checkbox-confirmation-1"]').should("be.checked");
|
||||
});
|
||||
|
||||
|
|
@ -64,15 +57,25 @@ describe("assignment completion", () => {
|
|||
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||
.clear()
|
||||
.type("Hallovelo");
|
||||
// wait because of input debounce
|
||||
cy.wait(550);
|
||||
cy.reload();
|
||||
|
||||
// 2 Step forward
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
cy.get('[data-cy="it-textarea-user-text-input-1"]').should(
|
||||
"have.value",
|
||||
"Hallovelo"
|
||||
);
|
||||
});
|
||||
|
||||
it("can visit sub step directly via url", () => {
|
||||
cy.visit(
|
||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice?page=3"
|
||||
);
|
||||
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
|
||||
});
|
||||
|
||||
it("can visit sub step by clicking navigation bar", () => {
|
||||
cy.get('[data-cy="nav-progress-step-4"]').click();
|
||||
cy.testLearningContentTitle("Teilaufgabe 4: Deine Empfehlungen");
|
||||
});
|
||||
});
|
||||
|
|
@ -10,9 +10,10 @@ from grapple import urls as grapple_urls
|
|||
from ratelimit.exceptions import Ratelimited
|
||||
|
||||
from vbv_lernwelt.assignment.views import (
|
||||
grade_assignment_completion,
|
||||
evaluate_assignment_completion,
|
||||
request_assignment_completion,
|
||||
request_assignment_completion_for_user,
|
||||
request_assignment_completion_status,
|
||||
upsert_user_assignment_completion,
|
||||
)
|
||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||
|
|
@ -87,7 +88,7 @@ urlpatterns = [
|
|||
|
||||
# course
|
||||
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
|
||||
path(r"api/course/sessions/<course_slug>/users/", get_course_session_users,
|
||||
path(r"api/course/sessions/<course_session_id>/users/", get_course_session_users,
|
||||
name="get_course_session_users"),
|
||||
path(r"api/course/page/<slug_or_id>/", course_page_api_view,
|
||||
name="course_page_api_view"),
|
||||
|
|
@ -102,11 +103,14 @@ urlpatterns = [
|
|||
# assignment
|
||||
path(r"api/assignment/upsert/", upsert_user_assignment_completion,
|
||||
name="upsert_user_assignment_completion"),
|
||||
path(r"api/assignment/grade/", grade_assignment_completion,
|
||||
name="grade_assignment_completion"),
|
||||
path(r"api/assignment/evaluate/", evaluate_assignment_completion,
|
||||
name="evaluate_assignment_completion"),
|
||||
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/",
|
||||
request_assignment_completion,
|
||||
name="request_assignment_completion"),
|
||||
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/status/",
|
||||
request_assignment_completion_status,
|
||||
name="request_assignment_completion_status"),
|
||||
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/<int:user_id>/",
|
||||
request_assignment_completion_for_user,
|
||||
name="request_assignment_completion_for_user"),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
from vbv_lernwelt.assignment.models import TaskContentStreamBlock
|
||||
from vbv_lernwelt.assignment.models import (
|
||||
EvaluationSubTaskBlock,
|
||||
TaskContentStreamBlock,
|
||||
)
|
||||
from vbv_lernwelt.assignment.tests.assignment_factories import (
|
||||
AssignmentFactory,
|
||||
AssignmentListPageFactory,
|
||||
EvaluationSubTaskBlockFactory,
|
||||
EvaluationTaskBlockFactory,
|
||||
ExplanationBlockFactory,
|
||||
PerformanceObjectiveBlockFactory,
|
||||
TaskBlockFactory,
|
||||
|
|
@ -11,6 +16,8 @@ from vbv_lernwelt.core.utils import replace_whitespace
|
|||
from vbv_lernwelt.course.consts import COURSE_TEST_ID, COURSE_UK
|
||||
from vbv_lernwelt.course.models import CoursePage
|
||||
from wagtail.blocks import StreamValue
|
||||
from wagtail.blocks.list_block import ListBlock, ListValue
|
||||
from wagtail.rich_text import RichText
|
||||
|
||||
|
||||
def create_uk_assignments(course_id=COURSE_UK):
|
||||
|
|
@ -46,8 +53,187 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
),
|
||||
),
|
||||
],
|
||||
assessment_document_url="https://www.vbv.ch",
|
||||
assessment_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
||||
evaluation_document_url="/static/media/assignments/UK_03_09_NACH_KN_Beurteilungsraster.pdf",
|
||||
evaluation_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
||||
)
|
||||
|
||||
assignment.evaluation_tasks = []
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Ausgangslage des Auftrags",
|
||||
description=RichText(
|
||||
"Beschreibt der/die Lernende die Ausgangslage des Auftrags vollständig?"
|
||||
),
|
||||
max_points=6,
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage des Auftrag ist vollständig beschrieben.",
|
||||
description=RichText(
|
||||
replace_whitespace(
|
||||
"""
|
||||
<ul>
|
||||
<li>Worum geht es? Was ist die Aufgabe?</li>
|
||||
<li>Sind das Kundenprofil und die Kundenbeziehung vollständig und nachvollziehbar dargestellt?</li>
|
||||
<li>Ist das Alter des Fahrzeugs dokumentiert?</li>
|
||||
<li>Welche Ressourcen stehen zur Verfügung?</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
),
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage ist grösstenteils vollständig beschrieben.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage ist unvollständig - nur 2 Punkte wurden beschrieben.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage des Auftrag ist unvollständig - es fehlen mehr als 2 Punkte in der Beschreibung.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Inhaltsanalyse und Struktur",
|
||||
max_points=6,
|
||||
description=RichText(
|
||||
"Sind die Deckungen der Police vollständig und nachvollziehbar dokumentiert?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse beinhaltet alle in der Police vorhandenen Deckungen und ist logisch aufgebaut.",
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse beinhaltet die meisten vorhandenen Deckungen in der Police und ist grösstenteils logisch aufgebaut.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse ist unvollständig (es fehlen mehr als 3 Deckungen) und der rote Faden ist nicht erkennbar.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse ist insgesamt nicht nachvollziehbar und es fehlen einige Deckungen.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Sinnvolle Empfehlungen",
|
||||
max_points=6,
|
||||
description=RichText(
|
||||
"Leitet die lernende Person sinnvolle und geeignete Empfehlungen ab?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind durchgängig sinnvoll und nachvollziehbar begründet.",
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind grösstenteils sinnvoll und nachvollziehbar begründet.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind wenig sinnvoll und unvollständig begründet.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind weder sinnvoll nch nachvollziehbar begründet.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Qualität der Reflexion",
|
||||
max_points=3,
|
||||
description=RichText(
|
||||
"Reflektiert die lernende Person die Durchführung der geleiteten Fallarbeit?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich auf die geleitete Fallarbeit und umfasst nachvollziehbare positive wie negative Aspekte.",
|
||||
points=3,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich auf die geleitete Fallarbeit und umfasst grösstenteils nachvollziehbare positive wie negative Aspekte.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion ist unvollständig.",
|
||||
points=1,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich nicht auf die geleitete Fallarbeit.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Eignung der Learnings",
|
||||
max_points=3,
|
||||
description=RichText(
|
||||
"Leitet die lernende Person geeignete Learnings aus der Reflexion ab?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich auf die geleitete Fallarbeit und sind inhaltlich sinnvoll.",
|
||||
points=3,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich grösstenteils auf die geleitete Fallarbeit und sind inhaltlich sinnvoll.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich teilweise auf die geleitete Fallarbeit und sind inhaltlich wenig sinnvoll.",
|
||||
points=1,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich nicht auf die geleitete Fallarbeit.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assignment.tasks = []
|
||||
|
|
@ -57,18 +243,24 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
TaskBlockFactory(
|
||||
title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden",
|
||||
# it is hard to create a StreamValue programmatically, we have to
|
||||
# create a `StreamValue` manually. Ask the Daniel and/or Ramon
|
||||
# create a `StreamValue` manually. Ask Daniel and/or Ramon
|
||||
content=StreamValue(
|
||||
TaskContentStreamBlock(),
|
||||
stream_data=[
|
||||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(text="Dies ist ein Beispieltext."),
|
||||
ExplanationBlockFactory(
|
||||
text=RichText(
|
||||
"Bitte jemand aus deiner Familie oder deinem Freundeskreis darum, dir seine/ihre Motorfahrzeugversicherungspolice zur Verfügung zu stellen."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_confirmation",
|
||||
ExplanationBlockFactory(
|
||||
text="Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
text=RichText(
|
||||
"Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -88,12 +280,16 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Erläutere die Kundensituation und die Ausgangslage.
|
||||
* Hast du alle Informationen, die du für den Policen-Check benötigst?
|
||||
* Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.)
|
||||
"""
|
||||
text=RichText(
|
||||
replace_whitespace(
|
||||
"""
|
||||
Erläutere die Kundensituation und die Ausgangslage.
|
||||
<ul>
|
||||
<li>Hast du alle Informationen, die du für den Policen-Check benötigst?</li>
|
||||
<li>Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -116,10 +312,8 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist.
|
||||
"""
|
||||
text=RichText(
|
||||
"Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist."
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -141,40 +335,32 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person.
|
||||
"""
|
||||
text=RichText(
|
||||
"Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung
|
||||
"""
|
||||
text=RichText(
|
||||
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung.
|
||||
"""
|
||||
text=RichText(
|
||||
"Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist.
|
||||
"""
|
||||
text=RichText(
|
||||
"Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist."
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -195,40 +381,32 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:
|
||||
"""
|
||||
text=RichText(
|
||||
"Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung.
|
||||
"""
|
||||
text=RichText(
|
||||
"War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was ist dir bei der Bearbeitung des Auftrags gut gelungen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was ist dir bei der Bearbeitung des Auftrags gut gelungen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -249,30 +427,24 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab.
|
||||
"""
|
||||
text=RichText(
|
||||
"Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was würdest du beim nächsten Mal anders machen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was würdest du beim nächsten Mal anders machen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was hast du beim Bearbeiten des Auftrags Neues gelernt?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was hast du beim Bearbeiten des Auftrags Neues gelernt?"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -284,6 +456,8 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
|
||||
assignment.save()
|
||||
|
||||
return assignment
|
||||
|
||||
|
||||
def create_test_assignment(course_id=COURSE_TEST_ID):
|
||||
course_page = CoursePage.objects.get(course_id=course_id)
|
||||
|
|
@ -318,8 +492,187 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
),
|
||||
),
|
||||
],
|
||||
assessment_document_url="https://www.vbv.ch",
|
||||
assessment_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
||||
evaluation_document_url="/static/media/assignments/UK_03_09_NACH_KN_Beurteilungsraster.pdf",
|
||||
evaluation_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
||||
)
|
||||
|
||||
assignment.evaluation_tasks = []
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Ausgangslage des Auftrags",
|
||||
description=RichText(
|
||||
"Beschreibt der/die Lernende die Ausgangslage des Auftrags vollständig?"
|
||||
),
|
||||
max_points=6,
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage des Auftrag ist vollständig beschrieben.",
|
||||
description=RichText(
|
||||
replace_whitespace(
|
||||
"""
|
||||
<ul>
|
||||
<li>Worum geht es? Was ist die Aufgabe?</li>
|
||||
<li>Sind das Kundenprofil und die Kundenbeziehung vollständig und nachvollziehbar dargestellt?</li>
|
||||
<li>Ist das Alter des Fahrzeugs dokumentiert?</li>
|
||||
<li>Welche Ressourcen stehen zur Verfügung?</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
),
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage ist grösstenteils vollständig beschrieben.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage ist unvollständig - nur 2 Punkte wurden beschrieben.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage des Auftrag ist unvollständig - es fehlen mehr als 2 Punkte in der Beschreibung.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Inhaltsanalyse und Struktur",
|
||||
max_points=6,
|
||||
description=RichText(
|
||||
"Sind die Deckungen der Police vollständig und nachvollziehbar dokumentiert?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse beinhaltet alle in der Police vorhandenen Deckungen und ist logisch aufgebaut.",
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse beinhaltet die meisten vorhandenen Deckungen in der Police und ist grösstenteils logisch aufgebaut.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse ist unvollständig (es fehlen mehr als 3 Deckungen) und der rote Faden ist nicht erkennbar.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse ist insgesamt nicht nachvollziehbar und es fehlen einige Deckungen.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Sinnvolle Empfehlungen",
|
||||
max_points=6,
|
||||
description=RichText(
|
||||
"Leitet die lernende Person sinnvolle und geeignete Empfehlungen ab?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind durchgängig sinnvoll und nachvollziehbar begründet.",
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind grösstenteils sinnvoll und nachvollziehbar begründet.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind wenig sinnvoll und unvollständig begründet.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind weder sinnvoll nch nachvollziehbar begründet.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Qualität der Reflexion",
|
||||
max_points=3,
|
||||
description=RichText(
|
||||
"Reflektiert die lernende Person die Durchführung der geleiteten Fallarbeit?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich auf die geleitete Fallarbeit und umfasst nachvollziehbare positive wie negative Aspekte.",
|
||||
points=3,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich auf die geleitete Fallarbeit und umfasst grösstenteils nachvollziehbare positive wie negative Aspekte.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion ist unvollständig.",
|
||||
points=1,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich nicht auf die geleitete Fallarbeit.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Eignung der Learnings",
|
||||
max_points=3,
|
||||
description=RichText(
|
||||
"Leitet die lernende Person geeignete Learnings aus der Reflexion ab?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich auf die geleitete Fallarbeit und sind inhaltlich sinnvoll.",
|
||||
points=3,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich grösstenteils auf die geleitete Fallarbeit und sind inhaltlich sinnvoll.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich teilweise auf die geleitete Fallarbeit und sind inhaltlich wenig sinnvoll.",
|
||||
points=1,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich nicht auf die geleitete Fallarbeit.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assignment.tasks = []
|
||||
|
|
@ -329,18 +682,24 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
TaskBlockFactory(
|
||||
title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden",
|
||||
# it is hard to create a StreamValue programmatically, we have to
|
||||
# create a `StreamValue` manually. Ask the Daniel and/or Ramon
|
||||
# create a `StreamValue` manually. Ask Daniel and/or Ramon
|
||||
content=StreamValue(
|
||||
TaskContentStreamBlock(),
|
||||
stream_data=[
|
||||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(text="Dies ist ein Beispieltext."),
|
||||
ExplanationBlockFactory(
|
||||
text=RichText(
|
||||
"Bitte jemand aus deiner Familie oder deinem Freundeskreis darum, dir seine/ihre Motorfahrzeugversicherungspolice zur Verfügung zu stellen."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_confirmation",
|
||||
ExplanationBlockFactory(
|
||||
text="Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
text=RichText(
|
||||
"Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -360,12 +719,16 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Erläutere die Kundensituation und die Ausgangslage.
|
||||
* Hast du alle Informationen, die du für den Policen-Check benötigst?
|
||||
* Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.)
|
||||
"""
|
||||
text=RichText(
|
||||
replace_whitespace(
|
||||
"""
|
||||
Erläutere die Kundensituation und die Ausgangslage.
|
||||
<ul>
|
||||
<li>Hast du alle Informationen, die du für den Policen-Check benötigst?</li>
|
||||
<li>Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -388,10 +751,8 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist.
|
||||
"""
|
||||
text=RichText(
|
||||
"Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist."
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -413,40 +774,32 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person.
|
||||
"""
|
||||
text=RichText(
|
||||
"Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung
|
||||
"""
|
||||
text=RichText(
|
||||
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung.
|
||||
"""
|
||||
text=RichText(
|
||||
"Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist.
|
||||
"""
|
||||
text=RichText(
|
||||
"Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist."
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -467,40 +820,32 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:
|
||||
"""
|
||||
text=RichText(
|
||||
"Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung.
|
||||
"""
|
||||
text=RichText(
|
||||
"War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was ist dir bei der Bearbeitung des Auftrags gut gelungen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was ist dir bei der Bearbeitung des Auftrags gut gelungen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -521,30 +866,24 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab.
|
||||
"""
|
||||
text=RichText(
|
||||
"Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was würdest du beim nächsten Mal anders machen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was würdest du beim nächsten Mal anders machen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was hast du beim Bearbeiten des Auftrags Neues gelernt?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was hast du beim Bearbeiten des Auftrags Neues gelernt?"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
# Generated by Django 3.2.13 on 2023-04-11 09:30
|
||||
# Generated by Django 3.2.13 on 2023-05-05 14:10
|
||||
|
||||
import django.db.models.deletion
|
||||
import wagtail.blocks
|
||||
import wagtail.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import vbv_lernwelt.assignment.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("wagtailcore", "0069_log_entry_jsonfield"),
|
||||
("wagtailcore", "0083_workflowcontenttype"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("course", "0006_alter_coursesession_attendance_days"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
@ -33,7 +34,9 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"starting_position",
|
||||
models.TextField(help_text="Erläuterung der Ausgangslage"),
|
||||
wagtail.fields.RichTextField(
|
||||
help_text="Erläuterung der Ausgangslage"
|
||||
),
|
||||
),
|
||||
(
|
||||
"effort_required",
|
||||
|
|
@ -57,20 +60,6 @@ class Migration(migrations.Migration):
|
|||
use_json_field=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"assessment_description",
|
||||
models.TextField(
|
||||
blank=True, help_text="Beschreibung der Bewertung"
|
||||
),
|
||||
),
|
||||
(
|
||||
"assessment_document_url",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="URL zum Beurteilungsinstrument",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"tasks",
|
||||
wagtail.fields.StreamField(
|
||||
|
|
@ -94,14 +83,35 @@ class Migration(migrations.Migration):
|
|||
[
|
||||
(
|
||||
"text",
|
||||
wagtail.blocks.TextBlock(),
|
||||
wagtail.blocks.RichTextBlock(
|
||||
features=[
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
vbv_lernwelt.assignment.models.UserTextInputBlock(),
|
||||
wagtail.blocks.StructBlock(
|
||||
[
|
||||
(
|
||||
"text",
|
||||
wagtail.blocks.RichTextBlock(
|
||||
blank=True,
|
||||
features=[
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
],
|
||||
required=False,
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_confirmation",
|
||||
|
|
@ -109,7 +119,13 @@ class Migration(migrations.Migration):
|
|||
[
|
||||
(
|
||||
"text",
|
||||
wagtail.blocks.TextBlock(),
|
||||
wagtail.blocks.RichTextBlock(
|
||||
features=[
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
|
|
@ -127,6 +143,78 @@ class Migration(migrations.Migration):
|
|||
use_json_field=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_description",
|
||||
wagtail.fields.RichTextField(
|
||||
blank=True, help_text="Beschreibung der Bewertung"
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_document_url",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="URL zum Beurteilungsinstrument",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_tasks",
|
||||
wagtail.fields.StreamField(
|
||||
[
|
||||
(
|
||||
"task",
|
||||
wagtail.blocks.StructBlock(
|
||||
[
|
||||
("title", wagtail.blocks.TextBlock()),
|
||||
(
|
||||
"description",
|
||||
wagtail.blocks.RichTextBlock(
|
||||
blank=True,
|
||||
features=["ul", "bold", "italic"],
|
||||
required=False,
|
||||
),
|
||||
),
|
||||
("max_points", wagtail.blocks.IntegerBlock()),
|
||||
(
|
||||
"sub_tasks",
|
||||
wagtail.blocks.ListBlock(
|
||||
wagtail.blocks.StructBlock(
|
||||
[
|
||||
(
|
||||
"title",
|
||||
wagtail.blocks.TextBlock(),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
wagtail.blocks.RichTextBlock(
|
||||
blank=True,
|
||||
features=[
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
],
|
||||
required=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"points",
|
||||
wagtail.blocks.IntegerBlock(),
|
||||
),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
use_json_field=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
],
|
||||
blank=True,
|
||||
help_text="Beurteilungsschritte",
|
||||
use_json_field=True,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Auftrag",
|
||||
|
|
@ -153,4 +241,155 @@ class Migration(migrations.Migration):
|
|||
},
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AssignmentCompletionAuditLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"completion_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(1, "in_progress"),
|
||||
(2, "submitted"),
|
||||
(3, "evaluation_in_progress"),
|
||||
(4, "evaluation_submitted"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("completion_data", models.JSONField(default=dict)),
|
||||
("additional_json_data", models.JSONField(default=dict)),
|
||||
("assignment_user_email", models.CharField(max_length=255)),
|
||||
("assignment_slug", models.CharField(max_length=255)),
|
||||
(
|
||||
"evaluation_user_email",
|
||||
models.CharField(blank=True, default="", max_length=255),
|
||||
),
|
||||
("evaluation_grade", models.FloatField(blank=True, null=True)),
|
||||
("evaluation_points", models.FloatField(blank=True, null=True)),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="assignment.assignment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"assignment_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"course_session",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="course.coursesession",
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AssignmentCompletion",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("submitted_at", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"evaluation_submitted_at",
|
||||
models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
("evaluation_grade", models.FloatField(blank=True, null=True)),
|
||||
("evaluation_points", models.FloatField(blank=True, null=True)),
|
||||
(
|
||||
"completion_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(1, "in_progress"),
|
||||
(2, "submitted"),
|
||||
(3, "evaluation_in_progress"),
|
||||
(4, "evaluation_submitted"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("completion_data", models.JSONField(default=dict)),
|
||||
("additional_json_data", models.JSONField(default=dict)),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="assignment.assignment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"assignment_user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"course_session",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="course.coursesession",
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="assignmentcompletion",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("assignment_user", "assignment", "course_session"),
|
||||
name="assignment_completion_unique_user_assignment_course_session",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
# Generated by Django 3.2.13 on 2023-04-25 06:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("course", "0004_coursesession_assignment_details_list"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("assignment", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AssignmentCompletionAuditLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"completion_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(1, "in_progress"),
|
||||
(2, "submitted"),
|
||||
(3, "grading_in_progress"),
|
||||
(4, "graded"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("completion_data", models.JSONField(default=dict)),
|
||||
("additional_json_data", models.JSONField(default=dict)),
|
||||
("assignment_user_email", models.CharField(max_length=255)),
|
||||
("assignment_slug", models.CharField(max_length=255)),
|
||||
(
|
||||
"grading_user_email",
|
||||
models.CharField(blank=True, default="", max_length=255),
|
||||
),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="assignment.assignment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"assignment_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"course_session",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="course.coursesession",
|
||||
),
|
||||
),
|
||||
(
|
||||
"grading_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AssignmentCompletion",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("submitted_at", models.DateTimeField(blank=True, null=True)),
|
||||
("graded_at", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"completion_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(1, "in_progress"),
|
||||
(2, "submitted"),
|
||||
(3, "grading_in_progress"),
|
||||
(4, "graded"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("completion_data", models.JSONField(default=dict)),
|
||||
("additional_json_data", models.JSONField(default=dict)),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="assignment.assignment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"assignment_user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"course_session",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="course.coursesession",
|
||||
),
|
||||
),
|
||||
(
|
||||
"grading_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="assignmentcompletion",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("assignment_user", "assignment", "course_session"),
|
||||
name="assignment_completion_unique_user_assignment_course_session",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -5,9 +5,10 @@ from django.db.models import UniqueConstraint
|
|||
from slugify import slugify
|
||||
from wagtail import blocks
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.fields import StreamField
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.models import Page
|
||||
|
||||
from vbv_lernwelt.core.constants import DEFAULT_RICH_TEXT_FEATURES
|
||||
from vbv_lernwelt.core.model_utils import find_available_slug
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseBasePage
|
||||
|
|
@ -28,12 +29,8 @@ class AssignmentListPage(CourseBasePage):
|
|||
return f"{self.title}"
|
||||
|
||||
|
||||
# class AssignmentSubmission(modModel):
|
||||
# created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class ExplanationBlock(blocks.StructBlock):
|
||||
text = blocks.TextBlock()
|
||||
text = blocks.RichTextBlock(features=DEFAULT_RICH_TEXT_FEATURES)
|
||||
|
||||
class Meta:
|
||||
icon = "comment"
|
||||
|
|
@ -47,14 +44,16 @@ class PerformanceObjectiveBlock(blocks.StructBlock):
|
|||
|
||||
|
||||
class UserTextInputBlock(blocks.StructBlock):
|
||||
text = blocks.TextBlock(blank=True)
|
||||
text = blocks.RichTextBlock(
|
||||
blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
icon = "edit"
|
||||
|
||||
|
||||
class UserConfirmationBlock(blocks.StructBlock):
|
||||
text = blocks.TextBlock()
|
||||
text = blocks.RichTextBlock(features=DEFAULT_RICH_TEXT_FEATURES)
|
||||
|
||||
class Meta:
|
||||
icon = "tick-inverse"
|
||||
|
|
@ -78,17 +77,47 @@ class TaskBlock(blocks.StructBlock):
|
|||
label = "Teilauftrag"
|
||||
|
||||
|
||||
class EvaluationSubTaskBlock(blocks.StructBlock):
|
||||
title = blocks.TextBlock()
|
||||
description = blocks.RichTextBlock(
|
||||
blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES
|
||||
)
|
||||
points = blocks.IntegerBlock()
|
||||
|
||||
class Meta:
|
||||
icon = "tick"
|
||||
label = "Beurteilung"
|
||||
|
||||
|
||||
class EvaluationTaskBlock(blocks.StructBlock):
|
||||
title = blocks.TextBlock()
|
||||
description = blocks.RichTextBlock(
|
||||
blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES
|
||||
)
|
||||
max_points = blocks.IntegerBlock()
|
||||
sub_tasks = blocks.ListBlock(
|
||||
EvaluationSubTaskBlock(), blank=True, use_json_field=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
icon = "tasks"
|
||||
label = "Beurteilungskriterium"
|
||||
|
||||
|
||||
class Assignment(CourseBasePage):
|
||||
serialize_field_names = [
|
||||
"starting_position",
|
||||
"effort_required",
|
||||
"performance_objectives",
|
||||
"assessment_description",
|
||||
"assessment_document_url",
|
||||
"evaluation_description",
|
||||
"evaluation_document_url",
|
||||
"tasks",
|
||||
"evaluation_tasks",
|
||||
]
|
||||
|
||||
starting_position = models.TextField(help_text="Erläuterung der Ausgangslage")
|
||||
starting_position = RichTextField(
|
||||
help_text="Erläuterung der Ausgangslage", features=DEFAULT_RICH_TEXT_FEATURES
|
||||
)
|
||||
effort_required = models.CharField(
|
||||
max_length=100, help_text="Zeitaufwand als Text", blank=True
|
||||
)
|
||||
|
|
@ -101,14 +130,6 @@ class Assignment(CourseBasePage):
|
|||
blank=True,
|
||||
help_text="Leistungsziele des Auftrags",
|
||||
)
|
||||
assessment_description = models.TextField(
|
||||
blank=True, help_text="Beschreibung der Bewertung"
|
||||
)
|
||||
assessment_document_url = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="URL zum Beurteilungsinstrument",
|
||||
)
|
||||
|
||||
tasks = StreamField(
|
||||
[
|
||||
|
|
@ -119,13 +140,34 @@ class Assignment(CourseBasePage):
|
|||
help_text="Teilaufgaben",
|
||||
)
|
||||
|
||||
evaluation_description = RichTextField(
|
||||
blank=True,
|
||||
help_text="Beschreibung der Bewertung",
|
||||
features=DEFAULT_RICH_TEXT_FEATURES,
|
||||
)
|
||||
evaluation_document_url = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="URL zum Beurteilungsinstrument",
|
||||
)
|
||||
|
||||
evaluation_tasks = StreamField(
|
||||
[
|
||||
("task", EvaluationTaskBlock()),
|
||||
],
|
||||
use_json_field=True,
|
||||
blank=True,
|
||||
help_text="Beurteilungsschritte",
|
||||
)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("starting_position"),
|
||||
FieldPanel("effort_required"),
|
||||
FieldPanel("performance_objectives"),
|
||||
FieldPanel("assessment_description"),
|
||||
FieldPanel("assessment_document_url"),
|
||||
FieldPanel("tasks"),
|
||||
FieldPanel("evaluation_description"),
|
||||
FieldPanel("evaluation_document_url"),
|
||||
FieldPanel("evaluation_tasks"),
|
||||
]
|
||||
|
||||
subpage_types = []
|
||||
|
|
@ -172,10 +214,16 @@ class Assignment(CourseBasePage):
|
|||
if sub_dict["type"] in subtask_types
|
||||
]
|
||||
|
||||
def get_evaluation_tasks(self):
|
||||
return [task for task in self.evaluation_tasks.raw_data]
|
||||
|
||||
def get_input_tasks(self):
|
||||
return self.filter_user_subtasks() + self.get_evaluation_tasks()
|
||||
|
||||
|
||||
AssignmentCompletionStatus = Enum(
|
||||
"AssignmentCompletionStatus",
|
||||
["in_progress", "submitted", "grading_in_progress", "graded"],
|
||||
["in_progress", "submitted", "evaluation_in_progress", "evaluation_submitted"],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -184,14 +232,16 @@ class AssignmentCompletion(models.Model):
|
|||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
submitted_at = models.DateTimeField(null=True, blank=True)
|
||||
graded_at = models.DateTimeField(null=True, blank=True)
|
||||
grading_user = models.ForeignKey(
|
||||
evaluation_submitted_at = models.DateTimeField(null=True, blank=True)
|
||||
evaluation_user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="+",
|
||||
)
|
||||
evaluation_grade = models.FloatField(null=True, blank=True)
|
||||
evaluation_points = models.FloatField(null=True, blank=True)
|
||||
|
||||
assignment_user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
|
||||
|
|
@ -217,12 +267,12 @@ class AssignmentCompletion(models.Model):
|
|||
|
||||
class AssignmentCompletionAuditLog(models.Model):
|
||||
"""
|
||||
This model is used to store the "submitted" and "graded" data separately
|
||||
This model is used to store the "submitted" and "evaluation_submitted" data separately
|
||||
"""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
grading_user = models.ForeignKey(
|
||||
evaluation_user = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, null=True, blank=True, related_name="+"
|
||||
)
|
||||
assignment_user = models.ForeignKey(
|
||||
|
|
@ -246,4 +296,6 @@ class AssignmentCompletionAuditLog(models.Model):
|
|||
|
||||
assignment_user_email = models.CharField(max_length=255)
|
||||
assignment_slug = models.CharField(max_length=255)
|
||||
grading_user_email = models.CharField(max_length=255, blank=True, default="")
|
||||
evaluation_user_email = models.CharField(max_length=255, blank=True, default="")
|
||||
evaluation_grade = models.FloatField(null=True, blank=True)
|
||||
evaluation_points = models.FloatField(null=True, blank=True)
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@ class AssignmentCompletionSerializer(serializers.ModelSerializer):
|
|||
"created_at",
|
||||
"updated_at",
|
||||
"submitted_at",
|
||||
"graded_at",
|
||||
"evaluation_submitted_at",
|
||||
"assignment_user",
|
||||
"assignment",
|
||||
"course_session",
|
||||
"completion_status",
|
||||
"completion_data",
|
||||
"grading_user",
|
||||
"evaluation_user",
|
||||
"additional_json_data",
|
||||
"evaluation_grade",
|
||||
"evaluation_points",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ def update_assignment_completion(
|
|||
course_session: CourseSession,
|
||||
completion_data=None,
|
||||
completion_status: Type[AssignmentCompletionStatus] = "in_progress",
|
||||
grading_user: User | None = None,
|
||||
evaluation_user: User | None = None,
|
||||
evaluation_grade: float | None = None,
|
||||
evaluation_points: float | None = None,
|
||||
validate_completion_status_change: bool = True,
|
||||
copy_task_data: bool = False,
|
||||
) -> AssignmentCompletion:
|
||||
|
|
@ -40,7 +42,7 @@ def update_assignment_completion(
|
|||
},
|
||||
}
|
||||
:param copy_task_data: if true, the task data will be copied to the completion data
|
||||
used for "submitted" and "graded" status, so that we don't lose the question
|
||||
used for "submitted" and "evaluation_submitted" status, so that we don't lose the question
|
||||
context
|
||||
:return: AssignmentCompletion
|
||||
"""
|
||||
|
|
@ -56,31 +58,61 @@ def update_assignment_completion(
|
|||
if validate_completion_status_change:
|
||||
# TODO: check time?
|
||||
if completion_status == "submitted":
|
||||
if ac.completion_status in ["submitted", "grading_in_progress", "graded"]:
|
||||
if ac.completion_status in [
|
||||
"submitted",
|
||||
"evaluation_in_progress",
|
||||
"evaluation_submitted",
|
||||
]:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"completion_status": f"Cannot update completion status from {ac.completion_status} to submitted"
|
||||
}
|
||||
)
|
||||
elif completion_status == "graded":
|
||||
if ac.completion_status == "graded":
|
||||
elif completion_status == "evaluation_submitted":
|
||||
if ac.completion_status == "evaluation_submitted":
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"completion_status": f"Cannot update completion status from {ac.completion_status} to graded"
|
||||
"completion_status": f"Cannot update completion status from {ac.completion_status} to evaluation_submitted"
|
||||
}
|
||||
)
|
||||
|
||||
if completion_status in ["graded", "grading_in_progress"]:
|
||||
if grading_user is None:
|
||||
if completion_status == "in_progress" and ac.completion_status != "in_progress":
|
||||
raise serializers.ValidationError(
|
||||
{"grading_user": "grading_user is required for graded status"}
|
||||
{
|
||||
"completion_status": f"Cannot set completion status to in_progress when it is {ac.completion_status}"
|
||||
}
|
||||
)
|
||||
ac.grading_user = grading_user
|
||||
|
||||
if completion_status in ["evaluation_submitted", "evaluation_in_progress"]:
|
||||
if evaluation_user is None:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"evaluation_user": "evaluation_user is required for evaluation_submitted status"
|
||||
}
|
||||
)
|
||||
ac.evaluation_user = evaluation_user
|
||||
|
||||
if completion_status == "evaluation_submitted":
|
||||
if evaluation_grade is None:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"evaluation_grade": "evaluation_grade is required for evaluation_submitted status"
|
||||
}
|
||||
)
|
||||
if evaluation_points is None:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"evaluation_points": "evaluation_points is required for evaluation_submitted status"
|
||||
}
|
||||
)
|
||||
|
||||
ac.evaluation_grade = evaluation_grade
|
||||
ac.evaluation_points = evaluation_points
|
||||
|
||||
if completion_status == "submitted":
|
||||
ac.submitted_at = timezone.now()
|
||||
elif completion_status == "graded":
|
||||
ac.graded_at = timezone.now()
|
||||
elif completion_status == "evaluation_submitted":
|
||||
ac.evaluation_submitted_at = timezone.now()
|
||||
|
||||
ac.completion_status = completion_status
|
||||
|
||||
|
|
@ -94,32 +126,34 @@ def update_assignment_completion(
|
|||
|
||||
if copy_task_data:
|
||||
# copy over the question data, so that we don't lose the context
|
||||
substasks = assignment.filter_user_subtasks()
|
||||
substasks = assignment.get_input_tasks()
|
||||
for key, value in ac.completion_data.items():
|
||||
task_data = find_first(substasks, pred=lambda x: x["id"] == key)
|
||||
ac.completion_data[key].update(task_data)
|
||||
if task_data:
|
||||
ac.completion_data[key].update(task_data)
|
||||
|
||||
ac.save()
|
||||
|
||||
if completion_status in ["graded", "submitted"]:
|
||||
if completion_status in ["evaluation_submitted", "submitted"]:
|
||||
acl = AssignmentCompletionAuditLog.objects.create(
|
||||
assignment_user=assignment_user,
|
||||
assignment=assignment,
|
||||
course_session=course_session,
|
||||
grading_user=grading_user,
|
||||
evaluation_user=evaluation_user,
|
||||
completion_status=completion_status,
|
||||
assignment_user_email=assignment_user.email,
|
||||
assignment_slug=assignment.slug,
|
||||
completion_data=deepcopy(ac.completion_data),
|
||||
)
|
||||
if grading_user:
|
||||
acl.grading_user_email = grading_user.email
|
||||
if evaluation_user:
|
||||
acl.evaluation_user_email = evaluation_user.email
|
||||
|
||||
# copy over the question data, so that we don't lose the context
|
||||
substasks = assignment.filter_user_subtasks()
|
||||
subtasks = assignment.get_input_tasks()
|
||||
for key, value in acl.completion_data.items():
|
||||
task_data = find_first(substasks, pred=lambda x: x["id"] == key)
|
||||
acl.completion_data[key].update(task_data)
|
||||
task_data = find_first(subtasks, pred=lambda x: x["id"] == key)
|
||||
if task_data:
|
||||
acl.completion_data[key].update(task_data)
|
||||
acl.save()
|
||||
|
||||
return ac
|
||||
|
|
@ -129,12 +163,8 @@ def _remove_unknown_entries(assignment, completion_data):
|
|||
"""
|
||||
Removes all entries from completion_data which are not known to the assignment
|
||||
"""
|
||||
possible_subtask_uuids = [
|
||||
subtask["id"] for subtask in assignment.filter_user_subtasks()
|
||||
]
|
||||
input_task_ids = [task["id"] for task in assignment.get_input_tasks()]
|
||||
filtered_completion_data = {
|
||||
key: value
|
||||
for key, value in completion_data.items()
|
||||
if key in possible_subtask_uuids
|
||||
key: value for key, value in completion_data.items() if key in input_task_ids
|
||||
}
|
||||
return filtered_completion_data
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import wagtail_factories
|
||||
from factory import SubFactory
|
||||
from wagtail.rich_text import RichText
|
||||
|
||||
from vbv_lernwelt.assignment.models import (
|
||||
Assignment,
|
||||
AssignmentListPage,
|
||||
EvaluationSubTaskBlock,
|
||||
EvaluationTaskBlock,
|
||||
ExplanationBlock,
|
||||
PerformanceObjectiveBlock,
|
||||
TaskBlock,
|
||||
|
|
@ -15,14 +18,16 @@ from vbv_lernwelt.core.utils import replace_whitespace
|
|||
|
||||
|
||||
class ExplanationBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
text = "Dies ist ein Beispieltext."
|
||||
text = RichText("Dies ist ein Beispieltext.")
|
||||
|
||||
class Meta:
|
||||
model = ExplanationBlock
|
||||
|
||||
|
||||
class UserConfirmationBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
text = "Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
text = RichText(
|
||||
"Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = UserConfirmationBlock
|
||||
|
|
@ -50,6 +55,24 @@ class TaskBlockFactory(wagtail_factories.StructBlockFactory):
|
|||
model = TaskBlock
|
||||
|
||||
|
||||
class EvaluationSubTaskBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
title = "Beurteilung"
|
||||
description = RichText("")
|
||||
points = 6
|
||||
|
||||
class Meta:
|
||||
model = EvaluationSubTaskBlock
|
||||
|
||||
|
||||
class EvaluationTaskBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
title = "Beurteilungskriterum"
|
||||
description = RichText("")
|
||||
max_points = 6
|
||||
|
||||
class Meta:
|
||||
model = EvaluationTaskBlock
|
||||
|
||||
|
||||
class PerformanceObjectiveBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
text = "Die Teilnehmer können die wichtigsten Eckwerte eines Versicherungsverhältnisses erfassen."
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
|
||||
# make api call
|
||||
self.client.login(username="admin", password="test")
|
||||
url = f"/api/assignment/grade/"
|
||||
url = f"/api/assignment/evaluate/"
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
|
|
@ -181,7 +181,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
"assignment_id": self.assignment.id,
|
||||
"assignment_user_id": self.student.id,
|
||||
"course_session_id": self.cs.id,
|
||||
"completion_status": "grading_in_progress",
|
||||
"completion_status": "evaluation_in_progress",
|
||||
"completion_data": {
|
||||
user_text_input["id"]: {
|
||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||
|
|
@ -196,7 +196,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response_json["assignment_user"], self.student.id)
|
||||
self.assertEqual(response_json["assignment"], self.assignment.id)
|
||||
self.assertEqual(response_json["completion_status"], "grading_in_progress")
|
||||
self.assertEqual(response_json["completion_status"], "evaluation_in_progress")
|
||||
self.assertDictEqual(
|
||||
response_json["completion_data"],
|
||||
{
|
||||
|
|
@ -212,7 +212,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
course_session_id=self.cs.id,
|
||||
assignment_id=self.assignment.id,
|
||||
)
|
||||
self.assertEqual(db_entry.completion_status, "grading_in_progress")
|
||||
self.assertEqual(db_entry.completion_status, "evaluation_in_progress")
|
||||
self.assertDictEqual(
|
||||
db_entry.completion_data,
|
||||
{
|
||||
|
|
@ -230,12 +230,14 @@ class AssignmentApiTestCase(APITestCase):
|
|||
"assignment_id": self.assignment.id,
|
||||
"assignment_user_id": self.student.id,
|
||||
"course_session_id": self.cs.id,
|
||||
"completion_status": "graded",
|
||||
"completion_status": "evaluation_submitted",
|
||||
"completion_data": {
|
||||
user_text_input["id"]: {
|
||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||
},
|
||||
},
|
||||
"evaluation_grade": 4.5,
|
||||
"evaluation_points": 16,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
|
@ -245,7 +247,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response_json["assignment_user"], self.student.id)
|
||||
self.assertEqual(response_json["assignment"], self.assignment.id)
|
||||
self.assertEqual(response_json["completion_status"], "graded")
|
||||
self.assertEqual(response_json["completion_status"], "evaluation_submitted")
|
||||
self.assertDictEqual(
|
||||
response_json["completion_data"],
|
||||
{
|
||||
|
|
@ -261,7 +263,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
course_session_id=self.cs.id,
|
||||
assignment_id=self.assignment.id,
|
||||
)
|
||||
self.assertEqual(db_entry.completion_status, "graded")
|
||||
self.assertEqual(db_entry.completion_status, "evaluation_submitted")
|
||||
self.assertDictEqual(
|
||||
db_entry.completion_data,
|
||||
{
|
||||
|
|
@ -272,12 +274,12 @@ class AssignmentApiTestCase(APITestCase):
|
|||
},
|
||||
)
|
||||
|
||||
# `graded` will create a new AssignmentCompletionAuditLog
|
||||
# `evaluation_submitted` will create a new AssignmentCompletionAuditLog
|
||||
acl = AssignmentCompletionAuditLog.objects.get(
|
||||
assignment_user=self.student,
|
||||
course_session_id=self.cs.id,
|
||||
assignment_id=self.assignment.id,
|
||||
completion_status="graded",
|
||||
completion_status="evaluation_submitted",
|
||||
)
|
||||
self.maxDiff = None
|
||||
self.assertDictEqual(
|
||||
|
|
|
|||
|
|
@ -296,7 +296,42 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
)
|
||||
)
|
||||
|
||||
def test_can_add_grading_data_without_loosing_user_input_data(self):
|
||||
def test_can_evaluate_with_evaluation_tasks(self):
|
||||
ac = AssignmentCompletion.objects.create(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_status="submitted",
|
||||
)
|
||||
|
||||
evaluation_task = self.assignment.get_evaluation_tasks()[0]
|
||||
|
||||
update_assignment_completion(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_data={
|
||||
evaluation_task["id"]: {
|
||||
"expert_data": {"points": 2, "text": "Gut gemacht!"}
|
||||
},
|
||||
},
|
||||
completion_status="evaluation_in_progress",
|
||||
evaluation_user=self.trainer,
|
||||
)
|
||||
|
||||
ac = AssignmentCompletion.objects.get(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
)
|
||||
|
||||
self.assertEqual(ac.completion_status, "evaluation_in_progress")
|
||||
trainer_input = ac.completion_data[evaluation_task["id"]]
|
||||
self.assertDictEqual(
|
||||
trainer_input["expert_data"], {"points": 2, "text": "Gut gemacht!"}
|
||||
)
|
||||
|
||||
def test_can_add_evaluation_data_without_loosing_user_input_data(self):
|
||||
subtasks = self.assignment.filter_user_subtasks(
|
||||
subtask_types=["user_text_input"]
|
||||
)
|
||||
|
|
@ -329,8 +364,8 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||
},
|
||||
},
|
||||
completion_status="grading_in_progress",
|
||||
grading_user=self.trainer,
|
||||
completion_status="evaluation_in_progress",
|
||||
evaluation_user=self.trainer,
|
||||
)
|
||||
|
||||
ac = AssignmentCompletion.objects.get(
|
||||
|
|
@ -339,7 +374,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
course_session=self.course_session,
|
||||
)
|
||||
|
||||
self.assertEqual(ac.completion_status, "grading_in_progress")
|
||||
self.assertEqual(ac.completion_status, "evaluation_in_progress")
|
||||
user_input = ac.completion_data[user_text_input["id"]]
|
||||
self.assertDictEqual(
|
||||
user_input["expert_data"], {"points": 1, "comment": "Gut gemacht!"}
|
||||
|
|
@ -348,7 +383,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
user_input["user_data"]["text"], "Ich würde nichts weiteres empfehlen."
|
||||
)
|
||||
|
||||
def test_cannot_grading_data_without_grading_user(self):
|
||||
def test_cannot_evaluate_data_without_evaluation_user(self):
|
||||
subtasks = self.assignment.filter_user_subtasks(
|
||||
subtask_types=["user_text_input"]
|
||||
)
|
||||
|
|
@ -377,7 +412,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_status="grading_in_progress",
|
||||
completion_status="evaluation_in_progress",
|
||||
completion_data={
|
||||
user_text_input["id"]: {
|
||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||
|
|
@ -386,5 +421,122 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
)
|
||||
|
||||
self.assertTrue(
|
||||
"grading_user" in error.exception.detail,
|
||||
"evaluation_user" in error.exception.detail,
|
||||
)
|
||||
|
||||
def test_can_submit_evaluation(self):
|
||||
subtasks = self.assignment.filter_user_subtasks(
|
||||
subtask_types=["user_text_input"]
|
||||
)
|
||||
user_text_input = find_first(
|
||||
subtasks,
|
||||
pred=lambda x: (value := x.get("value"))
|
||||
and value.get("text", "").startswith(
|
||||
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest?"
|
||||
),
|
||||
)
|
||||
|
||||
ac = AssignmentCompletion.objects.create(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_status="submitted",
|
||||
completion_data={
|
||||
user_text_input["id"]: {
|
||||
"user_data": {"text": "Ich würde nichts weiteres empfehlen."}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
evaluation_task = self.assignment.get_evaluation_tasks()[0]
|
||||
|
||||
update_assignment_completion(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_data={
|
||||
evaluation_task["id"]: {
|
||||
"expert_data": {"points": 2, "text": "Gut gemacht!"}
|
||||
},
|
||||
},
|
||||
completion_status="evaluation_in_progress",
|
||||
evaluation_user=self.trainer,
|
||||
)
|
||||
|
||||
with self.assertRaises(serializers.ValidationError) as error:
|
||||
# not setting grade will raise an error
|
||||
update_assignment_completion(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_data={},
|
||||
completion_status="evaluation_submitted",
|
||||
evaluation_user=self.trainer,
|
||||
evaluation_grade=None,
|
||||
evaluation_points=None,
|
||||
)
|
||||
|
||||
update_assignment_completion(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_data={},
|
||||
completion_status="evaluation_submitted",
|
||||
evaluation_user=self.trainer,
|
||||
evaluation_grade=4.5,
|
||||
evaluation_points=16,
|
||||
)
|
||||
|
||||
ac = AssignmentCompletion.objects.get(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
)
|
||||
|
||||
self.assertEqual(ac.completion_status, "evaluation_submitted")
|
||||
self.assertEqual(ac.evaluation_grade, 4.5)
|
||||
self.assertEqual(ac.evaluation_points, 16)
|
||||
trainer_input = ac.completion_data[evaluation_task["id"]]
|
||||
self.assertDictEqual(
|
||||
trainer_input["expert_data"], {"points": 2, "text": "Gut gemacht!"}
|
||||
)
|
||||
user_input = ac.completion_data[user_text_input["id"]]
|
||||
self.assertDictEqual(
|
||||
user_input["user_data"], {"text": "Ich würde nichts weiteres empfehlen."}
|
||||
)
|
||||
|
||||
# will create AssignmentCompletionAuditLog entry
|
||||
acl = AssignmentCompletionAuditLog.objects.get(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_status="evaluation_submitted",
|
||||
)
|
||||
self.assertEqual(acl.created_at.date(), date.today())
|
||||
self.assertEqual(acl.assignment_user_email, "student")
|
||||
self.assertEqual(
|
||||
acl.assignment_slug,
|
||||
"versicherungsvermittler-in-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice",
|
||||
)
|
||||
trainer_input = acl.completion_data[evaluation_task["id"]]
|
||||
self.assertDictEqual(
|
||||
trainer_input["expert_data"], {"points": 2, "text": "Gut gemacht!"}
|
||||
)
|
||||
user_input = acl.completion_data[user_text_input["id"]]
|
||||
self.assertDictEqual(
|
||||
user_input["user_data"], {"text": "Ich würde nichts weiteres empfehlen."}
|
||||
)
|
||||
|
||||
# AssignmentCompletionAuditLog entry will remain event after deletion of foreign keys
|
||||
ac.delete()
|
||||
self.user.delete()
|
||||
self.assignment.delete()
|
||||
acl = AssignmentCompletionAuditLog.objects.get(id=acl.id)
|
||||
self.assertEqual(acl.created_at.date(), date.today())
|
||||
self.assertEqual(acl.assignment_user_email, "student")
|
||||
self.assertEqual(
|
||||
acl.assignment_slug,
|
||||
"versicherungsvermittler-in-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice",
|
||||
)
|
||||
self.assertIsNone(acl.assignment_user)
|
||||
self.assertIsNone(acl.assignment)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,19 @@ def request_assignment_completion_for_user(
|
|||
raise PermissionDenied()
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def request_assignment_completion_status(request, assignment_id, course_session_id):
|
||||
# TODO quickfix before GraphQL...
|
||||
if is_course_session_expert(request.user, course_session_id):
|
||||
qs = AssignmentCompletion.objects.filter(
|
||||
course_session_id=course_session_id,
|
||||
assignment_id=assignment_id,
|
||||
).values("id", "assignment_user_id", "completion_status", "evaluation_grade")
|
||||
return Response(status=200, data=qs)
|
||||
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def upsert_user_assignment_completion(request):
|
||||
try:
|
||||
|
|
@ -107,13 +120,17 @@ def upsert_user_assignment_completion(request):
|
|||
|
||||
|
||||
@api_view(["POST"])
|
||||
def grade_assignment_completion(request):
|
||||
def evaluate_assignment_completion(request):
|
||||
try:
|
||||
assignment_id = request.data.get("assignment_id")
|
||||
assignment_user_id = request.data.get("assignment_user_id")
|
||||
course_session_id = request.data.get("course_session_id")
|
||||
completion_status = request.data.get("completion_status", "grading_in_progress")
|
||||
completion_status = request.data.get(
|
||||
"completion_status", "evaluation_in_progress"
|
||||
)
|
||||
completion_data = request.data.get("completion_data", {})
|
||||
evaluation_grade = request.data.get("evaluation_grade", None)
|
||||
evaluation_points = request.data.get("evaluation_grade", None)
|
||||
|
||||
assignment_page = Page.objects.get(id=assignment_id)
|
||||
assignment_user = User.objects.get(id=assignment_user_id)
|
||||
|
|
@ -133,7 +150,9 @@ def grade_assignment_completion(request):
|
|||
completion_data=completion_data,
|
||||
completion_status=completion_status,
|
||||
copy_task_data=False,
|
||||
grading_user=request.user,
|
||||
evaluation_user=request.user,
|
||||
evaluation_grade=evaluation_grade,
|
||||
evaluation_points=evaluation_points,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -144,7 +163,9 @@ def grade_assignment_completion(request):
|
|||
assignment_user_id=assignment_user_id,
|
||||
course_session_id=course_session_id,
|
||||
completion_status=completion_status,
|
||||
grading_user_id=request.user.id,
|
||||
evaluation_user_id=request.user.id,
|
||||
evaluation_grade=evaluation_grade,
|
||||
evaluation_points=evaluation_points,
|
||||
)
|
||||
|
||||
return Response(status=200, data=AssignmentCompletionSerializer(ac).data)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
DEFAULT_RICH_TEXT_FEATURES = [
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
]
|
||||
|
|
@ -176,6 +176,7 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
|
|||
first_name="Lina",
|
||||
last_name="Egger",
|
||||
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
||||
password="myvbv1234",
|
||||
)
|
||||
_create_student_user(
|
||||
email="evelyn.schmid@example.com",
|
||||
|
|
@ -229,6 +230,23 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
|
|||
language="fr",
|
||||
)
|
||||
|
||||
# users for cypress tests
|
||||
_create_student_user(
|
||||
email="test-trainer1@example.com",
|
||||
first_name="Test",
|
||||
last_name="Trainer1",
|
||||
)
|
||||
_create_student_user(
|
||||
email="test-student1@example.com",
|
||||
first_name="Test",
|
||||
last_name="Student1",
|
||||
)
|
||||
_create_student_user(
|
||||
email="test-student2@example.com",
|
||||
first_name="Test",
|
||||
last_name="Student2",
|
||||
)
|
||||
|
||||
|
||||
def _get_or_create_user(user_model, *args, **kwargs):
|
||||
username = kwargs.get("username", None)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import djclick as click
|
||||
|
||||
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseCompletion
|
||||
from vbv_lernwelt.notify.models import Notification
|
||||
|
|
@ -10,4 +11,5 @@ def command():
|
|||
print("cypress reset data")
|
||||
CourseCompletion.objects.all().delete()
|
||||
Notification.objects.all().delete()
|
||||
AssignmentCompletion.objects.all().delete()
|
||||
User.objects.all().update(language="de")
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ def cypress_reset_view(request):
|
|||
if settings.APP_ENVIRONMENT != "production":
|
||||
call_command("cypress_reset")
|
||||
|
||||
return HttpResponseRedirect("/admin/")
|
||||
return HttpResponseRedirect("/server/admin/")
|
||||
|
||||
|
||||
@django_view_authentication_exempt
|
||||
|
|
|
|||
|
|
@ -13,10 +13,18 @@ from vbv_lernwelt.competence.factories import (
|
|||
PerformanceCriteriaFactory,
|
||||
)
|
||||
from vbv_lernwelt.competence.models import CompetencePage
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
|
||||
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
||||
from vbv_lernwelt.course.factories import CoursePageFactory
|
||||
from vbv_lernwelt.course.models import Course, CourseCategory, CoursePage, CourseSession
|
||||
from vbv_lernwelt.course.models import (
|
||||
Course,
|
||||
CourseCategory,
|
||||
CoursePage,
|
||||
CourseSession,
|
||||
CourseSessionUser,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||
AssignmentBlockFactory,
|
||||
AttendanceDayBlockFactory,
|
||||
|
|
@ -55,15 +63,40 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
|||
|
||||
if with_sessions:
|
||||
# course sessions
|
||||
CourseSession.objects.create(
|
||||
cs_bern = CourseSession.objects.create(
|
||||
course_id=COURSE_TEST_ID,
|
||||
title="Bern 2022 a",
|
||||
)
|
||||
CourseSession.objects.create(
|
||||
cs_zurich = CourseSession.objects.create(
|
||||
course_id=COURSE_TEST_ID,
|
||||
title="Zürich 2022 a",
|
||||
)
|
||||
|
||||
trainer1 = User.objects.get(email="test-trainer1@example.com")
|
||||
csu = CourseSessionUser.objects.create(
|
||||
course_session=cs_bern,
|
||||
user=trainer1,
|
||||
role=CourseSessionUser.Role.EXPERT,
|
||||
)
|
||||
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
||||
|
||||
student1 = User.objects.get(email="test-student1@example.com")
|
||||
_csu = CourseSessionUser.objects.create(
|
||||
course_session=cs_bern,
|
||||
user=student1,
|
||||
)
|
||||
|
||||
student2 = User.objects.get(email="test-student2@example.com")
|
||||
_csu = CourseSessionUser.objects.create(
|
||||
course_session=cs_bern,
|
||||
user=student2,
|
||||
)
|
||||
student2 = User.objects.get(email="test-student2@example.com")
|
||||
_csu = CourseSessionUser.objects.create(
|
||||
course_session=cs_zurich,
|
||||
user=student2,
|
||||
)
|
||||
|
||||
return course
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ def command(course):
|
|||
slug="überbetriebliche-kurse-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
||||
),
|
||||
course_session=CourseSession.objects.get(title="Bern 2023 a"),
|
||||
user=User.objects.get(email="michael.meier@example.com"),
|
||||
user=User.objects.get(email="lina.egger@example.com"),
|
||||
)
|
||||
|
||||
if COURSE_UK_FR in course:
|
||||
|
|
@ -166,7 +166,8 @@ def create_course_uk_de():
|
|||
"learningContentId": LearningContent.objects.get(
|
||||
slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||
).id,
|
||||
"deadlineDateTimeUtc": "2023-05-30T19:00:00Z",
|
||||
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
|
||||
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
|
@ -320,6 +321,12 @@ def create_course_uk_de_assignment_completion_data(assignment, course_session, u
|
|||
}
|
||||
},
|
||||
)
|
||||
update_assignment_completion(
|
||||
assignment_user=user,
|
||||
assignment=assignment,
|
||||
course_session=course_session,
|
||||
completion_status="submitted",
|
||||
)
|
||||
|
||||
|
||||
def create_course_uk_de_completion_data(course_session):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.13 on 2023-04-26 16:28
|
||||
|
||||
import django_jsonform.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("course", "0005_alter_coursesession_attendance_days"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="coursesession",
|
||||
name="attendance_days",
|
||||
field=django_jsonform.models.fields.JSONField(blank=True, default=list),
|
||||
),
|
||||
]
|
||||
|
|
@ -149,12 +149,9 @@ def get_course_sessions(request):
|
|||
|
||||
|
||||
@api_view(["GET"])
|
||||
def get_course_session_users(request, course_slug):
|
||||
def get_course_session_users(request, course_session_id):
|
||||
try:
|
||||
course_sessions = course_sessions_for_user_qs(request.user).filter(
|
||||
course__slug=course_slug
|
||||
)
|
||||
qs = CourseSessionUser.objects.filter(course_session__in=course_sessions)
|
||||
qs = CourseSessionUser.objects.filter(course_session_id=course_session_id)
|
||||
|
||||
user_data = [csu.to_dict() for csu in qs]
|
||||
return Response(status=200, data=user_data)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -9,10 +9,6 @@
|
|||
{% csrf_token %}
|
||||
<button class="btn" name="">Testdaten zurück setzen</button>
|
||||
</form>
|
||||
<form action="/api/core/schemareset/" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="btn" name="">Datenbank zurück setzen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue