From d4cb978de3a59de529f4f34aa10d84b099ce9c91 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Wed, 4 Oct 2023 14:14:56 +0200 Subject: [PATCH 1/7] Add assigment task file upload --- client/src/gql/gql.ts | 8 +- client/src/gql/graphql.ts | 9 +- client/src/gql/schema.graphql | 1 + client/src/graphql/mutations.ts | 1 + client/src/graphql/queries.ts | 1 + .../assignment/AssignmentTaskView.vue | 36 ++++++-- .../assignment/AttachmentSection.vue | 85 +++++++++++++++++++ client/src/services/files.ts | 24 ++++-- client/src/types.ts | 28 +++++- server/config/urls.py | 11 ++- server/requirements/requirements.in | 1 - server/requirements/requirements.txt | 3 +- .../vbv_lernwelt/assignment/graphql/types.py | 24 +++--- server/vbv_lernwelt/assignment/models.py | 32 ++++++- server/vbv_lernwelt/assignment/serializers.py | 1 + server/vbv_lernwelt/assignment/services.py | 16 +++- .../assignment/tests/test_graphql.py | 43 +++++++++- server/vbv_lernwelt/course_session/models.py | 6 +- server/vbv_lernwelt/files/integrations.py | 8 +- server/vbv_lernwelt/files/serializers.py | 6 ++ server/vbv_lernwelt/files/services.py | 14 ++- server/vbv_lernwelt/files/views.py | 33 +++++++ 22 files changed, 330 insertions(+), 61 deletions(-) create mode 100644 client/src/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue create mode 100644 server/vbv_lernwelt/files/serializers.py create mode 100644 server/vbv_lernwelt/files/views.py diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index fdde1523..d971bb1d 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -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. */ diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index 0f417008..9a81624b 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -94,6 +94,7 @@ export type AssignmentCompletionObjectType = { id: Scalars['UUID']['output']; learning_content_page_id?: Maybe; submitted_at?: Maybe; + task_completion_data?: Maybe; updated_at: Scalars['DateTime']['output']; }; @@ -798,7 +799,7 @@ export type UpsertAssignmentCompletionMutationVariables = Exact<{ }>; -export type UpsertAssignmentCompletionMutation = { __typename?: 'Mutation', upsert_assignment_completion?: { __typename?: 'AssignmentCompletionMutation', assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: any, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_points?: number | null, completion_data?: any | null } | null } | null }; +export type UpsertAssignmentCompletionMutation = { __typename?: 'Mutation', upsert_assignment_completion?: { __typename?: 'AssignmentCompletionMutation', assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: any, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_points?: number | null, completion_data?: any | null, task_completion_data?: any | null } | null } | null }; type CoursePageFieldsAssignmentObjectTypeFragment = { __typename?: 'AssignmentObjectType', title?: string | null, id?: string | null, slug?: string | null, content_type?: string | null, frontend_url?: string | null } & { ' $fragmentName'?: 'CoursePageFieldsAssignmentObjectTypeFragment' }; @@ -836,7 +837,7 @@ export type AssignmentCompletionQueryQueryVariables = Exact<{ export type AssignmentCompletionQueryQuery = { __typename?: 'Query', assignment?: { __typename?: 'AssignmentObjectType', assignment_type: AssignmentAssignmentAssignmentTypeChoices, needs_expert_evaluation: boolean, max_points?: number | null, content_type?: string | null, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id?: string | null, intro_text: string, performance_objectives?: any | null, slug?: string | null, tasks?: any | null, title?: string | null, translation_key?: string | null, competence_certificate?: ( { __typename?: 'CompetenceCertificateObjectType' } & { ' $fragmentRefs'?: { 'CoursePageFieldsCompetenceCertificateObjectTypeFragment': CoursePageFieldsCompetenceCertificateObjectTypeFragment } } - ) | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: any, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_points?: number | null, evaluation_max_points?: number | null, evaluation_passed?: boolean | null, edoniq_extended_time_flag: boolean, completion_data?: any | null, evaluation_user?: { __typename?: 'UserType', id: any } | null, assignment_user: { __typename?: 'UserType', id: any } } | null }; + ) | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: any, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_points?: number | null, evaluation_max_points?: number | null, evaluation_passed?: boolean | null, edoniq_extended_time_flag: boolean, completion_data?: any | null, task_completion_data?: any | null, evaluation_user?: { __typename?: 'UserType', id: any } | null, assignment_user: { __typename?: 'UserType', id: any } } | null }; export type CourseQueryQueryVariables = Exact<{ courseId: Scalars['ID']['input']; @@ -911,9 +912,9 @@ export type SendFeedbackMutationMutation = { __typename?: 'Mutation', send_feedb export const CoursePageFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode; export const AttendanceCheckMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AttendanceCheckMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"attendanceCourseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"attendanceUserList"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AttendanceUserInputType"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update_course_session_attendance_course_users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"attendanceCourseId"}}},{"kind":"Argument","name":{"kind":"Name","value":"attendance_user_list"},"value":{"kind":"Variable","name":{"kind":"Name","value":"attendanceUserList"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session_attendance_course"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"attendance_user_list"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"first_name"}},{"kind":"Field","name":{"kind":"Name","value":"last_name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const UpsertAssignmentCompletionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertAssignmentCompletion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AssignmentCompletionStatus"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"initializeCompletion"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsert_assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_data_string"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_points"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}}},{"kind":"Argument","name":{"kind":"Name","value":"initialize_completion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"initializeCompletion"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UpsertAssignmentCompletionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertAssignmentCompletion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AssignmentCompletionStatus"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"initializeCompletion"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsert_assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_data_string"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_points"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}}},{"kind":"Argument","name":{"kind":"Name","value":"initialize_completion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"initializeCompletion"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}},{"kind":"Field","name":{"kind":"Name","value":"task_completion_data"}}]}}]}}]}}]} as unknown as DocumentNode; export const AttendanceCheckQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"attendanceCheckQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session_attendance_course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"attendance_user_list"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode; -export const AssignmentCompletionQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"assignmentCompletionQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"needs_expert_evaluation"}},{"kind":"Field","name":{"kind":"Name","value":"max_points"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"effort_required"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_description"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_document_url"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_tasks"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"intro_text"}},{"kind":"Field","name":{"kind":"Name","value":"performance_objectives"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tasks"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"translation_key"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_max_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_passed"}},{"kind":"Field","name":{"kind":"Name","value":"edoniq_extended_time_flag"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode; +export const AssignmentCompletionQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"assignmentCompletionQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"needs_expert_evaluation"}},{"kind":"Field","name":{"kind":"Name","value":"max_points"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"effort_required"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_description"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_document_url"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_tasks"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"intro_text"}},{"kind":"Field","name":{"kind":"Name","value":"performance_objectives"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tasks"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"translation_key"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_max_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_passed"}},{"kind":"Field","name":{"kind":"Name","value":"edoniq_extended_time_flag"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}},{"kind":"Field","name":{"kind":"Name","value":"task_completion_data"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode; export const CourseQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"category_name"}},{"kind":"Field","name":{"kind":"Name","value":"learning_path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const CompetenceCertificateQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"competenceCertificateQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_list"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"max_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_max_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_passed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}},{"kind":"Field","name":{"kind":"Name","value":"circle"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode; export const CourseSessionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseSessionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"course"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"first_name"}},{"kind":"Field","name":{"kind":"Name","value":"last_name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar_url"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"attendance_courses"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"trainer"}},{"kind":"Field","name":{"kind":"Name","value":"due_date"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"end"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content_id"}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"circle"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"submission_deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}}]}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"edoniq_tests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"end"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index 87b8817d..8120f07c 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -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 } diff --git a/client/src/graphql/mutations.ts b/client/src/graphql/mutations.ts index 19ee3349..f2cf586a 100644 --- a/client/src/graphql/mutations.ts +++ b/client/src/graphql/mutations.ts @@ -51,6 +51,7 @@ export const UPSERT_ASSIGNMENT_COMPLETION_MUTATION = graphql(` evaluation_submitted_at evaluation_points completion_data + task_completion_data } } } diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index 68268a6d..91bfd7c7 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -71,6 +71,7 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(` evaluation_passed edoniq_extended_time_flag completion_data + task_completion_data } } `); diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue index 3d25b001..0fb5bcb8 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue @@ -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; +}); diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue b/client/src/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue new file mode 100644 index 00000000..5d59db4b --- /dev/null +++ b/client/src/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue @@ -0,0 +1,85 @@ + + + diff --git a/client/src/services/files.ts b/client/src/services/files.ts index c2ada811..3a6ae62d 100644 --- a/client/src/services/files.ts +++ b/client/src/services/files.ts @@ -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, + }); +} diff --git a/client/src/types.ts b/client/src/types.ts index d8b92282..391184e5 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -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_data": {"confirmation": true}}, // } [key: string]: { - user_data?: UserDataText | UserDataConfirmation; + user_data?: UserDataText | UserDataConfirmation | UserDataFile; expert_data?: ExpertData; }; } +export interface AssignmentTaskCompletionData { + // { + // "": {"user_data": {"text": "some text from user"}}, + // "": {"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; diff --git a/server/config/urls.py b/server/config/urls.py index 44c43545..6bb8ec03 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -10,6 +10,9 @@ from django.views import defaults as default_views from django.views.decorators.csrf import csrf_exempt from django_ratelimit.exceptions import Ratelimited from graphene_django.views import GraphQLView +from wagtail import urls as wagtail_urls +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.documents import urls as wagtaildocs_urls from vbv_lernwelt.assignment.views import request_assignment_completion_status from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt @@ -47,15 +50,13 @@ 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, t2l_sync, ) from vbv_lernwelt.notify.views import email_notification_settings -from wagtail import urls as wagtail_urls -from wagtail.admin import urls as wagtailadmin_urls -from wagtail.documents import urls as wagtaildocs_urls class SignedIntConverter(IntConverter): @@ -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//summary/', get_expert_feedbacks_for_course, diff --git a/server/requirements/requirements.in b/server/requirements/requirements.in index 76388ad2..3b5ae91e 100644 --- a/server/requirements/requirements.in +++ b/server/requirements/requirements.in @@ -25,7 +25,6 @@ django-ratelimit django-ipware django-csp django-storages -django-storages[azure] django-notifications-hq django-jsonform django-constance diff --git a/server/requirements/requirements.txt b/server/requirements/requirements.txt index a0818296..d526fed6 100644 --- a/server/requirements/requirements.txt +++ b/server/requirements/requirements.txt @@ -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 diff --git a/server/vbv_lernwelt/assignment/graphql/types.py b/server/vbv_lernwelt/assignment/graphql/types.py index d98e40b0..d1681bdd 100644 --- a/server/vbv_lernwelt/assignment/graphql/types.py +++ b/server/vbv_lernwelt/assignment/graphql/types.py @@ -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" ) @@ -72,11 +74,11 @@ class AssignmentObjectType(DjangoObjectType): return self.find_attached_learning_content() def resolve_completion( - self, - info, - course_session_id, - learning_content_page_id=None, - assignment_user_id=None, + self, + info, + course_session_id, + learning_content_page_id=None, + assignment_user_id=None, ): if learning_content_page_id is None: lp = self.find_attached_learning_content() @@ -92,17 +94,17 @@ class AssignmentObjectType(DjangoObjectType): def resolve_assignment_completion( - info, - assignment_id, - course_session_id, - learning_content_page_id=None, - assignment_user_id=None, + info, + assignment_id, + course_session_id, + learning_content_page_id=None, + assignment_user_id=None, ): if assignment_user_id is None: assignment_user_id = info.context.user.id if str(assignment_user_id) == str(info.context.user.id) or is_course_session_expert( - info.context.user, course_session_id + info.context.user, course_session_id ): course_id = CourseSession.objects.get(id=course_session_id).course_id if has_course_access(info.context.user, course_id): diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index d5ab6d74..8d4abd0a 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -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): @@ -302,7 +304,7 @@ class AssignmentCompletionStatus(Enum): def is_valid_assignment_completion_status( - completion_status: AssignmentCompletionStatus, + completion_status: AssignmentCompletionStatus, ): return completion_status.value in AssignmentCompletionStatus.__members__ @@ -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): """ diff --git a/server/vbv_lernwelt/assignment/serializers.py b/server/vbv_lernwelt/assignment/serializers.py index 78791487..746aa6c2 100644 --- a/server/vbv_lernwelt/assignment/serializers.py +++ b/server/vbv_lernwelt/assignment/serializers.py @@ -17,6 +17,7 @@ class AssignmentCompletionSerializer(serializers.ModelSerializer): "course_session", "completion_status", "completion_data", + "task_completion_data", "evaluation_user", "additional_json_data", "evaluation_points", diff --git a/server/vbv_lernwelt/assignment/services.py b/server/vbv_lernwelt/assignment/services.py index 87d31695..6f30df5e 100644 --- a/server/vbv_lernwelt/assignment/services.py +++ b/server/vbv_lernwelt/assignment/services.py @@ -108,8 +108,8 @@ def update_assignment_completion( ) if ( - completion_status == AssignmentCompletionStatus.IN_PROGRESS - and ac.completion_status != "IN_PROGRESS" + completion_status == AssignmentCompletionStatus.IN_PROGRESS + and ac.completion_status != "IN_PROGRESS" ): raise serializers.ValidationError( { @@ -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) diff --git a/server/vbv_lernwelt/assignment/tests/test_graphql.py b/server/vbv_lernwelt/assignment/tests/test_graphql.py index debebc04..dea42c5f 100644 --- a/server/vbv_lernwelt/assignment/tests/test_graphql.py +++ b/server/vbv_lernwelt/assignment/tests/test_graphql.py @@ -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,9 +101,25 @@ 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} + } + } + ) + # check DB data db_entry = AssignmentCompletion.objects.get( assignment_user=self.student, @@ -93,6 +131,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 +175,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 +191,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}} }, ) @@ -227,7 +268,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase): user_text_input = find_first( subtasks, pred=lambda x: (value := x.get("value")) - and value.get("text", "").startswith( + and value.get("text", "").startswith( "Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest?" ), ) diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py index 9c094de1..576599b5 100644 --- a/server/vbv_lernwelt/course_session/models.py +++ b/server/vbv_lernwelt/course_session/models.py @@ -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) """ @@ -138,8 +138,8 @@ class CourseSessionAssignment(models.Model): url_expert = f"/course/{self.course_session.course.slug}/cockpit/assignment/{self.learning_content_id}?courseSessionId={self.course_session.id}" if assignment_type in ( - AssignmentType.CASEWORK.value, - AssignmentType.PREP_ASSIGNMENT.value, + AssignmentType.CASEWORK.value, + AssignmentType.PREP_ASSIGNMENT.value, ): if not self.submission_deadline_id: self.submission_deadline = DueDate.objects.create( diff --git a/server/vbv_lernwelt/files/integrations.py b/server/vbv_lernwelt/files/integrations.py index 7e7b5d5f..53f2a8c3 100644 --- a/server/vbv_lernwelt/files/integrations.py +++ b/server/vbv_lernwelt/files/integrations.py @@ -47,16 +47,21 @@ 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 ) def s3_generate_presigned_post( - *, file_path: str, file_type: str, file_name: str + *, file_path: str, file_type: str, file_name: str ) -> Dict[str, Any]: credentials = s3_get_credentials() s3_client = s3_get_client() @@ -99,6 +104,7 @@ def s3_generate_presigned_post( ["starts-with", "$Content-Disposition", ""], ], ExpiresIn=expires_in, + ) return presigned_data diff --git a/server/vbv_lernwelt/files/serializers.py b/server/vbv_lernwelt/files/serializers.py new file mode 100644 index 00000000..4d6fd14d --- /dev/null +++ b/server/vbv_lernwelt/files/serializers.py @@ -0,0 +1,6 @@ +from rest_framework import serializers + + +class PresignInputSerializer(serializers.Serializer): + file_name = serializers.CharField() + file_type = serializers.CharField() diff --git a/server/vbv_lernwelt/files/services.py b/server/vbv_lernwelt/files/services.py index 36b8ccac..51de24ff 100644 --- a/server/vbv_lernwelt/files/services.py +++ b/server/vbv_lernwelt/files/services.py @@ -43,7 +43,7 @@ class FileStandardUploadService: self.file_obj = file_obj def _infer_file_name_and_type( - self, file_name: str = "", file_type: str = "" + self, file_name: str = "", file_type: str = "" ) -> Tuple[str, str]: if not file_name: file_name = self.file_obj.name @@ -80,7 +80,7 @@ class FileStandardUploadService: @transaction.atomic def update( - self, file: UploadFile, file_name: str = "", file_type: str = "" + self, file: UploadFile, file_name: str = "", file_type: str = "" ) -> UploadFile: _validate_file_size(self.file_obj) @@ -114,7 +114,7 @@ class FileDirectUploadService: @transaction.atomic def start( - self, file_name: str, file_type: str + self, file_name: str, file_type: str ) -> Tuple[UploadFile, Dict[str, Any]]: file = UploadFile( original_file_name=file_name, @@ -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: diff --git a/server/vbv_lernwelt/files/views.py b/server/vbv_lernwelt/files/views.py new file mode 100644 index 00000000..5afc101e --- /dev/null +++ b/server/vbv_lernwelt/files/views.py @@ -0,0 +1,33 @@ +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 + + +@api_view(["POST"]) +def presign(request): + print("presign", request.data) + + 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, + } + } + ) From 75351b9986b19c0cf79eee2cebba9cb610f60936 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Wed, 4 Oct 2023 14:26:09 +0200 Subject: [PATCH 2/7] chore: format --- server/config/urls.py | 6 ++--- .../vbv_lernwelt/assignment/graphql/types.py | 22 ++++++++--------- server/vbv_lernwelt/assignment/models.py | 2 +- server/vbv_lernwelt/assignment/services.py | 4 ++-- .../assignment/tests/test_graphql.py | 24 ++++++++----------- server/vbv_lernwelt/course_session/models.py | 4 ++-- server/vbv_lernwelt/files/integrations.py | 7 +++--- server/vbv_lernwelt/files/services.py | 6 ++--- server/vbv_lernwelt/files/views.py | 5 ++-- 9 files changed, 38 insertions(+), 42 deletions(-) diff --git a/server/config/urls.py b/server/config/urls.py index 6bb8ec03..59544756 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -10,9 +10,6 @@ from django.views import defaults as default_views from django.views.decorators.csrf import csrf_exempt from django_ratelimit.exceptions import Ratelimited from graphene_django.views import GraphQLView -from wagtail import urls as wagtail_urls -from wagtail.admin import urls as wagtailadmin_urls -from wagtail.documents import urls as wagtaildocs_urls from vbv_lernwelt.assignment.views import request_assignment_completion_status from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt @@ -57,6 +54,9 @@ from vbv_lernwelt.importer.views import ( t2l_sync, ) from vbv_lernwelt.notify.views import email_notification_settings +from wagtail import urls as wagtail_urls +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.documents import urls as wagtaildocs_urls class SignedIntConverter(IntConverter): diff --git a/server/vbv_lernwelt/assignment/graphql/types.py b/server/vbv_lernwelt/assignment/graphql/types.py index d1681bdd..b66614a9 100644 --- a/server/vbv_lernwelt/assignment/graphql/types.py +++ b/server/vbv_lernwelt/assignment/graphql/types.py @@ -74,11 +74,11 @@ class AssignmentObjectType(DjangoObjectType): return self.find_attached_learning_content() def resolve_completion( - self, - info, - course_session_id, - learning_content_page_id=None, - assignment_user_id=None, + self, + info, + course_session_id, + learning_content_page_id=None, + assignment_user_id=None, ): if learning_content_page_id is None: lp = self.find_attached_learning_content() @@ -94,17 +94,17 @@ class AssignmentObjectType(DjangoObjectType): def resolve_assignment_completion( - info, - assignment_id, - course_session_id, - learning_content_page_id=None, - assignment_user_id=None, + info, + assignment_id, + course_session_id, + learning_content_page_id=None, + assignment_user_id=None, ): if assignment_user_id is None: assignment_user_id = info.context.user.id if str(assignment_user_id) == str(info.context.user.id) or is_course_session_expert( - info.context.user, course_session_id + info.context.user, course_session_id ): course_id = CourseSession.objects.get(id=course_session_id).course_id if has_course_access(info.context.user, course_id): diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index 8d4abd0a..e0de99d1 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -304,7 +304,7 @@ class AssignmentCompletionStatus(Enum): def is_valid_assignment_completion_status( - completion_status: AssignmentCompletionStatus, + completion_status: AssignmentCompletionStatus, ): return completion_status.value in AssignmentCompletionStatus.__members__ diff --git a/server/vbv_lernwelt/assignment/services.py b/server/vbv_lernwelt/assignment/services.py index 6f30df5e..72280d5f 100644 --- a/server/vbv_lernwelt/assignment/services.py +++ b/server/vbv_lernwelt/assignment/services.py @@ -108,8 +108,8 @@ def update_assignment_completion( ) if ( - completion_status == AssignmentCompletionStatus.IN_PROGRESS - and ac.completion_status != "IN_PROGRESS" + completion_status == AssignmentCompletionStatus.IN_PROGRESS + and ac.completion_status != "IN_PROGRESS" ): raise serializers.ValidationError( { diff --git a/server/vbv_lernwelt/assignment/tests/test_graphql.py b/server/vbv_lernwelt/assignment/tests/test_graphql.py index dea42c5f..023ff374 100644 --- a/server/vbv_lernwelt/assignment/tests/test_graphql.py +++ b/server/vbv_lernwelt/assignment/tests/test_graphql.py @@ -101,7 +101,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase): data["completion_data"], { user_text_input["id"]: {"user_data": {"text": "Hallo via API"}}, - task_id: {"user_data": {"fileId": file_id}} + task_id: {"user_data": {"fileId": file_id}}, }, ) @@ -109,15 +109,11 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase): self.assertDictEqual( task_data, { - "user_data": - { - "fileId": file_id, - "fileInfo": { - "id": file_id, - "name": 'file.txt', - "url": file_url} - } - } + "user_data": { + "fileId": file_id, + "fileInfo": {"id": file_id, "name": "file.txt", "url": file_url}, + } + }, ) # check DB data @@ -131,7 +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}} + task_id: {"user_data": {"fileId": file_id}}, }, ) @@ -175,7 +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}} + task_id: {"user_data": {"fileId": file_id}}, }, ) @@ -191,7 +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}} + task_id: {"user_data": {"fileId": file_id}}, }, ) @@ -268,7 +264,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase): user_text_input = find_first( subtasks, pred=lambda x: (value := x.get("value")) - and value.get("text", "").startswith( + and value.get("text", "").startswith( "Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest?" ), ) diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py index 576599b5..a436fd95 100644 --- a/server/vbv_lernwelt/course_session/models.py +++ b/server/vbv_lernwelt/course_session/models.py @@ -138,8 +138,8 @@ class CourseSessionAssignment(models.Model): url_expert = f"/course/{self.course_session.course.slug}/cockpit/assignment/{self.learning_content_id}?courseSessionId={self.course_session.id}" if assignment_type in ( - AssignmentType.CASEWORK.value, - AssignmentType.PREP_ASSIGNMENT.value, + AssignmentType.CASEWORK.value, + AssignmentType.PREP_ASSIGNMENT.value, ): if not self.submission_deadline_id: self.submission_deadline = DueDate.objects.create( diff --git a/server/vbv_lernwelt/files/integrations.py b/server/vbv_lernwelt/files/integrations.py index 53f2a8c3..a47899f2 100644 --- a/server/vbv_lernwelt/files/integrations.py +++ b/server/vbv_lernwelt/files/integrations.py @@ -48,7 +48,7 @@ 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) + s3 = boto3.client("s3", region_name=credentials.region_name) endpoint_url = s3.meta.endpoint_url return boto3.client( @@ -56,12 +56,12 @@ def s3_get_client(): 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 + endpoint_url=endpoint_url, ) def s3_generate_presigned_post( - *, file_path: str, file_type: str, file_name: str + *, file_path: str, file_type: str, file_name: str ) -> Dict[str, Any]: credentials = s3_get_credentials() s3_client = s3_get_client() @@ -104,7 +104,6 @@ def s3_generate_presigned_post( ["starts-with", "$Content-Disposition", ""], ], ExpiresIn=expires_in, - ) return presigned_data diff --git a/server/vbv_lernwelt/files/services.py b/server/vbv_lernwelt/files/services.py index 51de24ff..de882bc0 100644 --- a/server/vbv_lernwelt/files/services.py +++ b/server/vbv_lernwelt/files/services.py @@ -43,7 +43,7 @@ class FileStandardUploadService: self.file_obj = file_obj def _infer_file_name_and_type( - self, file_name: str = "", file_type: str = "" + self, file_name: str = "", file_type: str = "" ) -> Tuple[str, str]: if not file_name: file_name = self.file_obj.name @@ -80,7 +80,7 @@ class FileStandardUploadService: @transaction.atomic def update( - self, file: UploadFile, file_name: str = "", file_type: str = "" + self, file: UploadFile, file_name: str = "", file_type: str = "" ) -> UploadFile: _validate_file_size(self.file_obj) @@ -114,7 +114,7 @@ class FileDirectUploadService: @transaction.atomic def start( - self, file_name: str, file_type: str + self, file_name: str, file_type: str ) -> Tuple[UploadFile, Dict[str, Any]]: file = UploadFile( original_file_name=file_name, diff --git a/server/vbv_lernwelt/files/views.py b/server/vbv_lernwelt/files/views.py index 5afc101e..30791398 100644 --- a/server/vbv_lernwelt/files/views.py +++ b/server/vbv_lernwelt/files/views.py @@ -18,7 +18,8 @@ def presign(request): 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"] + file_name=serializer.validated_data["file_name"], + file_type=serializer.validated_data["file_type"], ) return Response( @@ -28,6 +29,6 @@ def presign(request): "id": upload_file.id, "name": upload_file.original_file_name, "url": upload_file.url, - } + }, } ) From 7a9cf339f919b17c9ff41918da7017aca2b26561 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Wed, 4 Oct 2023 15:19:30 +0200 Subject: [PATCH 3/7] add files to overview --- .../AssignmentEvaluationPage.vue | 3 +++ .../assignment/AssignmentSubmissionResponses.vue | 14 ++++++++++++++ .../assignment/AssignmentSubmissionView.vue | 5 +++++ 3 files changed, 22 insertions(+) diff --git a/client/src/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue b/client/src/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue index fd42b7c8..bb3fd1b0 100644 --- a/client/src/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue +++ b/client/src/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue @@ -110,6 +110,9 @@ const assignment = computed( diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue index 32965cbc..da9fc215 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue @@ -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 }}

+ + diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue index d132b180..6a4163eb 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue @@ -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 () => { From de1949407b135f523d7905146de882ae59ca7484 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Tue, 10 Oct 2023 17:03:59 +0200 Subject: [PATCH 4/7] disable doc updates if assigment not in progress --- .../assignment/AssignmentTaskView.vue | 1 + .../assignment/AttachmentSection.vue | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue index 0fb5bcb8..37108815 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentTaskView.vue @@ -157,6 +157,7 @@ const taskFileInfo = computed(() => { -import { ref } from "vue"; +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(props.fileInfo); +const selectedFile = ref(); + +watch( + () => props.fileInfo, + (newVal) => { + selectedFile.value = newVal; + }, + { immediate: true } +); const loading = ref(false); const uploadError = ref(false); @@ -49,8 +58,12 @@ function handleDelete() {