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:
Daniel Egger 2023-04-25 15:06:25 +02:00
parent 90d9734f9b
commit fef864df25
39 changed files with 1806 additions and 512 deletions

View File

@ -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(() => {

View File

@ -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"

View File

@ -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);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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]"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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,
},
],
},
{

View File

@ -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 };
});
}

View File

@ -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;
},

View File

@ -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 {

View File

@ -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;
})
);

View File

@ -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;
};

View File

@ -27,6 +27,11 @@ body {
hyphens: auto;
}
.default-wagtail-rich-text ul {
list-style-type: disc;
margin-left: 24px;
}
svg {
@apply fill-current;
}

View File

@ -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"),

View File

@ -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

View File

@ -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",
),
),
]

View File

@ -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",
),
),
]

View File

@ -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="")

View File

@ -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",
]

View File

@ -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

View File

@ -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."

View File

@ -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(

View File

@ -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,
)

View File

@ -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)

View File

@ -0,0 +1,5 @@
DEFAULT_RICH_TEXT_FEATURES = [
"ul",
"bold",
"italic",
]

View File

@ -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",

View File

@ -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):

View File

@ -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),
),
]

View File

@ -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)