Add EvaluationSummary

This commit is contained in:
Daniel Egger 2023-05-05 12:19:31 +02:00
parent d9a6f2dd94
commit 2d6cee9f9f
12 changed files with 262 additions and 46 deletions

View File

@ -4,14 +4,9 @@ import EvaluationContainer from "@/pages/cockpit/assignmentEvaluationPage/Evalua
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 { Assignment, CourseSessionAssignmentDetails, CourseSessionUser } from "@/types";
import log from "loglevel";
import { onMounted, reactive } from "vue";
import { computed, onMounted, reactive } from "vue";
import { useRouter } from "vue-router";
const props = defineProps<{
@ -25,14 +20,12 @@ 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,
});
@ -51,7 +44,7 @@ onMounted(async () => {
try {
state.assignment = await assignmentStore.loadAssignment(props.assignmentId);
state.completionData = await assignmentStore.loadAssignmentCompletion(
await assignmentStore.loadAssignmentCompletion(
props.assignmentId,
courseSessionStore.currentCourseSession.id,
props.userId
@ -66,11 +59,13 @@ function exit() {
path: `/course/${props.courseSlug}/cockpit/assignment`,
});
}
const assignmentCompletion = computed(() => assignmentStore.assignmentCompletion);
</script>
<template>
<div class="absolute bottom-0 top-0 z-10 w-full bg-white">
<div v-if="state.assignment && state.completionData" class="relative">
<div v-if="state.assignment && assignmentCompletion" class="relative">
<header
class="relative flex h-12 w-full items-center justify-between border-b border-b-gray-400 bg-white px-4 lg:h-16 lg:px-8"
>
@ -102,14 +97,14 @@ function exit() {
</div>
<AssignmentSubmissionResponses
:assignment="state.assignment"
:assignment-completion-data="state.completionData?.completion_data"
:assignment-completion-data="assignmentCompletion.completion_data"
:allow-edit="false"
></AssignmentSubmissionResponses>
</div>
</div>
<div class="w-1/2 overflow-y-auto bg-gray-200">
<EvaluationContainer
:assignment-completion="state.completionData"
:assignment-completion="assignmentCompletion"
:assignment-user="state.assignmentUser"
:assignment="state.assignment"
></EvaluationContainer>

View File

@ -1,12 +1,19 @@
<script setup lang="ts">
import EvaluationIntro from "@/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue";
import EvaluationSummary from "@/pages/cockpit/assignmentEvaluationPage/EvaluationSummary.vue";
import EvaluationTask from "@/pages/cockpit/assignmentEvaluationPage/EvaluationTask.vue";
import { calcAssignmentLearningContents } from "@/services/assignmentService";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import type { Assignment, AssignmentCompletion, CourseSessionUser } from "@/types";
import type {
Assignment,
AssignmentCompletion,
AssignmentEvaluationTask,
CourseSessionUser,
} from "@/types";
import dayjs from "dayjs";
import { findIndex } from "lodash";
import * as log from "loglevel";
import { computed, reactive } from "vue";
@ -24,7 +31,7 @@ interface StateInterface {
}
const state: StateInterface = reactive({
pageIndex: 0,
pageIndex: 6,
});
const courseSessionStore = useCourseSessionsStore();
@ -41,6 +48,15 @@ function nextPage() {
state.pageIndex = Math.min(numTasks.value + 1, state.pageIndex + 1);
}
function editTask(task: AssignmentEvaluationTask) {
log.debug("editTask", task);
const taskIndex =
findIndex(props.assignment.evaluation_tasks, {
id: task.id,
}) ?? 0;
state.pageIndex = taskIndex + 1;
}
function findAssignmentDetail() {
const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
@ -88,10 +104,18 @@ const dueDate = computed(() =>
:assignment="props.assignment"
:task-index="state.pageIndex - 1"
/>
<EvaluationSummary
v-else
:assignment-user="props.assignmentUser"
:assignment="props.assignment"
:assignment-completion="props.assignmentCompletion"
:due-date="dueDate"
@edit-task="editTask($event)"
></EvaluationSummary>
</section>
</div>
<nav class="sticky bottom-0 border-t p-6">
<nav class="sticky bottom-0 border-t bg-gray-200 p-6" v-if="state.pageIndex > 0">
<div class="relative flex flex-row place-content-end">
<button
v-if="true"

View File

@ -51,9 +51,8 @@ async function startEvaluation() {
</p>
<p class="my-4">
Auf Grund dieser Bewertung wird eine Gesamtpunktzahl und die daraus reslutierende
Note berechnet. Genauere Informationen dazu findest du im folgenden
Beurteilungsinstrument:
Die Gesamtpunktzahl und die daruas resultierende Note wird auf Grund des
hinterlegeten Beurteilungsinstrument berechnet. Willst du mehr dazu erfahren:
</p>
<div>

View File

@ -0,0 +1,131 @@
<script setup lang="ts">
import {
maxAssignmentPoints,
pointsToGrade,
userAssignmentPoints,
} from "@/services/assignmentService";
import { useAssignmentStore } from "@/stores/assignmentStore";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { Assignment, AssignmentEvaluationTask, CourseSessionUser } from "@/types";
import { AssignmentCompletion } from "@/types";
import { Dayjs } from "dayjs";
import * as log from "loglevel";
import { computed } from "vue";
const props = defineProps<{
assignmentUser: CourseSessionUser;
assignment: Assignment;
assignmentCompletion: AssignmentCompletion;
dueDate?: Dayjs;
}>();
const emit = defineEmits(["submitEvaluation", "editTask"]);
log.debug("EvaluationSummary setup");
const courseSessionStore = useCourseSessionsStore();
const assignmentStore = useAssignmentStore();
async function submitEvaluation() {
log.debug("submitEvaluation");
await assignmentStore.evaluateAssignmentCompletion({
assignment_user_id: Number(props.assignmentUser.user_id),
assignment_id: props.assignment.id,
course_session_id: courseSessionStore.currentCourseSession!.id,
completion_data: {},
completion_status: "evaluation_submitted",
});
emit("submitEvaluation");
}
function subTaskByPoints(task: AssignmentEvaluationTask, points = 0) {
return task.value.sub_tasks.find((subTask) => subTask.points === points);
}
function evaluationForTask(task: AssignmentEvaluationTask) {
const expertData = props.assignmentCompletion.completion_data[task.id]?.expert_data;
if (task.id === "0e701176-a817-427b-b8ea-a7cd59f212cb") {
console.log("######################## ", expertData.text, expertData.points);
}
if (!expertData) {
return {
points: 0,
text: "",
};
}
return expertData;
}
const maxPoints = computed(() => maxAssignmentPoints(props.assignment));
const userPoints = computed(() =>
userAssignmentPoints(props.assignment, props.assignmentCompletion)
);
const grade = computed(() => pointsToGrade(userPoints.value, maxPoints.value));
</script>
<template>
<div>
<h3>Bewertung Freigabe</h3>
<section class="mb-6 border p-6">
<div class="text-lg font-bold">Note: {{ grade }}</div>
<div>Gesamtpunktezahl {{ userPoints }} / {{ maxPoints }}</div>
<p class="my-4">
Die Gesamtpunktzahl und die daraus resultierende Note wird auf Grund des
hinterlegeten Beurteilungsinstrument berechnet. Willst du mehr dazu erfahren:
</p>
<div>
<button class="btn-primary" @click="submitEvaluation()">
Bewertung freigeben
</button>
</div>
</section>
<section>
<div v-for="(task, index) in props.assignment.evaluation_tasks" :key="task.id">
<article class="border-t py-4">
<div class="flex flex-row justify-between">
<div class="mb-4">
Bewertungskriterium {{ index + 1 }}: {{ task.value.title }}
</div>
<div>
<button
class="link pl-2text-sm whitespace-nowrap"
@click="emit('editTask', task)"
>
{{ $t("assignment.edit") }}
</button>
</div>
</div>
<div class="default-wagtail-rich-text mb-2 font-bold">
{{ task.value.description }}
</div>
<section class="mb-4">
<div
v-html="subTaskByPoints(task, evaluationForTask(task).points).title"
></div>
<p
class="default-wagtail-rich-text"
v-html="subTaskByPoints(task, evaluationForTask(task).points).description"
></p>
<div class="text-sm text-gray-800">
{{ evaluationForTask(task).points }} Punkte
</div>
</section>
<div>
<span class="font-bold">Begründung:</span>
{{ evaluationForTask(task).text }}
</div>
</article>
</div>
</section>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -7,7 +7,6 @@ import { useAssignmentStore } from "@/stores/assignmentStore";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type {
Assignment,
AssignmentCompletion,
AssignmentTask,
CourseSessionAssignmentDetails,
LearningContent,
@ -24,14 +23,12 @@ const assignmentStore = useAssignmentStore();
interface State {
assignment: Assignment | undefined;
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
assignmentCompletion: AssignmentCompletion | undefined;
pageIndex: number;
}
const state: State = reactive({
assignment: undefined,
courseSessionAssignmentDetails: undefined,
assignmentCompletion: undefined,
// 0 = introduction, 1 - n = tasks, n+1 = submission
pageIndex: 0,
});
@ -51,7 +48,7 @@ onMounted(async () => {
state.courseSessionAssignmentDetails = courseSessionsStore.findAssignmentDetails(
props.learningContent.id
);
state.assignmentCompletion = await assignmentStore.loadAssignmentCompletion(
await assignmentStore.loadAssignmentCompletion(
props.assignmentId,
courseSessionId.value
);
@ -78,6 +75,7 @@ const currentTask = computed(() => {
}
return undefined;
});
const assignmentCompletion = computed(() => assignmentStore.assignmentCompletion);
const handleBack = () => {
log.debug("handleBack");
@ -115,7 +113,7 @@ const getTitle = () => {
</script>
<template>
<div v-if="state.assignment && state.assignmentCompletion"></div>
<div v-if="state.assignment && assignmentCompletion"></div>
<LearningContentMultiLayout
:current-step="state.pageIndex"
:subtitle="state.assignment?.title ?? ''"
@ -147,7 +145,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 ?? {}"
:assignment-completion-data="assignmentCompletion?.completion_data ?? {}"
:course-session-id="courseSessionId!"
@edit-task="jumpToTask($event)"
></AssignmentSubmissionView>

View File

@ -0,0 +1,32 @@
import { describe, it } from "vitest";
import { pointsToGrade } from "../assignmentService";
describe("assignmentService", () => {
it("pointsToGrade", () => {
expect(pointsToGrade(24, 24)).toBe(6);
expect(pointsToGrade(23, 24)).toBe(6);
expect(pointsToGrade(22, 24)).toBe(5.5);
expect(pointsToGrade(21, 24)).toBe(5.5);
expect(pointsToGrade(20, 24)).toBe(5);
expect(pointsToGrade(19, 24)).toBe(5);
expect(pointsToGrade(18, 24)).toBe(5);
expect(pointsToGrade(17, 24)).toBe(4.5);
expect(pointsToGrade(16, 24)).toBe(4.5);
expect(pointsToGrade(15, 24)).toBe(4);
expect(pointsToGrade(14, 24)).toBe(4);
expect(pointsToGrade(13, 24)).toBe(3.5);
expect(pointsToGrade(12, 24)).toBe(3.5);
expect(pointsToGrade(11, 24)).toBe(3.5);
expect(pointsToGrade(10, 24)).toBe(3);
expect(pointsToGrade(9, 24)).toBe(3);
expect(pointsToGrade(8, 24)).toBe(2.5);
expect(pointsToGrade(7, 24)).toBe(2.5);
expect(pointsToGrade(6, 24)).toBe(2.5);
expect(pointsToGrade(5, 24)).toBe(2);
expect(pointsToGrade(4, 24)).toBe(2);
expect(pointsToGrade(3, 24)).toBe(1.5);
expect(pointsToGrade(2, 24)).toBe(1.5);
expect(pointsToGrade(1, 24)).toBe(1);
expect(pointsToGrade(0, 24)).toBe(1);
});
});

View File

@ -3,10 +3,14 @@ import { itGet } from "@/fetchHelpers";
import type { LearningPath } from "@/services/learningPath";
import { useCockpitStore } from "@/stores/cockpit";
import type {
Assignment,
AssignmentCompletion,
AssignmentCompletionStatus,
CourseSessionUser,
LearningContent,
} from "@/types";
import { sum } from "d3";
import pick from "lodash/pick";
export interface AssignmentLearningContent extends LearningContent {
assignmentId: number;
@ -59,10 +63,42 @@ export function calcUserAssignmentCompletionStatus(
userStatus = userAssignmentStatus.completion_status;
}
let progressStatus: StatusCountKey = "unknown";
if (["submitted", "evaluation_in_progress", "evaluated"].includes(userStatus)) {
if (
["submitted", "evaluation_in_progress", "evaluation_submitted"].includes(
userStatus
)
) {
progressStatus = "success";
}
return { userId: u.user_id, userStatus, progressStatus };
});
}
export function maxAssignmentPoints(assignment: Assignment) {
return sum(assignment.evaluation_tasks.map((task) => task.value.max_points));
}
export function userAssignmentPoints(
assignment: Assignment,
assignmentCompletion: AssignmentCompletion
) {
const evaluationTaskIds = assignment.evaluation_tasks.map((task) => {
return task.id;
});
return sum(
Object.entries(pick(assignmentCompletion.completion_data, evaluationTaskIds)).map(
(entry) => {
return entry[1]?.expert_data?.points ?? 0;
}
)
);
}
export function pointsToGrade(points: number, maxPoints: number) {
// round to half-grades
const grade = Math.round((points / maxPoints) * 10);
const halfGrade = grade / 2;
return Math.min(halfGrade, 5) + 1;
}

View File

@ -509,7 +509,7 @@ export type AssignmentCompletionStatus =
| "in_progress"
| "submitted"
| "evaluation_in_progress"
| "evaluated";
| "evaluation_submitted";
export interface UserDataText {
text: string;

View File

@ -8,7 +8,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
@ -261,7 +260,7 @@ class Migration(migrations.Migration):
(1, "in_progress"),
(2, "submitted"),
(3, "evaluation_in_progress"),
(4, "evaluated"),
(4, "evaluation_submitted"),
],
default="in_progress",
max_length=255,
@ -337,7 +336,7 @@ class Migration(migrations.Migration):
(1, "in_progress"),
(2, "submitted"),
(3, "evaluation_in_progress"),
(4, "evaluated"),
(4, "evaluation_submitted"),
],
default="in_progress",
max_length=255,

View File

@ -220,7 +220,7 @@ class Assignment(CourseBasePage):
AssignmentCompletionStatus = Enum(
"AssignmentCompletionStatus",
["in_progress", "submitted", "evaluation_in_progress", "evaluated"],
["in_progress", "submitted", "evaluation_in_progress", "evaluation_submitted"],
)
@ -262,7 +262,7 @@ class AssignmentCompletion(models.Model):
class AssignmentCompletionAuditLog(models.Model):
"""
This model is used to store the "submitted" and "evaluated" data separately
This model is used to store the "submitted" and "evaluation_submitted" data separately
"""
created_at = models.DateTimeField(auto_now_add=True)

View File

@ -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 "evaluated" status, so that we don't lose the question
used for "submitted" and "evaluation_submitted" status, so that we don't lose the question
context
:return: AssignmentCompletion
"""
@ -59,31 +59,33 @@ def update_assignment_completion(
if ac.completion_status in [
"submitted",
"evaluation_in_progress",
"evaluated",
"evaluation_submitted",
]:
raise serializers.ValidationError(
{
"completion_status": f"Cannot update completion status from {ac.completion_status} to submitted"
}
)
elif completion_status == "evaluated":
if ac.completion_status == "evaluated":
elif completion_status == "evaluation_submitted":
if ac.completion_status == "evaluation_submitted":
raise serializers.ValidationError(
{
"completion_status": f"Cannot update completion status from {ac.completion_status} to evaluated"
"completion_status": f"Cannot update completion status from {ac.completion_status} to evaluation_submitted"
}
)
if completion_status in ["evaluated", "evaluation_in_progress"]:
if completion_status in ["evaluation_submitted", "evaluation_in_progress"]:
if evaluation_user is None:
raise serializers.ValidationError(
{"evaluation_user": "evaluation_user is required for evaluated status"}
{
"evaluation_user": "evaluation_user is required for evaluation_submitted status"
}
)
ac.evaluation_user = evaluation_user
if completion_status == "submitted":
ac.submitted_at = timezone.now()
elif completion_status == "evaluated":
elif completion_status == "evaluation_submitted":
ac.evaluated_at = timezone.now()
ac.completion_status = completion_status
@ -105,7 +107,7 @@ def update_assignment_completion(
ac.save()
if completion_status in ["evaluated", "submitted"]:
if completion_status in ["evaluation_submitted", "submitted"]:
acl = AssignmentCompletionAuditLog.objects.create(
assignment_user=assignment_user,
assignment=assignment,

View File

@ -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": "evaluated",
"completion_status": "evaluation_submitted",
"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"], "evaluated")
self.assertEqual(response_json["completion_status"], "evaluation_submitted")
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, "evaluated")
self.assertEqual(db_entry.completion_status, "evaluation_submitted")
self.assertDictEqual(
db_entry.completion_data,
{
@ -272,12 +272,12 @@ class AssignmentApiTestCase(APITestCase):
},
)
# `evaluated` will create a new AssignmentCompletionAuditLog
# `evaluation_submitted` will create a new AssignmentCompletionAuditLog
acl = AssignmentCompletionAuditLog.objects.get(
assignment_user=self.student,
course_session_id=self.cs.id,
assignment_id=self.assignment.id,
completion_status="evaluated",
completion_status="evaluation_submitted",
)
self.maxDiff = None
self.assertDictEqual(