VBV-321: squash current work
Load and display AssignmentCompletionStatus Datenmodell um Bewertungskriterien erweitern Refactor assignment pages Show user results Update some data fields Show user assignment evaluation Changes after rebase Rename `grading` -> `evaluation` Add evaluation data to AssignmentCompletion Refactor usage of `assignmentStore` Store points for evaluation Only debounce text Show user responses on evaluation page
This commit is contained in:
parent
90d9734f9b
commit
fef864df25
|
|
@ -1,12 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
export type StatusCountKey = "fail" | "success" | "unknown";
|
||||
export type StatusCount = Record<StatusCountKey, number>;
|
||||
|
||||
const props = defineProps<{
|
||||
statusCount?: {
|
||||
fail: number;
|
||||
success: number;
|
||||
unknown: number;
|
||||
};
|
||||
statusCount?: StatusCount;
|
||||
}>();
|
||||
|
||||
const total = computed(() => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2 v-if="label" class="heading-1 mb-8 block">{{ label }}</h2>
|
||||
<div v-if="label" class="mb-2 block">{{ label }}</div>
|
||||
<textarea
|
||||
:value="modelValue"
|
||||
class="h-40 w-full border-gray-500"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import * as log from "loglevel";
|
||||
|
|
@ -15,21 +16,25 @@ const props = defineProps<{
|
|||
const cockpitStore = useCockpitStore();
|
||||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("CockpitParentPage mounted", props.courseSlug);
|
||||
|
||||
try {
|
||||
await cockpitStore.loadCourseSessionUsers(props.courseSlug);
|
||||
cockpitStore.courseSessionUsers?.forEach((csu) => {
|
||||
competenceStore.loadCompetenceProfilePage(
|
||||
props.courseSlug + "-competence",
|
||||
csu.user_id
|
||||
);
|
||||
const currentCourseSession = courseSessionStore.currentCourseSession;
|
||||
if (currentCourseSession?.id) {
|
||||
await cockpitStore.loadCourseSessionUsers(currentCourseSession.id);
|
||||
cockpitStore.courseSessionUsers?.forEach((csu) => {
|
||||
competenceStore.loadCompetenceProfilePage(
|
||||
props.courseSlug + "-competence",
|
||||
csu.user_id
|
||||
);
|
||||
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
|
||||
});
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", useUserStore().id);
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
|
||||
});
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", useUserStore().id);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
<script setup lang="ts">
|
||||
import type { StatusCount, StatusCountKey } from "@/components/ui/ItProgress.vue";
|
||||
import UserEvaluation from "@/pages/cockpit/assignmentEvaluationPage/UserEvaluation.vue";
|
||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
CourseSessionAssignmentDetails,
|
||||
CourseSessionUser,
|
||||
} from "@/types";
|
||||
import log from "loglevel";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
assignmentId: number;
|
||||
userId: string;
|
||||
}>();
|
||||
|
||||
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId);
|
||||
|
||||
export interface StateInterface {
|
||||
assignment: Assignment | undefined;
|
||||
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
|
||||
completionData: AssignmentCompletion | undefined;
|
||||
assignmentUser: CourseSessionUser | undefined;
|
||||
}
|
||||
|
||||
const state: StateInterface = reactive({
|
||||
assignment: undefined,
|
||||
courseSessionAssignmentDetails: undefined,
|
||||
completionData: undefined,
|
||||
assignmentUser: undefined,
|
||||
});
|
||||
|
||||
const assignmentStore = useAssignmentStore();
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("AssignmentView mounted", props.assignmentId, props.learningContent);
|
||||
|
||||
if (courseSessionStore.currentCourseSession) {
|
||||
state.assignmentUser = courseSessionStore.currentCourseSession.users.find(
|
||||
(user) => user.user_id == props.userId
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
state.assignment = await assignmentStore.loadAssignment(props.assignmentId);
|
||||
state.completionData = await assignmentStore.loadAssignmentCompletion(
|
||||
props.assignmentId,
|
||||
courseSessionStore.currentCourseSession.id,
|
||||
props.userId
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
function exit() {
|
||||
router.push({
|
||||
path: `/course/${props.courseSlug}/cockpit/assignment`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="state.assignment && state.completionData">
|
||||
<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>Geleitete Fallarbeit: {{ state.assignment.title }}</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="exit()"
|
||||
>
|
||||
<it-icon-close></it-icon-close>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="flex">
|
||||
<div class="w-1/2 flex-initial bg-white 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="state.completionData?.completion_data"
|
||||
:allow-edit="false"
|
||||
></AssignmentSubmissionResponses>
|
||||
</div>
|
||||
<div class="w-1/2 flex-initial bg-gray-200 p-10">
|
||||
<UserEvaluation
|
||||
:assignment-completion="state.completionData"
|
||||
:assignment-user="state.assignmentUser"
|
||||
:assignment="state.assignment"
|
||||
></UserEvaluation>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
<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;
|
||||
}>();
|
||||
|
||||
log.debug("EvaluationTask setup", props.taskIndex);
|
||||
|
||||
const courseSessionStore = 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: courseSessionStore.currentCourseSession!.id,
|
||||
completion_data: completionData,
|
||||
completion_status: "evaluation_in_progress",
|
||||
});
|
||||
}
|
||||
|
||||
const evaluateAssignmentCompletionDebounced = useDebounceFn(
|
||||
evaluateAssignmentCompletion,
|
||||
500
|
||||
);
|
||||
</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="mb-8">{{ 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"
|
||||
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"
|
||||
@update:model-value="onUpdateText($event)"
|
||||
></ItTextarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import UserEvaluationTask from "@/pages/cockpit/assignmentEvaluationPage/EvaluationTask.vue";
|
||||
import type { Assignment, AssignmentCompletion, CourseSessionUser } from "@/types";
|
||||
import dayjs from "dayjs";
|
||||
import * as log from "loglevel";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentUser: CourseSessionUser;
|
||||
assignmentCompletion: AssignmentCompletion;
|
||||
assignment: Assignment;
|
||||
}>();
|
||||
|
||||
log.debug("UserEvaluation setup");
|
||||
</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>
|
||||
|
||||
<UserEvaluationTask
|
||||
:assignment-user="props.assignmentUser"
|
||||
:assignment="props.assignment"
|
||||
:task-index="0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$nav-height: 86px;
|
||||
|
||||
.h-content {
|
||||
height: calc(100vh - $nav-height);
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: $nav-height;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<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 { useCockpitStore } from "@/stores/cockpit";
|
||||
import type { AssignmentCompletionStatus, CourseSession } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { onMounted, reactive } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
assignment: AssignmentLearningContent;
|
||||
}>();
|
||||
|
||||
log.debug("AssignmentDetails created", props.assignment.assignmentId);
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
|
||||
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
|
||||
);
|
||||
});
|
||||
|
||||
function submissionStatusForUser(userId: number) {
|
||||
return state.statusByUser.find((s) => s.userId === userId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="state.statusByUser.length">
|
||||
<div class="text-large font-bold">
|
||||
{{ assignment.title }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
{{ submissionStatusForUser(csu.user_id)?.userStatus || "-" }}
|
||||
</template>
|
||||
<template #link>
|
||||
<div
|
||||
v-if="submissionStatusForUser(csu.user_id)?.progressStatus === 'success'"
|
||||
>
|
||||
<router-link
|
||||
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${assignment.assignmentId}/${csu.user_id}`"
|
||||
class="w-full text-right underline"
|
||||
>
|
||||
Ergebnisse anzeigen
|
||||
</router-link>
|
||||
</div>
|
||||
</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 courseSessionStore = useCourseSessionsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("AssignmentsPage mounted");
|
||||
});
|
||||
|
||||
const assignments = computed(() => {
|
||||
// TODO: filter by selected circle
|
||||
if (!courseSessionStore.currentCourseSession) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return calcAssignmentLearningContents(
|
||||
learningPathStore.learningPathForUser(
|
||||
courseSessionStore.currentCourseSession.course.slug,
|
||||
userStore.id
|
||||
)
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div v-if="courseSessionStore.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-lc-assignment class="h-16 w-16"></it-icon-lc-assignment>
|
||||
<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="courseSessionStore.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-lc-assignment class="h-16 w-16"></it-icon-lc-assignment>
|
||||
<div>Geleitete Fallarbeiten</div>
|
||||
</h3>
|
||||
|
||||
<div v-for="assignment in assignments" :key="assignment.id">
|
||||
<AssignmentSubmissionProgress
|
||||
:show-title="true"
|
||||
:course-session="props.courseSession"
|
||||
:assignment="assignment"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<router-link
|
||||
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment`"
|
||||
class="link"
|
||||
>
|
||||
Alle anzeigen
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -5,6 +5,7 @@ import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
|||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
|
||||
import AssignmentsTile from "@/pages/cockpit/cockpitPage/AssignmentsTile.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
|
|
@ -92,7 +93,7 @@ function setActiveClasses(translationKey: string) {
|
|||
<button
|
||||
class="mr-4 rounded-full border-2 border-blue-900 px-4 last:mr-0"
|
||||
:class="setActiveClasses(circle.translation_key)"
|
||||
@click="cockpitStore.toggleCourseSelection(circle.translation_key)"
|
||||
@click="cockpitStore.toggleCircleSelection(circle.translation_key)"
|
||||
>
|
||||
{{ circle.title }}
|
||||
</button>
|
||||
|
|
@ -101,17 +102,10 @@ function setActiveClasses(translationKey: string) {
|
|||
</div>
|
||||
<!-- Status -->
|
||||
<div class="mb-4 grid grid-rows-2 gap-4 lg:grid-cols-2 lg:grid-rows-none">
|
||||
<div class="bg-white px-6 py-5">
|
||||
<h1
|
||||
class="heading-3 mb-4 bg-assignment bg-60 bg-no-repeat pl-[68px] leading-[60px]"
|
||||
>
|
||||
{{ $t("general.transferTask", 2) }}
|
||||
</h1>
|
||||
<div class="mb-4">
|
||||
<ItProgress :status-count="data.transferProgress"></ItProgress>
|
||||
</div>
|
||||
<p>{{ $t("cockpit.tasksDone") }}</p>
|
||||
</div>
|
||||
<AssignmentsTile
|
||||
v-if="courseSessionStore.currentCourseSession"
|
||||
:course-session="courseSessionStore.currentCourseSession"
|
||||
/>
|
||||
<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]"
|
||||
|
|
@ -55,8 +55,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
</p>
|
||||
|
||||
<h3 class="mt-8">{{ $t("assignment.assessmentTitle") }}</h3>
|
||||
<p class="text-large">{{ props.assignment.assessment_description }}</p>
|
||||
<a :href="props.assignment.assessment_document_url" class="text-large underline">
|
||||
<p class="text-large">{{ props.assignment.evaluation_description }}</p>
|
||||
<a :href="props.assignment.evaluation_document_url" class="text-large underline">
|
||||
{{ $t("assignment.showAssessmentDocument") }}
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import type { UserDataText } from "@/stores/assignmentStore";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import type { AssignmentTask } from "@/types";
|
||||
import { computed } from "vue";
|
||||
import type { AssignmentCompletionData, AssignmentTask, UserDataText } from "@/types";
|
||||
import { Assignment } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
assignmentCompletionData: AssignmentCompletionData;
|
||||
allowEdit: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "editTask", task: AssignmentTask): void;
|
||||
}>();
|
||||
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
||||
const completionData = computed(() => assignmentStore.completion_data);
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-for="task in assignmentStore.assignment?.tasks ?? []"
|
||||
v-for="task in props.assignment.tasks ?? []"
|
||||
:key="task.id"
|
||||
class="mb-6 border-t border-gray-400"
|
||||
>
|
||||
<div class="flex flex-row justify-between pt-8">
|
||||
<p class="text-sm text-gray-900">{{ task.value.title }}</p>
|
||||
<button
|
||||
class="link whitespace-nowrap pl-2 text-sm"
|
||||
v-if="props.allowEdit"
|
||||
class="link whitespace-nowrap pl-2text-sm"
|
||||
@click="emit('editTask', task)"
|
||||
>
|
||||
{{ $t("assignment.edit") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="taskBlock in task.value.content" :key="taskBlock.id">
|
||||
<p class="pt-6 text-base font-bold">{{ taskBlock.value.text }}</p>
|
||||
<p v-if="completionData && taskBlock.id in completionData" class="font-normal">
|
||||
{{ (completionData[taskBlock.id].user_data as UserDataText).text }}
|
||||
<p
|
||||
class="default-wagtail-rich-text pt-6 text-base font-bold"
|
||||
v-html="taskBlock.value.text"
|
||||
></p>
|
||||
<p
|
||||
v-if="
|
||||
props.assignmentCompletionData &&
|
||||
taskBlock.id in props.assignmentCompletionData
|
||||
"
|
||||
class="font-normal"
|
||||
>
|
||||
{{ (assignmentCompletionData[taskBlock.id].user_data as UserDataText).text }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
|||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { Assignment, AssignmentTask } from "@/types";
|
||||
import type { Assignment, AssignmentCompletionData, AssignmentTask } from "@/types";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
|
|
@ -13,6 +13,7 @@ import { useI18n } from "vue-i18n";
|
|||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
assignmentCompletionData: AssignmentCompletionData;
|
||||
courseSessionId: number;
|
||||
dueDate: Dayjs;
|
||||
}>();
|
||||
|
|
@ -49,12 +50,12 @@ const onSubmit = async () => {
|
|||
log.error("Invalid courseSessionId");
|
||||
return;
|
||||
}
|
||||
await assignmentStore.upsertAssignmentCompletion(
|
||||
props.assignment.id,
|
||||
{},
|
||||
courseSessionId,
|
||||
true
|
||||
);
|
||||
await assignmentStore.upsertAssignmentCompletion({
|
||||
assignment_id: props.assignment.id,
|
||||
course_session_id: courseSessionId,
|
||||
completion_data: {},
|
||||
completion_status: "submitted",
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("Could not submit assignment", error);
|
||||
}
|
||||
|
|
@ -126,6 +127,9 @@ const onSubmit = async () => {
|
|||
</div>
|
||||
</div>
|
||||
<AssignmentSubmissionResponses
|
||||
:assignment="props.assignment"
|
||||
:assignment-completion-data="props.assignmentCompletionData"
|
||||
:allow-edit="true"
|
||||
@edit-task="onEditTask"
|
||||
></AssignmentSubmissionResponses>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||
import type {
|
||||
AssignmentCompletionData,
|
||||
BlockId,
|
||||
UserDataConfirmation,
|
||||
UserDataText,
|
||||
} from "@/stores/assignmentStore";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type { AssignmentTask } from "@/types";
|
||||
import type {
|
||||
AssignmentCompletionData,
|
||||
AssignmentTask,
|
||||
UserDataConfirmation,
|
||||
UserDataText,
|
||||
} from "@/types";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import dayjs from "dayjs";
|
||||
import { reactive, ref } from "vue";
|
||||
|
|
@ -22,7 +21,7 @@ const props = defineProps<{
|
|||
const lastSaved = ref(dayjs());
|
||||
const lastSaveUnsuccessful = ref(false);
|
||||
|
||||
const checkboxState = reactive({} as Record<BlockId, boolean>);
|
||||
const checkboxState = reactive({} as Record<string, boolean>);
|
||||
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const assignmentStore = useAssignmentStore();
|
||||
|
|
@ -34,12 +33,12 @@ async function upsertAssignmentCompletion(completion_data: AssignmentCompletionD
|
|||
console.error("Invalid courseSessionId");
|
||||
return;
|
||||
}
|
||||
await assignmentStore.upsertAssignmentCompletion(
|
||||
props.assignmentId,
|
||||
completion_data,
|
||||
courseSessionId,
|
||||
false
|
||||
);
|
||||
await assignmentStore.upsertAssignmentCompletion({
|
||||
assignment_id: props.assignmentId,
|
||||
course_session_id: courseSessionId,
|
||||
completion_data: completion_data,
|
||||
completion_status: "in_progress",
|
||||
});
|
||||
lastSaved.value = dayjs();
|
||||
lastSaveUnsuccessful.value = false;
|
||||
console.debug("Saved user input");
|
||||
|
|
@ -54,7 +53,7 @@ const upsertAssignmentCompletionDebounced = useDebounceFn(
|
|||
500
|
||||
);
|
||||
|
||||
const onUpdateText = (id: BlockId, value: string) => {
|
||||
const onUpdateText = (id: string, value: string) => {
|
||||
const data: AssignmentCompletionData = {};
|
||||
data[id] = {
|
||||
user_data: {
|
||||
|
|
@ -64,7 +63,7 @@ const onUpdateText = (id: BlockId, value: string) => {
|
|||
upsertAssignmentCompletionDebounced(data);
|
||||
};
|
||||
|
||||
const onUpdateConfirmation = (id: BlockId, value: boolean) => {
|
||||
const onUpdateConfirmation = (id: string, value: boolean) => {
|
||||
const data: AssignmentCompletionData = {};
|
||||
data[id] = {
|
||||
user_data: {
|
||||
|
|
@ -74,7 +73,7 @@ const onUpdateConfirmation = (id: BlockId, value: boolean) => {
|
|||
upsertAssignmentCompletion(data);
|
||||
};
|
||||
|
||||
const getBlockData = (id: BlockId) => {
|
||||
const getBlockData = (id: string) => {
|
||||
const userData = assignmentStore.getCompletionDataForUserInput(id)?.user_data;
|
||||
if (userData && "text" in userData) {
|
||||
return userData.text;
|
||||
|
|
@ -84,7 +83,7 @@ const getBlockData = (id: BlockId) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const onToggleCheckbox = (id: BlockId) => {
|
||||
const onToggleCheckbox = (id: string) => {
|
||||
checkboxState[id] = !checkboxState[id];
|
||||
onUpdateConfirmation(id, checkboxState[id]);
|
||||
};
|
||||
|
|
@ -94,7 +93,7 @@ const onToggleCheckbox = (id: BlockId) => {
|
|||
<div class="flex flex-col space-y-10">
|
||||
<div v-for="(block, index) in props.task.value.content" :key="block.id">
|
||||
<div v-if="block.type === 'explanation'">
|
||||
<p class="text-large">{{ block.value.text }}</p>
|
||||
<p class="default-wagtail-rich-text text-large" v-html="block.value.text"></p>
|
||||
</div>
|
||||
<div v-if="block.type === 'user_confirmation'">
|
||||
<ItCheckbox
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import AssignmentIntroductionView from "@/pages/learningPath/learningContentPage
|
|||
import AssignmentSubmissionView from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue";
|
||||
import AssignmentTaskView from "@/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue";
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import type { AssignmentCompletionData } from "@/stores/assignmentStore";
|
||||
import { useAssignmentStore } from "@/stores/assignmentStore";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
AssignmentTask,
|
||||
CourseSessionAssignmentDetails,
|
||||
LearningContent,
|
||||
|
|
@ -24,14 +24,14 @@ const assignmentStore = useAssignmentStore();
|
|||
interface State {
|
||||
assignment: Assignment | undefined;
|
||||
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
|
||||
assignmentCompletionData: AssignmentCompletionData | undefined;
|
||||
assignmentCompletion: AssignmentCompletion | undefined;
|
||||
pageIndex: number;
|
||||
}
|
||||
|
||||
const state: State = reactive({
|
||||
assignment: undefined,
|
||||
courseSessionAssignmentDetails: undefined,
|
||||
assignmentCompletionData: undefined,
|
||||
assignmentCompletion: undefined,
|
||||
// 0 = introduction, 1 - n = tasks, n+1 = submission
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
|
@ -51,7 +51,7 @@ onMounted(async () => {
|
|||
state.courseSessionAssignmentDetails = courseSessionsStore.findAssignmentDetails(
|
||||
props.learningContent.id
|
||||
);
|
||||
state.assignmentCompletionData = await assignmentStore.loadAssignmentCompletion(
|
||||
state.assignmentCompletion = await assignmentStore.loadAssignmentCompletion(
|
||||
props.assignmentId,
|
||||
courseSessionId.value
|
||||
);
|
||||
|
|
@ -115,6 +115,7 @@ const getTitle = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="state.assignment && state.assignmentCompletion"></div>
|
||||
<LearningContentMultiLayout
|
||||
:current-step="state.pageIndex"
|
||||
:subtitle="state.assignment?.title ?? ''"
|
||||
|
|
@ -146,6 +147,7 @@ const getTitle = () => {
|
|||
v-if="state.pageIndex + 1 === numPages && state.assignment && courseSessionId"
|
||||
:due-date="dueDate"
|
||||
:assignment="state.assignment!"
|
||||
:assignment-completion-data="state.assignmentCompletion?.completion_data ?? {}"
|
||||
:course-session-id="courseSessionId!"
|
||||
@edit-task="jumpToTask($event)"
|
||||
></AssignmentSubmissionView>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ const router = createRouter({
|
|||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: () => import("@/pages/cockpit/CockpitIndexPage.vue"),
|
||||
component: () => import("@/pages/cockpit/cockpitPage/CockpitPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -129,6 +129,20 @@ const router = createRouter({
|
|||
component: () => import("@/pages/cockpit/FeedbackPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "assignment",
|
||||
component: () =>
|
||||
import("@/pages/cockpit/assignmentsPage/AssignmentsPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "assignment/:assignmentId/:userId",
|
||||
component: () =>
|
||||
import(
|
||||
"@/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue"
|
||||
),
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
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 {
|
||||
AssignmentCompletionStatus,
|
||||
CourseSessionUser,
|
||||
LearningContent,
|
||||
} from "@/types";
|
||||
|
||||
export interface AssignmentLearningContent extends LearningContent {
|
||||
assignmentId: number;
|
||||
}
|
||||
|
||||
export function calcAssignmentLearningContents(learningPath: LearningPath) {
|
||||
// TODO: filter by circle
|
||||
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/`
|
||||
);
|
||||
|
||||
const courseSessionUsers = await cockpitStore.loadCourseSessionUsers(courseSessionId);
|
||||
|
||||
return calcUserAssignmentCompletionStatus(
|
||||
courseSessionUsers,
|
||||
assignmentCompletionData
|
||||
);
|
||||
}
|
||||
|
||||
export function calcUserAssignmentCompletionStatus(
|
||||
courseSessionUsers: CourseSessionUser[],
|
||||
assignmentCompletionStatusData: any
|
||||
) {
|
||||
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", "evaluated"].includes(userStatus)) {
|
||||
progressStatus = "success";
|
||||
}
|
||||
|
||||
return { userId: u.user_id, userStatus, progressStatus };
|
||||
});
|
||||
}
|
||||
|
|
@ -1,39 +1,25 @@
|
|||
import { itGet, itPost } from "@/fetchHelpers";
|
||||
import type { Assignment } from "@/types";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
EvaluationCompletionData,
|
||||
UpsertUserAssignmentCompletion,
|
||||
} from "@/types";
|
||||
import { merge } from "lodash";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type AssignmentStoreState = {
|
||||
assignment: Assignment | undefined;
|
||||
completion_data: AssignmentCompletionData;
|
||||
submitted: boolean;
|
||||
assignmentCompletion: AssignmentCompletion | undefined;
|
||||
};
|
||||
|
||||
export type BlockId = string;
|
||||
|
||||
export interface UserDataText {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface UserDataConfirmation {
|
||||
confirmation: boolean;
|
||||
}
|
||||
|
||||
export interface AssignmentCompletionData {
|
||||
// {
|
||||
// "<user_text_input:uuid>": {"user_data": {"text": "some text from user"}},
|
||||
// "<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
|
||||
// }
|
||||
[key: BlockId]: { user_data: UserDataText | UserDataConfirmation };
|
||||
}
|
||||
|
||||
export const useAssignmentStore = defineStore({
|
||||
id: "assignmentStore",
|
||||
state: () => {
|
||||
return {
|
||||
assignment: undefined,
|
||||
completion_data: {},
|
||||
submitted: false,
|
||||
assignmentCompletion: undefined,
|
||||
} as AssignmentStoreState;
|
||||
},
|
||||
getters: {},
|
||||
|
|
@ -48,37 +34,46 @@ export const useAssignmentStore = defineStore({
|
|||
this.assignment = assignmentData;
|
||||
return this.assignment;
|
||||
},
|
||||
async loadAssignmentCompletion(assignmentId: number, courseSessionId: number) {
|
||||
log.debug("load assignment completion", assignmentId, courseSessionId);
|
||||
async loadAssignmentCompletion(
|
||||
assignmentId: number,
|
||||
courseSessionId: number,
|
||||
userId: string | undefined = undefined
|
||||
) {
|
||||
log.debug("load assignment completion", assignmentId, courseSessionId, userId);
|
||||
try {
|
||||
const data = await itGet(`/api/assignment/${assignmentId}/${courseSessionId}/`);
|
||||
this.completion_data = data.completion_data;
|
||||
this.submitted = data.completion_status === "submitted";
|
||||
let url = `/api/assignment/${assignmentId}/${courseSessionId}/`;
|
||||
if (userId) {
|
||||
url += `${userId}/`;
|
||||
}
|
||||
this.assignmentCompletion = await itGet(url);
|
||||
} catch (e) {
|
||||
log.debug("no completion data found ", e);
|
||||
return undefined;
|
||||
}
|
||||
return this.completion_data;
|
||||
return this.assignmentCompletion;
|
||||
},
|
||||
getCompletionDataForUserInput(id: BlockId) {
|
||||
return this.completion_data[id];
|
||||
getCompletionDataForUserInput(id: string) {
|
||||
return this.assignmentCompletion?.completion_data[id];
|
||||
},
|
||||
async upsertAssignmentCompletion(
|
||||
assignmentId: number,
|
||||
completion_data: AssignmentCompletionData,
|
||||
courseSessionId: number,
|
||||
submit: boolean
|
||||
) {
|
||||
const data = {
|
||||
assignment_id: assignmentId,
|
||||
completion_status: submit ? "submitted" : "in_progress",
|
||||
course_session_id: courseSessionId,
|
||||
completion_data: completion_data,
|
||||
};
|
||||
async upsertAssignmentCompletion(data: UpsertUserAssignmentCompletion) {
|
||||
if (this.assignmentCompletion) {
|
||||
merge(this.assignmentCompletion.completion_data, data.completion_data);
|
||||
this.assignmentCompletion.completion_status = data.completion_status;
|
||||
}
|
||||
const responseData = await itPost(`/api/assignment/upsert/`, data);
|
||||
if (responseData) {
|
||||
this.completion_data = responseData.completion_data;
|
||||
this.submitted = responseData.completion_status === "submitted";
|
||||
this.assignmentCompletion = responseData;
|
||||
}
|
||||
return responseData;
|
||||
},
|
||||
async evaluateAssignmentCompletion(data: EvaluationCompletionData) {
|
||||
if (this.assignmentCompletion) {
|
||||
merge(this.assignmentCompletion.completion_data, data.completion_data);
|
||||
this.assignmentCompletion.completion_status = data.completion_status;
|
||||
}
|
||||
const responseData = await itPost(`/api/assignment/evaluate/`, data);
|
||||
if (responseData) {
|
||||
this.assignmentCompletion = responseData;
|
||||
}
|
||||
return responseData;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,11 +21,14 @@ export const useCockpitStore = defineStore({
|
|||
} as CockpitStoreState;
|
||||
},
|
||||
actions: {
|
||||
async loadCourseSessionUsers(courseSlug: string, reload = false) {
|
||||
async loadCourseSessionUsers(courseSessionId: number, reload = false) {
|
||||
log.debug("loadCockpitData called");
|
||||
const users = (await itGetCached(`/api/course/sessions/${courseSlug}/users/`, {
|
||||
reload: reload,
|
||||
})) as CourseSessionUser[];
|
||||
const users = (await itGetCached(
|
||||
`/api/course/sessions/${courseSessionId}/users/`,
|
||||
{
|
||||
reload: reload,
|
||||
}
|
||||
)) as CourseSessionUser[];
|
||||
|
||||
this.courseSessionUsers = users.filter((user) => user.role === "MEMBER");
|
||||
|
||||
|
|
@ -45,7 +48,7 @@ export const useCockpitStore = defineStore({
|
|||
}
|
||||
return this.courseSessionUsers;
|
||||
},
|
||||
toggleCourseSelection(translationKey: string) {
|
||||
toggleCircleSelection(translationKey: string) {
|
||||
if (this.selectedCircles.indexOf(translationKey) < 0) {
|
||||
this.selectedCircles.push(translationKey);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -39,12 +39,9 @@ function loadCourseSessionsData(reload = false) {
|
|||
// TODO: refactor after implementing of Klassenkonzept
|
||||
await Promise.all(
|
||||
courseSessions.value.map(async (cs) => {
|
||||
const users = (await itGetCached(
|
||||
`/api/course/sessions/${cs.course.slug}/users/`,
|
||||
{
|
||||
reload: reload,
|
||||
}
|
||||
)) as CourseSessionUser[];
|
||||
const users = (await itGetCached(`/api/course/sessions/${cs.id}/users/`, {
|
||||
reload: reload,
|
||||
})) as CourseSessionUser[];
|
||||
cs.users = users;
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ export interface BaseLearningContentBlock {
|
|||
|
||||
export interface AssignmentBlock extends BaseLearningContentBlock {
|
||||
readonly type: "assignment";
|
||||
readonly value: {
|
||||
description: string;
|
||||
url: string;
|
||||
assignment: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BookBlock extends BaseLearningContentBlock {
|
||||
|
|
@ -332,14 +337,32 @@ export interface AssignmentTask {
|
|||
};
|
||||
}
|
||||
|
||||
export interface AssignmentEvaluationSubTask {
|
||||
title: string;
|
||||
description: string;
|
||||
points: number;
|
||||
}
|
||||
|
||||
export interface AssignmentEvaluationTask {
|
||||
readonly type: "task";
|
||||
readonly id: string;
|
||||
readonly value: {
|
||||
title: string;
|
||||
description: string;
|
||||
max_points: number;
|
||||
sub_tasks: AssignmentEvaluationSubTask[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Assignment extends BaseCourseWagtailPage {
|
||||
readonly type: "assignment.Assignment";
|
||||
readonly starting_position: string;
|
||||
readonly effort_required: string;
|
||||
readonly performance_objectives: AssignmentPerformanceObjective[];
|
||||
readonly assessment_description: string;
|
||||
readonly assessment_document_url: string;
|
||||
readonly evaluation_description: string;
|
||||
readonly evaluation_document_url: string;
|
||||
readonly tasks: AssignmentTask[];
|
||||
readonly evaluation_tasks: AssignmentEvaluationTask[];
|
||||
}
|
||||
|
||||
export interface PerformanceCriteria extends BaseCourseWagtailPage {
|
||||
|
|
@ -479,3 +502,59 @@ export interface Notification {
|
|||
actor_avatar_url: string | null;
|
||||
course: string | null;
|
||||
}
|
||||
|
||||
export type AssignmentCompletionStatus =
|
||||
| "unknwown"
|
||||
| "in_progress"
|
||||
| "submitted"
|
||||
| "evaluation_in_progress"
|
||||
| "evaluated";
|
||||
|
||||
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;
|
||||
evaluated_at: string | null;
|
||||
assignment_user: number;
|
||||
assignment: number;
|
||||
course_session: number;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
evaluation_user: number | null;
|
||||
completion_data: AssignmentCompletionData;
|
||||
}
|
||||
|
||||
export type UpsertUserAssignmentCompletion = {
|
||||
assignment_id: number;
|
||||
course_session_id: number;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
completion_data: AssignmentCompletionData;
|
||||
};
|
||||
|
||||
export type EvaluationCompletionData = UpsertUserAssignmentCompletion & {
|
||||
assignment_user_id: number;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ body {
|
|||
hyphens: auto;
|
||||
}
|
||||
|
||||
.default-wagtail-rich-text ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
svg {
|
||||
@apply fill-current;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,15 @@ from django.urls import include, path, re_path
|
|||
from django.views import defaults as default_views
|
||||
from grapple import urls as grapple_urls
|
||||
from ratelimit.exceptions import Ratelimited
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
from vbv_lernwelt.assignment.views import (
|
||||
grade_assignment_completion,
|
||||
evaluate_assignment_completion,
|
||||
request_assignment_completion,
|
||||
request_assignment_completion_for_user,
|
||||
request_assignment_completion_status,
|
||||
upsert_user_assignment_completion,
|
||||
)
|
||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||
|
|
@ -44,9 +48,6 @@ from vbv_lernwelt.feedback.views import (
|
|||
get_feedback_for_circle,
|
||||
)
|
||||
from vbv_lernwelt.notify.views import email_notification_settings
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
|
||||
def raise_example_error(request):
|
||||
|
|
@ -87,7 +88,7 @@ urlpatterns = [
|
|||
|
||||
# course
|
||||
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
|
||||
path(r"api/course/sessions/<course_slug>/users/", get_course_session_users,
|
||||
path(r"api/course/sessions/<course_session_id>/users/", get_course_session_users,
|
||||
name="get_course_session_users"),
|
||||
path(r"api/course/page/<slug_or_id>/", course_page_api_view,
|
||||
name="course_page_api_view"),
|
||||
|
|
@ -102,11 +103,14 @@ urlpatterns = [
|
|||
# assignment
|
||||
path(r"api/assignment/upsert/", upsert_user_assignment_completion,
|
||||
name="upsert_user_assignment_completion"),
|
||||
path(r"api/assignment/grade/", grade_assignment_completion,
|
||||
path(r"api/assignment/evaluate/", evaluate_assignment_completion,
|
||||
name="grade_assignment_completion"),
|
||||
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/",
|
||||
request_assignment_completion,
|
||||
name="request_assignment_completion"),
|
||||
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/status/",
|
||||
request_assignment_completion_status,
|
||||
name="request_assignment_completion_status"),
|
||||
path(r"api/assignment/<int:assignment_id>/<int:course_session_id>/<int:user_id>/",
|
||||
request_assignment_completion_for_user,
|
||||
name="request_assignment_completion_for_user"),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
from vbv_lernwelt.assignment.models import TaskContentStreamBlock
|
||||
from wagtail.blocks import StreamValue
|
||||
from wagtail.blocks.list_block import ListBlock, ListValue
|
||||
from wagtail.rich_text import RichText
|
||||
|
||||
from vbv_lernwelt.assignment.models import (
|
||||
EvaluationSubTaskBlock,
|
||||
TaskContentStreamBlock,
|
||||
)
|
||||
from vbv_lernwelt.assignment.tests.assignment_factories import (
|
||||
AssignmentFactory,
|
||||
AssignmentListPageFactory,
|
||||
EvaluationSubTaskBlockFactory,
|
||||
EvaluationTaskBlockFactory,
|
||||
ExplanationBlockFactory,
|
||||
PerformanceObjectiveBlockFactory,
|
||||
TaskBlockFactory,
|
||||
|
|
@ -10,7 +19,6 @@ from vbv_lernwelt.assignment.tests.assignment_factories import (
|
|||
from vbv_lernwelt.core.utils import replace_whitespace
|
||||
from vbv_lernwelt.course.consts import COURSE_TEST_ID, COURSE_UK
|
||||
from vbv_lernwelt.course.models import CoursePage
|
||||
from wagtail.blocks import StreamValue
|
||||
|
||||
|
||||
def create_uk_assignments(course_id=COURSE_UK):
|
||||
|
|
@ -46,8 +54,187 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
),
|
||||
),
|
||||
],
|
||||
assessment_document_url="https://www.vbv.ch",
|
||||
assessment_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
||||
evaluation_document_url="https://www.vbv.ch",
|
||||
evaluation_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
||||
)
|
||||
|
||||
assignment.evaluation_tasks = []
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Ausgangslage des Auftrags",
|
||||
description=RichText(
|
||||
"Beschreibt der/die Lernende die Ausgangslage des Auftrags vollständig?"
|
||||
),
|
||||
max_points=6,
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage des Auftrag ist vollständig beschrieben.",
|
||||
description=RichText(
|
||||
replace_whitespace(
|
||||
"""
|
||||
<ul>
|
||||
<li>Worum geht es? Was ist die Aufgabe?</li>
|
||||
<li>Sind das Kundenprofil und die Kundenbeziehung vollständig und nachvollziehbar dargestellt?</li>
|
||||
<li>Ist das Alter des Fahrzeugs dokumentiert?</li>
|
||||
<li>Welche Ressourcen stehen zur Verfügung?</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
),
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage ist grösstenteils vollständig beschrieben.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage ist unvollständig - nur 2 Punkte wurden beschrieben.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Ausgangslage des Auftrag ist unvollständig - es fehlen mehr als 2 Punkte in der Beschreibung.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Inhaltsanalyse und Struktur",
|
||||
max_points=6,
|
||||
description=RichText(
|
||||
"Sind die Deckungen der Police vollständig und nachvollziehbar dokumentiert?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse beinhaltet alle in der Police vorhandenen Deckungen und ist logisch aufgebaut.",
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse beinhaltet die meisten vorhandenen Deckungen in der Police und ist grösstenteils logisch aufgebaut.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse ist unvollständig (es fehlen mehr als 3 Deckungen) und der rote Faden ist nicht erkennbar.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Analyse ist insgesamt nicht nachvollziehbar und es fehlen einige Deckungen.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Sinnvolle Empfehlungen",
|
||||
max_points=6,
|
||||
description=RichText(
|
||||
"Leitet die lernende Person sinnvolle und geeignete Empfehlungen ab?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind durchgängig sinnvoll und nachvollziehbar begründet.",
|
||||
points=6,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind grösstenteils sinnvoll und nachvollziehbar begründet.",
|
||||
points=4,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind wenig sinnvoll und unvollständig begründet.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Empfehlungen sind weder sinnvoll nch nachvollziehbar begründet.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Qualität der Reflexion",
|
||||
max_points=3,
|
||||
description=RichText(
|
||||
"Reflektiert die lernende Person die Durchführung der geleiteten Fallarbeit?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich auf die geleitete Fallarbeit und umfasst nachvollziehbare positive wie negative Aspekte.",
|
||||
points=3,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich auf die geleitete Fallarbeit und umfasst grösstenteils nachvollziehbare positive wie negative Aspekte.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion ist unvollständig.",
|
||||
points=1,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Reflexion bezieht sich nicht auf die geleitete Fallarbeit.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
assignment.evaluation_tasks.append(
|
||||
(
|
||||
"task",
|
||||
EvaluationTaskBlockFactory(
|
||||
title="Eignung der Learnings",
|
||||
max_points=3,
|
||||
description=RichText(
|
||||
"Leitet die lernende Person geeignete Learnings aus der Reflexion ab?"
|
||||
),
|
||||
sub_tasks=ListValue(
|
||||
ListBlock(EvaluationSubTaskBlock()),
|
||||
values=[
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich auf die geleitete Fallarbeit und sind inhaltlich sinnvoll.",
|
||||
points=3,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich grösstenteils auf die geleitete Fallarbeit und sind inhaltlich sinnvoll.",
|
||||
points=2,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich teilweise auf die geleitete Fallarbeit und sind inhaltlich wenig sinnvoll.",
|
||||
points=1,
|
||||
),
|
||||
EvaluationSubTaskBlockFactory(
|
||||
title="Die Learnings beziehen sich nicht auf die geleitete Fallarbeit.",
|
||||
points=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assignment.tasks = []
|
||||
|
|
@ -57,18 +244,24 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
TaskBlockFactory(
|
||||
title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden",
|
||||
# it is hard to create a StreamValue programmatically, we have to
|
||||
# create a `StreamValue` manually. Ask the Daniel and/or Ramon
|
||||
# create a `StreamValue` manually. Ask Daniel and/or Ramon
|
||||
content=StreamValue(
|
||||
TaskContentStreamBlock(),
|
||||
stream_data=[
|
||||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(text="Dies ist ein Beispieltext."),
|
||||
ExplanationBlockFactory(
|
||||
text=RichText(
|
||||
"Bitte jemand aus deiner Familie oder deinem Freundeskreis darum, dir seine/ihre Motorfahrzeugversicherungspolice zur Verfügung zu stellen."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_confirmation",
|
||||
ExplanationBlockFactory(
|
||||
text="Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
text=RichText(
|
||||
"Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -88,12 +281,16 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Erläutere die Kundensituation und die Ausgangslage.
|
||||
* Hast du alle Informationen, die du für den Policen-Check benötigst?
|
||||
* Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.)
|
||||
"""
|
||||
text=RichText(
|
||||
replace_whitespace(
|
||||
"""
|
||||
Erläutere die Kundensituation und die Ausgangslage.
|
||||
<ul>
|
||||
<li>Hast du alle Informationen, die du für den Policen-Check benötigst?</li>
|
||||
<li>Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -116,10 +313,8 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist.
|
||||
"""
|
||||
text=RichText(
|
||||
"Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist."
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -141,40 +336,32 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person.
|
||||
"""
|
||||
text=RichText(
|
||||
"Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung
|
||||
"""
|
||||
text=RichText(
|
||||
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung.
|
||||
"""
|
||||
text=RichText(
|
||||
"Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist.
|
||||
"""
|
||||
text=RichText(
|
||||
"Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist."
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -195,40 +382,32 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:
|
||||
"""
|
||||
text=RichText(
|
||||
"Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung.
|
||||
"""
|
||||
text=RichText(
|
||||
"War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was ist dir bei der Bearbeitung des Auftrags gut gelungen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was ist dir bei der Bearbeitung des Auftrags gut gelungen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -249,30 +428,24 @@ def create_uk_assignments(course_id=COURSE_UK):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab.
|
||||
"""
|
||||
text=RichText(
|
||||
"Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was würdest du beim nächsten Mal anders machen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was würdest du beim nächsten Mal anders machen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was hast du beim Bearbeiten des Auftrags Neues gelernt?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was hast du beim Bearbeiten des Auftrags Neues gelernt?"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -318,8 +491,8 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
),
|
||||
),
|
||||
],
|
||||
assessment_document_url="https://www.vbv.ch",
|
||||
assessment_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
||||
evaluation_document_url="https://www.vbv.ch",
|
||||
evaluation_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
|
||||
)
|
||||
|
||||
assignment.tasks = []
|
||||
|
|
@ -335,12 +508,16 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
stream_data=[
|
||||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(text="Dies ist ein Beispieltext."),
|
||||
ExplanationBlockFactory(
|
||||
text=RichText("Dies ist ein Beispieltext.")
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_confirmation",
|
||||
ExplanationBlockFactory(
|
||||
text="Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
text=RichText(
|
||||
"Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -360,12 +537,16 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Erläutere die Kundensituation und die Ausgangslage.
|
||||
* Hast du alle Informationen, die du für den Policen-Check benötigst?
|
||||
* Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.)
|
||||
"""
|
||||
text=RichText(
|
||||
replace_whitespace(
|
||||
"""
|
||||
Erläutere die Kundensituation und die Ausgangslage.
|
||||
<ul>
|
||||
<li>Hast du alle Informationen, die du für den Policen-Check benötigst?</li>
|
||||
<li>Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -388,10 +569,8 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist.
|
||||
"""
|
||||
text=RichText(
|
||||
"Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist."
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -413,40 +592,32 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person.
|
||||
"""
|
||||
text=RichText(
|
||||
"Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung
|
||||
"""
|
||||
text=RichText(
|
||||
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung.
|
||||
"""
|
||||
text=RichText(
|
||||
"Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist.
|
||||
"""
|
||||
text=RichText(
|
||||
"Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist."
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -467,40 +638,32 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:
|
||||
"""
|
||||
text=RichText(
|
||||
"Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei:"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung.
|
||||
"""
|
||||
text=RichText(
|
||||
"War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was ist dir bei der Bearbeitung des Auftrags gut gelungen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was ist dir bei der Bearbeitung des Auftrags gut gelungen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -521,30 +684,24 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
(
|
||||
"explanation",
|
||||
ExplanationBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab.
|
||||
"""
|
||||
text=RichText(
|
||||
"Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab."
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was würdest du beim nächsten Mal anders machen?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was würdest du beim nächsten Mal anders machen?"
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
UserTextInputBlockFactory(
|
||||
text=replace_whitespace(
|
||||
"""
|
||||
Was hast du beim Bearbeiten des Auftrags Neues gelernt?
|
||||
"""
|
||||
text=RichText(
|
||||
"Was hast du beim Bearbeiten des Auftrags Neues gelernt?"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
@ -554,6 +711,185 @@ def create_test_assignment(course_id=COURSE_TEST_ID):
|
|||
)
|
||||
)
|
||||
|
||||
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.save()
|
||||
|
||||
return assignment
|
||||
|
|
|
|||
|
|
@ -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-01 15:43
|
||||
|
||||
import django.db.models.deletion
|
||||
import wagtail.blocks
|
||||
import wagtail.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import vbv_lernwelt.assignment.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("wagtailcore", "0069_log_entry_jsonfield"),
|
||||
("course", "0006_alter_coursesession_attendance_days"),
|
||||
("wagtailcore", "0083_workflowcontenttype"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
@ -33,7 +34,9 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"starting_position",
|
||||
models.TextField(help_text="Erläuterung der Ausgangslage"),
|
||||
wagtail.fields.RichTextField(
|
||||
help_text="Erläuterung der Ausgangslage"
|
||||
),
|
||||
),
|
||||
(
|
||||
"effort_required",
|
||||
|
|
@ -57,20 +60,6 @@ class Migration(migrations.Migration):
|
|||
use_json_field=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"assessment_description",
|
||||
models.TextField(
|
||||
blank=True, help_text="Beschreibung der Bewertung"
|
||||
),
|
||||
),
|
||||
(
|
||||
"assessment_document_url",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="URL zum Beurteilungsinstrument",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"tasks",
|
||||
wagtail.fields.StreamField(
|
||||
|
|
@ -94,14 +83,35 @@ class Migration(migrations.Migration):
|
|||
[
|
||||
(
|
||||
"text",
|
||||
wagtail.blocks.TextBlock(),
|
||||
wagtail.blocks.RichTextBlock(
|
||||
features=[
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_text_input",
|
||||
vbv_lernwelt.assignment.models.UserTextInputBlock(),
|
||||
wagtail.blocks.StructBlock(
|
||||
[
|
||||
(
|
||||
"text",
|
||||
wagtail.blocks.RichTextBlock(
|
||||
blank=True,
|
||||
features=[
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
],
|
||||
required=False,
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_confirmation",
|
||||
|
|
@ -109,7 +119,13 @@ class Migration(migrations.Migration):
|
|||
[
|
||||
(
|
||||
"text",
|
||||
wagtail.blocks.TextBlock(),
|
||||
wagtail.blocks.RichTextBlock(
|
||||
features=[
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
|
|
@ -127,6 +143,78 @@ class Migration(migrations.Migration):
|
|||
use_json_field=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_description",
|
||||
wagtail.fields.RichTextField(
|
||||
blank=True, help_text="Beschreibung der Bewertung"
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_document_url",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="URL zum Beurteilungsinstrument",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_tasks",
|
||||
wagtail.fields.StreamField(
|
||||
[
|
||||
(
|
||||
"task",
|
||||
wagtail.blocks.StructBlock(
|
||||
[
|
||||
("title", wagtail.blocks.TextBlock()),
|
||||
(
|
||||
"description",
|
||||
wagtail.blocks.RichTextBlock(
|
||||
blank=True,
|
||||
features=["ul", "bold", "italic"],
|
||||
required=False,
|
||||
),
|
||||
),
|
||||
("max_points", wagtail.blocks.IntegerBlock()),
|
||||
(
|
||||
"sub_tasks",
|
||||
wagtail.blocks.ListBlock(
|
||||
wagtail.blocks.StructBlock(
|
||||
[
|
||||
(
|
||||
"title",
|
||||
wagtail.blocks.TextBlock(),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
wagtail.blocks.RichTextBlock(
|
||||
blank=True,
|
||||
features=[
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
],
|
||||
required=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"points",
|
||||
wagtail.blocks.IntegerBlock(),
|
||||
),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
use_json_field=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
],
|
||||
blank=True,
|
||||
help_text="Beurteilungsschritte",
|
||||
use_json_field=True,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Auftrag",
|
||||
|
|
@ -153,4 +241,148 @@ class Migration(migrations.Migration):
|
|||
},
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AssignmentCompletionAuditLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"completion_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(1, "in_progress"),
|
||||
(2, "submitted"),
|
||||
(3, "evaluation_in_progress"),
|
||||
(4, "evaluated"),
|
||||
],
|
||||
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),
|
||||
),
|
||||
(
|
||||
"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)),
|
||||
("evaluated_at", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"completion_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(1, "in_progress"),
|
||||
(2, "submitted"),
|
||||
(3, "evaluation_in_progress"),
|
||||
(4, "evaluated"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("completion_data", models.JSONField(default=dict)),
|
||||
("additional_json_data", models.JSONField(default=dict)),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="assignment.assignment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"assignment_user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"course_session",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="course.coursesession",
|
||||
),
|
||||
),
|
||||
(
|
||||
"evaluation_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="assignmentcompletion",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("assignment_user", "assignment", "course_session"),
|
||||
name="assignment_completion_unique_user_assignment_course_session",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
# Generated by Django 3.2.13 on 2023-04-25 06:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("course", "0004_coursesession_assignment_details_list"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("assignment", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AssignmentCompletionAuditLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"completion_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(1, "in_progress"),
|
||||
(2, "submitted"),
|
||||
(3, "grading_in_progress"),
|
||||
(4, "graded"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("completion_data", models.JSONField(default=dict)),
|
||||
("additional_json_data", models.JSONField(default=dict)),
|
||||
("assignment_user_email", models.CharField(max_length=255)),
|
||||
("assignment_slug", models.CharField(max_length=255)),
|
||||
(
|
||||
"grading_user_email",
|
||||
models.CharField(blank=True, default="", max_length=255),
|
||||
),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="assignment.assignment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"assignment_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"course_session",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="course.coursesession",
|
||||
),
|
||||
),
|
||||
(
|
||||
"grading_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AssignmentCompletion",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("submitted_at", models.DateTimeField(blank=True, null=True)),
|
||||
("graded_at", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"completion_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(1, "in_progress"),
|
||||
(2, "submitted"),
|
||||
(3, "grading_in_progress"),
|
||||
(4, "graded"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("completion_data", models.JSONField(default=dict)),
|
||||
("additional_json_data", models.JSONField(default=dict)),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="assignment.assignment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"assignment_user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"course_session",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="course.coursesession",
|
||||
),
|
||||
),
|
||||
(
|
||||
"grading_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="assignmentcompletion",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("assignment_user", "assignment", "course_session"),
|
||||
name="assignment_completion_unique_user_assignment_course_session",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -5,9 +5,10 @@ from django.db.models import UniqueConstraint
|
|||
from slugify import slugify
|
||||
from wagtail import blocks
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.fields import StreamField
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.models import Page
|
||||
|
||||
from vbv_lernwelt.core.constants import DEFAULT_RICH_TEXT_FEATURES
|
||||
from vbv_lernwelt.core.model_utils import find_available_slug
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseBasePage
|
||||
|
|
@ -28,12 +29,8 @@ class AssignmentListPage(CourseBasePage):
|
|||
return f"{self.title}"
|
||||
|
||||
|
||||
# class AssignmentSubmission(modModel):
|
||||
# created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class ExplanationBlock(blocks.StructBlock):
|
||||
text = blocks.TextBlock()
|
||||
text = blocks.RichTextBlock(features=DEFAULT_RICH_TEXT_FEATURES)
|
||||
|
||||
class Meta:
|
||||
icon = "comment"
|
||||
|
|
@ -47,14 +44,16 @@ class PerformanceObjectiveBlock(blocks.StructBlock):
|
|||
|
||||
|
||||
class UserTextInputBlock(blocks.StructBlock):
|
||||
text = blocks.TextBlock(blank=True)
|
||||
text = blocks.RichTextBlock(
|
||||
blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
icon = "edit"
|
||||
|
||||
|
||||
class UserConfirmationBlock(blocks.StructBlock):
|
||||
text = blocks.TextBlock()
|
||||
text = blocks.RichTextBlock(features=DEFAULT_RICH_TEXT_FEATURES)
|
||||
|
||||
class Meta:
|
||||
icon = "tick-inverse"
|
||||
|
|
@ -78,17 +77,47 @@ class TaskBlock(blocks.StructBlock):
|
|||
label = "Teilauftrag"
|
||||
|
||||
|
||||
class EvaluationSubTaskBlock(blocks.StructBlock):
|
||||
title = blocks.TextBlock()
|
||||
description = blocks.RichTextBlock(
|
||||
blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES
|
||||
)
|
||||
points = blocks.IntegerBlock()
|
||||
|
||||
class Meta:
|
||||
icon = "tick"
|
||||
label = "Beurteilung"
|
||||
|
||||
|
||||
class EvaluationTaskBlock(blocks.StructBlock):
|
||||
title = blocks.TextBlock()
|
||||
description = blocks.RichTextBlock(
|
||||
blank=True, required=False, features=DEFAULT_RICH_TEXT_FEATURES
|
||||
)
|
||||
max_points = blocks.IntegerBlock()
|
||||
sub_tasks = blocks.ListBlock(
|
||||
EvaluationSubTaskBlock(), blank=True, use_json_field=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
icon = "tasks"
|
||||
label = "Beurteilungskriterium"
|
||||
|
||||
|
||||
class Assignment(CourseBasePage):
|
||||
serialize_field_names = [
|
||||
"starting_position",
|
||||
"effort_required",
|
||||
"performance_objectives",
|
||||
"assessment_description",
|
||||
"assessment_document_url",
|
||||
"evaluation_description",
|
||||
"evaluation_document_url",
|
||||
"tasks",
|
||||
"evaluation_tasks",
|
||||
]
|
||||
|
||||
starting_position = models.TextField(help_text="Erläuterung der Ausgangslage")
|
||||
starting_position = RichTextField(
|
||||
help_text="Erläuterung der Ausgangslage", features=DEFAULT_RICH_TEXT_FEATURES
|
||||
)
|
||||
effort_required = models.CharField(
|
||||
max_length=100, help_text="Zeitaufwand als Text", blank=True
|
||||
)
|
||||
|
|
@ -101,14 +130,6 @@ class Assignment(CourseBasePage):
|
|||
blank=True,
|
||||
help_text="Leistungsziele des Auftrags",
|
||||
)
|
||||
assessment_description = models.TextField(
|
||||
blank=True, help_text="Beschreibung der Bewertung"
|
||||
)
|
||||
assessment_document_url = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="URL zum Beurteilungsinstrument",
|
||||
)
|
||||
|
||||
tasks = StreamField(
|
||||
[
|
||||
|
|
@ -119,13 +140,34 @@ class Assignment(CourseBasePage):
|
|||
help_text="Teilaufgaben",
|
||||
)
|
||||
|
||||
evaluation_description = RichTextField(
|
||||
blank=True,
|
||||
help_text="Beschreibung der Bewertung",
|
||||
features=DEFAULT_RICH_TEXT_FEATURES,
|
||||
)
|
||||
evaluation_document_url = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="URL zum Beurteilungsinstrument",
|
||||
)
|
||||
|
||||
evaluation_tasks = StreamField(
|
||||
[
|
||||
("task", EvaluationTaskBlock()),
|
||||
],
|
||||
use_json_field=True,
|
||||
blank=True,
|
||||
help_text="Beurteilungsschritte",
|
||||
)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("starting_position"),
|
||||
FieldPanel("effort_required"),
|
||||
FieldPanel("performance_objectives"),
|
||||
FieldPanel("assessment_description"),
|
||||
FieldPanel("assessment_document_url"),
|
||||
FieldPanel("tasks"),
|
||||
FieldPanel("evaluation_description"),
|
||||
FieldPanel("evaluation_document_url"),
|
||||
FieldPanel("evaluation_tasks"),
|
||||
]
|
||||
|
||||
subpage_types = []
|
||||
|
|
@ -172,10 +214,13 @@ class Assignment(CourseBasePage):
|
|||
if sub_dict["type"] in subtask_types
|
||||
]
|
||||
|
||||
def filter_evaluation_tasks(self):
|
||||
return self.evaluation_tasks.raw_data
|
||||
|
||||
|
||||
AssignmentCompletionStatus = Enum(
|
||||
"AssignmentCompletionStatus",
|
||||
["in_progress", "submitted", "grading_in_progress", "graded"],
|
||||
["in_progress", "submitted", "evaluation_in_progress", "evaluated"],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -184,8 +229,8 @@ class AssignmentCompletion(models.Model):
|
|||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
submitted_at = models.DateTimeField(null=True, blank=True)
|
||||
graded_at = models.DateTimeField(null=True, blank=True)
|
||||
grading_user = models.ForeignKey(
|
||||
evaluated_at = models.DateTimeField(null=True, blank=True)
|
||||
evaluation_user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
|
|
@ -217,12 +262,12 @@ class AssignmentCompletion(models.Model):
|
|||
|
||||
class AssignmentCompletionAuditLog(models.Model):
|
||||
"""
|
||||
This model is used to store the "submitted" and "graded" data separately
|
||||
This model is used to store the "submitted" and "evaluated" data separately
|
||||
"""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
grading_user = models.ForeignKey(
|
||||
evaluation_user = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, null=True, blank=True, related_name="+"
|
||||
)
|
||||
assignment_user = models.ForeignKey(
|
||||
|
|
@ -246,4 +291,4 @@ class AssignmentCompletionAuditLog(models.Model):
|
|||
|
||||
assignment_user_email = models.CharField(max_length=255)
|
||||
assignment_slug = models.CharField(max_length=255)
|
||||
grading_user_email = models.CharField(max_length=255, blank=True, default="")
|
||||
evaluation_user_email = models.CharField(max_length=255, blank=True, default="")
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ class AssignmentCompletionSerializer(serializers.ModelSerializer):
|
|||
"created_at",
|
||||
"updated_at",
|
||||
"submitted_at",
|
||||
"graded_at",
|
||||
"evaluated_at",
|
||||
"assignment_user",
|
||||
"assignment",
|
||||
"course_session",
|
||||
"completion_status",
|
||||
"completion_data",
|
||||
"grading_user",
|
||||
"evaluation_user",
|
||||
"additional_json_data",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def update_assignment_completion(
|
|||
course_session: CourseSession,
|
||||
completion_data=None,
|
||||
completion_status: Type[AssignmentCompletionStatus] = "in_progress",
|
||||
grading_user: User | None = None,
|
||||
evaluation_user: User | None = None,
|
||||
validate_completion_status_change: bool = True,
|
||||
copy_task_data: bool = False,
|
||||
) -> AssignmentCompletion:
|
||||
|
|
@ -40,7 +40,7 @@ def update_assignment_completion(
|
|||
},
|
||||
}
|
||||
:param copy_task_data: if true, the task data will be copied to the completion data
|
||||
used for "submitted" and "graded" status, so that we don't lose the question
|
||||
used for "submitted" and "evaluated" status, so that we don't lose the question
|
||||
context
|
||||
:return: AssignmentCompletion
|
||||
"""
|
||||
|
|
@ -56,31 +56,35 @@ def update_assignment_completion(
|
|||
if validate_completion_status_change:
|
||||
# TODO: check time?
|
||||
if completion_status == "submitted":
|
||||
if ac.completion_status in ["submitted", "grading_in_progress", "graded"]:
|
||||
if ac.completion_status in [
|
||||
"submitted",
|
||||
"evaluation_in_progress",
|
||||
"evaluated",
|
||||
]:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"completion_status": f"Cannot update completion status from {ac.completion_status} to submitted"
|
||||
}
|
||||
)
|
||||
elif completion_status == "graded":
|
||||
if ac.completion_status == "graded":
|
||||
elif completion_status == "evaluated":
|
||||
if ac.completion_status == "evaluated":
|
||||
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 evaluated"
|
||||
}
|
||||
)
|
||||
|
||||
if completion_status in ["graded", "grading_in_progress"]:
|
||||
if grading_user is None:
|
||||
if completion_status in ["evaluated", "evaluation_in_progress"]:
|
||||
if evaluation_user is None:
|
||||
raise serializers.ValidationError(
|
||||
{"grading_user": "grading_user is required for graded status"}
|
||||
{"evaluation_user": "evaluation_user is required for evaluated status"}
|
||||
)
|
||||
ac.grading_user = grading_user
|
||||
ac.evaluation_user = evaluation_user
|
||||
|
||||
if completion_status == "submitted":
|
||||
ac.submitted_at = timezone.now()
|
||||
elif completion_status == "graded":
|
||||
ac.graded_at = timezone.now()
|
||||
elif completion_status == "evaluated":
|
||||
ac.evaluated_at = timezone.now()
|
||||
|
||||
ac.completion_status = completion_status
|
||||
|
||||
|
|
@ -101,19 +105,19 @@ def update_assignment_completion(
|
|||
|
||||
ac.save()
|
||||
|
||||
if completion_status in ["graded", "submitted"]:
|
||||
if completion_status in ["evaluated", "submitted"]:
|
||||
acl = AssignmentCompletionAuditLog.objects.create(
|
||||
assignment_user=assignment_user,
|
||||
assignment=assignment,
|
||||
course_session=course_session,
|
||||
grading_user=grading_user,
|
||||
evaluation_user=evaluation_user,
|
||||
completion_status=completion_status,
|
||||
assignment_user_email=assignment_user.email,
|
||||
assignment_slug=assignment.slug,
|
||||
completion_data=deepcopy(ac.completion_data),
|
||||
)
|
||||
if grading_user:
|
||||
acl.grading_user_email = grading_user.email
|
||||
if evaluation_user:
|
||||
acl.evaluation_user_email = evaluation_user.email
|
||||
|
||||
# copy over the question data, so that we don't lose the context
|
||||
substasks = assignment.filter_user_subtasks()
|
||||
|
|
@ -132,9 +136,12 @@ def _remove_unknown_entries(assignment, completion_data):
|
|||
possible_subtask_uuids = [
|
||||
subtask["id"] for subtask in assignment.filter_user_subtasks()
|
||||
]
|
||||
possible_evaluation_uuids = [
|
||||
task["id"] for task in assignment.filter_evaluation_tasks()
|
||||
]
|
||||
filtered_completion_data = {
|
||||
key: value
|
||||
for key, value in completion_data.items()
|
||||
if key in possible_subtask_uuids
|
||||
if key in possible_subtask_uuids or key in possible_evaluation_uuids
|
||||
}
|
||||
return filtered_completion_data
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import wagtail_factories
|
||||
from factory import SubFactory
|
||||
from wagtail.rich_text import RichText
|
||||
|
||||
from vbv_lernwelt.assignment.models import (
|
||||
Assignment,
|
||||
AssignmentListPage,
|
||||
EvaluationSubTaskBlock,
|
||||
EvaluationTaskBlock,
|
||||
ExplanationBlock,
|
||||
PerformanceObjectiveBlock,
|
||||
TaskBlock,
|
||||
|
|
@ -15,14 +18,16 @@ from vbv_lernwelt.core.utils import replace_whitespace
|
|||
|
||||
|
||||
class ExplanationBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
text = "Dies ist ein Beispieltext."
|
||||
text = RichText("Dies ist ein Beispieltext.")
|
||||
|
||||
class Meta:
|
||||
model = ExplanationBlock
|
||||
|
||||
|
||||
class UserConfirmationBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
text = "Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
text = RichText(
|
||||
"Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten."
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = UserConfirmationBlock
|
||||
|
|
@ -50,6 +55,24 @@ class TaskBlockFactory(wagtail_factories.StructBlockFactory):
|
|||
model = TaskBlock
|
||||
|
||||
|
||||
class EvaluationSubTaskBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
title = "Beurteilung"
|
||||
description = RichText("")
|
||||
points = 6
|
||||
|
||||
class Meta:
|
||||
model = EvaluationSubTaskBlock
|
||||
|
||||
|
||||
class EvaluationTaskBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
title = "Beurteilungskriterum"
|
||||
description = RichText("")
|
||||
max_points = 6
|
||||
|
||||
class Meta:
|
||||
model = EvaluationTaskBlock
|
||||
|
||||
|
||||
class PerformanceObjectiveBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
text = "Die Teilnehmer können die wichtigsten Eckwerte eines Versicherungsverhältnisses erfassen."
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
"assignment_id": self.assignment.id,
|
||||
"assignment_user_id": self.student.id,
|
||||
"course_session_id": self.cs.id,
|
||||
"completion_status": "grading_in_progress",
|
||||
"completion_status": "evaluation_in_progress",
|
||||
"completion_data": {
|
||||
user_text_input["id"]: {
|
||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||
|
|
@ -196,7 +196,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response_json["assignment_user"], self.student.id)
|
||||
self.assertEqual(response_json["assignment"], self.assignment.id)
|
||||
self.assertEqual(response_json["completion_status"], "grading_in_progress")
|
||||
self.assertEqual(response_json["completion_status"], "evaluation_in_progress")
|
||||
self.assertDictEqual(
|
||||
response_json["completion_data"],
|
||||
{
|
||||
|
|
@ -212,7 +212,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
course_session_id=self.cs.id,
|
||||
assignment_id=self.assignment.id,
|
||||
)
|
||||
self.assertEqual(db_entry.completion_status, "grading_in_progress")
|
||||
self.assertEqual(db_entry.completion_status, "evaluation_in_progress")
|
||||
self.assertDictEqual(
|
||||
db_entry.completion_data,
|
||||
{
|
||||
|
|
@ -230,7 +230,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
"assignment_id": self.assignment.id,
|
||||
"assignment_user_id": self.student.id,
|
||||
"course_session_id": self.cs.id,
|
||||
"completion_status": "graded",
|
||||
"completion_status": "evaluated",
|
||||
"completion_data": {
|
||||
user_text_input["id"]: {
|
||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||
|
|
@ -245,7 +245,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response_json["assignment_user"], self.student.id)
|
||||
self.assertEqual(response_json["assignment"], self.assignment.id)
|
||||
self.assertEqual(response_json["completion_status"], "graded")
|
||||
self.assertEqual(response_json["completion_status"], "evaluated")
|
||||
self.assertDictEqual(
|
||||
response_json["completion_data"],
|
||||
{
|
||||
|
|
@ -261,7 +261,7 @@ class AssignmentApiTestCase(APITestCase):
|
|||
course_session_id=self.cs.id,
|
||||
assignment_id=self.assignment.id,
|
||||
)
|
||||
self.assertEqual(db_entry.completion_status, "graded")
|
||||
self.assertEqual(db_entry.completion_status, "evaluated")
|
||||
self.assertDictEqual(
|
||||
db_entry.completion_data,
|
||||
{
|
||||
|
|
@ -272,12 +272,12 @@ class AssignmentApiTestCase(APITestCase):
|
|||
},
|
||||
)
|
||||
|
||||
# `graded` will create a new AssignmentCompletionAuditLog
|
||||
# `evaluated` will create a new AssignmentCompletionAuditLog
|
||||
acl = AssignmentCompletionAuditLog.objects.get(
|
||||
assignment_user=self.student,
|
||||
course_session_id=self.cs.id,
|
||||
assignment_id=self.assignment.id,
|
||||
completion_status="graded",
|
||||
completion_status="evaluated",
|
||||
)
|
||||
self.maxDiff = None
|
||||
self.assertDictEqual(
|
||||
|
|
|
|||
|
|
@ -296,7 +296,42 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
)
|
||||
)
|
||||
|
||||
def test_can_add_grading_data_without_loosing_user_input_data(self):
|
||||
def test_can_evaluate_with_evaluation_tasks(self):
|
||||
ac = AssignmentCompletion.objects.create(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_status="submitted",
|
||||
)
|
||||
|
||||
evaluation_task = self.assignment.filter_evaluation_tasks()[0]
|
||||
|
||||
update_assignment_completion(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_data={
|
||||
evaluation_task["id"]: {
|
||||
"expert_data": {"points": 2, "text": "Gut gemacht!"}
|
||||
},
|
||||
},
|
||||
completion_status="evaluation_in_progress",
|
||||
evaluation_user=self.trainer,
|
||||
)
|
||||
|
||||
ac = AssignmentCompletion.objects.get(
|
||||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
)
|
||||
|
||||
self.assertEqual(ac.completion_status, "evaluation_in_progress")
|
||||
trainer_input = ac.completion_data[evaluation_task["id"]]
|
||||
self.assertDictEqual(
|
||||
trainer_input["expert_data"], {"points": 2, "text": "Gut gemacht!"}
|
||||
)
|
||||
|
||||
def test_can_add_evaluation_data_without_loosing_user_input_data(self):
|
||||
subtasks = self.assignment.filter_user_subtasks(
|
||||
subtask_types=["user_text_input"]
|
||||
)
|
||||
|
|
@ -329,8 +364,8 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||
},
|
||||
},
|
||||
completion_status="grading_in_progress",
|
||||
grading_user=self.trainer,
|
||||
completion_status="evaluation_in_progress",
|
||||
evaluation_user=self.trainer,
|
||||
)
|
||||
|
||||
ac = AssignmentCompletion.objects.get(
|
||||
|
|
@ -339,7 +374,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
course_session=self.course_session,
|
||||
)
|
||||
|
||||
self.assertEqual(ac.completion_status, "grading_in_progress")
|
||||
self.assertEqual(ac.completion_status, "evaluation_in_progress")
|
||||
user_input = ac.completion_data[user_text_input["id"]]
|
||||
self.assertDictEqual(
|
||||
user_input["expert_data"], {"points": 1, "comment": "Gut gemacht!"}
|
||||
|
|
@ -348,7 +383,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
user_input["user_data"]["text"], "Ich würde nichts weiteres empfehlen."
|
||||
)
|
||||
|
||||
def test_cannot_grading_data_without_grading_user(self):
|
||||
def test_cannot_evaluate_data_without_evaluation_user(self):
|
||||
subtasks = self.assignment.filter_user_subtasks(
|
||||
subtask_types=["user_text_input"]
|
||||
)
|
||||
|
|
@ -377,7 +412,7 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
assignment_user=self.user,
|
||||
assignment=self.assignment,
|
||||
course_session=self.course_session,
|
||||
completion_status="grading_in_progress",
|
||||
completion_status="evaluation_in_progress",
|
||||
completion_data={
|
||||
user_text_input["id"]: {
|
||||
"expert_data": {"points": 1, "comment": "Gut gemacht!"}
|
||||
|
|
@ -386,5 +421,5 @@ class UpdateAssignmentCompletionTestCase(TestCase):
|
|||
)
|
||||
|
||||
self.assertTrue(
|
||||
"grading_user" in error.exception.detail,
|
||||
"evaluation_user" in error.exception.detail,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,19 @@ def request_assignment_completion_for_user(
|
|||
raise PermissionDenied()
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def request_assignment_completion_status(request, assignment_id, course_session_id):
|
||||
# TODO quickfix before GraphQL...
|
||||
if is_course_session_expert(request.user, course_session_id):
|
||||
qs = AssignmentCompletion.objects.filter(
|
||||
course_session_id=course_session_id,
|
||||
assignment_id=assignment_id,
|
||||
).values("id", "assignment_user_id", "completion_status")
|
||||
return Response(status=200, data=qs)
|
||||
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def upsert_user_assignment_completion(request):
|
||||
try:
|
||||
|
|
@ -107,12 +120,14 @@ def upsert_user_assignment_completion(request):
|
|||
|
||||
|
||||
@api_view(["POST"])
|
||||
def grade_assignment_completion(request):
|
||||
def evaluate_assignment_completion(request):
|
||||
try:
|
||||
assignment_id = request.data.get("assignment_id")
|
||||
assignment_user_id = request.data.get("assignment_user_id")
|
||||
course_session_id = request.data.get("course_session_id")
|
||||
completion_status = request.data.get("completion_status", "grading_in_progress")
|
||||
completion_status = request.data.get(
|
||||
"completion_status", "evaluation_in_progress"
|
||||
)
|
||||
completion_data = request.data.get("completion_data", {})
|
||||
|
||||
assignment_page = Page.objects.get(id=assignment_id)
|
||||
|
|
@ -133,7 +148,7 @@ def grade_assignment_completion(request):
|
|||
completion_data=completion_data,
|
||||
completion_status=completion_status,
|
||||
copy_task_data=False,
|
||||
grading_user=request.user,
|
||||
evaluation_user=request.user,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -144,7 +159,7 @@ def grade_assignment_completion(request):
|
|||
assignment_user_id=assignment_user_id,
|
||||
course_session_id=course_session_id,
|
||||
completion_status=completion_status,
|
||||
grading_user_id=request.user.id,
|
||||
evaluation_user_id=request.user.id,
|
||||
)
|
||||
|
||||
return Response(status=200, data=AssignmentCompletionSerializer(ac).data)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
DEFAULT_RICH_TEXT_FEATURES = [
|
||||
"ul",
|
||||
"bold",
|
||||
"italic",
|
||||
]
|
||||
|
|
@ -176,6 +176,7 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
|
|||
first_name="Lina",
|
||||
last_name="Egger",
|
||||
avatar_url="/static/avatars/uk1.lina.egger.jpg",
|
||||
password="myvbv1234",
|
||||
)
|
||||
_create_student_user(
|
||||
email="evelyn.schmid@example.com",
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ def command(course):
|
|||
slug="überbetriebliche-kurse-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
||||
),
|
||||
course_session=CourseSession.objects.get(title="Bern 2023 a"),
|
||||
user=User.objects.get(email="michael.meier@example.com"),
|
||||
user=User.objects.get(email="lina.egger@example.com"),
|
||||
)
|
||||
|
||||
if COURSE_UK_FR in course:
|
||||
|
|
@ -320,6 +320,12 @@ def create_course_uk_de_assignment_completion_data(assignment, course_session, u
|
|||
}
|
||||
},
|
||||
)
|
||||
update_assignment_completion(
|
||||
assignment_user=user,
|
||||
assignment=assignment,
|
||||
course_session=course_session,
|
||||
completion_status="submitted",
|
||||
)
|
||||
|
||||
|
||||
def create_course_uk_de_completion_data(course_session):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.13 on 2023-04-26 16:28
|
||||
|
||||
import django_jsonform.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("course", "0005_alter_coursesession_attendance_days"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="coursesession",
|
||||
name="attendance_days",
|
||||
field=django_jsonform.models.fields.JSONField(blank=True, default=list),
|
||||
),
|
||||
]
|
||||
|
|
@ -149,12 +149,9 @@ def get_course_sessions(request):
|
|||
|
||||
|
||||
@api_view(["GET"])
|
||||
def get_course_session_users(request, course_slug):
|
||||
def get_course_session_users(request, course_session_id):
|
||||
try:
|
||||
course_sessions = course_sessions_for_user_qs(request.user).filter(
|
||||
course__slug=course_slug
|
||||
)
|
||||
qs = CourseSessionUser.objects.filter(course_session__in=course_sessions)
|
||||
qs = CourseSessionUser.objects.filter(course_session_id=course_session_id)
|
||||
|
||||
user_data = [csu.to_dict() for csu in qs]
|
||||
return Response(status=200, data=user_data)
|
||||
|
|
|
|||
Loading…
Reference in New Issue