Merged in feature/VBV-325-auftrag-dok-handling (pull request #215)
Feature/VBV-325 auftrag dok handling Approved-by: Daniel Egger Approved-by: Stéphanie Rotzetter
This commit is contained in:
commit
99508fec09
|
|
@ -87,6 +87,11 @@ def main(app_name, image_name, environment_file):
|
|||
"IT_DJANGO_SECRET_KEY": env.str(
|
||||
"IT_DJANGO_SECRET_KEY", generate_random_string(63)
|
||||
),
|
||||
"AWS_S3_ACCESS_KEY_ID": env.str("AWS_S3_ACCESS_KEY_ID", ""),
|
||||
"AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""),
|
||||
"AWS_S3_REGION_NAME": "eu-central-1",
|
||||
"AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch",
|
||||
"FILE_UPLOAD_STORAGE": "s3",
|
||||
"IT_DJANGO_DEBUG": "false",
|
||||
"IT_SERVE_VUE": "false",
|
||||
"IT_ALLOW_LOCAL_LOGIN": "true",
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ 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 ) {\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 ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n 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 ) {\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 ) {\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 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 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 }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n 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 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 }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||
"\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\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 }\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,
|
||||
|
|
@ -45,7 +45,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 ) {\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 ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n 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 ) {\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 ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n 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 ) {\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 ) {\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 ) {\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 ) {\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"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
@ -57,7 +57,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 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 }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n 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 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 }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n 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 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 }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\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 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 }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_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
|
|
@ -312,6 +312,7 @@ type AssignmentCompletionObjectType {
|
|||
completion_status: AssignmentAssignmentCompletionCompletionStatusChoices!
|
||||
completion_data: GenericScalar
|
||||
additional_json_data: JSONString!
|
||||
task_completion_data: GenericScalar
|
||||
learning_content_page_id: ID
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(`
|
|||
evaluation_submitted_at
|
||||
evaluation_points
|
||||
completion_data
|
||||
task_completion_data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
|
|||
evaluation_passed
|
||||
edoniq_extended_time_flag
|
||||
completion_data
|
||||
task_completion_data
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -110,6 +110,9 @@ const assignment = computed(
|
|||
<AssignmentSubmissionResponses
|
||||
:assignment="assignment"
|
||||
:assignment-completion-data="assignmentCompletion.completion_data"
|
||||
:assignment-task-completion-data="
|
||||
assignmentCompletion.task_completion_data
|
||||
"
|
||||
:allow-edit="false"
|
||||
></AssignmentSubmissionResponses>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import type {
|
|||
Assignment,
|
||||
AssignmentCompletionData,
|
||||
AssignmentTask,
|
||||
AssignmentTaskCompletionData,
|
||||
UserDataText,
|
||||
} from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
assignmentCompletionData: AssignmentCompletionData;
|
||||
assignmentTaskCompletionData: AssignmentTaskCompletionData;
|
||||
allowEdit: boolean;
|
||||
}>();
|
||||
|
||||
|
|
@ -48,5 +50,17 @@ const emit = defineEmits<{
|
|||
{{ (assignmentCompletionData[taskBlock.id].user_data as UserDataText).text }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="props.assignmentTaskCompletionData[task.id]?.user_data?.fileInfo"
|
||||
class="mt-4"
|
||||
>
|
||||
<a
|
||||
:href="props.assignmentTaskCompletionData[task.id]?.user_data?.fileInfo?.url"
|
||||
class="link"
|
||||
>
|
||||
{{ props.assignmentTaskCompletionData[task.id]?.user_data?.fileInfo?.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ const completionData = computed(() => {
|
|||
return props.assignmentCompletion?.completion_data ?? {};
|
||||
});
|
||||
|
||||
const completionTaskData = computed(() => {
|
||||
return props.assignmentCompletion?.task_completion_data ?? {};
|
||||
});
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return (
|
||||
!state.confirmInput ||
|
||||
|
|
@ -179,6 +183,7 @@ const onSubmit = async () => {
|
|||
<AssignmentSubmissionResponses
|
||||
:assignment="props.assignment"
|
||||
:assignment-completion-data="completionData"
|
||||
:assignment-task-completion-data="completionTaskData"
|
||||
:allow-edit="completionStatus === 'IN_PROGRESS'"
|
||||
@edit-task="onEditTask"
|
||||
></AssignmentSubmissionResponses>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import type {
|
|||
AssignmentCompletionData,
|
||||
AssignmentTask,
|
||||
UserDataConfirmation,
|
||||
UserDataFile,
|
||||
UserDataText,
|
||||
} from "@/types";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import log from "loglevel";
|
||||
import { computed, reactive } from "vue";
|
||||
import AttachmentSection from "@/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
assignmentId: string;
|
||||
|
|
@ -77,6 +79,16 @@ const onUpdateConfirmation = (id: string, value: boolean) => {
|
|||
upsertAssignmentCompletion(data);
|
||||
};
|
||||
|
||||
const onUpdateFile = (taskId: string, value: string | null) => {
|
||||
const data: AssignmentCompletionData = {};
|
||||
data[taskId] = {
|
||||
user_data: {
|
||||
fileId: value,
|
||||
} as UserDataFile,
|
||||
};
|
||||
upsertAssignmentCompletion(data);
|
||||
};
|
||||
|
||||
const getBlockData = (id: string) => {
|
||||
const userData = getCompletionDataForUserInput(id)?.user_data;
|
||||
if (userData && "text" in userData) {
|
||||
|
|
@ -95,6 +107,15 @@ const onToggleCheckbox = (id: string) => {
|
|||
const completionStatus = computed(() => {
|
||||
return props.assignmentCompletion?.completion_status ?? "IN_PROGRESS";
|
||||
});
|
||||
|
||||
const taskFileInfo = computed(() => {
|
||||
if (!props.assignmentCompletion) {
|
||||
return null;
|
||||
}
|
||||
const taskUserData =
|
||||
props.assignmentCompletion.task_completion_data[props.task.id]?.user_data;
|
||||
return taskUserData?.fileInfo ?? null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -133,12 +154,12 @@ const completionStatus = computed(() => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.task.value.file_submission_required">
|
||||
<p class="text-large">Datei hochladen</p>
|
||||
|
||||
<p class="text-sm text-gray-900">
|
||||
Mögliche Formate: .JPG, .PNG, .PDF, .DOC, .MOV, .PPT
|
||||
</p>
|
||||
</div>
|
||||
<AttachmentSection
|
||||
v-if="props.task.value.file_submission_required"
|
||||
:file-info="taskFileInfo"
|
||||
:read-only="completionStatus !== 'IN_PROGRESS'"
|
||||
class="mt-8"
|
||||
@file-uploaded="onUpdateFile(props.task.id, $event)"
|
||||
@file-deleted="onUpdateFile(props.task.id, null)"
|
||||
></AttachmentSection>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { presignUpload, uploadFile } from "@/services/files";
|
||||
import type { UserDataFileInfo } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
fileInfo: UserDataFileInfo | null;
|
||||
readOnly: boolean;
|
||||
}>();
|
||||
const emit = defineEmits(["fileUploaded", "fileDeleted"]);
|
||||
|
||||
const selectedFile = ref();
|
||||
|
||||
watch(
|
||||
() => props.fileInfo,
|
||||
(newVal) => {
|
||||
selectedFile.value = newVal;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
const uploadError = ref(false);
|
||||
|
||||
async function fileSelected(e: Event) {
|
||||
const { files } = e.target as HTMLInputElement;
|
||||
if (!files?.length) return;
|
||||
|
||||
try {
|
||||
uploadError.value = false;
|
||||
loading.value = true;
|
||||
const file = files[0];
|
||||
const presignData = await presignUpload(file);
|
||||
await uploadFile(presignData.pre_sign, file);
|
||||
selectedFile.value = presignData.file_info;
|
||||
emit("fileUploaded", presignData.file_info.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
uploadError.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
selectedFile.value = null;
|
||||
emit("fileDeleted");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h4 class="mb-2 text-xl">{{ $t("a.Datei hochladen") }}</h4>
|
||||
|
||||
<template v-if="loading">
|
||||
{{ $t("a.Laden...") }}
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<template v-if="!selectedFile">
|
||||
<div
|
||||
:class="[props.readOnly ? 'text-blue-600' : 'cursor-pointer text-blue-800']"
|
||||
class="relative mb-4 flex items-center"
|
||||
>
|
||||
<input
|
||||
v-if="!props.readOnly"
|
||||
id="upload"
|
||||
type="file"
|
||||
class="absolute opacity-0"
|
||||
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.mov,.ppt,.pptx"
|
||||
@change="fileSelected"
|
||||
/>
|
||||
<it-icon-document class="mr-1.5 h-7 w-7 text-blue-800" />
|
||||
{{ $t("a.Datei auswählen") }}
|
||||
</div>
|
||||
<p class="text-sm text-gray-900">
|
||||
{{ $t("a.Mögliche Formate") }}: .JPG, .PNG, .PDF, .DOC, .MOV, .PPT
|
||||
</p>
|
||||
<p class="mb-8 text-sm text-gray-900">
|
||||
{{ $t("a.Maximale Dateigrösse") }}: 20 MB
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<h4 class="mr-4 text-lg font-bold">{{ selectedFile.name }}</h4>
|
||||
<a v-if="!props.readOnly" class="flex cursor-pointer" @click="handleDelete">
|
||||
<it-icon-delete class="h-8 w-8" />
|
||||
</a>
|
||||
<a :href="selectedFile.url" class="flex">
|
||||
<it-icon-download class="h-8 w-8" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p v-if="uploadError" class="mt-3 text-red-700">
|
||||
{{ $t("a.Datei kann nicht gespeichert werden.") }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -20,7 +20,7 @@ async function startFileUpload(fileData: DocumentUploadData, courseSessionId: st
|
|||
});
|
||||
}
|
||||
|
||||
function uploadFile(fileData: FileData, file: File) {
|
||||
export async function uploadFile(fileData: FileData, file: File) {
|
||||
if (fileData.fields) {
|
||||
return s3Upload(fileData, file);
|
||||
} else {
|
||||
|
|
@ -44,7 +44,7 @@ function directUpload(fileData: FileData, file: File) {
|
|||
// @ts-ignore
|
||||
options.headers["X-CSRFToken"] = getCookieValue("csrftoken");
|
||||
|
||||
handleUpload(fileData.url, options);
|
||||
return handleUpload(fileData.url, options);
|
||||
}
|
||||
|
||||
function s3Upload(fileData: FileData, file: File) {
|
||||
|
|
@ -63,12 +63,13 @@ function s3Upload(fileData: FileData, file: File) {
|
|||
return handleUpload(fileData.url, options);
|
||||
}
|
||||
|
||||
function handleUpload(url: string, options: RequestInit) {
|
||||
return itFetch(url, options).then((response) => {
|
||||
return response.json().catch(() => {
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
});
|
||||
async function handleUpload(url: string, options: RequestInit) {
|
||||
const response = await itFetch(url, options);
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadCircleDocument(
|
||||
|
|
@ -105,3 +106,10 @@ export async function deleteCircleDocument(documentId: string, bustCacheUrlKey =
|
|||
export async function fetchCourseSessionDocuments(courseSessionId: string) {
|
||||
return itGetCached(`/api/core/document/list/${courseSessionId}/`);
|
||||
}
|
||||
|
||||
export async function presignUpload(file: File) {
|
||||
return await itPost(`/api/core/storage/presign/`, {
|
||||
file_type: file.type,
|
||||
file_name: file.name,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -395,7 +395,9 @@ export interface PerformanceCriteria extends BaseCourseWagtailPage {
|
|||
readonly competence_id: string;
|
||||
readonly circle: CircleLight;
|
||||
readonly course_category: CourseCategory;
|
||||
readonly learning_unit: BaseCourseWagtailPage & { evaluate_url: string };
|
||||
readonly learning_unit: BaseCourseWagtailPage & {
|
||||
evaluate_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompetencePage extends BaseCourseWagtailPage {
|
||||
|
|
@ -602,6 +604,17 @@ export interface UserDataConfirmation {
|
|||
confirmation: boolean;
|
||||
}
|
||||
|
||||
export interface UserDataFile {
|
||||
fileId?: string;
|
||||
fileInfo?: UserDataFileInfo;
|
||||
}
|
||||
|
||||
export interface UserDataFileInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ExpertData {
|
||||
points?: number;
|
||||
text?: string;
|
||||
|
|
@ -613,11 +626,21 @@ export interface AssignmentCompletionData {
|
|||
// "<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
|
||||
// }
|
||||
[key: string]: {
|
||||
user_data?: UserDataText | UserDataConfirmation;
|
||||
user_data?: UserDataText | UserDataConfirmation | UserDataFile;
|
||||
expert_data?: ExpertData;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AssignmentTaskCompletionData {
|
||||
// {
|
||||
// "<user_text_input:uuid>": {"user_data": {"text": "some text from user"}},
|
||||
// "<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
|
||||
// }
|
||||
[key: string]: {
|
||||
user_data?: UserDataFile;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AssignmentCompletion {
|
||||
id: string;
|
||||
created_at: string;
|
||||
|
|
@ -630,6 +653,7 @@ export interface AssignmentCompletion {
|
|||
completion_status: AssignmentCompletionStatus;
|
||||
evaluation_user: string | null;
|
||||
completion_data: AssignmentCompletionData;
|
||||
task_completion_data: AssignmentTaskCompletionData;
|
||||
edoniq_extended_time_flag: boolean;
|
||||
evaluation_points: number | null;
|
||||
evaluation_max_points: number | null;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from vbv_lernwelt.feedback.views import (
|
|||
get_expert_feedbacks_for_course,
|
||||
get_feedback_for_circle,
|
||||
)
|
||||
from vbv_lernwelt.files.views import presign
|
||||
from vbv_lernwelt.importer.views import (
|
||||
coursesessions_students_import,
|
||||
coursesessions_trainers_import,
|
||||
|
|
@ -143,6 +144,10 @@ urlpatterns = [
|
|||
get_course_session_documents,
|
||||
name='get_course_session_documents'),
|
||||
|
||||
# file storage
|
||||
path(r'api/core/storage/presign/', presign,
|
||||
name='storage_presign'),
|
||||
|
||||
# feedback
|
||||
path(r'api/core/feedback/<str:course_session_id>/summary/',
|
||||
get_expert_feedbacks_for_course,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ django-ratelimit
|
|||
django-ipware
|
||||
django-csp
|
||||
django-storages
|
||||
django-storages[azure]
|
||||
django-notifications-hq
|
||||
django-jsonform
|
||||
django-constance
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ azure-identity==1.14.0
|
|||
azure-storage-blob==12.17.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# django-storages
|
||||
bcrypt==4.0.1
|
||||
# via paramiko
|
||||
beautifulsoup4==4.11.2
|
||||
|
|
@ -129,7 +128,7 @@ django-ratelimit==4.1.0
|
|||
# via -r requirements.in
|
||||
django-redis==5.3.0
|
||||
# via -r requirements.in
|
||||
django-storages[azure]==1.13.2
|
||||
django-storages==1.13.2
|
||||
# via -r requirements.in
|
||||
django-taggit==4.0.0
|
||||
# via wagtail
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
|
|||
"task",
|
||||
TaskBlockFactory(
|
||||
title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden",
|
||||
file_submission_required=True,
|
||||
# it is hard to create a StreamValue programmatically, we have to
|
||||
# create a `StreamValue` manually. Ask Daniel and/or Ramon
|
||||
content=StreamValue(
|
||||
|
|
@ -317,7 +318,6 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
|
|||
"task",
|
||||
TaskBlockFactory(
|
||||
title="Teilaufgabe 3: Aktuelle Versicherung",
|
||||
# TODO: add document upload
|
||||
content=StreamValue(
|
||||
TaskContentStreamBlock(),
|
||||
stream_data=[
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
|||
|
||||
class AssignmentCompletionObjectType(DjangoObjectType):
|
||||
completion_data = GenericScalar()
|
||||
task_completion_data = GenericScalar()
|
||||
learning_content_page_id = graphene.ID(source="learning_content_page_id")
|
||||
|
||||
class Meta:
|
||||
|
|
@ -35,6 +36,7 @@ class AssignmentCompletionObjectType(DjangoObjectType):
|
|||
"evaluation_points",
|
||||
"evaluation_passed",
|
||||
"evaluation_max_points",
|
||||
"task_completion_data",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import copy
|
||||
import uuid
|
||||
from enum import Enum
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ from vbv_lernwelt.core.constants import (
|
|||
from vbv_lernwelt.core.model_utils import find_available_slug
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseBasePage
|
||||
from vbv_lernwelt.files.models import UploadFile
|
||||
|
||||
|
||||
class AssignmentListPage(CourseBasePage):
|
||||
|
|
@ -370,6 +372,34 @@ class AssignmentCompletion(models.Model):
|
|||
"""
|
||||
return f"{self.course_session.course.get_cockpit_url()}/assignment/{self.assignment.id}/{self.assignment_user.id}"
|
||||
|
||||
@property
|
||||
def task_completion_data(self):
|
||||
data = {}
|
||||
for task in self.assignment.tasks:
|
||||
data[task.id] = get_task_data(task, self.completion_data)
|
||||
return data
|
||||
|
||||
|
||||
def get_file_info(file_id):
|
||||
file_info = UploadFile.objects.filter(id=file_id).first()
|
||||
if file_info:
|
||||
return {
|
||||
"id": str(file_info.id),
|
||||
"name": file_info.original_file_name,
|
||||
"url": file_info.url,
|
||||
}
|
||||
|
||||
|
||||
def get_task_data(task, completion_data):
|
||||
task_data = copy.deepcopy(completion_data.get(task.id, {}))
|
||||
user_data = task_data.get("user_data", {})
|
||||
file_id = user_data.get("fileId")
|
||||
|
||||
if file_id:
|
||||
user_data["fileInfo"] = get_file_info(file_id)
|
||||
|
||||
return task_data
|
||||
|
||||
|
||||
class AssignmentCompletionAuditLog(models.Model):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class AssignmentCompletionSerializer(serializers.ModelSerializer):
|
|||
"course_session",
|
||||
"completion_status",
|
||||
"completion_data",
|
||||
"task_completion_data",
|
||||
"evaluation_user",
|
||||
"additional_json_data",
|
||||
"evaluation_points",
|
||||
|
|
|
|||
|
|
@ -179,6 +179,14 @@ def update_assignment_completion(
|
|||
ac.edoniq_extended_time_flag = edoniq_extended_time_flag
|
||||
ac.additional_json_data = ac.additional_json_data | additional_json_data
|
||||
|
||||
task_ids = [task.id for task in assignment.tasks]
|
||||
|
||||
for key, value in completion_data.items():
|
||||
if key in task_ids:
|
||||
stored_entry = ac.completion_data.get(key, {})
|
||||
stored_entry.update(value)
|
||||
ac.completion_data[key] = stored_entry
|
||||
|
||||
# TODO: make more validation of the provided input -> maybe with graphql
|
||||
completion_data = _remove_unknown_entries(assignment, completion_data)
|
||||
for key, value in completion_data.items():
|
||||
|
|
@ -189,9 +197,9 @@ def update_assignment_completion(
|
|||
|
||||
if copy_task_data:
|
||||
# copy over the question data, so that we don't lose the context
|
||||
substasks = assignment.get_input_tasks()
|
||||
sub_tasks = assignment.get_input_tasks()
|
||||
for key, value in ac.completion_data.items():
|
||||
task_data = find_first(substasks, pred=lambda x: x["id"] == key)
|
||||
task_data = find_first(sub_tasks, pred=lambda x: x["id"] == key)
|
||||
if task_data:
|
||||
ac.completion_data[key].update(task_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
from datetime import date
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.utils import timezone
|
||||
from graphene_django.utils import GraphQLTestCase
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ from vbv_lernwelt.core.models import User
|
|||
from vbv_lernwelt.core.utils import find_first
|
||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.files.models import UploadFile
|
||||
from vbv_lernwelt.notify.models import Notification
|
||||
|
||||
|
||||
|
|
@ -31,6 +33,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
|
||||
)
|
||||
self.assignment_subtasks = self.assignment.filter_user_subtasks()
|
||||
self.assignment_task_ids = [t.id for t in self.assignment.tasks]
|
||||
|
||||
# self.client.force_login(self.trainer)
|
||||
|
||||
|
|
@ -40,9 +43,27 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
self.assignment_subtasks, pred=lambda x: x["type"] == "user_text_input"
|
||||
)
|
||||
|
||||
task_id = self.assignment_task_ids[0]
|
||||
|
||||
# Create file
|
||||
uploaded_file = SimpleUploadedFile("file.txt", b"file_content")
|
||||
file = UploadFile(
|
||||
original_file_name="file.txt",
|
||||
file_name="file.txt",
|
||||
file_type="text/plain",
|
||||
uploaded_by=self.student,
|
||||
file=uploaded_file,
|
||||
)
|
||||
file.full_clean()
|
||||
file.save()
|
||||
|
||||
file_id = str(file.id)
|
||||
file_url = file.url
|
||||
|
||||
completion_data_string = json.dumps(
|
||||
{
|
||||
user_text_input["id"]: {"user_data": {"text": "Hallo via API"}},
|
||||
task_id: {"user_data": {"fileId": file_id}},
|
||||
}
|
||||
).replace('"', '\\"')
|
||||
|
||||
|
|
@ -58,6 +79,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
id
|
||||
completion_status
|
||||
completion_data
|
||||
task_completion_data
|
||||
assignment_user {{ id }}
|
||||
assignment {{ id }}
|
||||
}}
|
||||
|
|
@ -79,6 +101,18 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
data["completion_data"],
|
||||
{
|
||||
user_text_input["id"]: {"user_data": {"text": "Hallo via API"}},
|
||||
task_id: {"user_data": {"fileId": file_id}},
|
||||
},
|
||||
)
|
||||
|
||||
task_data = data["task_completion_data"][task_id]
|
||||
self.assertDictEqual(
|
||||
task_data,
|
||||
{
|
||||
"user_data": {
|
||||
"fileId": file_id,
|
||||
"fileInfo": {"id": file_id, "name": "file.txt", "url": file_url},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -93,6 +127,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
db_entry.completion_data,
|
||||
{
|
||||
user_text_input["id"]: {"user_data": {"text": "Hallo via API"}},
|
||||
task_id: {"user_data": {"fileId": file_id}},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -136,6 +171,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
data["completion_data"],
|
||||
{
|
||||
user_text_input["id"]: {"user_data": {"text": "Hallo via API 2"}},
|
||||
task_id: {"user_data": {"fileId": file_id}},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -151,6 +187,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
db_entry.completion_data,
|
||||
{
|
||||
user_text_input["id"]: {"user_data": {"text": "Hallo via API 2"}},
|
||||
task_id: {"user_data": {"fileId": file_id}},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class CourseSessionAttendanceCourse(models.Model):
|
|||
class CourseSessionAssignment(models.Model):
|
||||
"""
|
||||
Auftrag
|
||||
- Geletitete Fallarbeit ist eine speziefische ausprägung eines Auftrags (assignment_type)
|
||||
- Geleitete Fallarbeit ist eine spezifische Ausprägung eines Auftrags (assignment_type)
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -47,11 +47,16 @@ def s3_get_credentials() -> S3Credentials:
|
|||
def s3_get_client():
|
||||
credentials = s3_get_credentials()
|
||||
|
||||
# This is needed until https://github.com/boto/boto3/issues/3015 is fixed
|
||||
s3 = boto3.client("s3", region_name=credentials.region_name)
|
||||
endpoint_url = s3.meta.endpoint_url
|
||||
|
||||
return boto3.client(
|
||||
service_name="s3",
|
||||
aws_access_key_id=credentials.access_key_id,
|
||||
aws_secret_access_key=credentials.secret_access_key,
|
||||
region_name=credentials.region_name,
|
||||
endpoint_url=endpoint_url,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class PresignInputSerializer(serializers.Serializer):
|
||||
file_name = serializers.CharField()
|
||||
file_type = serializers.CharField()
|
||||
|
|
@ -134,19 +134,17 @@ class FileDirectUploadService:
|
|||
file.file = file.file.field.attr_class(file, file.file.field, upload_path)
|
||||
file.save()
|
||||
|
||||
presigned_data: Dict[str, Any] = {}
|
||||
|
||||
if settings.FILE_UPLOAD_STORAGE == FileUploadStorage.S3.value:
|
||||
presigned_data = s3_generate_presigned_post(
|
||||
pre_signed_data = s3_generate_presigned_post(
|
||||
file_path=upload_path, file_type=file.file_type, file_name=file_name
|
||||
)
|
||||
|
||||
else:
|
||||
presigned_data = {
|
||||
pre_signed_data = {
|
||||
"url": file_generate_local_upload_url(file_id=str(file.id)),
|
||||
}
|
||||
|
||||
return file, presigned_data
|
||||
return file, pre_signed_data
|
||||
|
||||
@transaction.atomic
|
||||
def finish(self, *, file: UploadFile) -> UploadFile:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import structlog
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from vbv_lernwelt.files.serializers import PresignInputSerializer
|
||||
from vbv_lernwelt.files.services import FileDirectUploadService
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def presign(request):
|
||||
logger.debug(
|
||||
"presign request",
|
||||
file_type=request.data.get("file_type"),
|
||||
file_name=request.data.get("file_name"),
|
||||
label="file_upload",
|
||||
)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response(status=401)
|
||||
|
||||
serializer = PresignInputSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
service = FileDirectUploadService(request.user)
|
||||
|
||||
upload_file, pre_signed_data = service.start(
|
||||
file_name=serializer.validated_data["file_name"],
|
||||
file_type=serializer.validated_data["file_type"],
|
||||
)
|
||||
|
||||
return Response(
|
||||
data={
|
||||
"pre_sign": pre_signed_data,
|
||||
"file_info": {
|
||||
"id": upload_file.id,
|
||||
"name": upload_file.original_file_name,
|
||||
"url": upload_file.url,
|
||||
},
|
||||
}
|
||||
)
|
||||
Loading…
Reference in New Issue