VBV-731: Bewertungen bearbeiten

This commit is contained in:
Daniel Egger 2024-09-23 18:06:08 +02:00
parent cd001a1269
commit a2b3d63d6d
19 changed files with 635 additions and 106 deletions

View File

@ -2,13 +2,15 @@
import EvaluationIntro from "@/components/assignment/evaluation/EvaluationIntro.vue";
import EvaluationSummary from "@/components/assignment/evaluation/EvaluationSummary.vue";
import EvaluationTask from "@/components/assignment/evaluation/EvaluationTask.vue";
import { useCourseSessionDetailQuery } from "@/composables";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import type {
Assignment,
AssignmentCompletion,
AssignmentEvaluationTask,
CourseSessionUser,
} from "@/types";
import { useMutation } from "@urql/vue";
import { useRouteQuery } from "@vueuse/router";
import dayjs from "dayjs";
import { findIndex } from "lodash";
@ -21,10 +23,12 @@ const props = defineProps<{
assignment: Assignment;
}>();
const emit = defineEmits(["close"]);
const emit = defineEmits(["close", "reopen"]);
log.debug("UserEvaluation setup");
const courseSession = useCurrentCourseSession();
// 0 = introduction, 1 - n = tasks, n+1 = submission
const stepIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
@ -58,6 +62,34 @@ function editTask(task: AssignmentEvaluationTask) {
stepIndex.value = taskIndex + 1;
}
function canReopen() {
return (
evaluationSubmitted.value &&
(props.assignment.assignment_type === "CASEWORK" ||
props.assignment.assignment_type === "PRAXIS_ASSIGNMENT")
);
}
const upsertAssignmentCompletionMutation = useMutation(
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
);
async function reopen() {
log.debug("reopen");
await upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.assignment.id,
courseSessionId: courseSession.value.id,
assignmentUserId: props.assignmentUser.id,
completionStatus: "EVALUATION_IN_PROGRESS",
completionDataString: JSON.stringify({}),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
id: props.assignmentCompletion?.id,
});
stepIndex.value = 1;
}
const courseSessionDetailResult = useCourseSessionDetailQuery();
const assignmentDetail = computed(() => {
@ -89,11 +121,11 @@ const taskExpertDataText = computed(() => {
const text = computed(() => {
if (props.assignment.assignment_type === "CASEWORK") {
return {
evaluationFinish: "a.Bewertung abschliessen",
evaluationFinish: "a.Schliessen",
};
} else if (props.assignment.assignment_type === "PRAXIS_ASSIGNMENT") {
return {
evaluationFinish: "a.Feedback abschliessen",
evaluationFinish: "a.Schliessen",
};
} else {
return {
@ -109,6 +141,16 @@ function nextButtonEnabled() {
return true;
}
function previousButtonVisible() {
if (
stepIndex.value > numTasks.value &&
props.assignmentCompletion.completion_status === "EVALUATION_SUBMITTED"
) {
return false;
}
return true;
}
function finishButtonEnabled() {
return props.assignmentCompletion.completion_status === "EVALUATION_SUBMITTED";
}
@ -150,18 +192,19 @@ function finishButtonEnabled() {
<nav v-if="stepIndex > 0" class="sticky bottom-0 border-t bg-gray-200 p-6">
<div class="relative flex flex-row place-content-end">
<button
v-if="true"
class="btn-secondary mr-2 flex items-center"
v-if="previousButtonVisible()"
class="btn-secondary flex items-center"
data-cy="previous-step"
@click="previousPage()"
>
<it-icon-arrow-left class="mr-2 h-6 w-6"></it-icon-arrow-left>
{{ $t("general.backCapitalized") }}
</button>
<button
v-if="stepIndex <= numTasks"
:disabled="!nextButtonEnabled()"
class="btn-secondary z-10 flex items-center"
class="btn-secondary z-10 ml-2 flex items-center"
data-cy="next-step"
@click="nextPage()"
>
@ -170,15 +213,25 @@ function finishButtonEnabled() {
</button>
<button
v-if="stepIndex > numTasks"
v-if="stepIndex > numTasks && canReopen()"
class="btn-secondary z-10 ml-2"
data-cy="btn-reopen"
@click="reopen()"
>
<span class="flex items-center">
{{ $t("a.Bewertung bearbeiten") }}
</span>
</button>
<button
v-if="stepIndex > numTasks && finishButtonEnabled()"
:disabled="!finishButtonEnabled()"
class="btn-secondary z-10"
data-cy="next-step"
class="btn-primary z-10 ml-2"
data-cy="btn-close"
@click="emit('close')"
>
<span class="flex items-center">
{{ $t(text.evaluationFinish) }}
<it-icon-check class="ml-2 h-6 w-6"></it-icon-check>
</span>
</button>
</div>

View File

@ -76,30 +76,8 @@ async function startEvaluation() {
<template>
<div>
<div class="mb-4">
{{
$t("assignment.x hat die Ergebnisse am y um z Uhr abgegeben", {
x: props.assignmentUser.first_name + " " + props.assignmentUser.last_name,
y: dayjs(props.assignmentCompletion.submitted_at).format("DD.MM.YYYY"),
z: dayjs(props.assignmentCompletion.submitted_at).format("HH.mm"),
})
}}
</div>
<h3 data-cy="title">{{ $t(text.evaluationTitle) }}</h3>
<p v-if="props.dueDate" class="my-4" data-cy="evaluation-duedate">
{{
$t(
"assignment.Du musst die Bewertung bis am x um y Uhr abschliessen und freigeben",
{
x: props.dueDate.format("DD.MM.YYYY"),
y: props.dueDate.format("HH.mm"),
}
)
}}
</p>
<p class="my-4" data-cy="instruction">
{{
$t(text.evaluationInstruction, {
@ -125,6 +103,11 @@ async function startEvaluation() {
</a>
</p>
<p v-if="props.dueDate" class="my-4" data-cy="evaluation-duedate">
{{ "a.Freigabetermin Bewertung" }}: {{ props.dueDate.format("DD.MM.YYYY") }}
{{ props.dueDate.format("HH.mm") }}
</p>
<div>
<button
class="btn-primary text-large"
@ -148,6 +131,18 @@ async function startEvaluation() {
<span v-else>{{ $t(text.evaluationStart) }}</span>
</button>
</div>
<section class="mt-8">
<div
v-for="historyEntry in props.assignmentCompletion.additional_json_data
?.submission_history ?? []"
:key="historyEntry.timestamp"
>
{{ dayjs(historyEntry.timestamp).format("DD.MM.YYYY HH.mm") }}:
{{ $t(historyEntry.translation_key) }}
({{ historyEntry.user_display_name }})
</div>
</section>
</div>
</template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue";
import RichText from "@/components/ui/RichText.vue";
import { useCurrentCourseSession } from "@/composables";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
@ -16,7 +17,7 @@ import type {
import { useMutation } from "@urql/vue";
import dayjs, { Dayjs } from "dayjs";
import * as log from "loglevel";
import { computed, reactive } from "vue";
import { computed, reactive, ref } from "vue";
const props = defineProps<{
assignmentUser: CourseSessionUser;
@ -40,6 +41,10 @@ const upsertAssignmentCompletionMutation = useMutation(
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
);
const evaluationComment = ref(
props.assignmentCompletion.completion_data.expert_evaluation_comment?.text ?? ""
);
const text = computed(() => {
if (props.assignment.assignment_type === "CASEWORK") {
return {
@ -77,7 +82,11 @@ async function submitEvaluation() {
courseSessionId: courseSession.value.id,
assignmentUserId: props.assignmentUser.id,
completionStatus: "EVALUATION_SUBMITTED",
completionDataString: JSON.stringify({}),
completionDataString: JSON.stringify({
expert_evaluation_comment: {
text: evaluationComment.value,
},
}),
evaluationPoints: userPoints.value,
// next line used for urql
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -112,9 +121,48 @@ const maxPoints = computed(() => {
}
return maxAssignmentPoints(props.assignment);
});
const userPoints = computed(() =>
userAssignmentPoints(props.assignment, props.assignmentCompletion)
);
const userPointsWithDeduction = computed(() => {
const points = userPoints.value;
if (points && props.assignmentCompletion.evaluation_points_deducted > 0) {
return points - props.assignmentCompletion.evaluation_points_deducted;
}
return points;
});
const percentage = computed(() => {
if (props.assignmentCompletion.completion_status === "EVALUATION_SUBMITTED") {
return (
((props.assignmentCompletion?.evaluation_points_final ??
userPointsWithDeduction.value ??
0) /
(props.assignmentCompletion?.evaluation_max_points ?? 1)) *
100
);
} else {
return ((userPointsWithDeduction.value ?? 0) / (maxPoints.value ?? 1)) * 100;
}
});
const showNotPassed = computed(() => {
if (props.assignment.assignment_type === "CASEWORK") {
return percentage.value < 55;
}
return false;
});
const showPassed = computed(() => {
if (props.assignment.assignment_type === "CASEWORK") {
return percentage.value >= 55;
}
return false;
});
</script>
<template>
@ -136,26 +184,23 @@ const userPoints = computed(() =>
<section v-if="props.assignment.assignment_type === 'CASEWORK'">
<div class="flex items-center">
<div class="heading-1 py-4" data-cy="user-points">
<template
v-if="
props.assignmentCompletion.completion_status == 'EVALUATION_SUBMITTED'
"
>
{{ props.assignmentCompletion.evaluation_points_final }}
</template>
<template v-else>
{{ userPoints }}
</template>
{{ userPointsWithDeduction }}
</div>
<div class="pl-2" data-cy="total-points">
{{ $t("assignment.von x Punkten", { x: maxPoints }) }}
({{
(
((props.assignmentCompletion?.evaluation_points_final ?? 0) /
(props.assignmentCompletion?.evaluation_max_points ?? 1)) *
100
).toFixed(0)
}}%)
({{ percentage.toFixed(0) }}%)
</div>
<div v-if="showNotPassed" class="ml-2">
<span class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5">
{{ $t("a.Nicht Bestanden") }}
</span>
</div>
<div v-if="showPassed" class="ml-2">
<span class="my-2 rounded-md bg-green-200 px-2.5 py-0.5">
{{ $t("a.Bestanden") }}
</span>
</div>
</div>
@ -206,6 +251,11 @@ const userPoints = computed(() =>
</p>
</div>
<p v-if="props.dueDate" class="my-4" data-cy="evaluation-duedate">
{{ "a.Freigabetermin Bewertung" }}: {{ props.dueDate.format("DD.MM.YYYY") }}
{{ props.dueDate.format("HH.mm") }}
</p>
<p
v-if="
props.assignment.assignment_type === 'PRAXIS_ASSIGNMENT' &&
@ -214,18 +264,23 @@ const userPoints = computed(() =>
>
{{ $t("a.assignment.evaluationFeedbackDescriptionText") }}
</p>
<section>
<div
v-for="historyEntry in props.assignmentCompletion.additional_json_data
?.submission_history ?? []"
:key="historyEntry.timestamp"
>
{{ dayjs(historyEntry.timestamp).format("DD.MM.YYYY HH.mm") }}:
{{ $t(historyEntry.translation_key) }}
({{ historyEntry.user_display_name }})
</div>
</section>
<div
v-if="props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED'"
v-if="props.assignmentCompletion.completion_status !== 'EVALUATION_SUBMITTED'"
class="mt-8"
>
{{ $t("assignment.dueDateEvaluation") }}:
{{
dayjs(props.assignmentCompletion.evaluation_submitted_at).format("DD.MM.YYYY")
}}
um
{{ dayjs(props.assignmentCompletion.evaluation_submitted_at).format("HH.mm") }}
Uhr
</div>
<div v-else>
<button
class="btn-primary text-large"
data-cy="submit-evaluation"
@ -233,6 +288,16 @@ const userPoints = computed(() =>
>
{{ $t(text.evaluationSubmit) }}
</button>
<ItTextarea
v-model="evaluationComment"
class="mt-8"
:placeholder="`${$t('a.Kommentar erfassen')}...`"
data-cy="reason-text"
></ItTextarea>
</div>
<div v-else-if="evaluationComment" class="mt-4">
{{ evaluationComment }}
</div>
<div v-if="state.showSuccessInfo" class="mt-4">

View File

@ -14,11 +14,11 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
*/
const documents = {
"\n mutation AttendanceCheckMutation(\n $attendanceCourseId: ID!\n $attendanceUserList: [AttendanceUserInputType]!\n ) {\n update_course_session_attendance_course_users(\n id: $attendanceCourseId\n attendance_user_list: $attendanceUserList\n ) {\n course_session_attendance_course {\n id\n attendance_user_list {\n user_id\n first_name\n last_name\n email\n status\n }\n }\n }\n }\n": types.AttendanceCheckMutationDocument,
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n completion_data\n task_completion_data\n additional_json_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
"\n mutation UpdateCourseSessionProfile($input: CourseSessionProfileMutationInput!) {\n update_course_session_profile(input: $input) {\n clientMutationId\n result {\n __typename\n ... on UpdateCourseProfileSuccess {\n user {\n id\n chosen_profile\n }\n }\n ... on UpdateCourseProfileError {\n message\n }\n }\n }\n }\n": types.UpdateCourseSessionProfileDocument,
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n\n additional_json_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query competenceCertificateQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userIds: [UUID!]\n ) {\n competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completions(course_session_id: $courseSessionId) {\n id\n assignment_user {\n id\n }\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_deducted\n evaluation_points_final\n evaluation_max_points\n evaluation_passed\n evaluation_percent\n course_session {\n id\n title\n }\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
"\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userIds: [UUID!]!\n ) {\n competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completions(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\n evaluation_max_points\n evaluation_passed\n evaluation_percent\n assignment_user {\n id\n }\n course_session {\n id\n title\n }\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateForUserQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n optional_attendance\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
@ -53,7 +53,7 @@ export function graphql(source: "\n mutation AttendanceCheckMutation(\n $att
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n"): (typeof documents)["\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n"];
export function graphql(source: "\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n completion_data\n task_completion_data\n additional_json_data\n }\n }\n }\n"): (typeof documents)["\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n $evaluationUserId: ID\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n evaluation_user_id: $evaluationUserId\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n completion_data\n task_completion_data\n additional_json_data\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@ -69,7 +69,7 @@ export function graphql(source: "\n query attendanceCheckQuery($courseSessionId
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"): (typeof documents)["\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"];
export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n\n additional_json_data\n }\n }\n"): (typeof documents)["\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n\n additional_json_data\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

File diff suppressed because one or more lines are too long

View File

@ -594,7 +594,7 @@ type AssignmentCompletionObjectType {
course_session: CourseSessionObjectType!
completion_status: AssignmentAssignmentCompletionCompletionStatusChoices!
completion_data: GenericScalar
additional_json_data: JSONString!
additional_json_data: GenericScalar
task_completion_data: GenericScalar
learning_content_page_id: ID
evaluation_points: Float
@ -650,14 +650,6 @@ String, Boolean, Int, Float, List or Object.
"""
scalar GenericScalar
"""
Allows use of a JSON String for input / output from the GraphQL schema.
Use of this type is *not recommended* as you lose the benefits of having a defined, static
schema (one of the key benefits of GraphQL).
"""
scalar JSONString
type ContentDocumentObjectType {
id: ID!
display_text: String!

View File

@ -55,7 +55,6 @@ export const GenericScalar = "GenericScalar";
export const ID = "ID";
export const Int = "Int";
export const JSONStreamField = "JSONStreamField";
export const JSONString = "JSONString";
export const LearningContentAssignmentObjectType = "LearningContentAssignmentObjectType";
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";

View File

@ -52,8 +52,13 @@ export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(`
submitted_at
evaluation_submitted_at
evaluation_points
evaluation_max_points
evaluation_points_deducted
evaluation_points_deducted_reason
evaluation_points_final
completion_data
task_completion_data
additional_json_data
}
}
}

View File

@ -85,6 +85,8 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
edoniq_extended_time_flag
completion_data
task_completion_data
additional_json_data
}
}
`);

View File

@ -100,7 +100,7 @@ describe("praxisauftrag.cy.js", () => {
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="submit-evaluation"]').click();
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="btn-close"]').click();
cy.visit("/");

View File

@ -59,6 +59,7 @@ DATABASES = {
default="postgres://postgres@localhost:5432/vbv_lernwelt",
)
}
DATABASES["default"]["ATOMIC_REQUESTS"] = env.bool(
"DATABASE_ATOMIC_REQUESTS", default=True
)

View File

@ -1,7 +1,11 @@
from django.contrib import admin
from django.db.models import JSONField
from vbv_lernwelt.assignment.models import AssignmentCompletion
from vbv_lernwelt.assignment.models import (
AssignmentCompletion,
AssignmentCompletionAuditLog,
)
from vbv_lernwelt.core.admin import LogAdmin
from vbv_lernwelt.core.admin_utils import PrettyJSONWidget
@ -16,6 +20,7 @@ class AssignmentCompletionAdmin(admin.ModelAdmin):
"assignment",
"get_circle",
"assignment_user",
"evaluation_user",
"course_session",
"completion_status",
"evaluation_points",
@ -27,7 +32,14 @@ class AssignmentCompletionAdmin(admin.ModelAdmin):
"course_session__course",
"course_session",
]
search_fields = ["assignment_user__email"]
search_fields = [
"assignment_user__email",
"assignment_user__first_name",
"assignment_user__last_name",
"evaluation_user__email",
"evaluation_user__first_name",
"evaluation_user__last_name",
]
readonly_fields = [
"assignment_user",
"assignment",
@ -53,3 +65,40 @@ class AssignmentCompletionAdmin(admin.ModelAdmin):
if change and "evaluation_points_deducted" in form.changed_data:
obj.evaluation_points_deducted_user = request.user
super().save_model(request, obj, form, change)
@admin.register(AssignmentCompletionAuditLog)
class AssignmentCompletionAuditLogAdmin(LogAdmin):
date_hierarchy = "created_at"
list_display = [
"created_at",
"assignment",
"get_circle",
"assignment_user",
"evaluation_user",
"course_session",
"completion_status",
"evaluation_points",
]
list_filter = [
"completion_status",
"assignment__assignment_type",
"course_session__course",
"course_session",
]
search_fields = [
"assignment_user__email",
"assignment_user__first_name",
"assignment_user__last_name",
"evaluation_user__email",
"evaluation_user__first_name",
"evaluation_user__last_name",
]
def get_circle(self, obj):
try:
return obj.learning_content_page.specific.get_circle().title
except Exception:
return ""
get_circle.short_description = "Circle"

View File

@ -16,6 +16,7 @@ class AssignmentCompletionObjectType(DjangoObjectType):
completion_data = GenericScalar()
task_completion_data = GenericScalar()
learning_content_page_id = graphene.ID(source="learning_content_page_id")
additional_json_data = GenericScalar()
# rounded to sensible representation
evaluation_points = graphene.Float()

View File

@ -0,0 +1,71 @@
import djclick as click
import structlog
from vbv_lernwelt.assignment.models import AssignmentCompletion
logger = structlog.get_logger(__name__)
def create_initial_submission_history(apps=None, schema_editor=None):
if apps is None:
# pylint: disable=import-outside-toplevel
from vbv_lernwelt.assignment.models import AssignmentCompletion
else:
AssignmentCompletion = apps.get_model("assignment", "AssignmentCompletion")
for ac in AssignmentCompletion.objects.filter(
assignment__assignment_type__in=["PRAXIS_ASSIGNMENT", "CASEWORK"]
):
num_entries = ac_create_initial_submission_history(ac)
# print(f"Created initial submission history for {ac} {num_entries}")
def ac_create_initial_submission_history(ac: AssignmentCompletion):
submission_history = ac.additional_json_data.get("submission_history", [])
num_entries = 0
if len(submission_history) > 0:
return num_entries
if ac.submitted_at:
entry = {
"timestamp": ac.submitted_at.isoformat(),
"status": "SUBMITTED",
"translation_key": "a.Ergebnisse abgegeben",
"user_id": str(ac.assignment_user.id),
"user_email": ac.assignment_user.email,
"user_display_name": (
f"{ac.assignment_user.first_name} {ac.assignment_user.last_name}"
),
}
submission_history.append(entry)
num_entries += 1
if ac.evaluation_submitted_at:
translation_key = "a.Bewertung freigegeben"
if ac.assignment.assignment_type == "PRAXIS_ASSIGNMENT":
translation_key = "Feedback freigegeben"
entry = {
"timestamp": ac.evaluation_submitted_at.isoformat(),
"status": "EVALUATION_SUBMITTED",
"translation_key": translation_key,
"user_id": str(ac.evaluation_user.id),
"user_email": ac.evaluation_user.email,
"user_display_name": (
f"{ac.evaluation_user.first_name} {ac.evaluation_user.last_name}"
),
}
submission_history.append(entry)
num_entries += 1
ac.additional_json_data["submission_history"] = submission_history
ac.save()
return num_entries
@click.command()
def command():
create_initial_submission_history()

View File

@ -0,0 +1,43 @@
# Generated by Django 4.2.13 on 2024-09-16 12:00
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("assignment", "0014_evaluation_points_deducted"),
]
operations = [
migrations.AddField(
model_name="assignmentcompletionauditlog",
name="evaluation_points_deducted",
field=models.FloatField(default=0.0, verbose_name="Punkteabzug"),
),
migrations.AddField(
model_name="assignmentcompletionauditlog",
name="evaluation_points_deducted_reason",
field=models.TextField(
blank=True, default="", verbose_name="Punkteabzug Begründung"
),
),
migrations.AddField(
model_name="assignmentcompletionauditlog",
name="evaluation_points_deducted_user",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="assignmentcompletionauditlog",
name="evaluation_points_deducted_user_email",
field=models.CharField(blank=True, default="", max_length=255),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.13 on 2024-09-17 11:37
from django.db import migrations
from vbv_lernwelt.assignment.management.commands.assignment_create_initial_submission_history import (
create_initial_submission_history,
)
class Migration(migrations.Migration):
dependencies = [
(
"assignment",
"0015_assignmentcompletionauditlog_evaluation_points_deducted_and_more",
),
]
operations = [migrations.RunPython(create_initial_submission_history)]

View File

@ -485,6 +485,23 @@ class AssignmentCompletionAuditLog(models.Model):
evaluation_max_points = models.FloatField(null=True, blank=True)
evaluation_passed = models.BooleanField(null=True, blank=True)
evaluation_points_deducted = models.FloatField(
default=0.0, verbose_name="Punkteabzug"
)
evaluation_points_deducted_reason = models.TextField(
default="", blank=True, verbose_name="Punkteabzug Begründung"
)
evaluation_points_deducted_user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="+",
)
evaluation_points_deducted_user_email = models.CharField(
max_length=255, blank=True, default=""
)
def recalculate_assignment_passed(ac: AssignmentCompletion):
if ac.evaluation_points_final is not None and ac.evaluation_max_points is not None:

View File

@ -142,6 +142,13 @@ def update_assignment_completion(
}
)
evaluation_reopened = False
if (
completion_status == AssignmentCompletionStatus.EVALUATION_IN_PROGRESS
and ac.completion_status == "EVALUATION_SUBMITTED"
):
evaluation_reopened = True
if evaluation_max_points is None:
ac.evaluation_max_points = assignment.get_max_points()
else:
@ -229,9 +236,16 @@ def update_assignment_completion(
evaluation_points=evaluation_points,
evaluation_max_points=ac.evaluation_max_points,
evaluation_passed=ac.evaluation_passed,
evaluation_points_deducted=ac.evaluation_points_deducted,
evaluation_points_deducted_reason=ac.evaluation_points_deducted_reason,
evaluation_points_deducted_user=ac.evaluation_points_deducted_user,
)
if evaluation_user:
acl.evaluation_user_email = evaluation_user.email
if ac.evaluation_points_deducted_user:
acl.evaluation_points_deducted_user_email = (
ac.evaluation_points_deducted_user.email
)
# copy over the question data, so that we don't lose the context
subtasks = assignment.get_input_tasks()
@ -241,23 +255,66 @@ def update_assignment_completion(
acl.completion_data[key].update(task_data)
acl.save()
if completion_status in [
if (
completion_status
in [
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
AssignmentCompletionStatus.SUBMITTED,
]:
learning_content = (
learning_content_page
if learning_content_page
else assignment.find_attached_learning_content()
]
or evaluation_reopened
) and assignment.assignment_type != AssignmentType.EDONIQ_TEST.value:
# make history entry
submission_history = ac.additional_json_data.get("submission_history", [])
entry = {
"timestamp": timezone.now().isoformat(),
"status": completion_status.value,
}
if completion_status == AssignmentCompletionStatus.SUBMITTED:
entry["translation_key"] = "a.Ergebnisse abgegeben"
entry["user_id"] = str(assignment_user.id)
entry["user_email"] = assignment_user.email
entry["user_display_name"] = (
f"{assignment_user.first_name} {assignment_user.last_name}"
)
else:
entry["user_id"] = str(evaluation_user.id)
entry["user_email"] = evaluation_user.email
entry["user_display_name"] = (
f"{evaluation_user.first_name} {evaluation_user.last_name}"
)
if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
if assignment.assignment_type == AssignmentType.PRAXIS_ASSIGNMENT.value:
entry["translation_key"] = "a.Feedback freigegeben"
else:
entry["translation_key"] = "a.Bewertung freigegeben"
elif completion_status == AssignmentCompletionStatus.EVALUATION_IN_PROGRESS:
if assignment.assignment_type == AssignmentType.PRAXIS_ASSIGNMENT.value:
entry["translation_key"] = "a.Feedback erneut bearbeitet"
else:
entry["translation_key"] = "a.Bewertung erneut bearbeitet"
submission_history.append(entry)
ac.additional_json_data["submission_history"] = submission_history
ac.save()
if completion_status in [
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
AssignmentCompletionStatus.SUBMITTED,
]:
learning_content = (
learning_content_page
if learning_content_page
else assignment.find_attached_learning_content()
)
if learning_content:
mark_course_completion(
user=assignment_user,
page=learning_content,
course_session=course_session,
completion_status=CourseCompletionStatus.SUCCESS.value,
)
if learning_content:
mark_course_completion(
user=assignment_user,
page=learning_content,
course_session=course_session,
completion_status=CourseCompletionStatus.SUCCESS.value,
)
return ac, created
@ -267,7 +324,8 @@ def _remove_unknown_entries(assignment, completion_data):
Removes all entries from completion_data which are not known to the assignment
"""
input_task_ids = [task["id"] for task in assignment.get_input_tasks()]
keys = set(input_task_ids) | {"expert_evaluation_comment"}
filtered_completion_data = {
key: value for key, value in completion_data.items() if key in input_task_ids
key: value for key, value in completion_data.items() if key in keys
}
return filtered_completion_data

View File

@ -190,6 +190,13 @@ class UpdateAssignmentCompletionTestCase(TestCase):
"test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice",
)
# will create submission_history entry
submission_history = ac.additional_json_data.get("submission_history", [])
self.assertEqual(len(submission_history), 1)
entry = submission_history[0]
self.assertEqual(entry["status"], "SUBMITTED")
self.assertEqual(entry["user_email"], "student")
# AssignmentCompletionAuditLog entry will remain event after deletion of foreign keys
ac.delete()
self.user.delete()
@ -512,6 +519,13 @@ class UpdateAssignmentCompletionTestCase(TestCase):
user_input["user_data"], {"text": "Ich würde nichts weiteres empfehlen."}
)
# will create submission_history entry
submission_history = ac.additional_json_data.get("submission_history", [])
self.assertEqual(len(submission_history), 1)
entry = submission_history[0]
self.assertEqual(entry["status"], "EVALUATION_SUBMITTED")
self.assertEqual(entry["user_email"], "admin")
# will create AssignmentCompletionAuditLog entry
acl = AssignmentCompletionAuditLog.objects.get(
assignment_user=self.user,
@ -553,3 +567,156 @@ class UpdateAssignmentCompletionTestCase(TestCase):
)
self.assertIsNone(acl.assignment_user)
self.assertIsNone(acl.assignment)
def test_can_reopen_evaluated_submission(self):
subtasks = self.assignment.filter_user_subtasks(
subtask_types=["user_text_input"]
)
user_text_input = find_first(
subtasks,
pred=lambda x: (value := x.get("value"))
and value.get("text", "").startswith(
"Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest?"
),
)
ac = AssignmentCompletion.objects.create(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
completion_data={
user_text_input["id"]: {
"user_data": {"text": "Ich würde nichts weiteres empfehlen."}
},
},
)
evaluation_task = self.assignment.get_evaluation_tasks()[0]
update_assignment_completion(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={
evaluation_task["id"]: {
"expert_data": {"points": 2, "text": "Gut gemacht!"}
},
},
completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
evaluation_user=self.trainer,
)
update_assignment_completion(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={},
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED,
evaluation_user=self.trainer,
evaluation_points=16,
)
ac = AssignmentCompletion.objects.get(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
self.assertEqual(ac.completion_status, "EVALUATION_SUBMITTED")
self.assertEqual(ac.evaluation_points, 16)
self.assertEqual(ac.evaluation_max_points, 24)
self.assertTrue(ac.evaluation_passed)
trainer_input = ac.completion_data[evaluation_task["id"]]
self.assertDictEqual(
trainer_input["expert_data"], {"points": 2, "text": "Gut gemacht!"}
)
user_input = ac.completion_data[user_text_input["id"]]
self.assertDictEqual(
user_input["user_data"], {"text": "Ich würde nichts weiteres empfehlen."}
)
update_assignment_completion(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={},
completion_status=AssignmentCompletionStatus.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")
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. Ich musste es noch einmal anschauen.",
}
},
},
completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
evaluation_user=self.trainer,
)
# will create submission_history entry
submission_history = ac.additional_json_data.get("submission_history", [])
self.assertEqual(len(submission_history), 2)
entry = submission_history[1]
self.assertEqual(entry["status"], "EVALUATION_IN_PROGRESS")
self.assertEqual(entry["user_email"], "admin")
update_assignment_completion(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_data={},
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED,
evaluation_user=self.trainer,
evaluation_points=16,
)
ac = AssignmentCompletion.objects.get(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
)
# will create submission_history entry
submission_history = ac.additional_json_data.get("submission_history", [])
self.assertEqual(len(submission_history), 3)
entry = submission_history[2]
self.assertEqual(entry["status"], "EVALUATION_SUBMITTED")
self.assertEqual(entry["user_email"], "admin")
self.assertEqual(ac.completion_status, "EVALUATION_SUBMITTED")
self.assertEqual(ac.evaluation_points, 16)
self.assertEqual(ac.evaluation_max_points, 24)
self.assertTrue(ac.evaluation_passed)
trainer_input = ac.completion_data[evaluation_task["id"]]
self.assertDictEqual(
trainer_input["expert_data"],
{"points": 2, "text": "Gut gemacht. Ich musste es noch einmal anschauen."},
)
user_input = ac.completion_data[user_text_input["id"]]
self.assertDictEqual(
user_input["user_data"], {"text": "Ich würde nichts weiteres empfehlen."}
)
# it will have created another AssignmentCompletionAuditLog entry
acl_qs = AssignmentCompletionAuditLog.objects.filter(
assignment_user=self.user,
assignment=self.assignment,
course_session=self.course_session,
completion_status="EVALUATION_SUBMITTED",
)
self.assertEqual(acl_qs.count(), 2)