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:
|
script:
|
||||||
- echo "Release ready!"
|
- echo "Release ready!"
|
||||||
- step:
|
- step:
|
||||||
|
<<: *deploy
|
||||||
name: deploy prod
|
name: deploy prod
|
||||||
deployment: prod
|
deployment: prod
|
||||||
trigger: manual
|
trigger: manual
|
||||||
script:
|
|
||||||
- source ./env/bitbucket/prepare_for_deployment.sh
|
|
||||||
- ./caprover_deploy.sh myvbv
|
|
||||||
custom:
|
custom:
|
||||||
deploy-stage:
|
deploy-stage:
|
||||||
- step:
|
- step:
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"@sentry/vue": "^7.20.0",
|
"@sentry/vue": "^7.20.0",
|
||||||
"@urql/vue": "^1.0.2",
|
"@urql/vue": "^1.0.2",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
|
"@vueuse/router": "^10.1.2",
|
||||||
"cypress": "^12.9.0",
|
"cypress": "^12.9.0",
|
||||||
"d3": "^7.6.1",
|
"d3": "^7.6.1",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
|
|
@ -25,7 +26,7 @@
|
||||||
"vue": "^3.2.38",
|
"vue": "^3.2.38",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-i18n-extract": "^2.0.7",
|
"vue-i18n-extract": "^2.0.7",
|
||||||
"vue-router": "^4.1.5"
|
"vue-router": "^4.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^2.13.12",
|
"@graphql-codegen/cli": "^2.13.12",
|
||||||
|
|
@ -7489,6 +7490,32 @@
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"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": {
|
"node_modules/@vueuse/shared": {
|
||||||
"version": "9.13.0",
|
"version": "9.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||||
|
|
@ -18979,9 +19006,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-demi": {
|
"node_modules/vue-demi": {
|
||||||
"version": "0.13.11",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.0.tgz",
|
||||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
"integrity": "sha512-gt58r2ogsNQeVoQ3EhoUAvUsH9xviydl0dWJj7dabBC/2L4uBId7ujtCwDRD0JhkGsV1i0CtfLAeyYKBht9oWg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
|
@ -25294,6 +25321,25 @@
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
||||||
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ=="
|
"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": {
|
"@vueuse/shared": {
|
||||||
"version": "9.13.0",
|
"version": "9.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||||
|
|
@ -33879,9 +33925,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-demi": {
|
"vue-demi": {
|
||||||
"version": "0.13.11",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.0.tgz",
|
||||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A=="
|
"integrity": "sha512-gt58r2ogsNQeVoQ3EhoUAvUsH9xviydl0dWJj7dabBC/2L4uBId7ujtCwDRD0JhkGsV1i0CtfLAeyYKBht9oWg=="
|
||||||
},
|
},
|
||||||
"vue-docgen-api": {
|
"vue-docgen-api": {
|
||||||
"version": "4.64.1",
|
"version": "4.64.1",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
"@sentry/vue": "^7.20.0",
|
"@sentry/vue": "^7.20.0",
|
||||||
"@urql/vue": "^1.0.2",
|
"@urql/vue": "^1.0.2",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
|
"@vueuse/router": "^10.1.2",
|
||||||
"cypress": "^12.9.0",
|
"cypress": "^12.9.0",
|
||||||
"d3": "^7.6.1",
|
"d3": "^7.6.1",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
|
|
@ -36,7 +37,7 @@
|
||||||
"vue": "^3.2.38",
|
"vue": "^3.2.38",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-i18n-extract": "^2.0.7",
|
"vue-i18n-extract": "^2.0.7",
|
||||||
"vue-router": "^4.1.5"
|
"vue-router": "^4.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^2.13.12",
|
"@graphql-codegen/cli": "^2.13.12",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-4 bg-white px-6 py-5">
|
<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]">
|
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||||
{{ $t("general.feedback", 2) }}
|
<it-icon-feedback-large class="h-16 w-16"></it-icon-feedback-large>
|
||||||
</h2>
|
<div>{{ $t("general.feedback", 2) }}</div>
|
||||||
|
</h3>
|
||||||
<ol v-if="feedbackSummary.length > 0">
|
<ol v-if="feedbackSummary.length > 0">
|
||||||
<ItRow v-for="feedbacks in feedbackSummary" :key="feedbacks.circle_id">
|
<ItRow v-for="feedbacks in feedbackSummary" :key="feedbacks.circle_id">
|
||||||
<template #firstRow>
|
<template #firstRow>
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,12 @@ const input = (e: Event) => {
|
||||||
@input="input"
|
@input="input"
|
||||||
/>
|
/>
|
||||||
<div class="ml-4 flex-col">
|
<div class="ml-4 flex-col">
|
||||||
<div v-if="checkboxItem.label">
|
<div v-if="checkboxItem.label" v-html="checkboxItem.label"></div>
|
||||||
{{ checkboxItem.label }}
|
<div
|
||||||
</div>
|
v-if="checkboxItem.subtitle"
|
||||||
<div v-if="checkboxItem.subtitle" class="text-gray-900">
|
class="text-gray-900"
|
||||||
{{ checkboxItem.subtitle }}
|
v-html="checkboxItem.subtitle"
|
||||||
</div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,15 @@ export interface Props {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
startBadgeText?: string;
|
startBadgeText?: string;
|
||||||
endBadgeText?: string;
|
endBadgeText?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
queryParam?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
startBadgeText: undefined,
|
startBadgeText: undefined,
|
||||||
endBadgeText: undefined,
|
endBadgeText: undefined,
|
||||||
|
baseUrl: undefined,
|
||||||
|
queryParam: "page",
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasStartBadge = computed(() => typeof props.startBadgeText !== "undefined");
|
const hasStartBadge = computed(() => typeof props.startBadgeText !== "undefined");
|
||||||
|
|
@ -46,6 +50,13 @@ const endBadgeClasses = computed(() => {
|
||||||
}
|
}
|
||||||
return "border";
|
return "border";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function calcStepIndex(step: number) {
|
||||||
|
if (props.startBadgeText) {
|
||||||
|
return step + 1;
|
||||||
|
}
|
||||||
|
return step;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -54,8 +65,12 @@ const endBadgeClasses = computed(() => {
|
||||||
v-if="props.startBadgeText"
|
v-if="props.startBadgeText"
|
||||||
class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-3xl px-3 text-sm"
|
class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-3xl px-3 text-sm"
|
||||||
:class="startBadgeClasses"
|
: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>
|
||||||
<div v-for="(_, step) in numNumberSteps" :key="step" class="flex flex-row">
|
<div v-for="(_, step) in numNumberSteps" :key="step" class="flex flex-row">
|
||||||
<hr
|
<hr
|
||||||
|
|
@ -65,8 +80,15 @@ const endBadgeClasses = computed(() => {
|
||||||
<div
|
<div
|
||||||
class="inline-flex h-7 w-7 items-center justify-center rounded-full px-3 py-1 text-sm"
|
class="inline-flex h-7 w-7 items-center justify-center rounded-full px-3 py-1 text-sm"
|
||||||
:class="getPillClasses(step)"
|
: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>
|
||||||
</div>
|
</div>
|
||||||
<hr v-if="hasEndBadge" class="w-8 self-center border border-gray-400" />
|
<hr v-if="hasEndBadge" class="w-8 self-center border border-gray-400" />
|
||||||
|
|
@ -74,8 +96,15 @@ const endBadgeClasses = computed(() => {
|
||||||
v-if="endBadgeText"
|
v-if="endBadgeText"
|
||||||
class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-3xl px-3 text-sm"
|
class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-3xl px-3 text-sm"
|
||||||
:class="endBadgeClasses"
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
export type StatusCountKey = "fail" | "success" | "unknown";
|
||||||
|
export type StatusCount = Record<StatusCountKey, number>;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
statusCount?: {
|
statusCount?: StatusCount;
|
||||||
fail: number;
|
|
||||||
success: number;
|
|
||||||
unknown: number;
|
|
||||||
};
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const total = computed(() => {
|
const total = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 v-if="label" class="heading-1 mb-8 block">{{ label }}</h2>
|
<div v-if="label" class="mb-2 block">{{ label }}</div>
|
||||||
<textarea
|
<textarea
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
class="h-40 w-full border-gray-500"
|
class="h-40 w-full border-gray-500"
|
||||||
:data-cy="`it-textarea-${cyKey}`"
|
:data-cy="`it-textarea-${cyKey}`"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:placeholder="placeholder"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
interface Props {
|
export interface Props {
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
label: string | undefined;
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
cyKey?: string;
|
cyKey?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
label: undefined,
|
label: undefined,
|
||||||
cyKey: "",
|
cyKey: "",
|
||||||
|
placeholder: "",
|
||||||
});
|
});
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCockpitStore } from "@/stores/cockpit";
|
import { useCockpitStore } from "@/stores/cockpit";
|
||||||
import { useCompetenceStore } from "@/stores/competence";
|
import { useCompetenceStore } from "@/stores/competence";
|
||||||
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import { useLearningPathStore } from "@/stores/learningPath";
|
import { useLearningPathStore } from "@/stores/learningPath";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
@ -15,21 +16,25 @@ const props = defineProps<{
|
||||||
const cockpitStore = useCockpitStore();
|
const cockpitStore = useCockpitStore();
|
||||||
const competenceStore = useCompetenceStore();
|
const competenceStore = useCompetenceStore();
|
||||||
const learningPathStore = useLearningPathStore();
|
const learningPathStore = useLearningPathStore();
|
||||||
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
log.debug("CockpitParentPage mounted", props.courseSlug);
|
log.debug("CockpitParentPage mounted", props.courseSlug);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cockpitStore.loadCourseSessionUsers(props.courseSlug);
|
const currentCourseSession = courseSessionsStore.currentCourseSession;
|
||||||
cockpitStore.courseSessionUsers?.forEach((csu) => {
|
if (currentCourseSession?.id) {
|
||||||
competenceStore.loadCompetenceProfilePage(
|
await cockpitStore.loadCourseSessionUsers(currentCourseSession.id);
|
||||||
props.courseSlug + "-competence",
|
cockpitStore.courseSessionUsers?.forEach((csu) => {
|
||||||
csu.user_id
|
competenceStore.loadCompetenceProfilePage(
|
||||||
);
|
props.courseSlug + "-competence",
|
||||||
|
csu.user_id
|
||||||
|
);
|
||||||
|
|
||||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
|
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
|
||||||
});
|
});
|
||||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", useUserStore().id);
|
learningPathStore.loadLearningPath(props.courseSlug + "-lp", useUserStore().id);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
log.debug("FeedbackPage created", props.circleId);
|
log.debug("FeedbackPage created", props.circleId);
|
||||||
|
|
||||||
const courseSessionStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const orderedQuestions = [
|
const orderedQuestions = [
|
||||||
|
|
@ -149,7 +149,7 @@ const feedbackData = reactive<FeedbackData>({ amount: 0, questions: {} });
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
log.debug("FeedbackPage mounted");
|
log.debug("FeedbackPage mounted");
|
||||||
const data = await itGet(
|
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);
|
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 ItProgress from "@/components/ui/ItProgress.vue";
|
||||||
import type { LearningPath } from "@/services/learningPath";
|
import type { LearningPath } from "@/services/learningPath";
|
||||||
|
|
||||||
|
import AssignmentsTile from "@/pages/cockpit/cockpitPage/AssignmentsTile.vue";
|
||||||
import { useCockpitStore } from "@/stores/cockpit";
|
import { useCockpitStore } from "@/stores/cockpit";
|
||||||
import { useCompetenceStore } from "@/stores/competence";
|
import { useCompetenceStore } from "@/stores/competence";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
|
@ -24,7 +25,7 @@ const userStore = useUserStore();
|
||||||
const cockpitStore = useCockpitStore();
|
const cockpitStore = useCockpitStore();
|
||||||
const competenceStore = useCompetenceStore();
|
const competenceStore = useCompetenceStore();
|
||||||
const learningPathStore = useLearningPathStore();
|
const learningPathStore = useLearningPathStore();
|
||||||
const courseSessionStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
|
||||||
function userCountStatusForCircle(userId: number, translationKey: string) {
|
function userCountStatusForCircle(userId: number, translationKey: string) {
|
||||||
const criteria = competenceStore.flatPerformanceCriteria(
|
const criteria = competenceStore.flatPerformanceCriteria(
|
||||||
|
|
@ -92,7 +93,7 @@ function setActiveClasses(translationKey: string) {
|
||||||
<button
|
<button
|
||||||
class="mr-4 rounded-full border-2 border-blue-900 px-4 last:mr-0"
|
class="mr-4 rounded-full border-2 border-blue-900 px-4 last:mr-0"
|
||||||
:class="setActiveClasses(circle.translation_key)"
|
:class="setActiveClasses(circle.translation_key)"
|
||||||
@click="cockpitStore.toggleCourseSelection(circle.translation_key)"
|
@click="cockpitStore.toggleCircleSelection(circle.translation_key)"
|
||||||
>
|
>
|
||||||
{{ circle.title }}
|
{{ circle.title }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -101,23 +102,15 @@ function setActiveClasses(translationKey: string) {
|
||||||
</div>
|
</div>
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<div class="mb-4 grid grid-rows-2 gap-4 lg:grid-cols-2 lg:grid-rows-none">
|
<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">
|
<div class="bg-white px-6 py-5">
|
||||||
<h1
|
<h3 class="heading-3 mb-4 flex items-center gap-2">
|
||||||
class="heading-3 mb-4 bg-assignment bg-60 bg-no-repeat pl-[68px] leading-[60px]"
|
<it-icon-test-large class="h-16 w-16"></it-icon-test-large>
|
||||||
>
|
<div>{{ $t("general.examResult", 2) }}</div>
|
||||||
{{ $t("general.transferTask", 2) }}
|
</h3>
|
||||||
</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>
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<ItProgress :status-count="data.transferProgress"></ItProgress>
|
<ItProgress :status-count="data.transferProgress"></ItProgress>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -131,8 +124,8 @@ function setActiveClasses(translationKey: string) {
|
||||||
learningPathStore.learningPathForUser(props.courseSlug, userStore.id)
|
learningPathStore.learningPathForUser(props.courseSlug, userStore.id)
|
||||||
?.circles || []
|
?.circles || []
|
||||||
"
|
"
|
||||||
:course-id="courseSessionStore.currentCourseSession?.course.id || 0"
|
:course-id="courseSessionsStore.currentCourseSession?.course.id || 0"
|
||||||
:url="courseSessionStore.currentCourseSession?.course_url || ''"
|
:url="courseSessionsStore.currentCourseSession?.course_url || ''"
|
||||||
></FeedbackSummary>
|
></FeedbackSummary>
|
||||||
<div>
|
<div>
|
||||||
<!-- progress -->
|
<!-- progress -->
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
<script setup lang="ts">
|
<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 { useCircleStore } from "@/stores/circle";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import type { CourseSessionUser } from "@/types";
|
import type { CourseSessionUser } from "@/types";
|
||||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||||
import sumBy from "lodash/sumBy";
|
import sumBy from "lodash/sumBy";
|
||||||
|
import log from "loglevel";
|
||||||
|
import { computed, onMounted } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
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 {
|
export interface Props {
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
@ -31,9 +29,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
log.debug("CirclePage created", props.readonly, props.profileUser);
|
log.debug("CirclePage created", props.readonly, props.profileUser);
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
appStore.showMainNavigationBar = true;
|
|
||||||
|
|
||||||
const circleStore = useCircleStore();
|
const circleStore = useCircleStore();
|
||||||
|
|
||||||
const duration = computed(() => {
|
const duration = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,7 @@ async function uploadDocument(data: DocumentUploadData) {
|
||||||
data,
|
data,
|
||||||
courseSessionsStore.currentCourseSession.id
|
courseSessionsStore.currentCourseSession.id
|
||||||
);
|
);
|
||||||
const courseSessionStore = useCourseSessionsStore();
|
courseSessionsStore.addDocument(newDocument);
|
||||||
courseSessionStore.addDocument(newDocument);
|
|
||||||
showUploadModal.value = false;
|
showUploadModal.value = false;
|
||||||
isUploading.value = false;
|
isUploading.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ defineEmits(["exit"]);
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="h-full"></div>
|
|
||||||
<div class="absolute bottom-0 top-0 w-full bg-white">
|
<div class="absolute bottom-0 top-0 w-full bg-white">
|
||||||
<div class="h-content overflow-y-auto">
|
<div class="h-content overflow-y-auto">
|
||||||
<header
|
<header
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningContent from "@/pages/learningPath/learningContentPage/LearningContent.vue";
|
import LearningContent from "@/pages/learningPath/learningContentPage/LearningContent.vue";
|
||||||
import { useAppStore } from "@/stores/app";
|
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import { useCircleStore } from "@/stores/circle";
|
||||||
import type { LearningContent as LearningContentType } from "@/types";
|
import type { LearningContent as LearningContentType } from "@/types";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
@ -16,9 +15,6 @@ const props = defineProps<{
|
||||||
|
|
||||||
const state: { learningContent?: LearningContentType } = reactive({});
|
const state: { learningContent?: LearningContentType } = reactive({});
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
appStore.showMainNavigationBar = false;
|
|
||||||
|
|
||||||
const circleStore = useCircleStore();
|
const circleStore = useCircleStore();
|
||||||
|
|
||||||
const loadLearningContent = async () => {
|
const loadLearningContent = async () => {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h3 class="mt-8">{{ $t("assignment.initialSituationTitle") }}</h3>
|
<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>
|
<h3 class="mt-8">{{ $t("assignment.taskDefinitionTitle") }}</h3>
|
||||||
<p class="text-large">
|
<p class="text-large">
|
||||||
|
|
@ -55,8 +59,16 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 class="mt-8">{{ $t("assignment.assessmentTitle") }}</h3>
|
<h3 class="mt-8">{{ $t("assignment.assessmentTitle") }}</h3>
|
||||||
<p class="text-large">{{ props.assignment.assessment_description }}</p>
|
<p
|
||||||
<a :href="props.assignment.assessment_document_url" class="text-large underline">
|
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") }}
|
{{ $t("assignment.showAssessmentDocument") }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,50 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { UserDataText } from "@/stores/assignmentStore";
|
import type {
|
||||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
Assignment,
|
||||||
import type { AssignmentTask } from "@/types";
|
AssignmentCompletionData,
|
||||||
import { computed } from "vue";
|
AssignmentTask,
|
||||||
|
UserDataText,
|
||||||
|
} from "@/types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
assignment: Assignment;
|
||||||
|
assignmentCompletionData: AssignmentCompletionData;
|
||||||
|
allowEdit: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "editTask", task: AssignmentTask): void;
|
(e: "editTask", task: AssignmentTask): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const assignmentStore = useAssignmentStore();
|
|
||||||
|
|
||||||
const completionData = computed(() => assignmentStore.completion_data);
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-for="task in assignmentStore.assignment?.tasks ?? []"
|
v-for="task in props.assignment.tasks ?? []"
|
||||||
:key="task.id"
|
:key="task.id"
|
||||||
class="mb-6 border-t border-gray-400"
|
class="mb-6 border-t border-gray-400"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row justify-between pt-8">
|
<div class="flex flex-row justify-between pt-8">
|
||||||
<p class="text-sm text-gray-900">{{ task.value.title }}</p>
|
<p class="text-sm text-gray-900">{{ task.value.title }}</p>
|
||||||
<button
|
<button
|
||||||
class="link whitespace-nowrap pl-2 text-sm"
|
v-if="props.allowEdit"
|
||||||
|
class="link pl-2text-sm whitespace-nowrap"
|
||||||
@click="emit('editTask', task)"
|
@click="emit('editTask', task)"
|
||||||
>
|
>
|
||||||
{{ $t("assignment.edit") }}
|
{{ $t("assignment.edit") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="taskBlock in task.value.content" :key="taskBlock.id">
|
<div v-for="taskBlock in task.value.content" :key="taskBlock.id">
|
||||||
<p class="pt-6 text-base font-bold">{{ taskBlock.value.text }}</p>
|
<p
|
||||||
<p v-if="completionData && taskBlock.id in completionData" class="font-normal">
|
class="default-wagtail-rich-text pt-6 text-base font-bold"
|
||||||
{{ (completionData[taskBlock.id].user_data as UserDataText).text }}
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import type { Assignment, AssignmentTask } from "@/types";
|
import type { Assignment, AssignmentCompletionData, AssignmentTask } from "@/types";
|
||||||
import type { Dayjs } from "dayjs";
|
import type { Dayjs } from "dayjs";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed, reactive } from "vue";
|
import { computed, reactive } from "vue";
|
||||||
|
|
@ -13,6 +13,7 @@ import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
assignment: Assignment;
|
assignment: Assignment;
|
||||||
|
assignmentCompletionData: AssignmentCompletionData;
|
||||||
courseSessionId: number;
|
courseSessionId: number;
|
||||||
dueDate: Dayjs;
|
dueDate: Dayjs;
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -22,7 +23,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const assignmentStore = useAssignmentStore();
|
const assignmentStore = useAssignmentStore();
|
||||||
const courseSessionStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
|
|
@ -31,30 +32,34 @@ const state = reactive({
|
||||||
});
|
});
|
||||||
|
|
||||||
const circleExpert = computed(() => {
|
const circleExpert = computed(() => {
|
||||||
return courseSessionStore.circleExperts[0];
|
return courseSessionsStore.circleExperts[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
const circleExpertName = computed(() => {
|
const circleExpertName = computed(() => {
|
||||||
return `${circleExpert.value?.first_name} ${circleExpert.value?.last_name}`;
|
return `${circleExpert.value?.first_name} ${circleExpert.value?.last_name}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const completionStatus = computed(() => {
|
||||||
|
return assignmentStore.assignmentCompletion?.completion_status ?? "in_progress";
|
||||||
|
});
|
||||||
|
|
||||||
const onEditTask = (task: AssignmentTask) => {
|
const onEditTask = (task: AssignmentTask) => {
|
||||||
emit("editTask", task);
|
emit("editTask", task);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const courseSessionId = courseSessionStore.currentCourseSession?.id;
|
const courseSessionId = courseSessionsStore.currentCourseSession?.id;
|
||||||
if (!courseSessionId) {
|
if (!courseSessionId) {
|
||||||
log.error("Invalid courseSessionId");
|
log.error("Invalid courseSessionId");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await assignmentStore.upsertAssignmentCompletion(
|
await assignmentStore.upsertAssignmentCompletion({
|
||||||
props.assignment.id,
|
assignment_id: props.assignment.id,
|
||||||
{},
|
course_session_id: courseSessionId,
|
||||||
courseSessionId,
|
completion_data: {},
|
||||||
true
|
completion_status: "submitted",
|
||||||
);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Could not submit assignment", error);
|
log.error("Could not submit assignment", error);
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +71,7 @@ const onSubmit = async () => {
|
||||||
{{ $t("assignment.acceptConditionsDisclaimer") }}
|
{{ $t("assignment.acceptConditionsDisclaimer") }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div v-if="!assignmentStore.submitted">
|
<div v-if="completionStatus === 'in_progress'">
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
class="w-full border-b border-gray-400 py-6"
|
class="w-full border-b border-gray-400 py-6"
|
||||||
:checkbox-item="{
|
:checkbox-item="{
|
||||||
|
|
@ -99,7 +104,7 @@ const onSubmit = async () => {
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-x-2 pt-6 text-base sm:flex-row">
|
<div class="flex flex-col space-x-2 pt-6 text-base sm:flex-row">
|
||||||
<p>{{ $t("assignment.assessmentDocumentDisclaimer") }}</p>
|
<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") }}
|
{{ $t("assignment.showAssessmentDocument") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -126,6 +131,9 @@ const onSubmit = async () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AssignmentSubmissionResponses
|
<AssignmentSubmissionResponses
|
||||||
|
:assignment="props.assignment"
|
||||||
|
:assignment-completion-data="props.assignmentCompletionData"
|
||||||
|
:allow-edit="completionStatus === 'in_progress'"
|
||||||
@edit-task="onEditTask"
|
@edit-task="onEditTask"
|
||||||
></AssignmentSubmissionResponses>
|
></AssignmentSubmissionResponses>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||||
import type {
|
|
||||||
AssignmentCompletionData,
|
|
||||||
BlockId,
|
|
||||||
UserDataConfirmation,
|
|
||||||
UserDataText,
|
|
||||||
} from "@/stores/assignmentStore";
|
|
||||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import type { AssignmentTask } from "@/types";
|
import type {
|
||||||
|
AssignmentCompletionData,
|
||||||
|
AssignmentTask,
|
||||||
|
UserDataConfirmation,
|
||||||
|
UserDataText,
|
||||||
|
} from "@/types";
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { reactive, ref } from "vue";
|
import { computed, reactive, ref } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
assignmentId: number;
|
assignmentId: number;
|
||||||
|
|
@ -22,24 +21,24 @@ const props = defineProps<{
|
||||||
const lastSaved = ref(dayjs());
|
const lastSaved = ref(dayjs());
|
||||||
const lastSaveUnsuccessful = ref(false);
|
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();
|
const assignmentStore = useAssignmentStore();
|
||||||
|
|
||||||
async function upsertAssignmentCompletion(completion_data: AssignmentCompletionData) {
|
async function upsertAssignmentCompletion(completion_data: AssignmentCompletionData) {
|
||||||
try {
|
try {
|
||||||
const courseSessionId = courseSessionStore.currentCourseSession?.id;
|
const courseSessionId = courseSessionsStore.currentCourseSession?.id;
|
||||||
if (!courseSessionId) {
|
if (!courseSessionId) {
|
||||||
console.error("Invalid courseSessionId");
|
console.error("Invalid courseSessionId");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await assignmentStore.upsertAssignmentCompletion(
|
await assignmentStore.upsertAssignmentCompletion({
|
||||||
props.assignmentId,
|
assignment_id: props.assignmentId,
|
||||||
completion_data,
|
course_session_id: courseSessionId,
|
||||||
courseSessionId,
|
completion_data: completion_data,
|
||||||
false
|
completion_status: "in_progress",
|
||||||
);
|
});
|
||||||
lastSaved.value = dayjs();
|
lastSaved.value = dayjs();
|
||||||
lastSaveUnsuccessful.value = false;
|
lastSaveUnsuccessful.value = false;
|
||||||
console.debug("Saved user input");
|
console.debug("Saved user input");
|
||||||
|
|
@ -54,7 +53,7 @@ const upsertAssignmentCompletionDebounced = useDebounceFn(
|
||||||
500
|
500
|
||||||
);
|
);
|
||||||
|
|
||||||
const onUpdateText = (id: BlockId, value: string) => {
|
const onUpdateText = (id: string, value: string) => {
|
||||||
const data: AssignmentCompletionData = {};
|
const data: AssignmentCompletionData = {};
|
||||||
data[id] = {
|
data[id] = {
|
||||||
user_data: {
|
user_data: {
|
||||||
|
|
@ -64,7 +63,7 @@ const onUpdateText = (id: BlockId, value: string) => {
|
||||||
upsertAssignmentCompletionDebounced(data);
|
upsertAssignmentCompletionDebounced(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUpdateConfirmation = (id: BlockId, value: boolean) => {
|
const onUpdateConfirmation = (id: string, value: boolean) => {
|
||||||
const data: AssignmentCompletionData = {};
|
const data: AssignmentCompletionData = {};
|
||||||
data[id] = {
|
data[id] = {
|
||||||
user_data: {
|
user_data: {
|
||||||
|
|
@ -74,7 +73,7 @@ const onUpdateConfirmation = (id: BlockId, value: boolean) => {
|
||||||
upsertAssignmentCompletion(data);
|
upsertAssignmentCompletion(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBlockData = (id: BlockId) => {
|
const getBlockData = (id: string) => {
|
||||||
const userData = assignmentStore.getCompletionDataForUserInput(id)?.user_data;
|
const userData = assignmentStore.getCompletionDataForUserInput(id)?.user_data;
|
||||||
if (userData && "text" in userData) {
|
if (userData && "text" in userData) {
|
||||||
return userData.text;
|
return userData.text;
|
||||||
|
|
@ -84,18 +83,23 @@ const getBlockData = (id: BlockId) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onToggleCheckbox = (id: BlockId) => {
|
const onToggleCheckbox = (id: string) => {
|
||||||
checkboxState[id] = !checkboxState[id];
|
checkboxState[id] = !checkboxState[id];
|
||||||
onUpdateConfirmation(id, checkboxState[id]);
|
onUpdateConfirmation(id, checkboxState[id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const completionStatus = computed(() => {
|
||||||
|
return assignmentStore.assignmentCompletion?.completion_status ?? "in_progress";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col space-y-10">
|
<div class="flex flex-col space-y-10">
|
||||||
<div v-for="(block, index) in props.task.value.content" :key="block.id">
|
<div v-for="(block, index) in props.task.value.content" :key="block.id">
|
||||||
<div v-if="block.type === 'explanation'">
|
<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>
|
||||||
|
|
||||||
<div v-if="block.type === 'user_confirmation'">
|
<div v-if="block.type === 'user_confirmation'">
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
:checkbox-item="{
|
:checkbox-item="{
|
||||||
|
|
@ -103,16 +107,20 @@ const onToggleCheckbox = (id: BlockId) => {
|
||||||
value: `confirmation-${index}`,
|
value: `confirmation-${index}`,
|
||||||
checked: getBlockData(block.id) as boolean,
|
checked: getBlockData(block.id) as boolean,
|
||||||
}"
|
}"
|
||||||
:disabled="assignmentStore.submitted"
|
:disabled="completionStatus !== 'in_progress'"
|
||||||
@toggle="onToggleCheckbox(block.id)"
|
@toggle="onToggleCheckbox(block.id)"
|
||||||
></ItCheckbox>
|
></ItCheckbox>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="block.type === 'user_text_input'">
|
<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
|
<ItTextarea
|
||||||
:model-value="(getBlockData(block.id) as string) ?? ''"
|
:model-value="(getBlockData(block.id) as string) ?? ''"
|
||||||
:cy-key="`user-text-input-${index}`"
|
:cy-key="`user-text-input-${index}`"
|
||||||
:disabled="assignmentStore.submitted"
|
:disabled="completionStatus !== 'in_progress'"
|
||||||
label=""
|
label=""
|
||||||
@update:model-value="onUpdateText(block.id, $event)"
|
@update:model-value="onUpdateText(block.id, $event)"
|
||||||
></ItTextarea>
|
></ItTextarea>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,38 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
|
||||||
import AssignmentIntroductionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue";
|
import AssignmentIntroductionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue";
|
||||||
import AssignmentSubmissionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue";
|
import AssignmentSubmissionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue";
|
||||||
import AssignmentTaskView from "@/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue";
|
import AssignmentTaskView from "@/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue";
|
||||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||||
import type { AssignmentCompletionData } from "@/stores/assignmentStore";
|
|
||||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
import type {
|
import type {
|
||||||
Assignment,
|
Assignment,
|
||||||
AssignmentTask,
|
AssignmentTask,
|
||||||
CourseSessionAssignmentDetails,
|
CourseSessionAssignmentDetails,
|
||||||
|
CourseSessionUser,
|
||||||
LearningContent,
|
LearningContent,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { computed, onMounted, reactive } from "vue";
|
import { computed, onMounted, reactive } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const courseSessionStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
const assignmentStore = useAssignmentStore();
|
const assignmentStore = useAssignmentStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
assignment: Assignment | undefined;
|
assignment: Assignment | undefined;
|
||||||
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
|
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
|
||||||
assignmentCompletionData: AssignmentCompletionData | undefined;
|
|
||||||
pageIndex: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = reactive({
|
const state: State = reactive({
|
||||||
assignment: undefined,
|
assignment: undefined,
|
||||||
courseSessionAssignmentDetails: undefined,
|
courseSessionAssignmentDetails: undefined,
|
||||||
assignmentCompletionData: undefined,
|
|
||||||
// 0 = introduction, 1 - n = tasks, n+1 = submission
|
|
||||||
pageIndex: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -41,6 +40,14 @@ const props = defineProps<{
|
||||||
learningContent: LearningContent;
|
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 () => {
|
onMounted(async () => {
|
||||||
log.debug("AssignmentView mounted", props.assignmentId, props.learningContent);
|
log.debug("AssignmentView mounted", props.assignmentId, props.learningContent);
|
||||||
|
|
||||||
|
|
@ -51,11 +58,17 @@ onMounted(async () => {
|
||||||
state.courseSessionAssignmentDetails = courseSessionsStore.findAssignmentDetails(
|
state.courseSessionAssignmentDetails = courseSessionsStore.findAssignmentDetails(
|
||||||
props.learningContent.id
|
props.learningContent.id
|
||||||
);
|
);
|
||||||
state.assignmentCompletionData = await assignmentStore.loadAssignmentCompletion(
|
await assignmentStore.loadAssignmentCompletion(
|
||||||
props.assignmentId,
|
props.assignmentId,
|
||||||
courseSessionId.value
|
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) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
|
|
@ -63,92 +76,119 @@ onMounted(async () => {
|
||||||
|
|
||||||
const numTasks = computed(() => state.assignment?.tasks?.length ?? 0);
|
const numTasks = computed(() => state.assignment?.tasks?.length ?? 0);
|
||||||
const numPages = computed(() => numTasks.value + 2);
|
const numPages = computed(() => numTasks.value + 2);
|
||||||
const showPreviousButton = computed(() => state.pageIndex != 0);
|
const showPreviousButton = computed(() => pageIndex.value != 0);
|
||||||
const showNextButton = computed(() => state.pageIndex + 1 < numPages.value);
|
const showNextButton = computed(() => pageIndex.value + 1 < numPages.value);
|
||||||
const showExitButton = computed(() => numPages.value === state.pageIndex + 1);
|
const showExitButton = computed(() => numPages.value === pageIndex.value + 1);
|
||||||
const dueDate = computed(() =>
|
const dueDate = computed(() =>
|
||||||
dayjs(state.courseSessionAssignmentDetails?.deadlineDateTimeUtc)
|
dayjs(state.courseSessionAssignmentDetails?.submissionDeadlineDateTimeUtc)
|
||||||
);
|
);
|
||||||
const courseSessionId = computed(
|
const courseSessionId = computed(
|
||||||
() => courseSessionStore.currentCourseSession?.id ?? 0
|
() => courseSessionsStore.currentCourseSession?.id ?? 0
|
||||||
);
|
);
|
||||||
const currentTask = computed(() => {
|
const currentTask = computed(() => {
|
||||||
if (state.pageIndex > 0 && state.pageIndex <= numTasks.value) {
|
if (pageIndex.value > 0 && pageIndex.value <= numTasks.value) {
|
||||||
return state.assignment?.tasks[state.pageIndex - 1];
|
return state.assignment?.tasks[pageIndex.value - 1];
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
log.debug("handleBack");
|
log.debug("handleBack");
|
||||||
if (state.pageIndex > 0) {
|
if (pageIndex.value > 0) {
|
||||||
state.pageIndex -= 1;
|
pageIndex.value -= 1;
|
||||||
}
|
}
|
||||||
log.debug(`pageIndex: ${state.pageIndex}`);
|
log.debug(`pageIndex: ${pageIndex.value}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContinue = () => {
|
const handleContinue = () => {
|
||||||
log.debug("handleContinue");
|
log.debug("handleContinue");
|
||||||
if (state.pageIndex + 1 < numPages.value) {
|
if (pageIndex.value + 1 < numPages.value) {
|
||||||
state.pageIndex += 1;
|
pageIndex.value += 1;
|
||||||
}
|
}
|
||||||
log.debug(`pageIndex: ${state.pageIndex}`);
|
log.debug(`pageIndex: ${pageIndex.value}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const jumpToTask = (task: AssignmentTask) => {
|
const jumpToTask = (task: AssignmentTask) => {
|
||||||
log.debug("jumpToTask", task);
|
log.debug("jumpToTask", task);
|
||||||
const index = state.assignment?.tasks.findIndex((t) => t.id === task.id);
|
const index = state.assignment?.tasks.findIndex((t) => t.id === task.id);
|
||||||
if (index && index >= 0) {
|
if (index && index >= 0) {
|
||||||
state.pageIndex = index + 1;
|
pageIndex.value = index + 1;
|
||||||
}
|
}
|
||||||
log.debug(`pageIndex: ${state.pageIndex}`);
|
log.debug(`pageIndex: ${pageIndex.value}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
if (0 === state.pageIndex) {
|
if (0 === pageIndex.value) {
|
||||||
return t("general.introduction");
|
return t("general.introduction");
|
||||||
} else if (state.pageIndex === numPages.value - 1) {
|
} else if (pageIndex.value === numPages.value - 1) {
|
||||||
return t("general.submission");
|
return t("general.submission");
|
||||||
}
|
}
|
||||||
return currentTask?.value?.value.title ?? "Unknown";
|
return currentTask?.value?.value.title ?? "Unknown";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const assignmentUser = computed(() => {
|
||||||
|
return (courseSessionsStore.currentCourseSession?.users ?? []).find(
|
||||||
|
(user) => user.user_id === Number(userStore.id)
|
||||||
|
) as CourseSessionUser;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LearningContentMultiLayout
|
<div v-if="state.assignment">
|
||||||
:current-step="state.pageIndex"
|
<div class="flex">
|
||||||
:subtitle="state.assignment?.title ?? ''"
|
<LearningContentMultiLayout
|
||||||
:title="getTitle()"
|
:current-step="pageIndex"
|
||||||
learning-content-type="assignment"
|
:subtitle="state.assignment?.title ?? ''"
|
||||||
:steps-count="numPages"
|
:title="getTitle()"
|
||||||
:show-next-button="showNextButton"
|
learning-content-type="assignment"
|
||||||
:show-exit-button="showExitButton"
|
:steps-count="numPages"
|
||||||
:show-start-button="false"
|
:show-next-button="showNextButton"
|
||||||
:show-previous-button="showPreviousButton"
|
:show-exit-button="showExitButton"
|
||||||
start-badge-text="Einleitung"
|
:show-start-button="false"
|
||||||
end-badge-text="Abgabe"
|
:show-previous-button="showPreviousButton"
|
||||||
close-button-variant="close"
|
:base-url="props.learningContent.frontend_url"
|
||||||
@previous="handleBack()"
|
query-param="page"
|
||||||
@next="handleContinue()"
|
start-badge-text="Einleitung"
|
||||||
>
|
end-badge-text="Abgabe"
|
||||||
<div>
|
close-button-variant="close"
|
||||||
<AssignmentIntroductionView
|
@previous="handleBack()"
|
||||||
v-if="state.pageIndex === 0 && state.assignment"
|
@next="handleContinue()"
|
||||||
:due-date="dueDate"
|
>
|
||||||
:assignment="state.assignment!"
|
<div class="flex">
|
||||||
></AssignmentIntroductionView>
|
<div>
|
||||||
<AssignmentTaskView
|
<AssignmentIntroductionView
|
||||||
v-if="currentTask"
|
v-if="pageIndex === 0 && state.assignment"
|
||||||
:task="currentTask"
|
:due-date="dueDate"
|
||||||
:assignment-id="props.assignmentId"
|
:assignment="state.assignment!"
|
||||||
></AssignmentTaskView>
|
></AssignmentIntroductionView>
|
||||||
<AssignmentSubmissionView
|
<AssignmentTaskView
|
||||||
v-if="state.pageIndex + 1 === numPages && state.assignment && courseSessionId"
|
v-if="currentTask"
|
||||||
:due-date="dueDate"
|
:task="currentTask"
|
||||||
:assignment="state.assignment!"
|
:assignment-id="props.assignmentId"
|
||||||
:course-session-id="courseSessionId!"
|
></AssignmentTaskView>
|
||||||
@edit-task="jumpToTask($event)"
|
<AssignmentSubmissionView
|
||||||
></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>
|
</div>
|
||||||
</LearningContentMultiLayout>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ interface Props {
|
||||||
startBadgeText?: string;
|
startBadgeText?: string;
|
||||||
endBadgeText?: string;
|
endBadgeText?: string;
|
||||||
closeButtonVariant?: ClosingButtonVariant;
|
closeButtonVariant?: ClosingButtonVariant;
|
||||||
|
baseUrl?: string;
|
||||||
|
queryParam?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -55,6 +57,8 @@ const emit = defineEmits(["previous", "next", "exit"]);
|
||||||
:start-badge-text="props.startBadgeText"
|
:start-badge-text="props.startBadgeText"
|
||||||
:steps="stepsCount"
|
:steps="stepsCount"
|
||||||
:end-badge-text="props.endBadgeText"
|
:end-badge-text="props.endBadgeText"
|
||||||
|
:base-url="props.baseUrl"
|
||||||
|
:query-param="props.queryParam"
|
||||||
class="overflow-hidden pb-12"
|
class="overflow-hidden pb-12"
|
||||||
></ItNavigationProgress>
|
></ItNavigationProgress>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
||||||
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
||||||
|
|
||||||
import { useAppStore } from "@/stores/app";
|
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import { useCircleStore } from "@/stores/circle";
|
||||||
import type { LearningUnit } from "@/types";
|
import type { LearningUnit } from "@/types";
|
||||||
import { onMounted, reactive } from "vue";
|
import { onMounted, reactive } from "vue";
|
||||||
|
|
@ -16,9 +14,6 @@ const props = defineProps<{
|
||||||
learningUnitSlug: string;
|
learningUnitSlug: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
appStore.showMainNavigationBar = false;
|
|
||||||
|
|
||||||
const circleStore = useCircleStore();
|
const circleStore = useCircleStore();
|
||||||
|
|
||||||
const state: { learningUnit?: LearningUnit } = reactive({});
|
const state: { learningUnit?: LearningUnit } = reactive({});
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ const router = createRouter({
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: () => import("@/pages/cockpit/CockpitIndexPage.vue"),
|
component: () => import("@/pages/cockpit/cockpitPage/CockpitPage.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -129,6 +129,20 @@ const router = createRouter({
|
||||||
component: () => import("@/pages/cockpit/FeedbackPage.vue"),
|
component: () => import("@/pages/cockpit/FeedbackPage.vue"),
|
||||||
props: true,
|
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(redirectToLoginIfRequired);
|
||||||
|
|
||||||
router.beforeEach((to) => {
|
router.beforeEach((to) => {
|
||||||
const courseSessionStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
if (to.params.courseSlug) {
|
if (to.params.courseSlug) {
|
||||||
courseSessionStore._currentCourseSlug = to.params.courseSlug as string;
|
courseSessionsStore._currentCourseSlug = to.params.courseSlug as string;
|
||||||
} else {
|
} 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 = {
|
export type AppState = {
|
||||||
userLoaded: boolean;
|
userLoaded: boolean;
|
||||||
routingFinished: 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({
|
export const useAppStore = defineStore({
|
||||||
id: "app",
|
id: "app",
|
||||||
state: () =>
|
state: () =>
|
||||||
({
|
({
|
||||||
showMainNavigationBar: showMainNavigationBarInitialState(),
|
|
||||||
userLoaded: false,
|
userLoaded: false,
|
||||||
routingFinished: false,
|
routingFinished: false,
|
||||||
} as AppState),
|
} as AppState),
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,29 @@
|
||||||
import { itGet, itPost } from "@/fetchHelpers";
|
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 log from "loglevel";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export type AssignmentStoreState = {
|
export type AssignmentStoreState = {
|
||||||
assignment: Assignment | undefined;
|
assignment: Assignment | undefined;
|
||||||
completion_data: AssignmentCompletionData;
|
assignmentCompletion: AssignmentCompletion | undefined;
|
||||||
submitted: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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({
|
export const useAssignmentStore = defineStore({
|
||||||
id: "assignmentStore",
|
id: "assignmentStore",
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
assignment: undefined,
|
assignment: undefined,
|
||||||
completion_data: {},
|
assignmentCompletion: undefined,
|
||||||
submitted: false,
|
|
||||||
} as AssignmentStoreState;
|
} as AssignmentStoreState;
|
||||||
},
|
},
|
||||||
getters: {},
|
getters: {},
|
||||||
|
|
@ -48,39 +38,73 @@ export const useAssignmentStore = defineStore({
|
||||||
this.assignment = assignmentData;
|
this.assignment = assignmentData;
|
||||||
return this.assignment;
|
return this.assignment;
|
||||||
},
|
},
|
||||||
async loadAssignmentCompletion(assignmentId: number, courseSessionId: number) {
|
async loadAssignmentCompletion(
|
||||||
log.debug("load assignment completion", assignmentId, courseSessionId);
|
assignmentId: number,
|
||||||
|
courseSessionId: number,
|
||||||
|
userId: string | undefined = undefined
|
||||||
|
) {
|
||||||
|
log.debug("load assignment completion", assignmentId, courseSessionId, userId);
|
||||||
|
this.assignmentCompletion = undefined;
|
||||||
try {
|
try {
|
||||||
const data = await itGet(`/api/assignment/${assignmentId}/${courseSessionId}/`);
|
let url = `/api/assignment/${assignmentId}/${courseSessionId}/`;
|
||||||
this.completion_data = data.completion_data;
|
if (userId) {
|
||||||
this.submitted = data.completion_status === "submitted";
|
url += `${userId}/`;
|
||||||
|
}
|
||||||
|
this.assignmentCompletion = await itGet(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.debug("no completion data found ", e);
|
log.debug("no completion data found ", e);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return this.completion_data;
|
return this.assignmentCompletion;
|
||||||
},
|
},
|
||||||
getCompletionDataForUserInput(id: BlockId) {
|
getCompletionDataForUserInput(id: string) {
|
||||||
return this.completion_data[id];
|
return this.assignmentCompletion?.completion_data[id];
|
||||||
},
|
},
|
||||||
async upsertAssignmentCompletion(
|
async upsertAssignmentCompletion(data: UpsertUserAssignmentCompletion) {
|
||||||
assignmentId: number,
|
if (this.assignmentCompletion) {
|
||||||
completion_data: AssignmentCompletionData,
|
merge(this.assignmentCompletion.completion_data, data.completion_data);
|
||||||
courseSessionId: number,
|
this.assignmentCompletion.completion_status = data.completion_status;
|
||||||
submit: boolean
|
}
|
||||||
) {
|
|
||||||
const data = {
|
|
||||||
assignment_id: assignmentId,
|
|
||||||
completion_status: submit ? "submitted" : "in_progress",
|
|
||||||
course_session_id: courseSessionId,
|
|
||||||
completion_data: completion_data,
|
|
||||||
};
|
|
||||||
const responseData = await itPost(`/api/assignment/upsert/`, data);
|
const responseData = await itPost(`/api/assignment/upsert/`, data);
|
||||||
if (responseData) {
|
if (responseData) {
|
||||||
this.completion_data = responseData.completion_data;
|
this.assignmentCompletion = responseData;
|
||||||
this.submitted = responseData.completion_status === "submitted";
|
|
||||||
}
|
}
|
||||||
return 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;
|
} as CockpitStoreState;
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async loadCourseSessionUsers(courseSlug: string, reload = false) {
|
async loadCourseSessionUsers(courseSessionId: number, reload = false) {
|
||||||
log.debug("loadCockpitData called");
|
log.debug("loadCockpitData called");
|
||||||
const users = (await itGetCached(`/api/course/sessions/${courseSlug}/users/`, {
|
const users = (await itGetCached(
|
||||||
reload: reload,
|
`/api/course/sessions/${courseSessionId}/users/`,
|
||||||
})) as CourseSessionUser[];
|
{
|
||||||
|
reload: reload,
|
||||||
|
}
|
||||||
|
)) as CourseSessionUser[];
|
||||||
|
|
||||||
this.courseSessionUsers = users.filter((user) => user.role === "MEMBER");
|
this.courseSessionUsers = users.filter((user) => user.role === "MEMBER");
|
||||||
|
|
||||||
|
|
@ -45,7 +48,7 @@ export const useCockpitStore = defineStore({
|
||||||
}
|
}
|
||||||
return this.courseSessionUsers;
|
return this.courseSessionUsers;
|
||||||
},
|
},
|
||||||
toggleCourseSelection(translationKey: string) {
|
toggleCircleSelection(translationKey: string) {
|
||||||
if (this.selectedCircles.indexOf(translationKey) < 0) {
|
if (this.selectedCircles.indexOf(translationKey) < 0) {
|
||||||
this.selectedCircles.push(translationKey);
|
this.selectedCircles.push(translationKey);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,9 @@ function loadCourseSessionsData(reload = false) {
|
||||||
// TODO: refactor after implementing of Klassenkonzept
|
// TODO: refactor after implementing of Klassenkonzept
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
courseSessions.value.map(async (cs) => {
|
courseSessions.value.map(async (cs) => {
|
||||||
const users = (await itGetCached(
|
const users = (await itGetCached(`/api/course/sessions/${cs.id}/users/`, {
|
||||||
`/api/course/sessions/${cs.course.slug}/users/`,
|
reload: reload,
|
||||||
{
|
})) as CourseSessionUser[];
|
||||||
reload: reload,
|
|
||||||
}
|
|
||||||
)) as CourseSessionUser[];
|
|
||||||
cs.users = users;
|
cs.users = users;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -236,9 +233,9 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findAssignmentDetails(
|
function findAssignmentDetails(
|
||||||
contentId: number
|
contentId?: number
|
||||||
): CourseSessionAssignmentDetails | undefined {
|
): CourseSessionAssignmentDetails | undefined {
|
||||||
if (currentCourseSession.value) {
|
if (contentId && currentCourseSession.value) {
|
||||||
return currentCourseSession.value.assignment_details_list.find(
|
return currentCourseSession.value.assignment_details_list.find(
|
||||||
(assignmentDetails) => assignmentDetails.learningContentId === contentId
|
(assignmentDetails) => assignmentDetails.learningContentId === contentId
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ export interface BaseLearningContentBlock {
|
||||||
|
|
||||||
export interface AssignmentBlock extends BaseLearningContentBlock {
|
export interface AssignmentBlock extends BaseLearningContentBlock {
|
||||||
readonly type: "assignment";
|
readonly type: "assignment";
|
||||||
|
readonly value: {
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
assignment: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookBlock extends BaseLearningContentBlock {
|
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 {
|
export interface Assignment extends BaseCourseWagtailPage {
|
||||||
readonly type: "assignment.Assignment";
|
readonly type: "assignment.Assignment";
|
||||||
readonly starting_position: string;
|
readonly starting_position: string;
|
||||||
readonly effort_required: string;
|
readonly effort_required: string;
|
||||||
readonly performance_objectives: AssignmentPerformanceObjective[];
|
readonly performance_objectives: AssignmentPerformanceObjective[];
|
||||||
readonly assessment_description: string;
|
readonly evaluation_description: string;
|
||||||
readonly assessment_document_url: string;
|
readonly evaluation_document_url: string;
|
||||||
readonly tasks: AssignmentTask[];
|
readonly tasks: AssignmentTask[];
|
||||||
|
readonly evaluation_tasks: AssignmentEvaluationTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PerformanceCriteria extends BaseCourseWagtailPage {
|
export interface PerformanceCriteria extends BaseCourseWagtailPage {
|
||||||
|
|
@ -407,7 +430,8 @@ export interface CourseSessionAttendanceDay {
|
||||||
|
|
||||||
export interface CourseSessionAssignmentDetails {
|
export interface CourseSessionAssignmentDetails {
|
||||||
learningContentId: number;
|
learningContentId: number;
|
||||||
deadlineDateTimeUtc: string;
|
submissionDeadlineDateTimeUtc: string;
|
||||||
|
evaluationDeadlineDateTimeUtc: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CourseSession {
|
export interface CourseSession {
|
||||||
|
|
@ -479,3 +503,69 @@ export interface Notification {
|
||||||
actor_avatar_url: string | null;
|
actor_avatar_url: string | null;
|
||||||
course: 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":
|
"handlungsfelder-overview":
|
||||||
"url('/static/icons/icon-handlungsfelder-overview.svg')",
|
"url('/static/icons/icon-handlungsfelder-overview.svg')",
|
||||||
"lernmedien-overview": "url('/static/icons/icon-lernmedien-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')",
|
message: "url('/static/icons/icon-message.svg')",
|
||||||
},
|
},
|
||||||
borderColor: (theme) => ({
|
borderColor: (theme) => ({
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ body {
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.default-wagtail-rich-text ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
@apply fill-current;
|
@apply fill-current;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
import { login } from "./helpers";
|
import { login } from "./helpers";
|
||||||
|
|
||||||
const navigateToAssignment = () => {
|
describe("student test", () => {
|
||||||
cy.visit(
|
|
||||||
"/course/überbetriebliche-kurse/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("assignment completion", () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.manageCommand("cypress_reset");
|
cy.manageCommand("cypress_reset");
|
||||||
login("admin", "test");
|
login("test-student1@example.com", "test");
|
||||||
navigateToAssignment();
|
cy.visit(
|
||||||
|
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open assignment", () => {
|
it("can open assignment", () => {
|
||||||
|
|
@ -27,10 +23,10 @@ describe("assignment completion", () => {
|
||||||
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
|
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2 Steps forward, 1 step backwards
|
cy.learningContentMultiLayoutNextStep();
|
||||||
for (let i = 0; i !== 2; i++) {
|
cy.learningContentMultiLayoutNextStep();
|
||||||
cy.learningContentMultiLayoutNextStep();
|
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
|
||||||
}
|
|
||||||
cy.learningContentMultiLayoutPreviousStep();
|
cy.learningContentMultiLayoutPreviousStep();
|
||||||
|
|
||||||
cy.testLearningContentTitle(
|
cy.testLearningContentTitle(
|
||||||
|
|
@ -46,10 +42,7 @@ describe("assignment completion", () => {
|
||||||
);
|
);
|
||||||
// Click confirmation
|
// Click confirmation
|
||||||
cy.get('[data-cy="it-checkbox-confirmation-1"]').click({ force: true });
|
cy.get('[data-cy="it-checkbox-confirmation-1"]').click({ force: true });
|
||||||
cy.wait(250);
|
|
||||||
cy.reload();
|
cy.reload();
|
||||||
// 1 Step forward
|
|
||||||
cy.learningContentMultiLayoutNextStep();
|
|
||||||
cy.get('[data-cy="it-checkbox-confirmation-1"]').should("be.checked");
|
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"]')
|
cy.get('[data-cy="it-textarea-user-text-input-1"]')
|
||||||
.clear()
|
.clear()
|
||||||
.type("Hallovelo");
|
.type("Hallovelo");
|
||||||
|
// wait because of input debounce
|
||||||
cy.wait(550);
|
cy.wait(550);
|
||||||
cy.reload();
|
cy.reload();
|
||||||
|
|
||||||
// 2 Step forward
|
|
||||||
cy.learningContentMultiLayoutNextStep();
|
|
||||||
cy.learningContentMultiLayoutNextStep();
|
|
||||||
cy.get('[data-cy="it-textarea-user-text-input-1"]').should(
|
cy.get('[data-cy="it-textarea-user-text-input-1"]').should(
|
||||||
"have.value",
|
"have.value",
|
||||||
"Hallovelo"
|
"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 ratelimit.exceptions import Ratelimited
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.views import (
|
from vbv_lernwelt.assignment.views import (
|
||||||
grade_assignment_completion,
|
evaluate_assignment_completion,
|
||||||
request_assignment_completion,
|
request_assignment_completion,
|
||||||
request_assignment_completion_for_user,
|
request_assignment_completion_for_user,
|
||||||
|
request_assignment_completion_status,
|
||||||
upsert_user_assignment_completion,
|
upsert_user_assignment_completion,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||||
|
|
@ -87,7 +88,7 @@ urlpatterns = [
|
||||||
|
|
||||||
# course
|
# course
|
||||||
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
|
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"),
|
name="get_course_session_users"),
|
||||||
path(r"api/course/page/<slug_or_id>/", course_page_api_view,
|
path(r"api/course/page/<slug_or_id>/", course_page_api_view,
|
||||||
name="course_page_api_view"),
|
name="course_page_api_view"),
|
||||||
|
|
@ -102,11 +103,14 @@ urlpatterns = [
|
||||||
# assignment
|
# assignment
|
||||||
path(r"api/assignment/upsert/", upsert_user_assignment_completion,
|
path(r"api/assignment/upsert/", upsert_user_assignment_completion,
|
||||||
name="upsert_user_assignment_completion"),
|
name="upsert_user_assignment_completion"),
|
||||||
path(r"api/assignment/grade/", grade_assignment_completion,
|
path(r"api/assignment/evaluate/", evaluate_assignment_completion,
|
||||||
name="grade_assignment_completion"),
|
name="evaluate_assignment_completion"),
|
||||||
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/",
|
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/",
|
||||||
request_assignment_completion,
|
request_assignment_completion,
|
||||||
name="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>/",
|
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/<int:user_id>/",
|
||||||
request_assignment_completion_for_user,
|
request_assignment_completion_for_user,
|
||||||
name="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 (
|
from vbv_lernwelt.assignment.tests.assignment_factories import (
|
||||||
AssignmentFactory,
|
AssignmentFactory,
|
||||||
AssignmentListPageFactory,
|
AssignmentListPageFactory,
|
||||||
|
EvaluationSubTaskBlockFactory,
|
||||||
|
EvaluationTaskBlockFactory,
|
||||||
ExplanationBlockFactory,
|
ExplanationBlockFactory,
|
||||||
PerformanceObjectiveBlockFactory,
|
PerformanceObjectiveBlockFactory,
|
||||||
TaskBlockFactory,
|
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.consts import COURSE_TEST_ID, COURSE_UK
|
||||||
from vbv_lernwelt.course.models import CoursePage
|
from vbv_lernwelt.course.models import CoursePage
|
||||||
from wagtail.blocks import StreamValue
|
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):
|
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",
|
evaluation_document_url="/static/media/assignments/UK_03_09_NACH_KN_Beurteilungsraster.pdf",
|
||||||
assessment_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
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 = []
|
assignment.tasks = []
|
||||||
|
|
@ -57,18 +243,24 @@ def create_uk_assignments(course_id=COURSE_UK):
|
||||||
TaskBlockFactory(
|
TaskBlockFactory(
|
||||||
title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden",
|
title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden",
|
||||||
# it is hard to create a StreamValue programmatically, we have to
|
# 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(
|
content=StreamValue(
|
||||||
TaskContentStreamBlock(),
|
TaskContentStreamBlock(),
|
||||||
stream_data=[
|
stream_data=[
|
||||||
(
|
(
|
||||||
"explanation",
|
"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",
|
"user_confirmation",
|
||||||
ExplanationBlockFactory(
|
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",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
replace_whitespace(
|
||||||
Erläutere die Kundensituation und die Ausgangslage.
|
"""
|
||||||
* Hast du alle Informationen, die du für den Policen-Check benötigst?
|
Erläutere die Kundensituation und die Ausgangslage.
|
||||||
* Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.)
|
<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",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist."
|
||||||
Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -141,40 +335,32 @@ def create_uk_assignments(course_id=COURSE_UK):
|
||||||
(
|
(
|
||||||
"explanation",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person."
|
||||||
Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung"
|
||||||
Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung."
|
||||||
Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist."
|
||||||
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",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:"
|
||||||
Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung."
|
||||||
War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Was ist dir bei der Bearbeitung des Auftrags gut gelungen?"
|
||||||
Was ist dir bei der Bearbeitung des Auftrags gut gelungen?
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?"
|
||||||
Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -249,30 +427,24 @@ def create_uk_assignments(course_id=COURSE_UK):
|
||||||
(
|
(
|
||||||
"explanation",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab."
|
||||||
Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Was würdest du beim nächsten Mal anders machen?"
|
||||||
Was würdest du beim nächsten Mal anders machen?
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Was hast du beim Bearbeiten des Auftrags Neues gelernt?"
|
||||||
Was hast du beim Bearbeiten des Auftrags Neues gelernt?
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -284,6 +456,8 @@ def create_uk_assignments(course_id=COURSE_UK):
|
||||||
|
|
||||||
assignment.save()
|
assignment.save()
|
||||||
|
|
||||||
|
return assignment
|
||||||
|
|
||||||
|
|
||||||
def create_test_assignment(course_id=COURSE_TEST_ID):
|
def create_test_assignment(course_id=COURSE_TEST_ID):
|
||||||
course_page = CoursePage.objects.get(course_id=course_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",
|
evaluation_document_url="/static/media/assignments/UK_03_09_NACH_KN_Beurteilungsraster.pdf",
|
||||||
assessment_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
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 = []
|
assignment.tasks = []
|
||||||
|
|
@ -329,18 +682,24 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
||||||
TaskBlockFactory(
|
TaskBlockFactory(
|
||||||
title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden",
|
title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden",
|
||||||
# it is hard to create a StreamValue programmatically, we have to
|
# 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(
|
content=StreamValue(
|
||||||
TaskContentStreamBlock(),
|
TaskContentStreamBlock(),
|
||||||
stream_data=[
|
stream_data=[
|
||||||
(
|
(
|
||||||
"explanation",
|
"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",
|
"user_confirmation",
|
||||||
ExplanationBlockFactory(
|
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",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
replace_whitespace(
|
||||||
Erläutere die Kundensituation und die Ausgangslage.
|
"""
|
||||||
* Hast du alle Informationen, die du für den Policen-Check benötigst?
|
Erläutere die Kundensituation und die Ausgangslage.
|
||||||
* Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.)
|
<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",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist."
|
||||||
Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -413,40 +774,32 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
||||||
(
|
(
|
||||||
"explanation",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person."
|
||||||
Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung"
|
||||||
Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung."
|
||||||
Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist."
|
||||||
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",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:"
|
||||||
Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung."
|
||||||
War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Was ist dir bei der Bearbeitung des Auftrags gut gelungen?"
|
||||||
Was ist dir bei der Bearbeitung des Auftrags gut gelungen?
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?"
|
||||||
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",
|
"explanation",
|
||||||
ExplanationBlockFactory(
|
ExplanationBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab."
|
||||||
Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab.
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Was würdest du beim nächsten Mal anders machen?"
|
||||||
Was würdest du beim nächsten Mal anders machen?
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"user_text_input",
|
||||||
UserTextInputBlockFactory(
|
UserTextInputBlockFactory(
|
||||||
text=replace_whitespace(
|
text=RichText(
|
||||||
"""
|
"Was hast du beim Bearbeiten des Auftrags Neues gelernt?"
|
||||||
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 django.db.models.deletion
|
||||||
import wagtail.blocks
|
import wagtail.blocks
|
||||||
import wagtail.fields
|
import wagtail.fields
|
||||||
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
import vbv_lernwelt.assignment.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("wagtailcore", "0069_log_entry_jsonfield"),
|
("wagtailcore", "0083_workflowcontenttype"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("course", "0006_alter_coursesession_attendance_days"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
@ -33,7 +34,9 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"starting_position",
|
"starting_position",
|
||||||
models.TextField(help_text="Erläuterung der Ausgangslage"),
|
wagtail.fields.RichTextField(
|
||||||
|
help_text="Erläuterung der Ausgangslage"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"effort_required",
|
"effort_required",
|
||||||
|
|
@ -57,20 +60,6 @@ class Migration(migrations.Migration):
|
||||||
use_json_field=True,
|
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",
|
"tasks",
|
||||||
wagtail.fields.StreamField(
|
wagtail.fields.StreamField(
|
||||||
|
|
@ -94,14 +83,35 @@ class Migration(migrations.Migration):
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"text",
|
"text",
|
||||||
wagtail.blocks.TextBlock(),
|
wagtail.blocks.RichTextBlock(
|
||||||
|
features=[
|
||||||
|
"ul",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user_text_input",
|
"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",
|
"user_confirmation",
|
||||||
|
|
@ -109,7 +119,13 @@ class Migration(migrations.Migration):
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"text",
|
"text",
|
||||||
wagtail.blocks.TextBlock(),
|
wagtail.blocks.RichTextBlock(
|
||||||
|
features=[
|
||||||
|
"ul",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
@ -127,6 +143,78 @@ class Migration(migrations.Migration):
|
||||||
use_json_field=True,
|
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={
|
options={
|
||||||
"verbose_name": "Auftrag",
|
"verbose_name": "Auftrag",
|
||||||
|
|
@ -153,4 +241,155 @@ class Migration(migrations.Migration):
|
||||||
},
|
},
|
||||||
bases=("wagtailcore.page",),
|
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 slugify import slugify
|
||||||
from wagtail import blocks
|
from wagtail import blocks
|
||||||
from wagtail.admin.panels import FieldPanel
|
from wagtail.admin.panels import FieldPanel
|
||||||
from wagtail.fields import StreamField
|
from wagtail.fields import RichTextField, StreamField
|
||||||
from wagtail.models import Page
|
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.model_utils import find_available_slug
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseBasePage
|
from vbv_lernwelt.course.models import CourseBasePage
|
||||||
|
|
@ -28,12 +29,8 @@ class AssignmentListPage(CourseBasePage):
|
||||||
return f"{self.title}"
|
return f"{self.title}"
|
||||||
|
|
||||||
|
|
||||||
# class AssignmentSubmission(modModel):
|
|
||||||
# created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
|
|
||||||
class ExplanationBlock(blocks.StructBlock):
|
class ExplanationBlock(blocks.StructBlock):
|
||||||
text = blocks.TextBlock()
|
text = blocks.RichTextBlock(features=DEFAULT_RICH_TEXT_FEATURES)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
icon = "comment"
|
icon = "comment"
|
||||||
|
|
@ -47,14 +44,16 @@ class PerformanceObjectiveBlock(blocks.StructBlock):
|
||||||
|
|
||||||
|
|
||||||
class UserTextInputBlock(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:
|
class Meta:
|
||||||
icon = "edit"
|
icon = "edit"
|
||||||
|
|
||||||
|
|
||||||
class UserConfirmationBlock(blocks.StructBlock):
|
class UserConfirmationBlock(blocks.StructBlock):
|
||||||
text = blocks.TextBlock()
|
text = blocks.RichTextBlock(features=DEFAULT_RICH_TEXT_FEATURES)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
icon = "tick-inverse"
|
icon = "tick-inverse"
|
||||||
|
|
@ -78,17 +77,47 @@ class TaskBlock(blocks.StructBlock):
|
||||||
label = "Teilauftrag"
|
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):
|
class Assignment(CourseBasePage):
|
||||||
serialize_field_names = [
|
serialize_field_names = [
|
||||||
"starting_position",
|
"starting_position",
|
||||||
"effort_required",
|
"effort_required",
|
||||||
"performance_objectives",
|
"performance_objectives",
|
||||||
"assessment_description",
|
"evaluation_description",
|
||||||
"assessment_document_url",
|
"evaluation_document_url",
|
||||||
"tasks",
|
"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(
|
effort_required = models.CharField(
|
||||||
max_length=100, help_text="Zeitaufwand als Text", blank=True
|
max_length=100, help_text="Zeitaufwand als Text", blank=True
|
||||||
)
|
)
|
||||||
|
|
@ -101,14 +130,6 @@ class Assignment(CourseBasePage):
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Leistungsziele des Auftrags",
|
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(
|
tasks = StreamField(
|
||||||
[
|
[
|
||||||
|
|
@ -119,13 +140,34 @@ class Assignment(CourseBasePage):
|
||||||
help_text="Teilaufgaben",
|
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 + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("starting_position"),
|
FieldPanel("starting_position"),
|
||||||
FieldPanel("effort_required"),
|
FieldPanel("effort_required"),
|
||||||
FieldPanel("performance_objectives"),
|
FieldPanel("performance_objectives"),
|
||||||
FieldPanel("assessment_description"),
|
|
||||||
FieldPanel("assessment_document_url"),
|
|
||||||
FieldPanel("tasks"),
|
FieldPanel("tasks"),
|
||||||
|
FieldPanel("evaluation_description"),
|
||||||
|
FieldPanel("evaluation_document_url"),
|
||||||
|
FieldPanel("evaluation_tasks"),
|
||||||
]
|
]
|
||||||
|
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
|
|
@ -172,10 +214,16 @@ class Assignment(CourseBasePage):
|
||||||
if sub_dict["type"] in subtask_types
|
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 = Enum(
|
||||||
"AssignmentCompletionStatus",
|
"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)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
submitted_at = models.DateTimeField(null=True, blank=True)
|
submitted_at = models.DateTimeField(null=True, blank=True)
|
||||||
graded_at = models.DateTimeField(null=True, blank=True)
|
evaluation_submitted_at = models.DateTimeField(null=True, blank=True)
|
||||||
grading_user = models.ForeignKey(
|
evaluation_user = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="+",
|
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_user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
|
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
|
||||||
|
|
@ -217,12 +267,12 @@ class AssignmentCompletion(models.Model):
|
||||||
|
|
||||||
class AssignmentCompletionAuditLog(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)
|
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="+"
|
User, on_delete=models.SET_NULL, null=True, blank=True, related_name="+"
|
||||||
)
|
)
|
||||||
assignment_user = models.ForeignKey(
|
assignment_user = models.ForeignKey(
|
||||||
|
|
@ -246,4 +296,6 @@ class AssignmentCompletionAuditLog(models.Model):
|
||||||
|
|
||||||
assignment_user_email = models.CharField(max_length=255)
|
assignment_user_email = models.CharField(max_length=255)
|
||||||
assignment_slug = 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",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"submitted_at",
|
"submitted_at",
|
||||||
"graded_at",
|
"evaluation_submitted_at",
|
||||||
"assignment_user",
|
"assignment_user",
|
||||||
"assignment",
|
"assignment",
|
||||||
"course_session",
|
"course_session",
|
||||||
"completion_status",
|
"completion_status",
|
||||||
"completion_data",
|
"completion_data",
|
||||||
"grading_user",
|
"evaluation_user",
|
||||||
"additional_json_data",
|
"additional_json_data",
|
||||||
|
"evaluation_grade",
|
||||||
|
"evaluation_points",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ def update_assignment_completion(
|
||||||
course_session: CourseSession,
|
course_session: CourseSession,
|
||||||
completion_data=None,
|
completion_data=None,
|
||||||
completion_status: Type[AssignmentCompletionStatus] = "in_progress",
|
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,
|
validate_completion_status_change: bool = True,
|
||||||
copy_task_data: bool = False,
|
copy_task_data: bool = False,
|
||||||
) -> AssignmentCompletion:
|
) -> 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
|
: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
|
context
|
||||||
:return: AssignmentCompletion
|
:return: AssignmentCompletion
|
||||||
"""
|
"""
|
||||||
|
|
@ -56,31 +58,61 @@ def update_assignment_completion(
|
||||||
if validate_completion_status_change:
|
if validate_completion_status_change:
|
||||||
# TODO: check time?
|
# TODO: check time?
|
||||||
if completion_status == "submitted":
|
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(
|
raise serializers.ValidationError(
|
||||||
{
|
{
|
||||||
"completion_status": f"Cannot update completion status from {ac.completion_status} to submitted"
|
"completion_status": f"Cannot update completion status from {ac.completion_status} to submitted"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif completion_status == "graded":
|
elif completion_status == "evaluation_submitted":
|
||||||
if ac.completion_status == "graded":
|
if ac.completion_status == "evaluation_submitted":
|
||||||
raise serializers.ValidationError(
|
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 completion_status == "in_progress" and ac.completion_status != "in_progress":
|
||||||
if grading_user is None:
|
|
||||||
raise serializers.ValidationError(
|
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":
|
if completion_status == "submitted":
|
||||||
ac.submitted_at = timezone.now()
|
ac.submitted_at = timezone.now()
|
||||||
elif completion_status == "graded":
|
elif completion_status == "evaluation_submitted":
|
||||||
ac.graded_at = timezone.now()
|
ac.evaluation_submitted_at = timezone.now()
|
||||||
|
|
||||||
ac.completion_status = completion_status
|
ac.completion_status = completion_status
|
||||||
|
|
||||||
|
|
@ -94,32 +126,34 @@ def update_assignment_completion(
|
||||||
|
|
||||||
if copy_task_data:
|
if copy_task_data:
|
||||||
# copy over the question data, so that we don't lose the context
|
# 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():
|
for key, value in ac.completion_data.items():
|
||||||
task_data = find_first(substasks, pred=lambda x: x["id"] == key)
|
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()
|
ac.save()
|
||||||
|
|
||||||
if completion_status in ["graded", "submitted"]:
|
if completion_status in ["evaluation_submitted", "submitted"]:
|
||||||
acl = AssignmentCompletionAuditLog.objects.create(
|
acl = AssignmentCompletionAuditLog.objects.create(
|
||||||
assignment_user=assignment_user,
|
assignment_user=assignment_user,
|
||||||
assignment=assignment,
|
assignment=assignment,
|
||||||
course_session=course_session,
|
course_session=course_session,
|
||||||
grading_user=grading_user,
|
evaluation_user=evaluation_user,
|
||||||
completion_status=completion_status,
|
completion_status=completion_status,
|
||||||
assignment_user_email=assignment_user.email,
|
assignment_user_email=assignment_user.email,
|
||||||
assignment_slug=assignment.slug,
|
assignment_slug=assignment.slug,
|
||||||
completion_data=deepcopy(ac.completion_data),
|
completion_data=deepcopy(ac.completion_data),
|
||||||
)
|
)
|
||||||
if grading_user:
|
if evaluation_user:
|
||||||
acl.grading_user_email = grading_user.email
|
acl.evaluation_user_email = evaluation_user.email
|
||||||
|
|
||||||
# copy over the question data, so that we don't lose the context
|
# 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():
|
for key, value in acl.completion_data.items():
|
||||||
task_data = find_first(substasks, pred=lambda x: x["id"] == key)
|
task_data = find_first(subtasks, pred=lambda x: x["id"] == key)
|
||||||
acl.completion_data[key].update(task_data)
|
if task_data:
|
||||||
|
acl.completion_data[key].update(task_data)
|
||||||
acl.save()
|
acl.save()
|
||||||
|
|
||||||
return ac
|
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
|
Removes all entries from completion_data which are not known to the assignment
|
||||||
"""
|
"""
|
||||||
possible_subtask_uuids = [
|
input_task_ids = [task["id"] for task in assignment.get_input_tasks()]
|
||||||
subtask["id"] for subtask in assignment.filter_user_subtasks()
|
|
||||||
]
|
|
||||||
filtered_completion_data = {
|
filtered_completion_data = {
|
||||||
key: value
|
key: value for key, value in completion_data.items() if key in input_task_ids
|
||||||
for key, value in completion_data.items()
|
|
||||||
if key in possible_subtask_uuids
|
|
||||||
}
|
}
|
||||||
return filtered_completion_data
|
return filtered_completion_data
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import wagtail_factories
|
import wagtail_factories
|
||||||
from factory import SubFactory
|
from factory import SubFactory
|
||||||
|
from wagtail.rich_text import RichText
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import (
|
from vbv_lernwelt.assignment.models import (
|
||||||
Assignment,
|
Assignment,
|
||||||
AssignmentListPage,
|
AssignmentListPage,
|
||||||
|
EvaluationSubTaskBlock,
|
||||||
|
EvaluationTaskBlock,
|
||||||
ExplanationBlock,
|
ExplanationBlock,
|
||||||
PerformanceObjectiveBlock,
|
PerformanceObjectiveBlock,
|
||||||
TaskBlock,
|
TaskBlock,
|
||||||
|
|
@ -15,14 +18,16 @@ from vbv_lernwelt.core.utils import replace_whitespace
|
||||||
|
|
||||||
|
|
||||||
class ExplanationBlockFactory(wagtail_factories.StructBlockFactory):
|
class ExplanationBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
text = "Dies ist ein Beispieltext."
|
text = RichText("Dies ist ein Beispieltext.")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExplanationBlock
|
model = ExplanationBlock
|
||||||
|
|
||||||
|
|
||||||
class UserConfirmationBlockFactory(wagtail_factories.StructBlockFactory):
|
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:
|
class Meta:
|
||||||
model = UserConfirmationBlock
|
model = UserConfirmationBlock
|
||||||
|
|
@ -50,6 +55,24 @@ class TaskBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
model = TaskBlock
|
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):
|
class PerformanceObjectiveBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
text = "Die Teilnehmer können die wichtigsten Eckwerte eines Versicherungsverhältnisses erfassen."
|
text = "Die Teilnehmer können die wichtigsten Eckwerte eines Versicherungsverhältnisses erfassen."
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ class AssignmentApiTestCase(APITestCase):
|
||||||
|
|
||||||
# make api call
|
# make api call
|
||||||
self.client.login(username="admin", password="test")
|
self.client.login(username="admin", password="test")
|
||||||
url = f"/api/assignment/grade/"
|
url = f"/api/assignment/evaluate/"
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
url,
|
url,
|
||||||
|
|
@ -181,7 +181,7 @@ class AssignmentApiTestCase(APITestCase):
|
||||||
"assignment_id": self.assignment.id,
|
"assignment_id": self.assignment.id,
|
||||||
"assignment_user_id": self.student.id,
|
"assignment_user_id": self.student.id,
|
||||||
"course_session_id": self.cs.id,
|
"course_session_id": self.cs.id,
|
||||||
"completion_status": "grading_in_progress",
|
"completion_status": "evaluation_in_progress",
|
||||||
"completion_data": {
|
"completion_data": {
|
||||||
user_text_input["id"]: {
|
user_text_input["id"]: {
|
||||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||||
|
|
@ -196,7 +196,7 @@ class AssignmentApiTestCase(APITestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response_json["assignment_user"], self.student.id)
|
self.assertEqual(response_json["assignment_user"], self.student.id)
|
||||||
self.assertEqual(response_json["assignment"], self.assignment.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(
|
self.assertDictEqual(
|
||||||
response_json["completion_data"],
|
response_json["completion_data"],
|
||||||
{
|
{
|
||||||
|
|
@ -212,7 +212,7 @@ class AssignmentApiTestCase(APITestCase):
|
||||||
course_session_id=self.cs.id,
|
course_session_id=self.cs.id,
|
||||||
assignment_id=self.assignment.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(
|
self.assertDictEqual(
|
||||||
db_entry.completion_data,
|
db_entry.completion_data,
|
||||||
{
|
{
|
||||||
|
|
@ -230,12 +230,14 @@ class AssignmentApiTestCase(APITestCase):
|
||||||
"assignment_id": self.assignment.id,
|
"assignment_id": self.assignment.id,
|
||||||
"assignment_user_id": self.student.id,
|
"assignment_user_id": self.student.id,
|
||||||
"course_session_id": self.cs.id,
|
"course_session_id": self.cs.id,
|
||||||
"completion_status": "graded",
|
"completion_status": "evaluation_submitted",
|
||||||
"completion_data": {
|
"completion_data": {
|
||||||
user_text_input["id"]: {
|
user_text_input["id"]: {
|
||||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"evaluation_grade": 4.5,
|
||||||
|
"evaluation_points": 16,
|
||||||
},
|
},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
@ -245,7 +247,7 @@ class AssignmentApiTestCase(APITestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response_json["assignment_user"], self.student.id)
|
self.assertEqual(response_json["assignment_user"], self.student.id)
|
||||||
self.assertEqual(response_json["assignment"], self.assignment.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(
|
self.assertDictEqual(
|
||||||
response_json["completion_data"],
|
response_json["completion_data"],
|
||||||
{
|
{
|
||||||
|
|
@ -261,7 +263,7 @@ class AssignmentApiTestCase(APITestCase):
|
||||||
course_session_id=self.cs.id,
|
course_session_id=self.cs.id,
|
||||||
assignment_id=self.assignment.id,
|
assignment_id=self.assignment.id,
|
||||||
)
|
)
|
||||||
self.assertEqual(db_entry.completion_status, "graded")
|
self.assertEqual(db_entry.completion_status, "evaluation_submitted")
|
||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
db_entry.completion_data,
|
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(
|
acl = AssignmentCompletionAuditLog.objects.get(
|
||||||
assignment_user=self.student,
|
assignment_user=self.student,
|
||||||
course_session_id=self.cs.id,
|
course_session_id=self.cs.id,
|
||||||
assignment_id=self.assignment.id,
|
assignment_id=self.assignment.id,
|
||||||
completion_status="graded",
|
completion_status="evaluation_submitted",
|
||||||
)
|
)
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
self.assertDictEqual(
|
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(
|
subtasks = self.assignment.filter_user_subtasks(
|
||||||
subtask_types=["user_text_input"]
|
subtask_types=["user_text_input"]
|
||||||
)
|
)
|
||||||
|
|
@ -329,8 +364,8 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
||||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
completion_status="grading_in_progress",
|
completion_status="evaluation_in_progress",
|
||||||
grading_user=self.trainer,
|
evaluation_user=self.trainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
ac = AssignmentCompletion.objects.get(
|
ac = AssignmentCompletion.objects.get(
|
||||||
|
|
@ -339,7 +374,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
||||||
course_session=self.course_session,
|
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"]]
|
user_input = ac.completion_data[user_text_input["id"]]
|
||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
user_input["expert_data"], {"points": 1, "comment": "Gut gemacht!"}
|
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."
|
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(
|
subtasks = self.assignment.filter_user_subtasks(
|
||||||
subtask_types=["user_text_input"]
|
subtask_types=["user_text_input"]
|
||||||
)
|
)
|
||||||
|
|
@ -377,7 +412,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
||||||
assignment_user=self.user,
|
assignment_user=self.user,
|
||||||
assignment=self.assignment,
|
assignment=self.assignment,
|
||||||
course_session=self.course_session,
|
course_session=self.course_session,
|
||||||
completion_status="grading_in_progress",
|
completion_status="evaluation_in_progress",
|
||||||
completion_data={
|
completion_data={
|
||||||
user_text_input["id"]: {
|
user_text_input["id"]: {
|
||||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||||
|
|
@ -386,5 +421,122 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(
|
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()
|
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"])
|
@api_view(["POST"])
|
||||||
def upsert_user_assignment_completion(request):
|
def upsert_user_assignment_completion(request):
|
||||||
try:
|
try:
|
||||||
|
|
@ -107,13 +120,17 @@ def upsert_user_assignment_completion(request):
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def grade_assignment_completion(request):
|
def evaluate_assignment_completion(request):
|
||||||
try:
|
try:
|
||||||
assignment_id = request.data.get("assignment_id")
|
assignment_id = request.data.get("assignment_id")
|
||||||
assignment_user_id = request.data.get("assignment_user_id")
|
assignment_user_id = request.data.get("assignment_user_id")
|
||||||
course_session_id = request.data.get("course_session_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", {})
|
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_page = Page.objects.get(id=assignment_id)
|
||||||
assignment_user = User.objects.get(id=assignment_user_id)
|
assignment_user = User.objects.get(id=assignment_user_id)
|
||||||
|
|
@ -133,7 +150,9 @@ def grade_assignment_completion(request):
|
||||||
completion_data=completion_data,
|
completion_data=completion_data,
|
||||||
completion_status=completion_status,
|
completion_status=completion_status,
|
||||||
copy_task_data=False,
|
copy_task_data=False,
|
||||||
grading_user=request.user,
|
evaluation_user=request.user,
|
||||||
|
evaluation_grade=evaluation_grade,
|
||||||
|
evaluation_points=evaluation_points,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
@ -144,7 +163,9 @@ def grade_assignment_completion(request):
|
||||||
assignment_user_id=assignment_user_id,
|
assignment_user_id=assignment_user_id,
|
||||||
course_session_id=course_session_id,
|
course_session_id=course_session_id,
|
||||||
completion_status=completion_status,
|
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)
|
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",
|
first_name="Lina",
|
||||||
last_name="Egger",
|
last_name="Egger",
|
||||||
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
||||||
|
password="myvbv1234",
|
||||||
)
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
email="evelyn.schmid@example.com",
|
email="evelyn.schmid@example.com",
|
||||||
|
|
@ -229,6 +230,23 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
|
||||||
language="fr",
|
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):
|
def _get_or_create_user(user_model, *args, **kwargs):
|
||||||
username = kwargs.get("username", None)
|
username = kwargs.get("username", None)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import djclick as click
|
import djclick as click
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseCompletion
|
from vbv_lernwelt.course.models import CourseCompletion
|
||||||
from vbv_lernwelt.notify.models import Notification
|
from vbv_lernwelt.notify.models import Notification
|
||||||
|
|
@ -10,4 +11,5 @@ def command():
|
||||||
print("cypress reset data")
|
print("cypress reset data")
|
||||||
CourseCompletion.objects.all().delete()
|
CourseCompletion.objects.all().delete()
|
||||||
Notification.objects.all().delete()
|
Notification.objects.all().delete()
|
||||||
|
AssignmentCompletion.objects.all().delete()
|
||||||
User.objects.all().update(language="de")
|
User.objects.all().update(language="de")
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ def cypress_reset_view(request):
|
||||||
if settings.APP_ENVIRONMENT != "production":
|
if settings.APP_ENVIRONMENT != "production":
|
||||||
call_command("cypress_reset")
|
call_command("cypress_reset")
|
||||||
|
|
||||||
return HttpResponseRedirect("/admin/")
|
return HttpResponseRedirect("/server/admin/")
|
||||||
|
|
||||||
|
|
||||||
@django_view_authentication_exempt
|
@django_view_authentication_exempt
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,18 @@ from vbv_lernwelt.competence.factories import (
|
||||||
PerformanceCriteriaFactory,
|
PerformanceCriteriaFactory,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.competence.models import CompetencePage
|
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.core.tests.helpers import create_locales_for_wagtail
|
||||||
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
||||||
from vbv_lernwelt.course.factories import CoursePageFactory
|
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 (
|
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
AssignmentBlockFactory,
|
AssignmentBlockFactory,
|
||||||
AttendanceDayBlockFactory,
|
AttendanceDayBlockFactory,
|
||||||
|
|
@ -55,15 +63,40 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
|
|
||||||
if with_sessions:
|
if with_sessions:
|
||||||
# course sessions
|
# course sessions
|
||||||
CourseSession.objects.create(
|
cs_bern = CourseSession.objects.create(
|
||||||
course_id=COURSE_TEST_ID,
|
course_id=COURSE_TEST_ID,
|
||||||
title="Bern 2022 a",
|
title="Bern 2022 a",
|
||||||
)
|
)
|
||||||
CourseSession.objects.create(
|
cs_zurich = CourseSession.objects.create(
|
||||||
course_id=COURSE_TEST_ID,
|
course_id=COURSE_TEST_ID,
|
||||||
title="Zürich 2022 a",
|
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
|
return course
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ def command(course):
|
||||||
slug="überbetriebliche-kurse-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
slug="überbetriebliche-kurse-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
||||||
),
|
),
|
||||||
course_session=CourseSession.objects.get(title="Bern 2023 a"),
|
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:
|
if COURSE_UK_FR in course:
|
||||||
|
|
@ -166,7 +166,8 @@ def create_course_uk_de():
|
||||||
"learningContentId": LearningContent.objects.get(
|
"learningContentId": LearningContent.objects.get(
|
||||||
slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"
|
slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||||
).id,
|
).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):
|
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"])
|
@api_view(["GET"])
|
||||||
def get_course_session_users(request, course_slug):
|
def get_course_session_users(request, course_session_id):
|
||||||
try:
|
try:
|
||||||
course_sessions = course_sessions_for_user_qs(request.user).filter(
|
qs = CourseSessionUser.objects.filter(course_session_id=course_session_id)
|
||||||
course__slug=course_slug
|
|
||||||
)
|
|
||||||
qs = CourseSessionUser.objects.filter(course_session__in=course_sessions)
|
|
||||||
|
|
||||||
user_data = [csu.to_dict() for csu in qs]
|
user_data = [csu.to_dict() for csu in qs]
|
||||||
return Response(status=200, data=user_data)
|
return Response(status=200, data=user_data)
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -9,10 +9,6 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn" name="">Testdaten zurück setzen</button>
|
<button class="btn" name="">Testdaten zurück setzen</button>
|
||||||
</form>
|
</form>
|
||||||
<form action="/api/core/schemareset/" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button class="btn" name="">Datenbank zurück setzen</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue