diff --git a/caprover_create_app.py b/caprover_create_app.py
index 50a55ccd..31f14b15 100644
--- a/caprover_create_app.py
+++ b/caprover_create_app.py
@@ -87,10 +87,14 @@ 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_ACCESS_KEY_ID": env.str(
+ "AWS_S3_ACCESS_KEY_ID", "AKIAZJLREPUVWNBTJ5VY"
+ ),
"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",
+ "AWS_S3_REGION_NAME": env.str("AWS_S3_REGION_NAME", "eu-central-1"),
+ "AWS_STORAGE_BUCKET_NAME": env.str(
+ "AWS_STORAGE_BUCKET_NAME", "myvbv-dev.iterativ.ch"
+ ),
"FILE_UPLOAD_STORAGE": "s3",
"IT_DJANGO_DEBUG": "false",
"IT_SERVE_VUE": "false",
diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts
index 548404ec..d3c8e668 100644
--- a/client/src/gql/gql.ts
+++ b/client/src/gql/gql.ts
@@ -17,14 +17,14 @@ const 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": 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 task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
+ "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\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 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 ...CoursePageFields\n circle {\n id\n title\n slug\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 enable_circle_documents\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,
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
- "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
+ "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
};
/**
@@ -60,7 +60,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 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"];
+export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\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 solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\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.
*/
@@ -88,7 +88,7 @@ export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n
/**
* 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 SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
+export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts
index 5c7a561d..cf18e35a 100644
--- a/client/src/gql/graphql.ts
+++ b/client/src/gql/graphql.ts
@@ -156,6 +156,7 @@ export type AssignmentObjectType = CoursePageInterface & {
needs_expert_evaluation: Scalars['Boolean']['output'];
performance_objectives?: Maybe;
slug: Scalars['String']['output'];
+ solution_sample?: Maybe;
tasks?: Maybe;
title: Scalars['String']['output'];
translation_key: Scalars['String']['output'];
@@ -308,6 +309,16 @@ export type CompetencesStatisticsType = {
summary: CompetencePerformanceStatisticsSummaryType;
};
+export type ContentDocumentObjectType = {
+ __typename?: 'ContentDocumentObjectType';
+ description: Scalars['String']['output'];
+ display_text: Scalars['String']['output'];
+ id: Scalars['ID']['output'];
+ link_display_text: Scalars['String']['output'];
+ thumbnail: Scalars['String']['output'];
+ url?: Maybe;
+};
+
/** An enumeration. */
export type CoreUserLanguageChoices =
/** Deutsch */
@@ -585,8 +596,25 @@ export type LearningContentEdoniqTestObjectType = CoursePageInterface & Learning
translation_key: Scalars['String']['output'];
};
-export type LearningContentFeedbackObjectType = CoursePageInterface & LearningContentInterface & {
- __typename?: 'LearningContentFeedbackObjectType';
+export type LearningContentFeedbackUkObjectType = CoursePageInterface & LearningContentInterface & {
+ __typename?: 'LearningContentFeedbackUKObjectType';
+ can_user_self_toggle_course_completion: Scalars['Boolean']['output'];
+ circle?: Maybe;
+ content_type: Scalars['String']['output'];
+ content_url: Scalars['String']['output'];
+ course?: Maybe;
+ description: Scalars['String']['output'];
+ frontend_url: Scalars['String']['output'];
+ id: Scalars['ID']['output'];
+ live: Scalars['Boolean']['output'];
+ minutes?: Maybe;
+ slug: Scalars['String']['output'];
+ title: Scalars['String']['output'];
+ translation_key: Scalars['String']['output'];
+};
+
+export type LearningContentFeedbackVvObjectType = CoursePageInterface & LearningContentInterface & {
+ __typename?: 'LearningContentFeedbackVVObjectType';
can_user_self_toggle_course_completion: Scalars['Boolean']['output'];
circle?: Maybe;
content_type: Scalars['String']['output'];
@@ -791,6 +819,7 @@ export type MutationSendFeedbackArgs = {
course_session_id: Scalars['ID']['input'];
data?: InputMaybe;
learning_content_page_id: Scalars['ID']['input'];
+ learning_content_type: Scalars['String']['input'];
submitted?: InputMaybe;
};
@@ -870,7 +899,8 @@ export type Query = {
learning_content_assignment?: Maybe;
learning_content_attendance_course?: Maybe;
learning_content_document_list?: Maybe;
- learning_content_feedback?: Maybe;
+ learning_content_feedback_uk?: Maybe;
+ learning_content_feedback_vv?: Maybe;
learning_content_knowledge_assessment?: Maybe;
learning_content_learning_module?: Maybe;
learning_content_media_library?: Maybe;
@@ -1045,7 +1075,9 @@ type CoursePageFieldsLearningContentDocumentListObjectTypeFragment = { __typenam
type CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment = { __typename?: 'LearningContentEdoniqTestObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment' };
-type CoursePageFieldsLearningContentFeedbackObjectTypeFragment = { __typename?: 'LearningContentFeedbackObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentFeedbackObjectTypeFragment' };
+type CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment = { __typename?: 'LearningContentFeedbackUKObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment' };
+
+type CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment = { __typename?: 'LearningContentFeedbackVVObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment' };
type CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment = { __typename?: 'LearningContentKnowledgeAssessmentObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment' };
@@ -1069,7 +1101,7 @@ type CoursePageFieldsPerformanceCriteriaObjectTypeFragment = { __typename?: 'Per
type CoursePageFieldsTopicObjectTypeFragment = { __typename?: 'TopicObjectType', title: string, id: string, slug: string, content_type: string, frontend_url: string } & { ' $fragmentName'?: 'CoursePageFieldsTopicObjectTypeFragment' };
-export type CoursePageFieldsFragment = CoursePageFieldsActionCompetenceObjectTypeFragment | CoursePageFieldsAssignmentObjectTypeFragment | CoursePageFieldsCircleObjectTypeFragment | CoursePageFieldsCompetenceCertificateListObjectTypeFragment | CoursePageFieldsCompetenceCertificateObjectTypeFragment | CoursePageFieldsLearningContentAssignmentObjectTypeFragment | CoursePageFieldsLearningContentAttendanceCourseObjectTypeFragment | CoursePageFieldsLearningContentDocumentListObjectTypeFragment | CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment | CoursePageFieldsLearningContentFeedbackObjectTypeFragment | CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment | CoursePageFieldsLearningContentLearningModuleObjectTypeFragment | CoursePageFieldsLearningContentMediaLibraryObjectTypeFragment | CoursePageFieldsLearningContentPlaceholderObjectTypeFragment | CoursePageFieldsLearningContentRichTextObjectTypeFragment | CoursePageFieldsLearningContentVideoObjectTypeFragment | CoursePageFieldsLearningPathObjectTypeFragment | CoursePageFieldsLearningSequenceObjectTypeFragment | CoursePageFieldsLearningUnitObjectTypeFragment | CoursePageFieldsPerformanceCriteriaObjectTypeFragment | CoursePageFieldsTopicObjectTypeFragment;
+export type CoursePageFieldsFragment = CoursePageFieldsActionCompetenceObjectTypeFragment | CoursePageFieldsAssignmentObjectTypeFragment | CoursePageFieldsCircleObjectTypeFragment | CoursePageFieldsCompetenceCertificateListObjectTypeFragment | CoursePageFieldsCompetenceCertificateObjectTypeFragment | CoursePageFieldsLearningContentAssignmentObjectTypeFragment | CoursePageFieldsLearningContentAttendanceCourseObjectTypeFragment | CoursePageFieldsLearningContentDocumentListObjectTypeFragment | CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment | CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment | CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment | CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment | CoursePageFieldsLearningContentLearningModuleObjectTypeFragment | CoursePageFieldsLearningContentMediaLibraryObjectTypeFragment | CoursePageFieldsLearningContentPlaceholderObjectTypeFragment | CoursePageFieldsLearningContentRichTextObjectTypeFragment | CoursePageFieldsLearningContentVideoObjectTypeFragment | CoursePageFieldsLearningPathObjectTypeFragment | CoursePageFieldsLearningSequenceObjectTypeFragment | CoursePageFieldsLearningUnitObjectTypeFragment | CoursePageFieldsPerformanceCriteriaObjectTypeFragment | CoursePageFieldsTopicObjectTypeFragment;
export type AttendanceCheckQueryQueryVariables = Exact<{
courseSessionId: Scalars['ID']['input'];
@@ -1086,7 +1118,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, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id: string, intro_text: string, performance_objectives?: any | null, slug: string, tasks?: any | null, title: string, translation_key: string, competence_certificate?: (
+export type AssignmentCompletionQueryQuery = { __typename?: 'Query', assignment?: { __typename?: 'AssignmentObjectType', assignment_type: AssignmentAssignmentAssignmentTypeChoices, needs_expert_evaluation: boolean, max_points?: number | null, content_type: string, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id: string, intro_text: string, performance_objectives?: any | null, slug: string, tasks?: any | null, title: string, translation_key: string, solution_sample?: { __typename?: 'ContentDocumentObjectType', id: string, url?: string | null } | null, competence_certificate?: (
{ __typename?: 'CompetenceCertificateObjectType' }
& { ' $fragmentRefs'?: { 'CoursePageFieldsCompetenceCertificateObjectTypeFragment': CoursePageFieldsCompetenceCertificateObjectTypeFragment } }
) | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: string | null, evaluation_submitted_at?: string | 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?: 'UserObjectType', id: string } | null, assignment_user: { __typename?: 'UserObjectType', id: string } } | null };
@@ -1113,8 +1145,11 @@ export type CompetenceCertificateQueryQuery = { __typename?: 'Query', competence
{ __typename?: 'LearningContentEdoniqTestObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment': CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment } }
) | (
- { __typename?: 'LearningContentFeedbackObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
- & { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackObjectTypeFragment': CoursePageFieldsLearningContentFeedbackObjectTypeFragment } }
+ { __typename?: 'LearningContentFeedbackUKObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
+ & { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment': CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment } }
+ ) | (
+ { __typename?: 'LearningContentFeedbackVVObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
+ & { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment': CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment } }
) | (
{ __typename?: 'LearningContentKnowledgeAssessmentObjectType', circle?: { __typename?: 'CircleLightObjectType', id: string, title: string, slug: string } | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment': CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment } }
@@ -1186,8 +1221,11 @@ export type CourseQueryQuery = { __typename?: 'Query', course?: { __typename?: '
) | null }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment': CoursePageFieldsLearningContentEdoniqTestObjectTypeFragment } }
) | (
- { __typename?: 'LearningContentFeedbackObjectType', can_user_self_toggle_course_completion: boolean, content_url: string, minutes?: number | null, description: string }
- & { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackObjectTypeFragment': CoursePageFieldsLearningContentFeedbackObjectTypeFragment } }
+ { __typename?: 'LearningContentFeedbackUKObjectType', can_user_self_toggle_course_completion: boolean, content_url: string, minutes?: number | null, description: string }
+ & { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment': CoursePageFieldsLearningContentFeedbackUkObjectTypeFragment } }
+ ) | (
+ { __typename?: 'LearningContentFeedbackVVObjectType', can_user_self_toggle_course_completion: boolean, content_url: string, minutes?: number | null, description: string }
+ & { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment': CoursePageFieldsLearningContentFeedbackVvObjectTypeFragment } }
) | (
{ __typename?: 'LearningContentKnowledgeAssessmentObjectType', can_user_self_toggle_course_completion: boolean, content_url: string, minutes?: number | null, description: string }
& { ' $fragmentRefs'?: { 'CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment': CoursePageFieldsLearningContentKnowledgeAssessmentObjectTypeFragment } }
@@ -1240,6 +1278,7 @@ export type CourseStatisticsQuery = { __typename?: 'Query', course_statistics?:
export type SendFeedbackMutationMutationVariables = Exact<{
courseSessionId: Scalars['ID']['input'];
learningContentId: Scalars['ID']['input'];
+ learningContentType: Scalars['String']['input'];
data: Scalars['GenericScalar']['input'];
submitted?: InputMaybe;
}>;
@@ -1251,11 +1290,11 @@ export const CoursePageFieldsFragmentDoc = {"kind":"Document","definitions":[{"k
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"}},{"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":"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 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":"solution_sample"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"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 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":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"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":"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":"enable_circle_documents"}}]}},{"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;
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":"slug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}}],"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":"category_name"}},{"kind":"Field","name":{"kind":"Name","value":"enable_circle_documents"}},{"kind":"Field","name":{"kind":"Name","value":"action_competences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"performance_criteria"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_id"}},{"kind":"Field","name":{"kind":"Name","value":"learning_unit"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"evaluate_url"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"topics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"is_visible"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"goals"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"learning_sequences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"learning_units"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"evaluate_url"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"performance_criteria"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"can_user_self_toggle_course_completion"}},{"kind":"Field","name":{"kind":"Name","value":"content_url"}},{"kind":"Field","name":{"kind":"Name","value":"minutes"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentAssignmentObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"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":"assignment_type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentEdoniqTestObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkbox_text"}},{"kind":"Field","name":{"kind":"Name","value":"has_extended_time_test"}},{"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":"assignment_type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentRichTextObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}}]}}]}}]}}]}}]}}]}}]}}]}},{"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 DashboardConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"dashboardConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dashboard_config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard_type"}}]}}]}}]} as unknown as DocumentNode;
export const DashboardProgressDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"dashboardProgress"},"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_progress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_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":"course_id"}},{"kind":"Field","name":{"kind":"Name","value":"session_to_continue_id"}},{"kind":"Field","name":{"kind":"Name","value":"competence"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"total_count"}},{"kind":"Field","name":{"kind":"Name","value":"success_count"}},{"kind":"Field","name":{"kind":"Name","value":"fail_count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"total_count"}},{"kind":"Field","name":{"kind":"Name","value":"points_max_count"}},{"kind":"Field","name":{"kind":"Name","value":"points_achieved_count"}}]}}]}}]}}]} as unknown as DocumentNode;
export const CourseStatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseStatistics"},"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_statistics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_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":"course_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_title"}},{"kind":"Field","name":{"kind":"Name","value":"course_slug"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"sessions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"generations"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"course_session_selection_ids"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_selection_metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"session_count"}},{"kind":"Field","name":{"kind":"Name","value":"participant_count"}},{"kind":"Field","name":{"kind":"Name","value":"expert_count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"attendance_day_presences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"due_date"}},{"kind":"Field","name":{"kind":"Name","value":"participants_present"}},{"kind":"Field","name":{"kind":"Name","value":"participants_total"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"days_completed"}},{"kind":"Field","name":{"kind":"Name","value":"participants_present"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"feedback_responses"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"experts"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_average"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_max"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_average"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_max"}},{"kind":"Field","name":{"kind":"Name","value":"total_responses"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"completed_count"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_assignment_id"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type_translation_key"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}},{"kind":"Field","name":{"kind":"Name","value":"deadline"}},{"kind":"Field","name":{"kind":"Name","value":"metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"passed_count"}},{"kind":"Field","name":{"kind":"Name","value":"failed_count"}},{"kind":"Field","name":{"kind":"Name","value":"unranked_count"}},{"kind":"Field","name":{"kind":"Name","value":"ranking_completed"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"competences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"success_total"}},{"kind":"Field","name":{"kind":"Name","value":"fail_total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"success_count"}},{"kind":"Field","name":{"kind":"Name","value":"fail_count"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}}]}}]}}]}}]} as unknown as DocumentNode;
-export const SendFeedbackMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendFeedbackMutation"},"variableDefinitions":[{"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":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GenericScalar"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"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":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"submitted"},"value":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedback_response"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"submitted"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
+export const SendFeedbackMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendFeedbackMutation"},"variableDefinitions":[{"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":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GenericScalar"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"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":"learning_content_type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentType"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"submitted"},"value":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedback_response"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"submitted"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql
index 56c4c08e..ad5c8411 100644
--- a/client/src/gql/schema.graphql
+++ b/client/src/gql/schema.graphql
@@ -9,7 +9,8 @@ type Query {
learning_content_media_library: LearningContentMediaLibraryObjectType
learning_content_assignment: LearningContentAssignmentObjectType
learning_content_attendance_course: LearningContentAttendanceCourseObjectType
- learning_content_feedback: LearningContentFeedbackObjectType
+ learning_content_feedback_uk: LearningContentFeedbackUKObjectType
+ learning_content_feedback_vv: LearningContentFeedbackVVObjectType
learning_content_learning_module: LearningContentLearningModuleObjectType
learning_content_knowledge_assessment: LearningContentKnowledgeAssessmentObjectType
learning_content_placeholder: LearningContentPlaceholderObjectType
@@ -486,6 +487,7 @@ type AssignmentObjectType implements CoursePageInterface {
max_points: Int
learning_content: LearningContentInterface
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
+ solution_sample: ContentDocumentObjectType
}
"""An enumeration."""
@@ -605,6 +607,15 @@ schema (one of the key benefits of GraphQL).
"""
scalar JSONString
+type ContentDocumentObjectType {
+ id: ID!
+ display_text: String!
+ description: String!
+ link_display_text: String!
+ thumbnail: String!
+ url: String
+}
+
"""An enumeration."""
enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
"""PRAXIS_ASSIGNMENT"""
@@ -708,7 +719,23 @@ type LearningContentMediaLibraryObjectType implements CoursePageInterface & Lear
circle: CircleLightObjectType
}
-type LearningContentFeedbackObjectType implements CoursePageInterface & LearningContentInterface {
+type LearningContentFeedbackUKObjectType implements CoursePageInterface & LearningContentInterface {
+ id: ID!
+ title: String!
+ slug: String!
+ content_type: String!
+ live: Boolean!
+ translation_key: String!
+ frontend_url: String!
+ course: CourseObjectType
+ minutes: Int
+ description: String!
+ content_url: String!
+ can_user_self_toggle_course_completion: Boolean!
+ circle: CircleLightObjectType
+}
+
+type LearningContentFeedbackVVObjectType implements CoursePageInterface & LearningContentInterface {
id: ID!
title: String!
slug: String!
@@ -834,7 +861,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
}
type Mutation {
- send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, submitted: Boolean = false): SendFeedbackMutation
+ send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
}
diff --git a/client/src/gql/typenames.ts b/client/src/gql/typenames.ts
index 84bf9ca1..e3dd36e4 100644
--- a/client/src/gql/typenames.ts
+++ b/client/src/gql/typenames.ts
@@ -23,6 +23,7 @@ export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"
export const CompetencePerformanceStatisticsSummaryType = "CompetencePerformanceStatisticsSummaryType";
export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
export const CompetencesStatisticsType = "CompetencesStatisticsType";
+export const ContentDocumentObjectType = "ContentDocumentObjectType";
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
export const CourseObjectType = "CourseObjectType";
export const CoursePageInterface = "CoursePageInterface";
@@ -54,7 +55,8 @@ export const LearningContentAssignmentObjectType = "LearningContentAssignmentObj
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
-export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType";
+export const LearningContentFeedbackUKObjectType = "LearningContentFeedbackUKObjectType";
+export const LearningContentFeedbackVVObjectType = "LearningContentFeedbackVVObjectType";
export const LearningContentInterface = "LearningContentInterface";
export const LearningContentKnowledgeAssessmentObjectType = "LearningContentKnowledgeAssessmentObjectType";
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";
diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts
index 09af6f30..a47af9e9 100644
--- a/client/src/graphql/queries.ts
+++ b/client/src/graphql/queries.ts
@@ -46,6 +46,10 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
tasks
title
translation_key
+ solution_sample {
+ id
+ url
+ }
competence_certificate {
...CoursePageFields
}
diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json
index 3dbcc42d..d7c6518d 100644
--- a/client/src/locales/de/translation.json
+++ b/client/src/locales/de/translation.json
@@ -87,6 +87,8 @@
"performanceObjectivesTitle": "Leistungsziele",
"showAssessmentDocument": "Bewertungsinstrument anzeigen",
"submissionNotificationDisclaimer": "{{name}} wird deine Ergebnisse bewerten. Du wirst per Benachrichtigung informiert, sobald die Bewertung für dich freigegeben wurde.",
+ "submissionShowSampleSolution": "Musterlösung anzeigen",
+ "submissionShowSampleSolutionText": "Hier findest du eine mögliche Lösung zu deinen Aufgaben. Vorgehen und Prozesse in deiner Organisation können von dieser Lösung abweichen.",
"submitAssignment": "Ergebnisse abgeben",
"taskDefinition": "Bearbeite die Teilaufgaben und dokumentiere deine Ergebnisse.",
"taskDefinitionTitle": "Aufgabenstellung",
diff --git a/client/src/pages/AppointmentsPage.vue b/client/src/pages/AppointmentsPage.vue
index aafdc19c..b5011f0c 100644
--- a/client/src/pages/AppointmentsPage.vue
+++ b/client/src/pages/AppointmentsPage.vue
@@ -91,6 +91,7 @@ const appointments = computed(() => {
.allDueDates()
.filter(
(dueDate) =>
+ hasDueDate(dueDate) &&
isMatchingCourse(dueDate) &&
isMatchingSession(dueDate) &&
isMatchingCircle(dueDate)
@@ -108,6 +109,10 @@ const isMatchingCircle = (dueDate: DueDate) =>
const isMatchingCourse = (dueDate: DueDate) =>
courseSessions.value.map((cs) => cs.id).includes(dueDate.course_session_id);
+const hasDueDate = (dueDate: DueDate) => {
+ return dueDate.start || dueDate.end;
+};
+
const numAppointmentsToShow = ref(7);
const canLoadMore = computed(() => {
return numAppointmentsToShow.value < appointments.value.length;
diff --git a/client/src/pages/cockpit/FeedbackPage.vue b/client/src/pages/cockpit/FeedbackPage.vue
index 69bdaf47..0a5c8384 100644
--- a/client/src/pages/cockpit/FeedbackPage.vue
+++ b/client/src/pages/cockpit/FeedbackPage.vue
@@ -18,70 +18,24 @@
{{ $t("feedback.feedbackPageInfo") }}
-
- -
-
-
-
-
-
-
+
+
diff --git a/client/src/pages/cockpit/FeedbackPageUK.vue b/client/src/pages/cockpit/FeedbackPageUK.vue
new file mode 100644
index 00000000..f9467de9
--- /dev/null
+++ b/client/src/pages/cockpit/FeedbackPageUK.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
diff --git a/client/src/pages/cockpit/FeedbackPageVV.vue b/client/src/pages/cockpit/FeedbackPageVV.vue
new file mode 100644
index 00000000..96ad8e98
--- /dev/null
+++ b/client/src/pages/cockpit/FeedbackPageVV.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
diff --git a/client/src/pages/cockpit/FeedbackResults.vue b/client/src/pages/cockpit/FeedbackResults.vue
new file mode 100644
index 00000000..29475fc1
--- /dev/null
+++ b/client/src/pages/cockpit/FeedbackResults.vue
@@ -0,0 +1,77 @@
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/cockpit/cockpitPage/SubmissionsOverview.vue b/client/src/pages/cockpit/cockpitPage/SubmissionsOverview.vue
index a13ff616..234b49f1 100644
--- a/client/src/pages/cockpit/cockpitPage/SubmissionsOverview.vue
+++ b/client/src/pages/cockpit/cockpitPage/SubmissionsOverview.vue
@@ -52,7 +52,8 @@ const submittables = computed(() => {
const learningContents = circleFlatLearningContents(circle).filter(
(lc) =>
lc.content_type === "learnpath.LearningContentAssignment" ||
- lc.content_type === "learnpath.LearningContentFeedback" ||
+ lc.content_type === "learnpath.LearningContentFeedbackUK" ||
+ lc.content_type === "learnpath.LearningContentFeedbackVV" ||
lc.content_type === "learnpath.LearningContentEdoniqTest"
);
@@ -72,7 +73,10 @@ const submittables = computed(() => {
});
const isFeedback = (lc: LearningContent) => {
- return lc.content_type === "learnpath.LearningContentFeedback";
+ return (
+ lc.content_type === "learnpath.LearningContentFeedbackUK" ||
+ lc.content_type === "learnpath.LearningContentFeedbackVV"
+ );
};
const isAssignment = (lc: LearningContent) => {
diff --git a/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue b/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue
index 8b5cd193..012f966f 100644
--- a/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue
+++ b/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue
@@ -14,12 +14,13 @@ import type { Component } from "vue";
import { computed, onUnmounted } from "vue";
import AssignmentBlock from "./blocks/AssignmentBlock.vue";
import AttendanceCourseBlock from "./blocks/AttendanceCourseBlock.vue";
-import FeedbackBlock from "./feedback/FeedbackBlock.vue";
import IframeBlock from "./blocks/IframeBlock.vue";
import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue";
import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
import RichTextBlock from "./blocks/RichTextBlock.vue";
import VideoBlock from "./blocks/VideoBlock.vue";
+import FeedbackBlockUK from "./feedback/FeedbackBlockUK.vue";
+import FeedbackBlockVV from "./feedback/FeedbackBlockVV.vue";
import { getPreviousRoute } from "@/router/history";
import { stringifyParse } from "@/utils/utils";
import { useCourseDataWithCompletion } from "@/composables";
@@ -42,7 +43,8 @@ const COMPONENTS: Record = {
"learnpath.LearningContentAssignment": AssignmentBlock,
"learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock,
"learnpath.LearningContentDocumentList": DocumentListBlock,
- "learnpath.LearningContentFeedback": FeedbackBlock,
+ "learnpath.LearningContentFeedbackUK": FeedbackBlockUK,
+ "learnpath.LearningContentFeedbackVV": FeedbackBlockVV,
"learnpath.LearningContentLearningModule": IframeBlock,
"learnpath.LearningContentKnowledgeAssessment": IframeBlock,
"learnpath.LearningContentMediaLibrary": MediaLibraryBlock,
diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue
index 60f0c059..01b7ad51 100644
--- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue
+++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionView.vue
@@ -104,6 +104,14 @@ const onEditTask = (task: AssignmentTask) => {
emit("editTask", task);
};
+const openSolutionSample = () => {
+ const url = props.assignment.solution_sample?.url ?? "";
+
+ if (props.assignment.solution_sample) {
+ window.open(url, "_blank");
+ }
+};
+
const onSubmit = async () => {
try {
await upsertAssignmentCompletionMutation.executeMutation({
@@ -119,14 +127,17 @@ const onSubmit = async () => {
bustItGetCache(
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
);
- eventBus.emit("finishedLearningContent", true);
+ // if solution sample is available, do not close the assigment automatically
+ if (!props.assignment.solution_sample) {
+ eventBus.emit("finishedLearningContent", true);
+ }
} catch (error) {
log.error("Could not submit assignment", error);
}
};
-
+
{{ $t("assignment.submitAssignment") }}
@@ -202,6 +213,26 @@ const onSubmit = async () => {
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
}}
+
+
+ {{ $t("assignment.submissionShowSampleSolutionText") }}
+
+
+
+ {{ $t("assignment.submissionShowSampleSolution") }}
+
+
-import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
-import ItTextarea from "@/components/ui/ItTextarea.vue";
import { graphql } from "@/gql";
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
-import {
- PERCENTAGES,
- RATINGS,
- YES_NO,
-} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
-import type { LearningContentFeedback } from "@/types";
+import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
import { useMutation } from "@urql/vue";
import { useRouteQuery } from "@vueuse/router";
import log from "loglevel";
import { computed, onMounted, reactive, ref } from "vue";
-import { useTranslation } from "i18next-vue";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
+import { bustItGetCache } from "@/fetchHelpers";
+import { useUserStore } from "@/stores/user";
const props = defineProps<{
- content: LearningContentFeedback;
+ content: LearningContentFeedbackVV | LearningContentFeedbackUK;
+ stepLabels: string[];
+ questionData: any[];
+ introduction: string;
+ title: string;
+ completionTitle: string;
+ completionDescription: string;
+ showAvatar: boolean;
}>();
const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery();
-const { t } = useTranslation();
-
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
-const title = computed(
- () => `«${props.content.circle?.title}»: ${t("feedback.areYouSatisfied")}`
-);
+const title = computed(() => `«${props.content.circle?.title}»: ${props.title}`);
const circleExperts = computed(() => {
if (props.content?.circle?.slug) {
@@ -38,34 +35,29 @@ const circleExperts = computed(() => {
return [];
});
-const stepLabels = [
- t("general.introduction"),
- t("feedback.satisfactionLabel"),
- t("feedback.goalAttainmentLabel"),
- t("feedback.proficiencyLabel"),
- t("feedback.preparationTaskClarityLabel"),
- t("feedback.instructorCompetenceLabel"),
- t("feedback.instructorRespectLabel"),
- t("feedback.instructorOpenFeedbackLabel"),
- t("feedback.recommendLabel"),
- t("feedback.coursePositiveFeedbackLabel"),
- t("feedback.courseNegativeFeedbackLabel"),
- t("general.submission"),
-];
+const localStepLabels = ref(props.stepLabels);
+const localQuestionData = ref(props.questionData);
+const feedbackData: FeedbackData = reactive(feedbackDataFactory());
-const numSteps = stepLabels.length;
+const numSteps = computed(() => localStepLabels.value.length);
+const textQuestionKeys = computed(() =>
+ props.questionData.filter((item) => isTextNode(item)).map((item) => item.modelKey)
+);
+const avatarUrl = computed(() => circleExperts.value[0]?.avatar_url);
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
const sendFeedbackMutation = graphql(`
mutation SendFeedbackMutation(
$courseSessionId: ID!
$learningContentId: ID!
+ $learningContentType: String!
$data: GenericScalar!
$submitted: Boolean
) {
send_feedback(
course_session_id: $courseSessionId
learning_content_page_id: $learningContentId
+ learning_content_type: $learningContentType
data: $data
submitted: $submitted
) {
@@ -90,69 +82,6 @@ interface FeedbackData {
[key: string]: number | string | null;
}
-const feedbackData: FeedbackData = reactive({
- satisfaction: null,
- goal_attainment: null,
- proficiency: null,
- preparation_task_clarity: null,
- instructor_competence: null,
- instructor_respect: null,
- instructor_open_feedback: "",
- would_recommend: null,
- course_positive_feedback: "",
- course_negative_feedback: "",
-});
-
-const questionData = [
- {
- modelKey: "satisfaction",
- items: RATINGS,
- component: ItRadioGroup,
- },
- {
- modelKey: "goal_attainment",
- items: RATINGS,
- component: ItRadioGroup,
- },
- {
- modelKey: "proficiency",
- items: PERCENTAGES,
- component: ItRadioGroup,
- },
- {
- modelKey: "preparation_task_clarity",
- items: YES_NO,
- component: ItRadioGroup,
- },
- {
- modelKey: "instructor_competence",
- items: RATINGS,
- component: ItRadioGroup,
- },
- {
- modelKey: "instructor_respect",
- items: RATINGS,
- component: ItRadioGroup,
- },
- {
- modelKey: "instructor_open_feedback",
- component: ItTextarea,
- },
- {
- modelKey: "would_recommend",
- items: YES_NO,
- component: ItRadioGroup,
- },
- {
- modelKey: "course_positive_feedback",
- component: ItTextarea,
- },
- {
- modelKey: "course_negative_feedback",
- component: ItTextarea,
- },
-];
-
const previousStep = () => {
if (stepNo.value > 0) {
stepNo.value -= 1;
@@ -160,23 +89,17 @@ const previousStep = () => {
};
const nextStep = () => {
- if (stepNo.value < numSteps && hasStepValidInput(stepNo.value)) {
+ if (stepNo.value < numSteps.value && hasStepValidInput(stepNo.value)) {
stepNo.value += 1;
}
- log.debug(`next step ${stepNo.value} of ${numSteps}`);
+ log.debug(`next step ${stepNo.value} of ${numSteps.value}`);
mutateFeedback(feedbackData);
};
function hasStepValidInput(stepNumber: number) {
- const question = questionData[stepNumber - 1];
+ const question = localQuestionData.value[stepNumber - 1];
if (question) {
- if (
- [
- "instructor_open_feedback",
- "course_negative_feedback",
- "course_positive_feedback",
- ].includes(question.modelKey)
- ) {
+ if (textQuestionKeys.value.includes(question.modelKey)) {
// text response questions need to have a "truthy" value (not "" or null)
return feedbackData[question.modelKey];
} else {
@@ -192,6 +115,7 @@ function mutateFeedback(data: FeedbackData, submit = false) {
return executeMutation({
courseSessionId: courseSession.value.id,
learningContentId: props.content.id,
+ learningContentType: props.content.content_type,
data: data,
submitted: submit,
})
@@ -199,29 +123,40 @@ function mutateFeedback(data: FeedbackData, submit = false) {
log.debug("feedback mutation result", result);
if (result.data?.send_feedback?.feedback_response?.data) {
const responseData = result.data.send_feedback.feedback_response.data;
- if (!responseData.instructor_open_feedback) {
- responseData.instructor_open_feedback = "";
- }
- if (!responseData.course_negative_feedback) {
- responseData.course_negative_feedback = "";
- }
- if (!responseData.course_positive_feedback) {
- responseData.course_positive_feedback = "";
- }
+ textQuestionKeys.value.map((key) => {
+ if (!responseData[key]) {
+ responseData[key] = "";
+ }
+ });
Object.assign(feedbackData, responseData);
log.debug("feedback data", feedbackData);
feedbackSubmitted.value =
result.data?.send_feedback?.feedback_response?.submitted || false;
}
+ bustItGetCache(
+ `/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
+ );
})
.catch((e) => log.error(e));
}
+function feedbackDataFactory() {
+ const data: FeedbackData = {};
+ localQuestionData.value.map((item) => {
+ data[item.modelKey] = isTextNode(item) ? "" : null;
+ });
+ return data;
+}
+
+function isTextNode(item: any) {
+ return item.component.props.placeholder;
+}
+
onMounted(async () => {
log.debug("Feedback mounted");
await mutateFeedback({});
if (feedbackSubmitted.value) {
- stepNo.value = numSteps - 1;
+ stepNo.value = numSteps.value - 1;
}
});
@@ -246,22 +181,22 @@ onMounted(async () => {
@next="nextStep()"
>
-
- {{
- $t("feedback.intro", {
- name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
- })
- }}
+
+ {{ introduction }}
-
- {{ stepLabels[stepNo] }}
+
+ {{ localStepLabels[stepNo] }}
{
diff --git a/client/src/pages/learningPath/learningContentPage/feedback/FeedbackBlockUK.vue b/client/src/pages/learningPath/learningContentPage/feedback/FeedbackBlockUK.vue
new file mode 100644
index 00000000..0c0f00e8
--- /dev/null
+++ b/client/src/pages/learningPath/learningContentPage/feedback/FeedbackBlockUK.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
diff --git a/client/src/pages/learningPath/learningContentPage/feedback/FeedbackBlockVV.vue b/client/src/pages/learningPath/learningContentPage/feedback/FeedbackBlockVV.vue
new file mode 100644
index 00000000..d64a3b1c
--- /dev/null
+++ b/client/src/pages/learningPath/learningContentPage/feedback/FeedbackBlockVV.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
diff --git a/client/src/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue b/client/src/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue
index 95d3cf1e..84152f21 100644
--- a/client/src/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue
+++ b/client/src/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue
@@ -4,7 +4,11 @@
-
![]()
+
{{ title }}
{{ description }}
@@ -33,6 +37,7 @@ interface Props {
title?: string;
description?: string;
feedbackSent?: boolean;
+ showAvatar?: boolean;
}
withDefaults(defineProps
(), {
@@ -40,6 +45,7 @@ withDefaults(defineProps(), {
title: "",
description: "",
feedbackSent: false,
+ showAvatar: true,
});
defineEmits(["sendFeedback"]);
diff --git a/client/src/types.ts b/client/src/types.ts
index fd22e391..6a50bc8a 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -11,7 +11,8 @@ import type {
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
- LearningContentFeedbackObjectType,
+ LearningContentFeedbackUkObjectType,
+ LearningContentFeedbackVvObjectType,
LearningContentKnowledgeAssessmentObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
@@ -68,8 +69,12 @@ export type LearningContentEdoniqTest = LearningContentEdoniqTestObjectType & {
readonly content_type: "learnpath.LearningContentEdoniqTest";
};
-export type LearningContentFeedback = LearningContentFeedbackObjectType & {
- readonly content_type: "learnpath.LearningContentFeedback";
+export type LearningContentFeedbackVV = LearningContentFeedbackVvObjectType & {
+ readonly content_type: "learnpath.LearningContentFeedbackVV";
+};
+
+export type LearningContentFeedbackUK = LearningContentFeedbackUkObjectType & {
+ readonly content_type: "learnpath.LearningContentFeedbackUK";
};
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
@@ -102,7 +107,8 @@ export type LearningContent =
| LearningContentAttendanceCourse
| LearningContentDocumentList
| LearningContentEdoniqTest
- | LearningContentFeedback
+ | LearningContentFeedbackUK
+ | LearningContentFeedbackVV
| LearningContentLearningModule
| LearningContentKnowledgeAssessment
| LearningContentMediaLibrary
@@ -560,3 +566,13 @@ export type DueDate = SimpleDueDate & {
course_session_id: string;
circle: CircleLight | null;
};
+
+export type FeedbackType = "uk" | "vv";
+
+export interface FeedbackData {
+ amount: number;
+ questions: {
+ [key: string]: any;
+ };
+ feedbackType: FeedbackType;
+}
diff --git a/client/src/utils/typeMaps.ts b/client/src/utils/typeMaps.ts
index b0858fe7..618353fe 100644
--- a/client/src/utils/typeMaps.ts
+++ b/client/src/utils/typeMaps.ts
@@ -46,7 +46,8 @@ export function learningContentTypeData(
return { title: t("learningContentTypes.test"), icon: "it-icon-lc-test" };
case "learnpath.LearningContentRichText":
return { title: t("learningContentTypes.text"), icon: "it-icon-lc-resource" };
- case "learnpath.LearningContentFeedback":
+ case "learnpath.LearningContentFeedbackUK":
+ case "learnpath.LearningContentFeedbackVV":
return { title: t("learningContentTypes.feedback"), icon: "it-icon-lc-feedback" };
case "learnpath.LearningContentPlaceholder":
return {
diff --git a/cypress/e2e/assignment/assignmentStudent.cy.js b/cypress/e2e/assignment/assignmentStudent.cy.js
index 15e9d581..d81afaa8 100644
--- a/cypress/e2e/assignment/assignmentStudent.cy.js
+++ b/cypress/e2e/assignment/assignmentStudent.cy.js
@@ -1,8 +1,6 @@
import { TEST_STUDENT1_USER_ID } from "../../consts";
import { login } from "../helpers";
-// Daniel: without this comment, my tool will reformat the login import out...
-
function completePraxisAssignment(selectExpert = false) {
cy.visit("/course/test-lehrgang/learn/reisen/mein-kundenstamm");
cy.learningContentMultiLayoutNextStep();
@@ -326,16 +324,23 @@ describe("assignmentStudent.cy.js", () => {
cy.get('[data-cy="submit-assignment"]').click();
cy.get('[data-cy="success-text"]').should("exist");
- // app goes back to circle view -> check if assignment is marked as completed
- cy.url().should((url) => {
- expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
- });
- cy.reload();
+ cy.get('[data-cy="confirm-container"]')
+ .find('[data-cy="show-sample-solution"]')
+ .then(($elements) => {
+ if ($elements.length > 0) {
+ // Ist die Musterlösung da?
+ cy.get('[data-cy="show-sample-solution"]').should("exist");
+ cy.get('[data-cy="show-sample-solution-button"]').should("exist");
+ }
+ });
+
+ cy.visit("/course/test-lehrgang/learn/fahrzeug/");
+
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
).should("have.class", "cy-checked");
- // reopening page should get directly to last step
+ //reopening page should get directly to last step
cy.visit(
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
);
diff --git a/cypress/e2e/assignment/assignmentTrainer.cy.js b/cypress/e2e/assignment/assignmentTrainer.cy.js
index 60f3fc75..5902ff3d 100644
--- a/cypress/e2e/assignment/assignmentTrainer.cy.js
+++ b/cypress/e2e/assignment/assignmentTrainer.cy.js
@@ -1,4 +1,4 @@
-import { TEST_STUDENT1_USER_ID } from "../../consts";
+import { TEST_TRAINER1_USER_ID } from "../../consts";
import { login } from "../helpers";
describe("assignmentTrainer.cy.js", () => {
@@ -85,8 +85,8 @@ describe("assignmentTrainer.cy.js", () => {
cy.wait(500);
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
- "assignment_user_id",
- TEST_STUDENT1_USER_ID
+ "evaluation_user_id",
+ TEST_TRAINER1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut");
@@ -187,8 +187,8 @@ describe("assignmentTrainer.cy.js", () => {
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
- "assignment_user_id",
- TEST_STUDENT1_USER_ID
+ "evaluation_user_id",
+ TEST_TRAINER1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
expect(ac.evaluation_points).to.equal(17);
@@ -237,8 +237,8 @@ describe("assignmentTrainer.cy.js", () => {
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
- "assignment_user_id",
- TEST_STUDENT1_USER_ID
+ "evaluation_user_id",
+ TEST_TRAINER1_USER_ID
).then((ac) => {
console.log(ac.completion_status);
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
@@ -323,8 +323,8 @@ describe("assignmentTrainer.cy.js", () => {
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
- "assignment_user_id",
- TEST_STUDENT1_USER_ID
+ "evaluation_user_id",
+ TEST_TRAINER1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
expect(ac.evaluation_max_points).to.equal(0);
diff --git a/cypress/e2e/dashboard/dashboardSupervisor.cy.js b/cypress/e2e/dashboard/dashboardSupervisor.cy.js
index e243f3aa..aca8b793 100644
--- a/cypress/e2e/dashboard/dashboardSupervisor.cy.js
+++ b/cypress/e2e/dashboard/dashboardSupervisor.cy.js
@@ -74,7 +74,7 @@ describe("dashboardSupervisor.cy.js", () => {
describe("feedback summary box", () => {
it("contains correct numbers", () => {
getDashboardStatistics("feedback.average").should("have.text", "3.3");
- getDashboardStatistics("feedback.count").should("have.text", "3");
+ getDashboardStatistics("feedback.count").should("have.text", "6");
});
it("contains correct details link", () => {
clickOnDetailsLink("feedback");
diff --git a/cypress/e2e/feedback/feedbackStudent.cy.js b/cypress/e2e/feedback/feedbackStudent.cy.js
index 8c77ae26..1d9d5056 100644
--- a/cypress/e2e/feedback/feedbackStudent.cy.js
+++ b/cypress/e2e/feedback/feedbackStudent.cy.js
@@ -5,153 +5,359 @@ describe("feedbackStudent.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
login("test-student1@example.com", "test");
- cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
});
- it("can open feedback page", () => {
- cy.testLearningContentTitle("Kursfeedback");
- cy.testLearningContentSubtitle("Feedback");
+ describe("Feedback UK", () => {
+ beforeEach(() => {
+ cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
+ });
+
+ it("can open feedback page", () => {
+ cy.testLearningContentTitle("Kursfeedback");
+ cy.testLearningContentSubtitle("Feedback");
+ });
+
+ it("can create feedback by giving answers to all steps", () => {
+ // initial wait for step 0 (or none with step==0) is required for pipelines
+ cy.url().should((url) => {
+ expect(url).to.match(/\/fahrzeug\/feedback(\?step=0)?$/);
+ });
+
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // fill feedback form
+ // step 1
+ cy.url().should("include", "step=1");
+ cy.get('[data-cy="question-1"]').should(
+ "contain",
+ "Zufriedenheit insgesamt"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-4"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 2
+ cy.url().should("include", "step=2");
+ cy.get('[data-cy="question-2"]').should(
+ "contain",
+ "Zielerreichung insgesamt"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ // the system should store after every step -> check stored data
+ cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
+ (ac) => {
+ expect(ac.submitted).to.be.false;
+ expect(ac.data.satisfaction).to.equal(4);
+ expect(ac.data.instructor_competence).to.equal(null);
+ }
+ );
+ cy.get('[data-cy="radio-3"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 3
+ cy.url().should("include", "step=3");
+ cy.get('[data-cy="question-3"]').should(
+ "contain",
+ "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-80"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 4
+ cy.url().should("include", "step=4");
+ cy.get('[data-cy="question-4"]').should(
+ "contain",
+ "Waren die Vorbereitungsaufträge klar und verständlich?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-false"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 5
+ cy.url().should("include", "step=5");
+ cy.get('[data-cy="question-5"]').should(
+ "contain",
+ "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-2"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 6
+ cy.url().should("include", "step=6");
+ cy.get('[data-cy="question-6"]').should(
+ "contain",
+ "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-1"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 7
+ cy.url().should("include", "step=7");
+ cy.get('[data-cy="question-7"]').should(
+ "contain",
+ "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="it-textarea-instructor_open_feedback"]').type(
+ "Der Kursleiter ist eigentlich ganz nett."
+ );
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 8
+ cy.url().should("include", "step=8");
+ cy.get('[data-cy="question-8"]').should(
+ "contain",
+ "Würdest du den Kurs weiterempfehlen?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-true"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 9
+ cy.url().should("include", "step=9");
+ cy.get('[data-cy="question-9"]').should(
+ "contain",
+ "Was hat dir besonders gut gefallen?"
+ );
+
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
+ "Ich bin zufrieden mit den meisten Dingen."
+ );
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 10
+ cy.url().should("include", "step=10");
+ cy.get('[data-cy="question-10"]').should(
+ "contain",
+ "Wo siehst du Verbesserungspotential?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
+ "Ich bin unzufrieden mit einigen Sachen."
+ );
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ cy.url().should("include", "step=11");
+ cy.get('[data-cy="sendFeedbackButton"]').click();
+ cy.get('[data-cy="complete-and-continue"]').click({ force: true });
+
+ // marked complete in circle
+ cy.url().should((url) => {
+ expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
+ });
+ cy.reload();
+ cy.get(
+ '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-feedback-checkbox"]'
+ ).should("have.class", "cy-checked");
+
+ // reopening page should get directly to last step
+ cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
+ cy.url().should("include", "step=11");
+
+ // check stored data
+ cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
+ (ac) => {
+ expect(ac.submitted).to.be.true;
+ expect(ac.data).to.deep.equal({
+ course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
+ course_positive_feedback:
+ "Ich bin zufrieden mit den meisten Dingen.",
+ goal_attainment: 3,
+ instructor_competence: 2,
+ instructor_open_feedback:
+ "Der Kursleiter ist eigentlich ganz nett.",
+ instructor_respect: 1,
+ preparation_task_clarity: false,
+ proficiency: 80,
+ satisfaction: 4,
+ would_recommend: true,
+ feedback_type: "uk",
+ });
+ }
+ );
+ });
});
- it("can create feedback by giving answers to all steps", () => {
- // initial wait for step 0 (or none with step==0) is required for pipelines
- cy.url().should((url) => {
- expect(url).to.match(/\/fahrzeug\/feedback(\?step=0)?$/);
+ describe("Feedback VV", () => {
+ beforeEach(() => {
+ cy.visit("/course/test-lehrgang/learn/reisen/feedback");
});
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // fill feedback form
- // step 1
- cy.url().should("include", "step=1");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="radio-4"]').click();
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 2
- cy.url().should("include", "step=2");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- // the system should store after every step -> check stored data
- cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
- (ac) => {
- expect(ac.submitted).to.be.false;
- expect(ac.data.satisfaction).to.equal(4);
- expect(ac.data.instructor_competence).to.equal(null);
- }
- );
- cy.get('[data-cy="radio-3"]').click();
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 3
- cy.url().should("include", "step=3");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="radio-80"]').click();
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 4
- cy.url().should("include", "step=4");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="radio-false"]').click();
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 5
- cy.url().should("include", "step=5");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="radio-2"]').click();
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 6
- cy.url().should("include", "step=6");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="radio-1"]').click();
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 7
- cy.url().should("include", "step=7");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="it-textarea-instructor_open_feedback"]').type(
- "Der Kursleiter ist eigentlich ganz nett."
- );
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 8
- cy.url().should("include", "step=8");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="radio-true"]').click();
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 9
- cy.url().should("include", "step=9");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
- "Ich bin zufrieden mit den meisten Dingen."
- );
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- // step 10
- cy.url().should("include", "step=10");
- cy.get('[data-cy="next-step"]').should("be.disabled");
- cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
- "Ich bin unzufrieden mit einigen Sachen."
- );
- cy.wait(200);
- cy.learningContentMultiLayoutNextStep();
- cy.wait(200);
-
- cy.url().should("include", "step=11");
- cy.get('[data-cy="sendFeedbackButton"]').click();
- cy.get('[data-cy="complete-and-continue"]').click({ force: true });
-
- // marked complete in circle
- cy.url().should((url) => {
- expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
+ it("can open feedback page", () => {
+ cy.testLearningContentTitle("Feedback");
+ cy.testLearningContentSubtitle("Feedback");
});
- cy.reload();
- cy.get(
- '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-feedback-checkbox"]'
- ).should("have.class", "cy-checked");
- // reopening page should get directly to last step
- cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
- cy.url().should("include", "step=11");
+ it("can create feedback by giving answers to all steps", () => {
+ // initial wait for step 0 (or none with step==0) is required for pipelines
+ cy.url().should((url) => {
+ expect(url).to.match(/\/reisen\/feedback(\?step=0)?$/);
+ });
+ cy.get('[data-cy="introduction"]').contains(
+ "Wir bitten dich um dein Feedback. Es hilft uns, damit wir deine Lernerlebnisse verbessern können."
+ );
- // check stored data
- cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
- (ac) => {
- expect(ac.submitted).to.be.true;
- expect(ac.data).to.deep.equal({
- course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
- course_positive_feedback: "Ich bin zufrieden mit den meisten Dingen.",
- goal_attainment: 3,
- instructor_competence: 2,
- instructor_open_feedback: "Der Kursleiter ist eigentlich ganz nett.",
- instructor_respect: 1,
- preparation_task_clarity: false,
- proficiency: 80,
- satisfaction: 4,
- would_recommend: true,
- });
- }
- );
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // fill feedback form
+ // step 1
+ cy.url().should("include", "step=1");
+ cy.get('[data-cy="question-1"]').should(
+ "contain",
+ "Zufriedenheit insgesamt"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-4"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 2
+ cy.url().should("include", "step=2");
+ cy.get('[data-cy="question-2"]').should(
+ "contain",
+ "Zielerreichung insgesamt"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ // the system should store after every step -> check stored data
+ cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
+ (ac) => {
+ expect(ac.submitted).to.be.false;
+ expect(ac.data.satisfaction).to.equal(4);
+ expect(ac.data.course_positive_feedback).to.equal(null);
+ }
+ );
+ cy.get('[data-cy="radio-3"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 3
+ cy.url().should("include", "step=3");
+ cy.get('[data-cy="question-3"]').should(
+ "contain",
+ "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-80"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 4
+ cy.url().should("include", "step=4");
+ cy.get('[data-cy="question-4"]').should(
+ "contain",
+ "Waren die Praxisaufträge klar und verständlich?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-false"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 5
+ cy.url().should("include", "step=5");
+ cy.get('[data-cy="question-5"]').should(
+ "contain",
+ "Würdest du den Circle weiterempfehlen?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="radio-false"]').click();
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 6
+ cy.url().should("include", "step=6");
+ cy.get('[data-cy="question-6"]').should(
+ "contain",
+ "Was hat dir besonders gut gefallen?"
+ );
+
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
+ "Der Circle ist eigentlich ganz nett."
+ );
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ // step 7
+ cy.url().should("include", "step=7");
+ cy.get('[data-cy="question-7"]').should(
+ "contain",
+ "Wo siehst du Verbesserungspotential?"
+ );
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+ cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
+ "Ich bin unzufrieden mit einigen Sachen."
+ );
+ cy.wait(200);
+ cy.learningContentMultiLayoutNextStep();
+ cy.wait(200);
+
+ cy.url().should("include", "step=8");
+ cy.get('[data-cy="sendFeedbackButton"]').click();
+ cy.get('[data-cy="complete-and-continue"]').click({ force: true });
+
+ // marked complete in circle
+ cy.url().should((url) => {
+ expect(url).to.match(/\/reisen#lu-transfer-reflexion-feedback?$/);
+ });
+ cy.reload();
+ cy.get(
+ '[data-cy="test-lehrgang-lp-circle-reisen-lc-feedback-checkbox"]'
+ ).should("have.class", "cy-checked");
+
+ // reopening page should get directly to last step
+ cy.visit("/course/test-lehrgang/learn/reisen/feedback");
+ cy.url().should("include", "step=8");
+
+ // check stored data
+ cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
+ (ac) => {
+ expect(ac.submitted).to.be.true;
+ expect(ac.data).to.deep.equal({
+ course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
+ course_positive_feedback: "Der Circle ist eigentlich ganz nett.",
+ goal_attainment: 3,
+ preparation_task_clarity: false,
+ proficiency: 80,
+ satisfaction: 4,
+ would_recommend: false,
+ feedback_type: "vv",
+ });
+ }
+ );
+ });
});
});
diff --git a/cypress/e2e/feedback/feedbackTrainer.cy.js b/cypress/e2e/feedback/feedbackTrainer.cy.js
index f2b26973..b410dd3e 100644
--- a/cypress/e2e/feedback/feedbackTrainer.cy.js
+++ b/cypress/e2e/feedback/feedbackTrainer.cy.js
@@ -16,77 +16,216 @@ describe("feedbackTrainer.cy.js", () => {
cy.get('[data-cy="feedback-data-amount"]').should("contain", "0");
});
- it("can open feedback results page with results", () => {
- cy.manageCommand("cypress_reset --create-feedback-responses");
- login("test-trainer1@example.com", "test");
- cy.visit("/course/test-lehrgang/cockpit");
- cy.get(
- '[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
- ).click();
+ describe("FeedbackUK", function () {
+ it("can open feedback results page with results", () => {
+ cy.manageCommand("cypress_reset --create-feedback-responses");
+ login("test-trainer1@example.com", "test");
+ cy.visit("/course/test-lehrgang/cockpit");
+ cy.get(
+ '[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
+ ).click();
- cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
+ cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
- cy.get('[data-cy="question-1"]')
- .find('[data-cy="rating-scale-average"]')
- .should("contain", "3.3");
+ // check titles of questions
+ cy.get('[data-cy="question-1"]').should(
+ "contain",
+ "Zufriedenheit insgesamt"
+ );
+ cy.get('[data-cy="question-2"]').should(
+ "contain",
+ "Zielerreichung insgesamt"
+ );
+ cy.get('[data-cy="question-3"]').should(
+ "contain",
+ "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
+ );
+ cy.get('[data-cy="question-4"]').should(
+ "contain",
+ "Waren die Vorbereitungsaufträge klar und verständlich?"
+ );
+ cy.get('[data-cy="question-5"]').should(
+ "contain",
+ "Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
+ );
+ cy.get('[data-cy="question-6"]').should(
+ "contain",
+ "Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
+ );
+ cy.get('[data-cy="question-7"]').should(
+ "contain",
+ "Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
+ );
+ cy.get('[data-cy="question-8"]').should(
+ "contain",
+ "Würdest du den Kurs weiterempfehlen?"
+ );
+ cy.get('[data-cy="question-9"]').should(
+ "contain",
+ "Wo siehst du Verbesserungspotential?"
+ );
+ cy.get('[data-cy="question-10"]').should(
+ "contain",
+ "Was hat dir besonders gut gefallen?"
+ );
- cy.get('[data-cy="question-2"]')
- .find('[data-cy="rating-scale-average"]')
- .should("contain", "3.0");
+ cy.get('[data-cy="question-1"]')
+ .find('[data-cy="rating-scale-average"]')
+ .should("contain", "3.3");
- cy.get('[data-cy="question-3"]')
- .find('[data-cy="percentage-value-40%"]')
- .should("contain", "33.3");
- cy.get('[data-cy="question-3"]')
- .find('[data-cy="percentage-value-80%"]')
- .should("contain", "33.3");
- cy.get('[data-cy="question-3"]')
- .find('[data-cy="percentage-value-100%"]')
- .should("contain", "33.3");
+ cy.get('[data-cy="question-2"]')
+ .find('[data-cy="rating-scale-average"]')
+ .should("contain", "3.0");
- cy.get('[data-cy="question-4"]')
- .find('[data-cy="popover-yes"]')
- .click()
- .find('[data-cy="num-yes"]')
- .should("contain", "3");
- cy.get('[data-cy="question-4"]')
- .find('[data-cy="popover-no"]')
- .click()
- .find('[data-cy="num-no"]')
- .should("contain", "0");
+ cy.get('[data-cy="question-3"]')
+ .find('[data-cy="percentage-value-40%"]')
+ .should("contain", "33.3");
+ cy.get('[data-cy="question-3"]')
+ .find('[data-cy="percentage-value-80%"]')
+ .should("contain", "33.3");
+ cy.get('[data-cy="question-3"]')
+ .find('[data-cy="percentage-value-100%"]')
+ .should("contain", "33.3");
- cy.get('[data-cy="question-5"]')
- .find('[data-cy="rating-scale-average"]')
- .should("contain", "2.7");
+ cy.get('[data-cy="question-4"]')
+ .find('[data-cy="popover-yes"]')
+ .click()
+ .find('[data-cy="num-yes"]')
+ .should("contain", "3");
+ cy.get('[data-cy="question-4"]')
+ .find('[data-cy="popover-no"]')
+ .click()
+ .find('[data-cy="num-no"]')
+ .should("contain", "0");
- cy.get('[data-cy="question-6"]')
- .find('[data-cy="rating-scale-average"]')
- .should("contain", "3.0");
+ cy.get('[data-cy="question-5"]')
+ .find('[data-cy="rating-scale-average"]')
+ .should("contain", "2.7");
- cy.get('[data-cy="question-7"]')
- .should("contain", "Super Kurs!")
- .should("contain", "Super, bin begeistert")
- .should("contain", "Ok, entspricht den Erwartungen");
+ cy.get('[data-cy="question-6"]')
+ .find('[data-cy="rating-scale-average"]')
+ .should("contain", "3.0");
- cy.get('[data-cy="question-8"]')
- .find('[data-cy="popover-yes"]')
- .click()
- .find('[data-cy="num-yes"]')
- .should("contain", "2");
- cy.get('[data-cy="question-8"]')
- .find('[data-cy="popover-no"]')
- .click()
- .find('[data-cy="num-no"]')
- .should("contain", "1");
+ cy.get('[data-cy="question-7"]')
+ .should("contain", "Super Kurs!")
+ .should("contain", "Super, bin begeistert")
+ .should("contain", "Ok, entspricht den Erwartungen");
- cy.get('[data-cy="question-9"]')
- .should("contain", "Nichts Schlechtes")
- .should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
- .should("contain", "Mehr Videos wären schön.");
+ cy.get('[data-cy="question-8"]')
+ .find('[data-cy="popover-yes"]')
+ .click()
+ .find('[data-cy="num-yes"]')
+ .should("contain", "2");
+ cy.get('[data-cy="question-8"]')
+ .find('[data-cy="popover-no"]')
+ .click()
+ .find('[data-cy="num-no"]')
+ .should("contain", "1");
- cy.get('[data-cy="question-10"]')
- .should("contain", "Nur Gutes.")
- .should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
- .should("contain", "Die Präsentation war super");
+ cy.get('[data-cy="question-9"]')
+ .should("contain", "Nichts Schlechtes")
+ .should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
+ .should("contain", "Mehr Videos wären schön.");
+
+ cy.get('[data-cy="question-10"]')
+ .should("contain", "Nur Gutes.")
+ .should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
+ .should("contain", "Die Präsentation war super");
+ });
+ });
+
+ describe("FeedbackVV", function () {
+ it("can open feedback results page with results", () => {
+ cy.manageCommand("cypress_reset --create-feedback-responses");
+ login("test-trainer1@example.com", "test");
+ cy.visit("/course/test-lehrgang/cockpit");
+ cy.get('[data-cy="dropdown-select"]').click();
+ cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
+ cy.get(
+ '[data-cy="show-feedback-btn-test-lehrgang-lp-circle-reisen-lc-feedback"]'
+ ).click();
+
+ cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
+
+ // check titles of questions
+ cy.get('[data-cy="question-1"]').should(
+ "contain",
+ "Zufriedenheit insgesamt"
+ );
+ cy.get('[data-cy="question-2"]').should(
+ "contain",
+ "Zielerreichung insgesamt"
+ );
+ cy.get('[data-cy="question-3"]').should(
+ "contain",
+ "Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
+ );
+ cy.get('[data-cy="question-4"]').should(
+ "contain",
+ "Waren die Praxisaufträge klar und verständlich?"
+ );
+ cy.get('[data-cy="question-5"]').should(
+ "contain",
+ "Würdest du den Circle weiterempfehlen?"
+ );
+ cy.get('[data-cy="question-6"]').should(
+ "contain",
+ "Wo siehst du Verbesserungspotential?"
+ );
+ cy.get('[data-cy="question-7"]').should(
+ "contain",
+ "Was hat dir besonders gut gefallen?"
+ );
+
+ cy.get('[data-cy="question-1"]')
+ .find('[data-cy="rating-scale-average"]')
+ .should("contain", "3.3");
+
+ cy.get('[data-cy="question-2"]')
+ .find('[data-cy="rating-scale-average"]')
+ .should("contain", "3.0");
+
+ cy.get('[data-cy="question-3"]')
+ .find('[data-cy="percentage-value-40%"]')
+ .should("contain", "33.3");
+ cy.get('[data-cy="question-3"]')
+ .find('[data-cy="percentage-value-80%"]')
+ .should("contain", "33.3");
+ cy.get('[data-cy="question-3"]')
+ .find('[data-cy="percentage-value-100%"]')
+ .should("contain", "33.3");
+
+ cy.get('[data-cy="question-4"]')
+ .find('[data-cy="popover-yes"]')
+ .click()
+ .find('[data-cy="num-yes"]')
+ .should("contain", "3");
+ cy.get('[data-cy="question-4"]')
+ .find('[data-cy="popover-no"]')
+ .click()
+ .find('[data-cy="num-no"]')
+ .should("contain", "0");
+
+ cy.get('[data-cy="question-5"]')
+ .find('[data-cy="popover-yes"]')
+ .click()
+ .find('[data-cy="num-yes"]')
+ .should("contain", "2");
+ cy.get('[data-cy="question-5"]')
+ .find('[data-cy="popover-no"]')
+ .click()
+ .find('[data-cy="num-no"]')
+ .should("contain", "1");
+
+ cy.get('[data-cy="question-6"]')
+ .should("contain", "Nichts Schlechtes")
+ .should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
+ .should("contain", "Mehr Videos wären schön.");
+
+ cy.get('[data-cy="question-7"]')
+ .should("contain", "Nur Gutes.")
+ .should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
+ .should("contain", "Die Präsentation war super");
+ });
});
});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 0ba844c0..a7d20f73 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -61,6 +61,7 @@ Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
"/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
"/Users/eliabieri/iterativ/vbv_lernwelt/.direnv/python-3.10.6/bin",
"/Users/christiancueni/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
+ "/Users/renzo/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
];
let bashCommand = `PATH=${pythonPaths.join(":")}:$PATH && ${execCommand}`;
return cy
diff --git a/docs/assets/files-presign.png b/docs/assets/files-presign.png
new file mode 100644
index 00000000..fe648b52
Binary files /dev/null and b/docs/assets/files-presign.png differ
diff --git a/docs/media_files_handling.md b/docs/media_files_handling.md
new file mode 100644
index 00000000..40551bc9
--- /dev/null
+++ b/docs/media_files_handling.md
@@ -0,0 +1,73 @@
+# Files handling
+
+This document describes how files are handled in this appication.
+
+# Types of files
+
+static files: files that are not changed by the application, e.g. images, fonts, etc.¨
+
+### content documents:
+
+Files that belong to the content and are managed by the content editors in the CMS (pdf, excel, word, etc.)
+
+### user documents:
+
+Files that are uploaded by the users (pdf, etc.). Therefore not visible in the CMS.
+Images are handled seprately from documents since images require additional processing (resizing, cropping, etc.).
+Visible in the django admin.
+
+### content images:
+
+Images that belong to the content and are managed by the content editors in the CMS.
+
+### user images:
+
+Images that are uploaded by the users. Therefore not visible in the CMS. Visible in the django admin.
+
+## Static files
+
+These files are publicly served on S3.
+
+## Content documents
+
+These files are part of the content. Such as a pdf thas cointains additional information to a course.
+These files are not publicly available. The content files are uploaded by the editors in the wagtail cms.
+
+https://www.hacksoft.io/blog/direct-to-s3-file-upload-with-django
+
+Django handles the permissions to these files. Via a view django checks if the user has permissions to access the file,
+and gerates a temporary url that is valid for a limited time. Still the documents are served by django. This done for
+usability reasons. The user sees the url mydomain.com/media/documents/ and not a url to S3. Therefore the
+user can share the url with other users. (still they need to login and have the permissions to access the file)
+
+The downside of this is that the django server processes these files. (could be circumvented by django-sendfile).
+
+
+
+- These Files are handled stored as wagtail documents. As a model and the file itself is stored in S3.
+
+### Frontend access to content documents
+
+For the frontend django generates a fixed url per file /media/documents/
+
+When the frontend requests this file, django checks if the user has permissions to access the file.
+If so, django generates a temporary url that is valid for a limited time. Then sends a redirect to the frontend.
+
+In this waz the frontend does not need to know about the permissions. Content grapql can be cached if needed and urls
+can be shared by the users.
+
+content_documents
+user_documents
+
+public files
+
+## User documents
+
+- User uploaded files are stored in S3. but the permissions is handled by django. Same process as content files.
+
+Same process as content files. But the url is /media/user-uploads/
+And the files are not managed by Wagtail. Due to another model, they are not visible to the user in the CMS.
+
+## Content images
+
+Content Images are served directly from S3. The permissions are handled by dja
diff --git a/env_secrets/local_chrigu.env b/env_secrets/local_chrigu.env
index e4b54548..ddbca2d5 100644
Binary files a/env_secrets/local_chrigu.env and b/env_secrets/local_chrigu.env differ
diff --git a/env_secrets/local_daniel.env b/env_secrets/local_daniel.env
index 9a1c2df5..e42a71d1 100644
Binary files a/env_secrets/local_daniel.env and b/env_secrets/local_daniel.env differ
diff --git a/env_secrets/local_lorenz.env b/env_secrets/local_lorenz.env
index 231e4a04..e80c38a0 100644
Binary files a/env_secrets/local_lorenz.env and b/env_secrets/local_lorenz.env differ
diff --git a/scripts/count_queries.py b/scripts/count_queries.py
index 484787f1..19d78ab8 100644
--- a/scripts/count_queries.py
+++ b/scripts/count_queries.py
@@ -18,8 +18,7 @@ def main():
from django.conf import settings
settings.DEBUG = True
- from django.db import connection
- from django.db import reset_queries
+ from django.db import connection, reset_queries
reset_queries()
diff --git a/scripts/graphene_count_queries.py b/scripts/graphene_count_queries.py
index 94b85982..d05a61df 100644
--- a/scripts/graphene_count_queries.py
+++ b/scripts/graphene_count_queries.py
@@ -12,16 +12,15 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
django.setup()
-from vbv_lernwelt.core.schema import Query
from vbv_lernwelt.core.models import User
+from vbv_lernwelt.core.schema import Query
def main():
from django.conf import settings
settings.DEBUG = True
- from django.db import connection
- from django.db import reset_queries
+ from django.db import connection, reset_queries
reset_queries()
diff --git a/scripts/send_sendgrid_email.py b/scripts/send_sendgrid_email.py
index c3dcb501..f86e6ebc 100644
--- a/scripts/send_sendgrid_email.py
+++ b/scripts/send_sendgrid_email.py
@@ -10,12 +10,12 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
django.setup()
+from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.notify.email.email_services import (
+ create_template_data_from_course_session_attendance_course,
EmailTemplate,
send_email,
- create_template_data_from_course_session_attendance_course,
)
-from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
def main():
diff --git a/server/config/settings/base.py b/server/config/settings/base.py
index 8ead2757..10fb9f13 100644
--- a/server/config/settings/base.py
+++ b/server/config/settings/base.py
@@ -116,6 +116,7 @@ THIRD_PARTY_APPS = [
LOCAL_APPS = [
"vbv_lernwelt.core",
+ "vbv_lernwelt.media_files",
"vbv_lernwelt.sso",
"vbv_lernwelt.course",
"vbv_lernwelt.learnpath",
@@ -213,23 +214,13 @@ STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
-USE_AWS = env("USE_AWS", False)
-AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", "")
-AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", "")
-AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", "")
-AWS_S3_CUSTOM_DOMAIN = "%s.s3.amazonaws.com" % AWS_STORAGE_BUCKET_NAME
-
# MEDIA
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR / "media")
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
-if USE_AWS:
- # https://wagtail.org/blog/amazon-s3-for-media-files/
- MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
- DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
-else:
- MEDIA_URL = "/server/media/"
+
+MEDIA_URL = "/server/media/"
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
@@ -253,7 +244,19 @@ WAGTAIL_ENABLE_UPDATE_CHECK = False
WAGTAIL_ENABLE_WHATS_NEW_BANNER = False
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES
-WAGTAILDOCS_DOCUMENT_MODEL = "media_library.LibraryDocument"
+WAGTAILDOCS_DOCUMENT_MODEL = "media_files.ContentDocument"
+WAGTAILIMAGES_IMAGE_MODEL = "media_files.ContentImage"
+
+# this setting makes that the document is served by django, and the url is the django url.
+# https://docs.wagtail.org/en/stable/reference/settings.html#wagtaildocs-serve-method
+# The file is served by django as streaming response. If it should be serverd by nginx, then install django sendfile
+WAGTAILDOCS_SERVE_METHOD = "serve_view"
+# WAGTAILDOCS_INLINE_CONTENT_TYPES = ['application/pdf', 'text/plain']
+
+
+WAGTAILIMAGES_MAX_UPLOAD_SIZE = 20 * 1024 * 1024 # 20MB
+# WAGTAILIMAGES_RENDITION_STORAGE = 'myapp.backends.MyCustomStorage'
+
WAGTAILADMIN_RICH_TEXT_EDITORS = {
"default": {
@@ -646,7 +649,7 @@ NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
# S3 BUCKET CONFIGURATION
-FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="local") # local | s3
+FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="s3") # local | s3
if FILE_UPLOAD_STORAGE == "local":
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=5242880)
@@ -655,18 +658,19 @@ if FILE_UPLOAD_STORAGE == "s3":
# Using django-storages
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
-
- AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID")
+ AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID", default="AKIAZJLREPUVWNBTJ5VY")
AWS_S3_SECRET_ACCESS_KEY = env("AWS_S3_SECRET_ACCESS_KEY")
- AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
- AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME")
+ AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", "eu-central-1")
AWS_S3_SIGNATURE_VERSION = env("AWS_S3_SIGNATURE_VERSION", default="s3v4")
+ AWS_STORAGE_BUCKET_NAME = env(
+ "AWS_STORAGE_BUCKET_NAME", default="myvbv-dev.iterativ.ch"
+ )
+ AWS_S3_FILE_OVERWRITE = env("AWS_S3_FILE_OVERWRITE", False)
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=20971520) # 20MB
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="private")
-
- AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=300) # seconds
+ AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=7200) # seconds
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = (
"jpg",
diff --git a/server/config/settings/test.py b/server/config/settings/test.py
index a29a49ba..48de7cf6 100644
--- a/server/config/settings/test.py
+++ b/server/config/settings/test.py
@@ -1,6 +1,7 @@
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
import os
+
os.environ["IT_APP_ENVIRONMENT"] = "local"
from .base import * # noqa
@@ -8,6 +9,7 @@ from .base import * # noqa
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
TEST_RUNNER = "django.test.runner.DiscoverRunner"
+# Select faster password hasher during tests
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
@@ -15,16 +17,7 @@ PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
WHITENOISE_MANIFEST_STRICT = False
-
-# Dummy data
-AWS_S3_ACCESS_KEY_ID = "SOMEKEY"
-AWS_S3_SECRET_ACCESS_KEY = "SOMEACCESSKEY"
-AWS_STORAGE_BUCKET_NAME = "myvbv-dev.iterativ.ch"
-AWS_S3_REGION_NAME = "eu-central-1"
-AWS_S3_SIGNATURE_VERSION = "s3v4"
-FILE_MAX_SIZE = 20971520 # 20MB
-AWS_DEFAULT_ACL = "private"
-AWS_PRESIGNED_EXPIRY = 300
+AWS_S3_FILE_OVERWRITE = True
class DisableMigrations(dict):
@@ -36,8 +29,3 @@ class DisableMigrations(dict):
MIGRATION_MODULES = DisableMigrations()
-
-# Select faster password hasher during tests
-PASSWORD_HASHERS = [
- "django.contrib.auth.hashers.MD5PasswordHasher",
-]
diff --git a/server/config/settings/test_cypress.py b/server/config/settings/test_cypress.py
index a82cbe93..926fcea3 100644
--- a/server/config/settings/test_cypress.py
+++ b/server/config/settings/test_cypress.py
@@ -2,6 +2,10 @@
import os
os.environ["IT_APP_ENVIRONMENT"] = "local"
+os.environ["AWS_S3_SECRET_ACCESS_KEY"] = os.environ.get(
+ "AWS_S3_SECRET_ACCESS_KEY",
+ "!!!default_for_quieting_cypress_within_pycharm!!!",
+)
from .base import * # noqa
diff --git a/server/config/urls.py b/server/config/urls.py
index 59202975..10a3e932 100644
--- a/server/config/urls.py
+++ b/server/config/urls.py
@@ -57,7 +57,7 @@ from vbv_lernwelt.importer.views import (
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
+from wagtail.documents import urls as media_library_urls
class SignedIntConverter(IntConverter):
@@ -89,7 +89,7 @@ urlpatterns = [
# wagtail urls
path('server/cms/', include(wagtailadmin_urls)),
- path('server/documents/', include(wagtaildocs_urls)),
+ path('server/documents/', include(media_library_urls)),
path('server/pages/', include(wagtail_urls)),
# core
@@ -138,6 +138,7 @@ urlpatterns = [
name="request_assignment_completion_status"),
# documents
+ # TODO: remfactor to files app
path(r'api/core/document/start/', document_upload_start,
name='file_upload_start'),
path(r'api/core/document//', document_delete,
diff --git a/server/requirements/requirements-dev.in b/server/requirements/requirements-dev.in
index bba2128e..de6fc888 100644
--- a/server/requirements/requirements-dev.in
+++ b/server/requirements/requirements-dev.in
@@ -9,6 +9,7 @@ mypy # https://github.com/python/mypy
django-stubs # https://github.com/typeddjango/django-stubs
pytest # https://github.com/pytest-dev/pytest
pytest-sugar # https://github.com/Frozenball/pytest-sugar
+pytest-xdist #
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs
diff --git a/server/requirements/requirements-dev.txt b/server/requirements/requirements-dev.txt
index cfec2bd4..5cb8055c 100644
--- a/server/requirements/requirements-dev.txt
+++ b/server/requirements/requirements-dev.txt
@@ -38,9 +38,7 @@ azure-core==1.29.1
azure-identity==1.14.0
# via -r requirements.in
azure-storage-blob==12.17.0
- # via
- # -r requirements.in
- # django-storages
+ # via -r requirements.in
backcall==0.2.0
# via ipython
bcrypt==4.0.1
@@ -176,7 +174,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-stubs==4.2.3
# via
@@ -209,6 +207,8 @@ exceptiongroup==1.1.2
# via
# anyio
# pytest
+execnet==2.0.2
+ # via pytest-xdist
executing==1.2.0
# via stack-data
factory-boy==3.3.0
@@ -397,7 +397,9 @@ pyflakes==3.1.0
pygments==2.16.1
# via ipython
pyjwt[crypto]==2.8.0
- # via msal
+ # via
+ # msal
+ # pyjwt
pylint==2.17.5
# via
# pylint-django
@@ -415,10 +417,13 @@ pytest==7.4.0
# -r requirements-dev.in
# pytest-django
# pytest-sugar
+ # pytest-xdist
pytest-django==4.5.2
# via -r requirements-dev.in
pytest-sugar==0.9.7
# via -r requirements-dev.in
+pytest-xdist==3.5.0
+ # via -r requirements-dev.in
python-dateutil==2.8.2
# via
# -r requirements.in
@@ -616,7 +621,9 @@ wheel==0.41.1
whitenoise[brotli]==6.5.0
# via -r requirements.in
willow[heif]==1.6.1
- # via wagtail
+ # via
+ # wagtail
+ # willow
wrapt==1.15.0
# via astroid
diff --git a/server/run_tests.sh b/server/run_tests.sh
index 73c974e7..5893fe14 100755
--- a/server/run_tests.sh
+++ b/server/run_tests.sh
@@ -1,4 +1,6 @@
#!/bin/bash
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-pytest --junitxml=../test-reports/coverage.xml
+
+# limit test to 6 parallel processes, otherwise ratelimit of s3 could be hit
+pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml
diff --git a/server/run_tests_coverage.sh b/server/run_tests_coverage.sh
index d1d12c02..71a8ce0c 100755
--- a/server/run_tests_coverage.sh
+++ b/server/run_tests_coverage.sh
@@ -3,7 +3,7 @@
set -e
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-coverage run -m pytest --junitxml=../test-reports/coverage.xml $1
+coverage run -m pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml $1
coverage_python=`coverage report -m | tail -n1 | awk '{print $4}'`
commit=`git rev-parse HEAD`
diff --git a/server/vbv_lernwelt/assignment/creators/create_assignments.py b/server/vbv_lernwelt/assignment/creators/create_assignments.py
index 23b8ecb9..4b30c326 100644
--- a/server/vbv_lernwelt/assignment/creators/create_assignments.py
+++ b/server/vbv_lernwelt/assignment/creators/create_assignments.py
@@ -20,6 +20,7 @@ from vbv_lernwelt.course.consts import (
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
)
from vbv_lernwelt.course.models import CoursePage
+from vbv_lernwelt.media_files.models import ContentDocument
from wagtail.blocks import StreamValue
from wagtail.blocks.list_block import ListBlock, ListValue
from wagtail.rich_text import RichText
@@ -39,6 +40,7 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
needs_expert_evaluation=True,
competence_certificate=competence_certificate,
effort_required="ca. 5 Stunden",
+ solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
intro_text=replace_whitespace(
"""
Ausgangslage
diff --git a/server/vbv_lernwelt/assignment/graphql/types.py b/server/vbv_lernwelt/assignment/graphql/types.py
index c3c933e7..70efc5b6 100644
--- a/server/vbv_lernwelt/assignment/graphql/types.py
+++ b/server/vbv_lernwelt/assignment/graphql/types.py
@@ -9,6 +9,7 @@ from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.iam.permissions import can_evaluate_assignments, has_course_access
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
+from vbv_lernwelt.media_files.graphql.types import ContentDocumentObjectType
class AssignmentCompletionObjectType(DjangoObjectType):
@@ -52,6 +53,7 @@ class AssignmentObjectType(DjangoObjectType):
learning_content_page_id=graphene.ID(required=False),
assignment_user_id=graphene.UUID(required=False),
)
+ solution_sample = graphene.Field(ContentDocumentObjectType)
class Meta:
model = Assignment
@@ -67,6 +69,9 @@ class AssignmentObjectType(DjangoObjectType):
"competence_certificate",
)
+ def resolve_solution_sample(self, info):
+ return self.solution_sample
+
def resolve_max_points(self, info):
return self.get_max_points()
diff --git a/server/vbv_lernwelt/assignment/migrations/0011_assignment_solution_sample.py b/server/vbv_lernwelt/assignment/migrations/0011_assignment_solution_sample.py
new file mode 100644
index 00000000..b377574d
--- /dev/null
+++ b/server/vbv_lernwelt/assignment/migrations/0011_assignment_solution_sample.py
@@ -0,0 +1,26 @@
+# Generated by Django 3.2.20 on 2023-12-05 16:15
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("media_files", "0001_initial"),
+ ("assignment", "0010_assignmentcompletion_edoniq_extended_time_flag"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="assignment",
+ name="solution_sample",
+ field=models.ForeignKey(
+ blank=True,
+ help_text="Musterlösung",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="media_files.contentdocument",
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py
index a10136aa..df31c473 100644
--- a/server/vbv_lernwelt/assignment/models.py
+++ b/server/vbv_lernwelt/assignment/models.py
@@ -200,12 +200,22 @@ class Assignment(CourseBasePage):
help_text="Beurteilungsschritte",
)
+ solution_sample = models.ForeignKey(
+ "media_files.ContentDocument",
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name="+",
+ help_text="Musterlösung",
+ )
+
content_panels = Page.content_panels + [
FieldPanel("assignment_type"),
FieldPanel("needs_expert_evaluation"),
PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"),
FieldPanel("intro_text"),
FieldPanel("effort_required"),
+ FieldPanel("solution_sample"),
FieldPanel("performance_objectives"),
FieldPanel("tasks"),
FieldPanel("evaluation_description"),
diff --git a/server/vbv_lernwelt/assignment/tests/test_graphql.py b/server/vbv_lernwelt/assignment/tests/test_graphql.py
index 023ff374..85a081b9 100644
--- a/server/vbv_lernwelt/assignment/tests/test_graphql.py
+++ b/server/vbv_lernwelt/assignment/tests/test_graphql.py
@@ -106,14 +106,14 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
)
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},
- }
- },
+ self.maxDiff = None
+ self.assertEqual(task_data["user_data"]["fileId"], file_id)
+ self.assertEqual(task_data["user_data"]["fileInfo"]["id"], file_id)
+ self.assertEqual(task_data["user_data"]["fileInfo"]["name"], "file.txt")
+ self.assertTrue(
+ task_data["user_data"]["fileInfo"]["url"].startswith(
+ "https://s3.eu-central-1.amazonaws.com/myvbv-dev.iterativ.ch"
+ )
)
# check DB data
@@ -194,31 +194,31 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
# check notification
self.assertEqual(Notification.objects.count(), 1)
notification = Notification.objects.first()
- self.assertEquals(
+ self.assertEqual(
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben.",
notification.verb,
)
- self.assertEquals(
+ self.assertEqual(
"test-trainer1@example.com",
notification.recipient.email,
)
- self.assertEquals(
+ self.assertEqual(
"test-student1@example.com",
notification.actor.email,
)
- self.assertEquals(
+ self.assertEqual(
"USER_INTERACTION",
notification.notification_category,
)
- self.assertEquals(
+ self.assertEqual(
"CASEWORK_SUBMITTED",
notification.notification_trigger,
)
- self.assertEquals(
+ self.assertEqual(
notification.action_object,
db_entry,
)
- self.assertEquals(
+ self.assertEqual(
notification.course_session,
self.course_session,
)
@@ -422,35 +422,35 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
# check notification
self.assertEqual(Notification.objects.count(), 1)
notification = Notification.objects.first()
- self.assertEquals(
+ self.assertEqual(
"Test Trainer1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» bewertet.",
notification.verb,
)
- self.assertEquals(
+ self.assertEqual(
"test-student1@example.com",
notification.recipient.email,
)
- self.assertEquals(
+ self.assertEqual(
"test-trainer1@example.com",
notification.actor.email,
)
- self.assertEquals(
+ self.assertEqual(
"USER_INTERACTION",
notification.notification_category,
)
- self.assertEquals(
+ self.assertEqual(
"CASEWORK_EVALUATED",
notification.notification_trigger,
)
- self.assertEquals(
+ self.assertEqual(
notification.action_object,
db_entry,
)
- self.assertEquals(
+ self.assertEqual(
notification.course_session,
self.course_session,
)
- self.assertEquals(
+ self.assertEqual(
notification.target_url,
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice",
)
diff --git a/server/vbv_lernwelt/core/management/commands/cypress_reset.py b/server/vbv_lernwelt/core/management/commands/cypress_reset.py
index 2b1ab3ea..9d9b7922 100644
--- a/server/vbv_lernwelt/core/management/commands/cypress_reset.py
+++ b/server/vbv_lernwelt/core/management/commands/cypress_reset.py
@@ -32,7 +32,8 @@ from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
- LearningContentFeedback,
+ LearningContentFeedbackUK,
+ LearningContentFeedbackVV,
)
from vbv_lernwelt.notify.models import Notification
@@ -155,7 +156,9 @@ def command(
if create_feedback_responses:
print("create_feedback_responses")
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
- learning_content_feedback_page = LearningContentFeedback.objects.get(
+
+ # feedback fahrzeug
+ learning_content_feedback_page = LearningContentFeedbackUK.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback"
)
create_feedback_response_data(
@@ -174,6 +177,7 @@ def command(
"would_recommend": True,
"course_negative_feedback": "Nichts Schlechtes",
"course_positive_feedback": "Nur Gutes.",
+ "feedback_type": "uk",
},
)
@@ -193,6 +197,7 @@ def command(
"would_recommend": True,
"course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
"course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
+ "feedback_type": "uk",
},
)
@@ -212,6 +217,62 @@ def command(
"would_recommend": False,
"course_negative_feedback": "Mehr Videos wären schön.",
"course_positive_feedback": "Die Präsentation war super",
+ "feedback_type": "uk",
+ },
+ )
+
+ # feedback reisen
+ learning_content_feedback_page = LearningContentFeedbackVV.objects.get(
+ slug="test-lehrgang-lp-circle-reisen-lc-feedback"
+ )
+ create_feedback_response_data(
+ feedback_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
+ course_session=course_session,
+ learning_content_feedback_page=learning_content_feedback_page,
+ submitted=True,
+ feedback_data={
+ "satisfaction": 4,
+ "goal_attainment": 3,
+ "proficiency": 80,
+ "preparation_task_clarity": True,
+ "would_recommend": True,
+ "course_negative_feedback": "Nichts Schlechtes",
+ "course_positive_feedback": "Nur Gutes.",
+ "feedback_type": "vv",
+ },
+ )
+
+ create_feedback_response_data(
+ feedback_user=User.objects.get(id=TEST_STUDENT2_USER_ID),
+ course_session=course_session,
+ learning_content_feedback_page=learning_content_feedback_page,
+ submitted=True,
+ feedback_data={
+ "satisfaction": 4,
+ "goal_attainment": 4,
+ "proficiency": 100,
+ "preparation_task_clarity": True,
+ "would_recommend": True,
+ "course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
+ "course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
+ "feedback_type": "vv",
+ },
+ )
+
+ create_feedback_response_data(
+ feedback_user=User.objects.get(id=TEST_STUDENT3_USER_ID),
+ course_session=course_session,
+ learning_content_feedback_page=learning_content_feedback_page,
+ submitted=True,
+ feedback_data={
+ "satisfaction": 2,
+ "goal_attainment": 2,
+ "proficiency": 40,
+ "preparation_task_clarity": True,
+ "would_recommend": False,
+ "course_negative_feedback": "Mehr Videos wären schön.",
+ "course_positive_feedback": "Die Präsentation war super",
+ "feedback_type": "vv",
},
)
diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py
index 6c659c9d..6bd91b58 100644
--- a/server/vbv_lernwelt/course/creators/test_course.py
+++ b/server/vbv_lernwelt/course/creators/test_course.py
@@ -70,7 +70,8 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningContentAssignmentFactory,
LearningContentAttendanceCourseFactory,
LearningContentEdoniqTestFactory,
- LearningContentFeedbackFactory,
+ LearningContentFeedbackUKFactory,
+ LearningContentFeedbackVVFactory,
LearningContentKnowledgeAssessmentFactory,
LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory,
@@ -82,6 +83,12 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningUnitFactory,
TopicFactory,
)
+from vbv_lernwelt.media_files.create_default_documents import (
+ create_default_collections,
+ create_default_content_documents,
+)
+from vbv_lernwelt.media_files.create_default_images import create_default_images
+from vbv_lernwelt.media_files.models import ContentDocument, ContentImage, UserImage
from vbv_lernwelt.media_library.tests.media_library_factories import (
MediaLibraryCategoryPageFactory,
MediaLibraryContentPageFactory,
@@ -92,6 +99,11 @@ from vbv_lernwelt.media_library.tests.media_library_factories import (
def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
# create_locales_for_wagtail()
+ create_default_collections()
+ create_default_content_documents()
+ if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
+ create_default_images()
+
course = create_test_course_with_categories()
competence_certificate = create_test_competence_navi()
@@ -360,6 +372,7 @@ def create_feedback_response_data(
"would_recommend": True,
"course_negative_feedback": "Nichts Schlechtes",
"course_positive_feedback": "Nur Gutes.",
+ "feedback_type": "uk",
}
return update_feedback_response(
@@ -523,6 +536,14 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
slug__startswith=f"test-lehrgang-assignment-reflexion"
),
),
+
+ assignment = Assignment.objects.get(
+ slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
+ )
+ assignment.solution_sample = ContentDocument.objects.get(
+ title="Musterlösung Fahrzeug"
+ )
+ assignment.save()
LearningContentAssignmentFactory(
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
parent=circle,
@@ -530,7 +551,8 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
),
)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
+ title="Feedback",
parent=circle,
)
@@ -614,7 +636,8 @@ def create_test_circle_reisen(lp):
title="Reflexion",
parent=parent,
)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
+ title="Feedback",
parent=parent,
)
diff --git a/server/vbv_lernwelt/course/creators/uk_course.py b/server/vbv_lernwelt/course/creators/uk_course.py
index eb5bdc8f..7e53d3cc 100644
--- a/server/vbv_lernwelt/course/creators/uk_course.py
+++ b/server/vbv_lernwelt/course/creators/uk_course.py
@@ -22,7 +22,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningContentAttendanceCourseFactory,
LearningContentDocumentListFactory,
LearningContentEdoniqTestFactory,
- LearningContentFeedbackFactory,
+ LearningContentFeedbackUKFactory,
LearningContentMediaLibraryFactory,
LearningContentPlaceholderFactory,
LearningPathFactory,
@@ -30,6 +30,11 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningUnitFactory,
TopicFactory,
)
+from vbv_lernwelt.media_files.create_default_documents import (
+ create_default_collections,
+ create_default_content_documents,
+)
+from vbv_lernwelt.media_files.create_default_images import create_default_images
from vbv_lernwelt.media_library.tests.media_library_factories import (
LearnMediaBlockFactory,
)
@@ -40,6 +45,7 @@ def create_uk_learning_path(course_id=COURSE_UK, user=None, skip_locales=True):
user = User.objects.get(username="info@iterativ.ch")
course_page = CoursePage.objects.get(course_id=course_id)
+
lp = LearningPathFactory(
title="Lernpfad",
parent=course_page,
@@ -254,7 +260,7 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
title="Unterlagen für den Unterricht",
parent=circle,
)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@@ -364,7 +370,7 @@ In diesem Circle erfährst du wie die überbetrieblichen Kurse aufgebaut sind. Z
# test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
# )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@@ -479,7 +485,7 @@ Dans ce cercle, tu apprendras comment les cours interentreprises sont structuré
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end")
@@ -594,7 +600,7 @@ In questo Circle imparerai come sono strutturati i corsi interaziendali. Imparer
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end")
@@ -699,7 +705,7 @@ In diesem Circle lernst du die wichtigsten Grundlagen bezüglich Versicherungswi
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@@ -809,7 +815,7 @@ Dans ce cercle, tu apprends les bases les plus importantes en matière d'assuran
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end")
@@ -918,7 +924,7 @@ In questo Circle imparerai le basi più importanti del settore assicurativo e de
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end")
@@ -1058,7 +1064,7 @@ def create_uk_circle_fahrzeug(lp, title="Fahrzeug"):
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
@@ -1192,7 +1198,7 @@ def create_uk_fr_circle_fahrzeug(lp, title="Véhicule"):
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
@@ -1330,7 +1336,7 @@ def create_uk_it_circle_fahrzeug(lp, title="Veicolo"):
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
- LearningContentFeedbackFactory(
+ LearningContentFeedbackUKFactory(
parent=circle,
)
diff --git a/server/vbv_lernwelt/course/graphql/queries.py b/server/vbv_lernwelt/course/graphql/queries.py
index cbe377e1..cc405a72 100644
--- a/server/vbv_lernwelt/course/graphql/queries.py
+++ b/server/vbv_lernwelt/course/graphql/queries.py
@@ -10,7 +10,8 @@ from vbv_lernwelt.learnpath.graphql.types import (
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
- LearningContentFeedbackObjectType,
+ LearningContentFeedbackUKObjectType,
+ LearningContentFeedbackVVObjectType,
LearningContentKnowledgeAssessmentObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
@@ -50,7 +51,8 @@ class CourseQuery(graphene.ObjectType):
learning_content_attendance_course = graphene.Field(
LearningContentAttendanceCourseObjectType
)
- learning_content_feedback = graphene.Field(LearningContentFeedbackObjectType)
+ learning_content_feedback_uk = graphene.Field(LearningContentFeedbackUKObjectType)
+ learning_content_feedback_vv = graphene.Field(LearningContentFeedbackVVObjectType)
learning_content_learning_module = graphene.Field(
LearningContentLearningModuleObjectType
)
diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py
index f6763d87..4bf1cc24 100644
--- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py
+++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py
@@ -100,6 +100,12 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAssignment,
LearningContentAttendanceCourse,
)
+from vbv_lernwelt.media_files.create_default_documents import (
+ create_default_collections,
+ create_default_content_documents,
+ create_default_user_documents,
+)
+from vbv_lernwelt.media_files.create_default_images import create_default_images
from vbv_lernwelt.media_library.create_default_media_library import (
create_default_media_library,
)
@@ -128,6 +134,11 @@ ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
def command(course):
print("Creating default courses", course)
+ create_default_collections()
+ create_default_content_documents()
+ create_default_user_documents()
+ create_default_images()
+
if COURSE_VERSICHERUNGSVERMITTLERIN_ID in course:
create_versicherungsvermittlerin_course()
@@ -285,6 +296,9 @@ def create_course_uk_de(course_id=COURSE_UK, lang="de"):
create_uk_competence_profile(course_id=course_id)
create_default_media_library(course_id=course_id)
+ create_default_collections()
+ create_default_content_documents()
+
def create_course_uk_de_course_sessions():
course = Course.objects.get(id=COURSE_UK)
diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py
index ca278d15..7579fd56 100644
--- a/server/vbv_lernwelt/dashboard/graphql/queries.py
+++ b/server/vbv_lernwelt/dashboard/graphql/queries.py
@@ -148,8 +148,8 @@ class DashboardQuery(graphene.ObjectType):
assignment=ProgressDashboardAssignmentType( # noqa
_id=course_id, # noqa
total_count=len(evaluation_results), # noqa
- points_max_count=points_max_count, # noqa
- points_achieved_count=points_achieved_count, # noqa
+ points_max_count=int(points_max_count), # noqa
+ points_achieved_count=int(points_achieved_count), # noqa
),
)
diff --git a/server/vbv_lernwelt/duedate/admin.py b/server/vbv_lernwelt/duedate/admin.py
index 048f4dee..68843156 100644
--- a/server/vbv_lernwelt/duedate/admin.py
+++ b/server/vbv_lernwelt/duedate/admin.py
@@ -1,6 +1,11 @@
from django.contrib import admin
from wagtail.models import Page
+from vbv_lernwelt.course_session.models import (
+ CourseSessionAssignment,
+ CourseSessionAttendanceCourse,
+ CourseSessionEdoniqTest,
+)
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.learnpath.models import (
Circle,
@@ -9,7 +14,16 @@ from vbv_lernwelt.learnpath.models import (
)
-# Register your models here.
+@admin.action(description="Re-sync URLs from LearningContent")
+def sync_wagtail_due_date_url(modeladmin, request, queryset):
+ for assignment in CourseSessionAssignment.objects.all():
+ assignment.save()
+ for edoniq_test in CourseSessionEdoniqTest.objects.all():
+ edoniq_test.save()
+ for attendance in CourseSessionAttendanceCourse.objects.all():
+ attendance.save()
+
+
@admin.register(DueDate)
class DueDateAdmin(admin.ModelAdmin):
date_hierarchy = "start"
@@ -23,6 +37,7 @@ class DueDateAdmin(admin.ModelAdmin):
]
list_filter = ["course_session__course", "course_session"]
readonly_fields = ["course_session", "page"]
+ actions = [sync_wagtail_due_date_url]
def get_readonly_fields(self, request, obj=None):
default_readonly = super(DueDateAdmin, self).get_readonly_fields(request, obj)
diff --git a/server/vbv_lernwelt/feedback/factories.py b/server/vbv_lernwelt/feedback/factories.py
index fcd76201..edc7321b 100644
--- a/server/vbv_lernwelt/feedback/factories.py
+++ b/server/vbv_lernwelt/feedback/factories.py
@@ -34,6 +34,7 @@ class FeedbackResponseFactory(DjangoModelFactory):
"Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
]
),
+ "feedback_type": FuzzyChoice(["uk", "vv"]),
}
)
diff --git a/server/vbv_lernwelt/feedback/graphql/mutations.py b/server/vbv_lernwelt/feedback/graphql/mutations.py
index e36ad651..9b5062c0 100644
--- a/server/vbv_lernwelt/feedback/graphql/mutations.py
+++ b/server/vbv_lernwelt/feedback/graphql/mutations.py
@@ -7,10 +7,16 @@ from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.feedback.graphql.types import (
FeedbackResponseObjectType as FeedbackResponseType,
)
-from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer
+from vbv_lernwelt.feedback.serializers import (
+ CourseFeedbackSerializerUK,
+ CourseFeedbackSerializerVV,
+)
from vbv_lernwelt.feedback.services import update_feedback_response
from vbv_lernwelt.iam.permissions import has_course_session_access
-from vbv_lernwelt.learnpath.models import LearningContentFeedback
+from vbv_lernwelt.learnpath.models import (
+ LearningContentFeedbackUK,
+ LearningContentFeedbackVV,
+)
logger = structlog.get_logger(__name__)
@@ -25,6 +31,7 @@ class SendFeedbackMutation(graphene.Mutation):
class Arguments:
course_session_id = graphene.ID(required=True)
learning_content_page_id = graphene.ID(required=True)
+ learning_content_type = graphene.String(required=True)
data = GenericScalar()
submitted = graphene.Boolean(required=False, default_value=False)
@@ -35,11 +42,29 @@ class SendFeedbackMutation(graphene.Mutation):
info,
course_session_id,
learning_content_page_id,
+ learning_content_type,
data,
submitted,
):
feedback_user_id = info.context.user.id
- learning_content = LearningContentFeedback.objects.get(
+
+ if learning_content_type == "learnpath.LearningContentFeedbackVV":
+ learningContentFeedbackModel = LearningContentFeedbackVV
+ serializerClass = CourseFeedbackSerializerVV
+ data["feedback_type"] = "vv"
+ elif learning_content_type == "learnpath.LearningContentFeedbackUK":
+ learningContentFeedbackModel = LearningContentFeedbackUK
+ serializerClass = CourseFeedbackSerializerUK
+ data["feedback_type"] = "uk"
+ else:
+ errors = [
+ ErrorType(
+ field="learningContentType", messages="Invalid learningContentType"
+ )
+ ]
+ return SendFeedbackMutation(errors=errors)
+
+ learning_content = learningContentFeedbackModel.objects.get(
id=learning_content_page_id
)
circle = learning_content.get_circle()
@@ -65,7 +90,7 @@ class SendFeedbackMutation(graphene.Mutation):
course_session_id=course_session_id,
)
- serializer = CourseFeedbackSerializer(data=data)
+ serializer = serializerClass(data=data)
if not serializer.is_valid():
logger.error(
diff --git a/server/vbv_lernwelt/feedback/migrations/0007_auto_20231207_1501.py b/server/vbv_lernwelt/feedback/migrations/0007_auto_20231207_1501.py
new file mode 100644
index 00000000..0d2bf771
--- /dev/null
+++ b/server/vbv_lernwelt/feedback/migrations/0007_auto_20231207_1501.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.2.20 on 2023-12-07 14:01
+
+from django.db import migrations
+
+
+def add_field_to_json(apps, _schema_editor):
+ FeedbackResponse = apps.get_model("feedback", "FeedbackResponse")
+ for instance in FeedbackResponse.objects.all():
+ if instance.data is None:
+ instance.data = {}
+ instance.data["feedback_type"] = "uk" # Set the default value
+ instance.save()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("feedback", "0006_auto_20230922_1131"),
+ ]
+
+ operations = [
+ migrations.RunPython(add_field_to_json),
+ ]
diff --git a/server/vbv_lernwelt/feedback/serializers.py b/server/vbv_lernwelt/feedback/serializers.py
index bfa9c8a1..6a3199be 100644
--- a/server/vbv_lernwelt/feedback/serializers.py
+++ b/server/vbv_lernwelt/feedback/serializers.py
@@ -5,6 +5,11 @@ from vbv_lernwelt.feedback.models import FeedbackResponse
logger = structlog.get_logger(__name__)
+FEEDBACK_TYPES = (
+ ("uk", "Feedback UK"),
+ ("vv", "Feedback VV"),
+)
+
class FeedbackIntegerField(serializers.IntegerField):
def __init__(self, **kwargs):
@@ -13,7 +18,8 @@ class FeedbackIntegerField(serializers.IntegerField):
)
-class CourseFeedbackSerializer(serializers.Serializer):
+class CourseFeedbackSerializerUK(serializers.Serializer):
+ feedback_type = serializers.ChoiceField(choices=FEEDBACK_TYPES)
satisfaction = FeedbackIntegerField()
goal_attainment = FeedbackIntegerField()
proficiency = serializers.IntegerField(required=False, allow_null=True)
@@ -33,6 +39,22 @@ class CourseFeedbackSerializer(serializers.Serializer):
)
+class CourseFeedbackSerializerVV(serializers.Serializer):
+ feedback_type = serializers.ChoiceField(choices=FEEDBACK_TYPES)
+ satisfaction = FeedbackIntegerField()
+ goal_attainment = FeedbackIntegerField()
+ proficiency = serializers.IntegerField(required=False, allow_null=True)
+ preparation_task_clarity = serializers.BooleanField(required=False, allow_null=True)
+ materials_rating = FeedbackIntegerField()
+ would_recommend = serializers.BooleanField(required=False, allow_null=True)
+ course_positive_feedback = serializers.CharField(
+ required=False, allow_null=True, allow_blank=True
+ )
+ course_negative_feedback = serializers.CharField(
+ required=False, allow_null=True, allow_blank=True
+ )
+
+
class CypressFeedbackResponseSerializer(serializers.ModelSerializer):
class Meta:
model = FeedbackResponse
diff --git a/server/vbv_lernwelt/feedback/services.py b/server/vbv_lernwelt/feedback/services.py
index 86460ea4..6d6979f7 100644
--- a/server/vbv_lernwelt/feedback/services.py
+++ b/server/vbv_lernwelt/feedback/services.py
@@ -1,10 +1,15 @@
+from typing import Union
+
import structlog
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.feedback.models import FeedbackResponse
-from vbv_lernwelt.learnpath.models import LearningContentFeedback
+from vbv_lernwelt.learnpath.models import (
+ LearningContentFeedbackUK,
+ LearningContentFeedbackVV,
+)
logger = structlog.get_logger(__name__)
@@ -12,7 +17,9 @@ logger = structlog.get_logger(__name__)
def update_feedback_response(
feedback_user: User,
course_session: CourseSession,
- learning_content_feedback_page: LearningContentFeedback,
+ learning_content_feedback_page: Union[
+ LearningContentFeedbackUK, LearningContentFeedbackVV
+ ],
submitted: bool,
validated_data: dict,
):
@@ -26,18 +33,7 @@ def update_feedback_response(
original_data = feedback_response.data
updated_data = validated_data
- initial_data = {
- "satisfaction": None,
- "goal_attainment": None,
- "proficiency": None,
- "preparation_task_clarity": None,
- "instructor_competence": None,
- "instructor_respect": None,
- "instructor_open_feedback": "",
- "would_recommend": None,
- "course_negative_feedback": "",
- "course_positive_feedback": "",
- }
+ initial_data = initial_data_for_feedback_page(learning_content_feedback_page)
merged_data = initial_data | {
key: updated_data[key]
@@ -71,3 +67,36 @@ def update_feedback_response(
)
return feedback_response
+
+
+def initial_data_for_feedback_page(
+ learning_content_feedback_page: Union[
+ LearningContentFeedbackUK, LearningContentFeedbackVV
+ ]
+):
+ if hasattr(learning_content_feedback_page, "learningcontentfeedbackuk"):
+ return {
+ "satisfaction": None,
+ "goal_attainment": None,
+ "proficiency": None,
+ "preparation_task_clarity": None,
+ "instructor_competence": None,
+ "instructor_respect": None,
+ "instructor_open_feedback": "",
+ "would_recommend": None,
+ "course_negative_feedback": "",
+ "course_positive_feedback": "",
+ "feedback_type": "uk",
+ }
+ if hasattr(learning_content_feedback_page, "learningcontentfeedbackvv"):
+ return {
+ "satisfaction": None,
+ "goal_attainment": None,
+ "proficiency": None,
+ "preparation_task_clarity": None,
+ "would_recommend": None,
+ "course_negative_feedback": "",
+ "course_positive_feedback": "",
+ "feedback_type": "vv",
+ }
+ return {}
diff --git a/server/vbv_lernwelt/feedback/tests/test_feedback_api.py b/server/vbv_lernwelt/feedback/tests/test_feedback_api.py
index 99e8c258..0fa83009 100644
--- a/server/vbv_lernwelt/feedback/tests/test_feedback_api.py
+++ b/server/vbv_lernwelt/feedback/tests/test_feedback_api.py
@@ -114,6 +114,7 @@ class FeedbackRestApiTestCase(FeedbackBaseTestCase):
"course_negative_feedback": self.feedback_data[
"course_negative_feedback"
][i],
+ "feedback_type": "uk",
},
feedback_user=self.feedback_users[i],
submitted=True,
@@ -129,6 +130,7 @@ class FeedbackRestApiTestCase(FeedbackBaseTestCase):
expected = {
"amount": 3,
"questions": self.feedback_data,
+ "feedbackType": "uk",
}
print(response.data)
diff --git a/server/vbv_lernwelt/feedback/tests/test_graphql.py b/server/vbv_lernwelt/feedback/tests/test_graphql.py
new file mode 100644
index 00000000..a5516212
--- /dev/null
+++ b/server/vbv_lernwelt/feedback/tests/test_graphql.py
@@ -0,0 +1,91 @@
+import json
+
+from graphene_django.utils.testing import GraphQLTestCase
+
+from vbv_lernwelt.core.create_default_users import create_default_users
+from vbv_lernwelt.core.models import User
+from vbv_lernwelt.course.consts import COURSE_TEST_ID
+from vbv_lernwelt.course.creators.test_course import create_test_course
+from vbv_lernwelt.course.models import CourseSession
+from vbv_lernwelt.feedback.models import FeedbackResponse
+from vbv_lernwelt.learnpath.models import LearningContentFeedbackUK
+
+
+class FeedbackMutationTestCase(GraphQLTestCase):
+ GRAPHQL_URL = "/server/graphql/"
+
+ def setUp(self):
+ create_default_users()
+ create_test_course(include_vv=False, with_sessions=True)
+ self.course_session = CourseSession.objects.get(title="Test Bern 2022 a")
+ self.learning_content_feedback_page = LearningContentFeedbackUK.objects.get(
+ slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback"
+ )
+ self.student = User.objects.get(username="test-student1@example.com")
+ self.client.force_login(self.student)
+
+ def test_creates_response(self):
+ data = {
+ "course_negative_feedback": "schlecht",
+ "course_positive_feedback": "gut",
+ "feedback_type": "uk",
+ "goal_attainment": 3,
+ "preparation_task_clarity": False,
+ "proficiency": 100,
+ "satisfaction": 3,
+ "would_recommend": False,
+ "instructor_competence": None,
+ "instructor_respect": None,
+ "instructor_open_feedback": None,
+ }
+
+ response = self.query(
+ f"""
+mutation {{
+ send_feedback(
+ course_session_id: "{COURSE_TEST_ID}"
+ learning_content_page_id: "{self.learning_content_feedback_page.id}"
+ learning_content_type: "learnpath.LearningContentFeedbackUK"
+ data: {{
+ course_negative_feedback: "{data['course_negative_feedback']}",
+ course_positive_feedback: "{data['course_positive_feedback']}",
+ feedback_type: null,
+ goal_attainment: {data['goal_attainment']},
+ preparation_task_clarity: {str(data['preparation_task_clarity']).lower()},
+ proficiency: {data['proficiency']},
+ satisfaction: {data['satisfaction']},
+ would_recommend: {str(data['would_recommend']).lower()},
+ instructor_competence: null,
+ instructor_respect: null,
+ instructor_open_feedback: null,
+ }},
+ submitted: false
+ ) {{
+ feedback_response {{
+ id
+ data
+ submitted
+ __typename
+ }}
+ errors {{
+ field
+ messages
+ __typename
+ }}
+ __typename
+ }}
+}}
+ """
+ )
+
+ content = json.loads(response.content)
+
+ self.assertResponseNoErrors(response)
+ self.assertDictEqual(
+ content["data"]["send_feedback"]["feedback_response"]["data"], data
+ )
+
+ feedback = FeedbackResponse.objects.first()
+ self.assertEqual(feedback.data, data)
+ self.assertEqual(feedback.submitted, False)
+ self.assertEqual(feedback.feedback_user, self.student)
diff --git a/server/vbv_lernwelt/feedback/views.py b/server/vbv_lernwelt/feedback/views.py
index 7d3bb27f..ecd11483 100644
--- a/server/vbv_lernwelt/feedback/views.py
+++ b/server/vbv_lernwelt/feedback/views.py
@@ -61,12 +61,13 @@ def get_feedback_for_circle(request, course_session_id, circle_id):
feedback_user__in=feedback_users(course_session_id),
).order_by("created_at")
- # I guess this is ok for the üK case
- feedback_data = {"amount": len(feedbacks), "questions": {}}
+ feedback_data = {"amount": len(feedbacks), "questions": {}, "feedbackType": None}
if feedback_data["amount"] == 0:
return Response(status=200, data=feedback_data)
+ feedback_data["feedbackType"] = feedbacks[0].data.get("feedback_type", None)
+
for field in FEEDBACK_FIELDS:
feedback_data["questions"][field] = []
diff --git a/server/vbv_lernwelt/files/tests/__init__.py b/server/vbv_lernwelt/files/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/files/tests/test_files_app.py b/server/vbv_lernwelt/files/tests/test_files_app.py
new file mode 100644
index 00000000..b9393e2d
--- /dev/null
+++ b/server/vbv_lernwelt/files/tests/test_files_app.py
@@ -0,0 +1,59 @@
+import datetime
+
+import requests
+from django.conf import settings
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import TestCase
+
+from vbv_lernwelt.core.models import User
+from vbv_lernwelt.files.integrations import s3_get_client
+from vbv_lernwelt.files.models import UploadFile
+
+
+class UploadFileIntegrationTest(TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.s3_client = s3_get_client()
+
+ def setUp(self):
+ self.user = User.objects.create(username="testuser")
+ # Creating a dummy file for upload
+ self.dummy_file = SimpleUploadedFile(
+ "testfile.txt", b"these are the file contents!"
+ )
+ self.upload_file = UploadFile.objects.create(
+ original_file_name="testfile.txt",
+ file_name="testfile123.txt",
+ file_type="text/plain",
+ uploaded_by=self.user,
+ file=self.dummy_file,
+ )
+
+ def tearDown(self):
+ self.upload_file.delete_file()
+
+ def test_upload_to_s3(self):
+ # Verify if file is uploaded to S3
+ response = self.s3_client.get_object(
+ Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=str(self.upload_file.file)
+ )
+ self.assertEqual(response["Body"].read(), b"these are the file contents!")
+
+ def test_url_property(self):
+ self.upload_file.upload_finished_at = datetime.datetime.now()
+ self.upload_file.save()
+ url = self.upload_file.url
+ response = requests.get(url)
+ # Assert that the URL is a valid presigned S3 URL and accessible
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, b"these are the file contents!")
+
+ def test_delete_file_method(self):
+ file_path = str(self.upload_file.file)
+ self.upload_file.delete_file()
+ # Assert that the file is deleted from S3
+ with self.assertRaises(self.s3_client.exceptions.NoSuchKey):
+ self.s3_client.get_object(
+ Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_path
+ )
diff --git a/server/vbv_lernwelt/files/tests/test_files_integrations.py b/server/vbv_lernwelt/files/tests/test_files_integrations.py
new file mode 100644
index 00000000..dc652e09
--- /dev/null
+++ b/server/vbv_lernwelt/files/tests/test_files_integrations.py
@@ -0,0 +1,81 @@
+import os
+
+import boto3
+import requests
+from django.conf import settings
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import TestCase
+
+from vbv_lernwelt.core.models import User
+from vbv_lernwelt.files.integrations import (
+ s3_delete_file,
+ s3_generate_presigned_post,
+ s3_generate_presigned_url,
+ s3_get_client,
+)
+
+
+class TestIntegrationsIntegrationTest(TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.s3_client = s3_get_client()
+
+ def setUp(self):
+ self.user = User.objects.create(username="testuser")
+
+ # Creating a dummy file for upload
+ self.dummy_file = SimpleUploadedFile(
+ "testfile.txt", b"these are the file contents!"
+ )
+
+ def test_s3_generate_presigned_post(self):
+ # Test generating a presigned POST for file upload
+ presigned_post_data = s3_generate_presigned_post(
+ file_path=f"{self.user.id}/testfile.txt",
+ file_type="text/plain",
+ file_name="testfile.txt",
+ )
+ self.assertIn("url", presigned_post_data)
+ self.assertIn("fields", presigned_post_data)
+
+ # Upload file using the presigned URL
+ files = {"file": self.dummy_file}
+ response = requests.post(
+ presigned_post_data["url"], data=presigned_post_data["fields"], files=files
+ )
+ self.assertEqual(response.status_code, 204)
+
+ def test_s3_generate_presigned_url(self):
+ # First, manually upload a file to S3 for testing
+ self.s3_client.upload_fileobj
+
+ self.s3_client.upload_fileobj(
+ self.dummy_file, settings.AWS_STORAGE_BUCKET_NAME, "testfile.txt"
+ )
+
+ # Test generating a presigned URL for the uploaded file
+ presigned_url = s3_generate_presigned_url(file_path="testfile.txt")
+ response = requests.get(presigned_url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, b"these are the file contents!")
+
+ def test_s3_delete_file(self):
+ # Upload a file to S3 for testing
+ self.s3_client.upload_fileobj(
+ self.dummy_file, settings.AWS_STORAGE_BUCKET_NAME, "testfile.txt"
+ )
+
+ # Test deleting the file
+ s3_delete_file(file_path="testfile.txt")
+ # Assert that the file no longer exists
+ with self.assertRaises(boto3.exceptions.botocore.client.ClientError):
+ self.s3_client.head_object(
+ Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key="testfile.txt"
+ )
+
+ @classmethod
+ def tearDownClass(cls):
+ # Clean up any remaining files in the S3 bucket
+ s3_delete_file(file_path="testfile.txt")
+ super().tearDownClass()
diff --git a/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py b/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py
index 6a55f618..915b2239 100644
--- a/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py
+++ b/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py
@@ -13,7 +13,7 @@ from vbv_lernwelt.course.models import CourseCategory, CoursePage
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory,
LearningContentAssignmentFactory,
- LearningContentFeedbackFactory,
+ LearningContentFeedbackVVFactory,
LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory,
LearningContentPlaceholderFactory,
@@ -201,7 +201,7 @@ def create_circle_basis(lp, title="Basis"):
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -278,7 +278,7 @@ def create_circle_gewinnen(lp, title="Gewinnen"):
slug__startswith=f"{course_slug}-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -368,7 +368,7 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
# slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
# ),
# ),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -554,7 +554,7 @@ def create_circle_reisen(lp, title="Reisen"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -647,7 +647,7 @@ def create_circle_einkommenssicherung(lp, title="Einkommenssicherung"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -700,7 +700,7 @@ def create_circle_wohneigentum(lp, title="Wohneigentum"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -782,7 +782,7 @@ def create_circle_pensionierung(lp, title="Pensionierung"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -839,7 +839,7 @@ def create_circle_erben(lp, title="Erben/Vererben"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -929,7 +929,7 @@ def create_circle_gesundheit(lp, title="Gesundheit"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=circle,
)
@@ -1352,7 +1352,7 @@ def create_learning_sequence_transfer(parent, title, lc_praxis_title=None):
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
),
),
- LearningContentFeedbackFactory(
+ LearningContentFeedbackVVFactory(
parent=parent,
)
diff --git a/server/vbv_lernwelt/learnpath/graphql/types.py b/server/vbv_lernwelt/learnpath/graphql/types.py
index a1eb723a..4e12cc4b 100644
--- a/server/vbv_lernwelt/learnpath/graphql/types.py
+++ b/server/vbv_lernwelt/learnpath/graphql/types.py
@@ -10,7 +10,8 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
LearningContentDocumentList,
LearningContentEdoniqTest,
- LearningContentFeedback,
+ LearningContentFeedbackUK,
+ LearningContentFeedbackVV,
LearningContentKnowledgeAssessment,
LearningContentLearningModule,
LearningContentMediaLibrary,
@@ -49,8 +50,10 @@ class LearningContentInterface(CoursePageInterface):
return LearningContentAssignmentObjectType
elif isinstance(instance, LearningContentAttendanceCourse):
return LearningContentAttendanceCourseObjectType
- elif isinstance(instance, LearningContentFeedback):
- return LearningContentFeedbackObjectType
+ elif isinstance(instance, LearningContentFeedbackUK):
+ return LearningContentFeedbackUKObjectType
+ elif isinstance(instance, LearningContentFeedbackVV):
+ return LearningContentFeedbackVVObjectType
elif isinstance(instance, LearningContentLearningModule):
return LearningContentLearningModuleObjectType
elif isinstance(instance, LearningContentKnowledgeAssessment):
@@ -105,9 +108,19 @@ class LearningContentPlaceholderObjectType(DjangoObjectType):
fields = []
-class LearningContentFeedbackObjectType(DjangoObjectType):
+class LearningContentFeedbackUKObjectType(DjangoObjectType):
class Meta:
- model = LearningContentFeedback
+ model = LearningContentFeedbackUK
+ interfaces = (
+ CoursePageInterface,
+ LearningContentInterface,
+ )
+ fields = []
+
+
+class LearningContentFeedbackVVObjectType(DjangoObjectType):
+ class Meta:
+ model = LearningContentFeedbackVV
interfaces = (
CoursePageInterface,
LearningContentInterface,
diff --git a/server/vbv_lernwelt/learnpath/migrations/0012_auto_20231129_0827.py b/server/vbv_lernwelt/learnpath/migrations/0012_auto_20231129_0827.py
new file mode 100644
index 00000000..8c82c436
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0012_auto_20231129_0827.py
@@ -0,0 +1,60 @@
+# Generated by Django 3.2.20 on 2023-11-29 07:27
+
+import django.db.models.deletion
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("wagtailcore", "0089_log_entry_data_json_null_to_object"),
+ ("learnpath", "0011_learningcontentknowledgeassessment"),
+ ]
+
+ operations = [
+ migrations.RenameModel("LearningContentFeedback", "LearningContentFeedbackUK"),
+ migrations.CreateModel(
+ name="LearningContentFeedbackVV",
+ fields=[
+ (
+ "page_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="wagtailcore.page",
+ ),
+ ),
+ ("minutes", models.PositiveIntegerField(default=15)),
+ ("description", wagtail.fields.RichTextField(blank=True)),
+ ("content_url", models.TextField(blank=True)),
+ ("has_course_completion_status", models.BooleanField(default=True)),
+ (
+ "can_user_self_toggle_course_completion",
+ models.BooleanField(default=False),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=("wagtailcore.page",),
+ ),
+ migrations.AlterField(
+ model_name="learningcontentassignment",
+ name="assignment_type",
+ field=models.CharField(
+ choices=[
+ ("VOLUNTARY_CASEWORK", "VOLUNTARY_CASEWORK"),
+ ("MANDATORY_CASEWORK", "MANDATORY_CASEWORK"),
+ ("PREP_ASSIGNMENT", "PREP_ASSIGNMENT"),
+ ("REFLECTION", "REFLECTION"),
+ ("CONDITION_ACCEPTANCE", "CONDITION_ACCEPTANCE"),
+ ("EDONIQ_TEST", "EDONIQ_TEST"),
+ ],
+ default="MANDATORY_CASEWORK",
+ max_length=50,
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py
index da56a8fc..64d260cc 100644
--- a/server/vbv_lernwelt/learnpath/models.py
+++ b/server/vbv_lernwelt/learnpath/models.py
@@ -72,7 +72,8 @@ class Circle(CourseBasePage):
"learnpath.LearningUnit",
"learnpath.LearningContentAssignment",
"learnpath.LearningContentAttendanceCourse",
- "learnpath.LearningContentFeedback",
+ "learnpath.LearningContentFeedbackUK",
+ "learnpath.LearningContentFeedbackVV",
"learnpath.LearningContentLearningModule",
"learnpath.LearningContentKnowledgeAssessment",
"learnpath.LearningContentMediaLibrary",
@@ -318,7 +319,13 @@ class LearningContentPlaceholder(LearningContent):
can_user_self_toggle_course_completion = models.BooleanField(default=True)
-class LearningContentFeedback(LearningContent):
+class LearningContentFeedbackUK(LearningContent):
+ parent_page_types = ["learnpath.Circle"]
+ subpage_types = []
+ can_user_self_toggle_course_completion = models.BooleanField(default=False)
+
+
+class LearningContentFeedbackVV(LearningContent):
parent_page_types = ["learnpath.Circle"]
subpage_types = []
can_user_self_toggle_course_completion = models.BooleanField(default=False)
diff --git a/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py b/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py
index e882ecca..85b8f45b 100644
--- a/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py
+++ b/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py
@@ -7,7 +7,8 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
LearningContentDocumentList,
LearningContentEdoniqTest,
- LearningContentFeedback,
+ LearningContentFeedbackUK,
+ LearningContentFeedbackVV,
LearningContentKnowledgeAssessment,
LearningContentLearningModule,
LearningContentMediaLibrary,
@@ -120,14 +121,24 @@ class LearningContentPlaceholderFactory(wagtail_factories.PageFactory):
model = LearningContentPlaceholder
-class LearningContentFeedbackFactory(wagtail_factories.PageFactory):
- title = "Feedback"
+class LearningContentFeedbackVVFactory(wagtail_factories.PageFactory):
+ title = "FeedbackVV"
minutes = 0
content_url = ""
description = RichText("")
class Meta:
- model = LearningContentFeedback
+ model = LearningContentFeedbackVV
+
+
+class LearningContentFeedbackUKFactory(wagtail_factories.PageFactory):
+ title = "FeedbackUK"
+ minutes = 0
+ content_url = ""
+ description = RichText("")
+
+ class Meta:
+ model = LearningContentFeedbackUK
class LearningContentLearningModuleFactory(wagtail_factories.PageFactory):
diff --git a/server/vbv_lernwelt/media_files/__init__.py b/server/vbv_lernwelt/media_files/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/media_files/admin.py b/server/vbv_lernwelt/media_files/admin.py
new file mode 100644
index 00000000..37b5cba4
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/admin.py
@@ -0,0 +1,54 @@
+from django.contrib import admin
+
+from vbv_lernwelt.media_files.models import UserDocument, UserImage
+
+
+@admin.register(UserDocument)
+class UserDocumentAdmin(admin.ModelAdmin):
+ list_display = (
+ "title",
+ "file",
+ "created_at",
+ "uploaded_by_user",
+ "file_size",
+ "file_hash",
+ )
+ search_fields = ("title", "uploaded_by_user__username", "tags__name")
+ list_filter = ("created_at", "uploaded_by_user")
+ autocomplete_fields = ["uploaded_by_user"]
+ date_hierarchy = "created_at"
+ readonly_fields = (
+ "file_size",
+ "file_hash",
+ "created_at",
+ "uploaded_by_user",
+ "file",
+ )
+
+
+@admin.register(UserImage)
+class UserImageAdmin(admin.ModelAdmin):
+ list_display = (
+ "title",
+ "file",
+ "created_at",
+ "uploaded_by_user",
+ "file_size",
+ )
+ search_fields = ("title", "uploaded_by_user__username")
+ list_filter = ("created_at", "uploaded_by_user")
+ autocomplete_fields = ["uploaded_by_user"]
+ date_hierarchy = "created_at"
+ readonly_fields = (
+ "file_size",
+ "file_hash",
+ "created_at",
+ "uploaded_by_user",
+ "file",
+ "tags",
+ "title",
+ "focal_point_x",
+ "focal_point_y",
+ "focal_point_width",
+ "focal_point_height",
+ )
diff --git a/server/vbv_lernwelt/media_files/apps.py b/server/vbv_lernwelt/media_files/apps.py
new file mode 100644
index 00000000..84c4be92
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class MediaLibraryConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "vbv_lernwelt.media_files"
diff --git a/server/vbv_lernwelt/media_files/create_default_documents.py b/server/vbv_lernwelt/media_files/create_default_documents.py
new file mode 100644
index 00000000..85194a05
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/create_default_documents.py
@@ -0,0 +1,83 @@
+import os
+
+import factory
+from django.conf import settings
+from wagtail.models import Collection
+
+from vbv_lernwelt.media_files.models import ContentDocument, UserDocument
+from vbv_lernwelt.media_files.tests.media_library_factories import (
+ ContentDocumentFactory,
+ UserDocumentFactory,
+)
+
+
+def delete_default_documents():
+ """deletes all documents"""
+ if "prod" in settings.APP_ENVIRONMENT:
+ raise Exception("This command must not be used in production environment")
+
+ ContentDocument.objects.all().delete()
+ UserDocument.objects.all().delete()
+
+
+def create_default_collections():
+ root, created = Collection.objects.get_or_create(name="Root", depth=0)
+
+
+def create_default_content_documents():
+ """creates a default document for testing purposes"""
+
+ path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "./tests/test_documents/"
+ )
+
+ filename = "Vermittler_Motorfahrzeug_Versicherung_Musterlösung.pdf"
+ document = ContentDocumentFactory(
+ title="Musterlösung Fahrzeug",
+ display_text="Musterlösung Fahrzeug",
+ description="Musterlösung für den Auftrag Fahrzeug",
+ link_display_text="Dokument laden",
+ file=factory.django.FileField(
+ from_path=os.path.join(path, filename), filename=filename
+ ),
+ )
+ document.tags.set(("Fahrzeug", "Musterlösung", "Vermittler"))
+ document.save()
+
+ filename = "TestExcelSheet.xlsx"
+ document = ContentDocumentFactory(
+ title="Mustertabelle",
+ display_text="Mustertabelle",
+ link_display_text="Dokument laden",
+ file=factory.django.FileField(
+ from_path=os.path.join(path, filename), filename=filename
+ ),
+ )
+ document.tags.set(("Vermittler"))
+ document.save()
+
+
+def create_default_user_documents():
+ """creates a default document for testing purposes"""
+
+ path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "./tests/test_documents/"
+ )
+
+ filename = "FallanalyseTeststudent.pdf"
+ document = UserDocumentFactory(
+ title="Lösung Fallanalyse",
+ file=factory.django.FileField(
+ from_path=os.path.join(path, filename), filename=filename
+ ),
+ )
+ document.save()
+
+ filename = "FallanalyseTeststudent.docx"
+ document = UserDocumentFactory(
+ title="Lösung Fallanalyse",
+ file=factory.django.FileField(
+ from_path=os.path.join(path, filename), filename=filename
+ ),
+ )
+ document.save()
diff --git a/server/vbv_lernwelt/media_files/create_default_images.py b/server/vbv_lernwelt/media_files/create_default_images.py
new file mode 100644
index 00000000..2005e962
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/create_default_images.py
@@ -0,0 +1,63 @@
+import os
+
+from django.conf import settings
+from django.core.files import File
+
+from vbv_lernwelt.media_files.models import ContentImage, UserImage
+
+
+def delete_default_images():
+ """deletes all images"""
+ if "prod" in settings.APP_ENVIRONMENT:
+ raise Exception("This command must not be used in production environment")
+ ContentImage.objects.all().delete()
+ UserImage.objects.all().delete()
+
+
+def create_default_images():
+ create_default_content_images()
+ create_default_user_images()
+
+
+def create_default_content_images():
+ path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "./tests/test_images/"
+ )
+
+ images = [
+ ("bike_accident.jpg", "Bike Accident"),
+ ("car_accident.jpg", "Car Accident"),
+ ]
+
+ for filename, title in images:
+ file_path = os.path.join(path, filename)
+ with open(file_path, "rb") as f:
+ image, _ = ContentImage.objects.get_or_create(
+ title=title,
+ file=File(f, name=filename),
+ focal_point_x=600,
+ focal_point_y=600,
+ focal_point_width=300,
+ focal_point_height=300,
+ )
+ image.tags.set(("Fahrzeug", "Unfall", "Vermittler"))
+ image.save()
+
+
+def create_default_user_images():
+ path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "./tests/test_images/"
+ )
+ filename, title = ("user1_profile.jpg", "User1 Profile")
+ file_path = os.path.join(path, filename)
+
+ with open(file_path, "rb") as f:
+ image, _ = UserImage.objects.get_or_create(
+ title=title,
+ file=File(f, name=filename),
+ focal_point_x=600,
+ focal_point_y=600,
+ focal_point_width=300,
+ focal_point_height=300,
+ )
+ image.save()
diff --git a/server/vbv_lernwelt/media_files/graphql/__init__.py b/server/vbv_lernwelt/media_files/graphql/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/media_files/graphql/types.py b/server/vbv_lernwelt/media_files/graphql/types.py
new file mode 100644
index 00000000..a9a48343
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/graphql/types.py
@@ -0,0 +1,18 @@
+import graphene
+from graphene_django import DjangoObjectType
+
+from vbv_lernwelt.media_files.models import ContentDocument
+
+
+class ContentDocumentObjectType(DjangoObjectType):
+ url = graphene.String(source="url")
+
+ class Meta:
+ model = ContentDocument
+ fields = (
+ "id",
+ "display_text",
+ "description",
+ "link_display_text",
+ "thumbnail",
+ )
diff --git a/server/vbv_lernwelt/media_files/management/commands/__init__.py b/server/vbv_lernwelt/media_files/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/media_files/management/commands/create_default_documents.py b/server/vbv_lernwelt/media_files/management/commands/create_default_documents.py
new file mode 100644
index 00000000..48eefd40
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/management/commands/create_default_documents.py
@@ -0,0 +1,15 @@
+import djclick as click
+
+from vbv_lernwelt.media_files.create_default_documents import (
+ create_default_collections,
+ create_default_content_documents,
+ create_default_user_documents,
+)
+
+
+@click.command()
+def command():
+ print("Creating default documents...")
+ create_default_collections()
+ create_default_content_documents()
+ create_default_user_documents()
diff --git a/server/vbv_lernwelt/media_files/management/commands/create_default_images.py b/server/vbv_lernwelt/media_files/management/commands/create_default_images.py
new file mode 100644
index 00000000..7d6c99f1
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/management/commands/create_default_images.py
@@ -0,0 +1,9 @@
+import djclick as click
+
+from vbv_lernwelt.media_files.create_default_images import create_default_images
+
+
+@click.command()
+def command():
+ print("Creating default images...")
+ create_default_images()
diff --git a/server/vbv_lernwelt/media_files/management/commands/delete_documents_and_images.py b/server/vbv_lernwelt/media_files/management/commands/delete_documents_and_images.py
new file mode 100644
index 00000000..05e91861
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/management/commands/delete_documents_and_images.py
@@ -0,0 +1,12 @@
+import djclick as click
+
+from vbv_lernwelt.media_files.create_default_documents import delete_default_documents
+from vbv_lernwelt.media_files.create_default_images import delete_default_images
+
+
+@click.command()
+def command():
+ print("Deleting all images...")
+ delete_default_images()
+ print("Deleting all documents...")
+ delete_default_documents()
diff --git a/server/vbv_lernwelt/media_files/migrations/0001_initial.py b/server/vbv_lernwelt/media_files/migrations/0001_initial.py
new file mode 100644
index 00000000..9f315e56
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/migrations/0001_initial.py
@@ -0,0 +1,436 @@
+# Generated by Django 3.2.20 on 2023-12-05 16:11
+
+import django.db.models.deletion
+import taggit.managers
+import wagtail.images.models
+import wagtail.models.collections
+import wagtail.search.index
+from django.conf import settings
+from django.db import migrations, models
+
+import vbv_lernwelt.media_files.storage_backends
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("taggit", "0005_auto_20220424_2025"),
+ ("wagtailcore", "0089_log_entry_data_json_null_to_object"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ContentImage",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=255, verbose_name="title")),
+ ("width", models.IntegerField(editable=False, verbose_name="width")),
+ ("height", models.IntegerField(editable=False, verbose_name="height")),
+ (
+ "created_at",
+ models.DateTimeField(
+ auto_now_add=True, db_index=True, verbose_name="created at"
+ ),
+ ),
+ ("focal_point_x", models.PositiveIntegerField(blank=True, null=True)),
+ ("focal_point_y", models.PositiveIntegerField(blank=True, null=True)),
+ (
+ "focal_point_width",
+ models.PositiveIntegerField(blank=True, null=True),
+ ),
+ (
+ "focal_point_height",
+ models.PositiveIntegerField(blank=True, null=True),
+ ),
+ ("file_size", models.PositiveIntegerField(editable=False, null=True)),
+ (
+ "file_hash",
+ models.CharField(
+ blank=True, db_index=True, editable=False, max_length=40
+ ),
+ ),
+ (
+ "file",
+ wagtail.images.models.WagtailImageField(
+ height_field="height",
+ storage=vbv_lernwelt.media_files.storage_backends.ContentImagesStorage,
+ upload_to=wagtail.images.models.get_upload_to,
+ verbose_name="file",
+ width_field="width",
+ ),
+ ),
+ (
+ "collection",
+ models.ForeignKey(
+ default=wagtail.models.collections.get_root_collection_id,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="+",
+ to="wagtailcore.collection",
+ verbose_name="collection",
+ ),
+ ),
+ (
+ "tags",
+ taggit.managers.TaggableManager(
+ blank=True,
+ help_text=None,
+ through="taggit.TaggedItem",
+ to="taggit.Tag",
+ verbose_name="tags",
+ ),
+ ),
+ (
+ "uploaded_by_user",
+ models.ForeignKey(
+ blank=True,
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="uploaded by user",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=(
+ wagtail.images.models.ImageFileMixin,
+ wagtail.search.index.Indexed,
+ models.Model,
+ ),
+ ),
+ migrations.CreateModel(
+ name="UserImage",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=255, verbose_name="title")),
+ ("width", models.IntegerField(editable=False, verbose_name="width")),
+ ("height", models.IntegerField(editable=False, verbose_name="height")),
+ (
+ "created_at",
+ models.DateTimeField(
+ auto_now_add=True, db_index=True, verbose_name="created at"
+ ),
+ ),
+ ("focal_point_x", models.PositiveIntegerField(blank=True, null=True)),
+ ("focal_point_y", models.PositiveIntegerField(blank=True, null=True)),
+ (
+ "focal_point_width",
+ models.PositiveIntegerField(blank=True, null=True),
+ ),
+ (
+ "focal_point_height",
+ models.PositiveIntegerField(blank=True, null=True),
+ ),
+ ("file_size", models.PositiveIntegerField(editable=False, null=True)),
+ (
+ "file_hash",
+ models.CharField(
+ blank=True, db_index=True, editable=False, max_length=40
+ ),
+ ),
+ (
+ "file",
+ wagtail.images.models.WagtailImageField(
+ height_field="height",
+ storage=vbv_lernwelt.media_files.storage_backends.UserImagesStorage,
+ upload_to=wagtail.images.models.get_upload_to,
+ verbose_name="file",
+ width_field="width",
+ ),
+ ),
+ (
+ "collection",
+ models.ForeignKey(
+ default=wagtail.models.collections.get_root_collection_id,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="+",
+ to="wagtailcore.collection",
+ verbose_name="collection",
+ ),
+ ),
+ (
+ "tags",
+ taggit.managers.TaggableManager(
+ blank=True,
+ help_text=None,
+ through="taggit.TaggedItem",
+ to="taggit.Tag",
+ verbose_name="tags",
+ ),
+ ),
+ (
+ "uploaded_by_user",
+ models.ForeignKey(
+ blank=True,
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="uploaded by user",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=(
+ wagtail.images.models.ImageFileMixin,
+ wagtail.search.index.Indexed,
+ models.Model,
+ ),
+ ),
+ migrations.CreateModel(
+ name="UserDocument",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=255, verbose_name="title")),
+ (
+ "created_at",
+ models.DateTimeField(auto_now_add=True, verbose_name="created at"),
+ ),
+ ("file_size", models.PositiveIntegerField(editable=False, null=True)),
+ (
+ "file_hash",
+ models.CharField(blank=True, editable=False, max_length=40),
+ ),
+ (
+ "file",
+ models.FileField(
+ storage=vbv_lernwelt.media_files.storage_backends.UserDocumentsStorage,
+ upload_to="documents",
+ verbose_name="file",
+ ),
+ ),
+ (
+ "collection",
+ models.ForeignKey(
+ default=wagtail.models.collections.get_root_collection_id,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="+",
+ to="wagtailcore.collection",
+ verbose_name="collection",
+ ),
+ ),
+ (
+ "tags",
+ taggit.managers.TaggableManager(
+ blank=True,
+ help_text=None,
+ through="taggit.TaggedItem",
+ to="taggit.Tag",
+ verbose_name="tags",
+ ),
+ ),
+ (
+ "uploaded_by_user",
+ models.ForeignKey(
+ blank=True,
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="uploaded by user",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "document",
+ "verbose_name_plural": "documents",
+ "abstract": False,
+ },
+ bases=(wagtail.search.index.Indexed, models.Model),
+ ),
+ migrations.CreateModel(
+ name="ContentDocument",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=255, verbose_name="title")),
+ (
+ "created_at",
+ models.DateTimeField(auto_now_add=True, verbose_name="created at"),
+ ),
+ ("file_size", models.PositiveIntegerField(editable=False, null=True)),
+ (
+ "file_hash",
+ models.CharField(blank=True, editable=False, max_length=40),
+ ),
+ (
+ "file",
+ models.FileField(
+ storage=vbv_lernwelt.media_files.storage_backends.ContentDocumentsStorage,
+ upload_to="documents",
+ verbose_name="file",
+ ),
+ ),
+ ("display_text", models.CharField(default="", max_length=1024)),
+ ("description", models.TextField(blank=True, default="")),
+ (
+ "link_display_text",
+ models.CharField(blank=True, default="", max_length=1024),
+ ),
+ (
+ "thumbnail",
+ models.CharField(blank=True, default="", max_length=1024),
+ ),
+ (
+ "collection",
+ models.ForeignKey(
+ default=wagtail.models.collections.get_root_collection_id,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="+",
+ to="wagtailcore.collection",
+ verbose_name="collection",
+ ),
+ ),
+ (
+ "tags",
+ taggit.managers.TaggableManager(
+ blank=True,
+ help_text=None,
+ through="taggit.TaggedItem",
+ to="taggit.Tag",
+ verbose_name="tags",
+ ),
+ ),
+ (
+ "uploaded_by_user",
+ models.ForeignKey(
+ blank=True,
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="uploaded by user",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "document",
+ "verbose_name_plural": "documents",
+ "abstract": False,
+ },
+ bases=(wagtail.search.index.Indexed, models.Model),
+ ),
+ migrations.CreateModel(
+ name="UserImageRendition",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("filter_spec", models.CharField(db_index=True, max_length=255)),
+ (
+ "file",
+ wagtail.images.models.WagtailImageField(
+ height_field="height",
+ upload_to=wagtail.images.models.get_rendition_upload_to,
+ width_field="width",
+ ),
+ ),
+ ("width", models.IntegerField(editable=False)),
+ ("height", models.IntegerField(editable=False)),
+ (
+ "focal_point_key",
+ models.CharField(
+ blank=True, default="", editable=False, max_length=16
+ ),
+ ),
+ (
+ "image",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="renditions",
+ to="media_files.userimage",
+ ),
+ ),
+ ],
+ options={
+ "unique_together": {("image", "filter_spec", "focal_point_key")},
+ },
+ bases=(wagtail.images.models.ImageFileMixin, models.Model),
+ ),
+ migrations.CreateModel(
+ name="ContentImageRendition",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("filter_spec", models.CharField(db_index=True, max_length=255)),
+ (
+ "file",
+ wagtail.images.models.WagtailImageField(
+ height_field="height",
+ upload_to=wagtail.images.models.get_rendition_upload_to,
+ width_field="width",
+ ),
+ ),
+ ("width", models.IntegerField(editable=False)),
+ ("height", models.IntegerField(editable=False)),
+ (
+ "focal_point_key",
+ models.CharField(
+ blank=True, default="", editable=False, max_length=16
+ ),
+ ),
+ (
+ "image",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="renditions",
+ to="media_files.contentimage",
+ ),
+ ),
+ ],
+ options={
+ "unique_together": {("image", "filter_spec", "focal_point_key")},
+ },
+ bases=(wagtail.images.models.ImageFileMixin, models.Model),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/media_files/migrations/__init__.py b/server/vbv_lernwelt/media_files/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/media_files/models.py b/server/vbv_lernwelt/media_files/models.py
new file mode 100644
index 00000000..05cc9f65
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/models.py
@@ -0,0 +1,106 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from wagtail.documents.models import AbstractDocument, Document
+from wagtail.images.models import (
+ AbstractImage,
+ AbstractRendition,
+ get_upload_to,
+ Image,
+ WagtailImageField,
+)
+
+from vbv_lernwelt.core.admin import User
+from vbv_lernwelt.media_files.storage_backends import (
+ ContentDocumentsStorage,
+ ContentImagesStorage,
+ UserDocumentsStorage,
+ UserImagesStorage,
+)
+
+
+class ContentDocument(AbstractDocument):
+ """
+ Content documents are documents that are handled by the CMS.
+ """
+
+ file = models.FileField(
+ upload_to="documents", verbose_name=_("file"), storage=ContentDocumentsStorage
+ )
+ display_text = models.CharField(max_length=1024, default="")
+ description = models.TextField(default="", blank=True)
+ link_display_text = models.CharField(max_length=1024, default="", blank=True)
+ thumbnail = models.CharField(default="", max_length=1024, blank=True)
+
+ admin_form_fields = Document.admin_form_fields + (
+ "display_text",
+ "description",
+ "link_display_text",
+ "thumbnail",
+ )
+
+ def has_permission(self, user: User):
+ # TODO: 20-11-2023 Renzo: add more advanced permission handling
+ if user.is_authenticated:
+ return True
+
+
+class UserDocument(AbstractDocument):
+ """
+ Documents that are uploaded by the user and not visible in the CMS.
+ Still they are inherited from the Wagtail Document model.
+ """
+
+ file = models.FileField(
+ upload_to="documents", verbose_name=_("file"), storage=UserDocumentsStorage
+ )
+
+
+class ContentImage(AbstractImage):
+ """
+ Content images are images that are handled by the CMS.
+ """
+
+ file = WagtailImageField(
+ verbose_name=_("file"),
+ upload_to=get_upload_to,
+ width_field="width",
+ height_field="height",
+ storage=ContentImagesStorage,
+ )
+ admin_form_fields = Image.admin_form_fields + (
+ # Then add the field names here to make them appear in the form:
+ # 'caption',
+ )
+
+
+class UserImage(AbstractImage):
+ """
+ User images are images that are uploaded by the user and not visible in the CMS.
+ Still they are inherited from the Wagtail Image model.
+ """
+
+ file = WagtailImageField(
+ verbose_name=_("file"),
+ upload_to=get_upload_to,
+ width_field="width",
+ height_field="height",
+ storage=UserImagesStorage,
+ )
+
+
+class ContentImageRendition(AbstractRendition):
+ image = models.ForeignKey(
+ ContentImage, on_delete=models.CASCADE, related_name="renditions"
+ )
+
+ class Meta:
+ unique_together = (("image", "filter_spec", "focal_point_key"),)
+
+
+class UserImageRendition(AbstractRendition):
+ image = models.ForeignKey(
+ UserImage, on_delete=models.CASCADE, related_name="renditions"
+ )
+
+ class Meta:
+ unique_together = (("image", "filter_spec", "focal_point_key"),)
diff --git a/server/vbv_lernwelt/media_files/storage_backends.py b/server/vbv_lernwelt/media_files/storage_backends.py
new file mode 100644
index 00000000..9bee1f3a
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/storage_backends.py
@@ -0,0 +1,24 @@
+from storages.backends.s3boto3 import S3Boto3Storage
+
+
+# inspired by https://theyashshahs.medium.com/aws-s3-signed-urls-in-django-d9e66853a42f
+
+
+class ContentDocumentsStorage(S3Boto3Storage):
+ location = "media/content_documents"
+ default_acl = "private"
+
+
+class ContentImagesStorage(S3Boto3Storage):
+ location = "media/content_images"
+ default_acl = "private"
+
+
+class UserDocumentsStorage(S3Boto3Storage):
+ location = "media/user_documents"
+ default_acl = "private"
+
+
+class UserImagesStorage(S3Boto3Storage):
+ location = "media/user_images"
+ default_acl = "private"
diff --git a/server/vbv_lernwelt/media_files/tests/__init__.py b/server/vbv_lernwelt/media_files/tests/__init__.py
new file mode 100644
index 00000000..ae29b5e3
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/tests/__init__.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+#
+# Iterativ GmbH
+# http://www.iterativ.ch/
+#
+# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
+#
+# Created on 2022-08-16
+# @author: lorenz.padberg@iterativ.ch
diff --git a/server/vbv_lernwelt/media_files/tests/media_library_factories.py b/server/vbv_lernwelt/media_files/tests/media_library_factories.py
new file mode 100644
index 00000000..3b6190de
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/tests/media_library_factories.py
@@ -0,0 +1,18 @@
+import wagtail_factories
+
+from vbv_lernwelt.media_files.models import ContentDocument, UserDocument
+
+
+class ContentDocumentFactory(wagtail_factories.DocumentFactory):
+ link_display_text = "Dokument herunter laden"
+ description = ""
+
+ class Meta:
+ model = ContentDocument
+ django_get_or_create = ("title", "description")
+
+
+class UserDocumentFactory(wagtail_factories.DocumentFactory):
+ class Meta:
+ model = UserDocument
+ django_get_or_create = ("title",)
diff --git a/server/vbv_lernwelt/media_files/tests/test_content_document_serving.py b/server/vbv_lernwelt/media_files/tests/test_content_document_serving.py
new file mode 100644
index 00000000..bcc09eee
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/tests/test_content_document_serving.py
@@ -0,0 +1,49 @@
+import datetime
+
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import TestCase
+from wagtail.models import Collection
+
+from vbv_lernwelt.core.create_default_users import create_default_users
+from vbv_lernwelt.core.models import User
+from vbv_lernwelt.media_files.models import ContentDocument
+
+TITLE = "Musterlösung Fahrzeug"
+
+
+class TestContentDocumentServing(TestCase):
+ def setUp(self):
+ create_default_users()
+ now_str = str(datetime.datetime.now().strftime("%d-%m-%Y-%H-%M"))
+ collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
+ document = ContentDocument.objects.create(
+ title=TITLE,
+ display_text="Musterlösung Fahrzeug",
+ description="Musterlösung für den Auftrag Fahrzeug",
+ link_display_text="Dokument laden",
+ file=SimpleUploadedFile(
+ f"testdocument_{now_str}.txt", b"these are the file contents!"
+ ),
+ collection=collection,
+ )
+ document.save()
+
+ def test_download_document_from_wagtail_logged_in_user_200(self):
+ self.user = User.objects.get(username="admin")
+ self.client.login(username="admin", password="test")
+ document = ContentDocument.objects.get(title=TITLE)
+ client = self.client
+
+ self.assertEqual(
+ document.url, f"/server/documents/{document.id}/{document.filename}"
+ )
+
+ response = client.get(document.url)
+ self.assertEqual(response.status_code, 200)
+
+ def test_download_document_from_wagtail_anonymous_user_redirect_to_login(self):
+ document = ContentDocument.objects.get(title=TITLE)
+ self.client.logout()
+ response = self.client.get(document.url)
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue("login" in response.url)
diff --git a/server/vbv_lernwelt/media_files/tests/test_content_document_storage.py b/server/vbv_lernwelt/media_files/tests/test_content_document_storage.py
new file mode 100644
index 00000000..60c7a503
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/tests/test_content_document_storage.py
@@ -0,0 +1,88 @@
+import datetime
+from unittest import skipIf
+
+from django.conf import settings
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import override_settings, TestCase
+from wagtail.models import Collection
+
+from vbv_lernwelt.media_files.models import ContentDocument
+
+TITLE = "Musterlösung Fahrzeug"
+
+
+class TestContentDocumentStorage(TestCase):
+ @override_settings(FILE_UPLOAD_STORAGE="s3")
+ def setUp(self):
+ now_str = str(datetime.datetime.now().strftime("%d-%m-%Y-%H"))
+ collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
+ document = ContentDocument.objects.create(
+ title=TITLE,
+ display_text="Musterlösung Fahrzeug",
+ description="Musterlösung für den Auftrag Fahrzeug",
+ link_display_text="Dokument laden",
+ file=SimpleUploadedFile(
+ f"testdocument_{now_str}.txt", b"these are the file contents!"
+ ),
+ collection=collection,
+ )
+ document.save()
+
+ def tearDown(self):
+ for doc in ContentDocument.objects.all():
+ doc.file.storage.delete(doc.file.name)
+ doc.delete()
+
+ def test_new_document_is_created(self):
+ self.assertEqual(ContentDocument.objects.all().count(), 1)
+ self.assertEqual(ContentDocument.objects.filter(title=TITLE).count(), 1)
+
+ def test_document_exists_on_s3(self):
+ document = ContentDocument.objects.get(title=TITLE)
+ self.assertTrue(document.file.storage.exists(document.file.name))
+
+ def test_download_document_from_s3(self):
+ document = ContentDocument.objects.get(title=TITLE)
+ self.assertEqual(document.file.read(), b"these are the file contents!")
+
+ def test_delete_document_from_s3(self):
+ document = ContentDocument.objects.get(title=TITLE)
+ document.file.storage.delete(document.file.name)
+ document.delete()
+ self.assertFalse(document.file.storage.exists(document.file.name))
+
+ @skipIf(
+ settings.AWS_S3_FILE_OVERWRITE,
+ "This test only works if AWS_S3_FILE_OVERWRITE is False",
+ )
+ def test_duplicate_title_and_filename(self):
+ collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
+ document = ContentDocument.objects.create(
+ title=TITLE,
+ display_text="Musterlösung Fahrzeug",
+ description="Musterlösung für den Auftrag Fahrzeug",
+ link_display_text="Dokument laden",
+ file=SimpleUploadedFile(
+ "testdocument.txt", b"these are the file contents! For sure!"
+ ),
+ collection=collection,
+ )
+
+ document2 = ContentDocument.objects.create(
+ title=TITLE,
+ display_text="Musterlösung Fahrzeug",
+ description="Musterlösung für den Auftrag Fahrzeug",
+ link_display_text="Dokument laden",
+ file=SimpleUploadedFile(
+ "testdocument.txt", b"these are the file contents! But different!"
+ ),
+ collection=collection,
+ )
+
+ self.assertEqual(
+ document.file.read(), b"these are the file contents! For sure!"
+ )
+ self.assertEqual(
+ document2.file.read(), b"these are the file contents! But different!"
+ )
+ self.assertTrue(document.file.name != document2.file.name)
diff --git a/server/vbv_lernwelt/media_files/tests/test_documents/FallanalyseTeststudent.docx b/server/vbv_lernwelt/media_files/tests/test_documents/FallanalyseTeststudent.docx
new file mode 100644
index 00000000..17aa0c4c
Binary files /dev/null and b/server/vbv_lernwelt/media_files/tests/test_documents/FallanalyseTeststudent.docx differ
diff --git a/server/vbv_lernwelt/media_files/tests/test_documents/FallanalyseTeststudent.pdf b/server/vbv_lernwelt/media_files/tests/test_documents/FallanalyseTeststudent.pdf
new file mode 100644
index 00000000..71d1f8fa
Binary files /dev/null and b/server/vbv_lernwelt/media_files/tests/test_documents/FallanalyseTeststudent.pdf differ
diff --git a/server/vbv_lernwelt/media_files/tests/test_documents/TestExcelSheet.xlsx b/server/vbv_lernwelt/media_files/tests/test_documents/TestExcelSheet.xlsx
new file mode 100644
index 00000000..ae30e42d
Binary files /dev/null and b/server/vbv_lernwelt/media_files/tests/test_documents/TestExcelSheet.xlsx differ
diff --git a/server/vbv_lernwelt/media_files/tests/test_documents/Vermittler_Motorfahrzeug_Versicherung_Musterlösung.pdf b/server/vbv_lernwelt/media_files/tests/test_documents/Vermittler_Motorfahrzeug_Versicherung_Musterlösung.pdf
new file mode 100644
index 00000000..00f39a67
Binary files /dev/null and b/server/vbv_lernwelt/media_files/tests/test_documents/Vermittler_Motorfahrzeug_Versicherung_Musterlösung.pdf differ
diff --git a/server/vbv_lernwelt/media_files/tests/test_images/bike_accident.jpg b/server/vbv_lernwelt/media_files/tests/test_images/bike_accident.jpg
new file mode 100644
index 00000000..68677856
Binary files /dev/null and b/server/vbv_lernwelt/media_files/tests/test_images/bike_accident.jpg differ
diff --git a/server/vbv_lernwelt/media_files/tests/test_images/car_accident.jpg b/server/vbv_lernwelt/media_files/tests/test_images/car_accident.jpg
new file mode 100644
index 00000000..e3f0035a
Binary files /dev/null and b/server/vbv_lernwelt/media_files/tests/test_images/car_accident.jpg differ
diff --git a/server/vbv_lernwelt/media_files/tests/test_images/user1_profile.jpg b/server/vbv_lernwelt/media_files/tests/test_images/user1_profile.jpg
new file mode 100644
index 00000000..5fd0707d
Binary files /dev/null and b/server/vbv_lernwelt/media_files/tests/test_images/user1_profile.jpg differ
diff --git a/server/vbv_lernwelt/media_library/create_default_documents.py b/server/vbv_lernwelt/media_library/create_default_documents.py
deleted file mode 100644
index a8380206..00000000
--- a/server/vbv_lernwelt/media_library/create_default_documents.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import os
-
-import factory
-from wagtail.core.models import Collection
-
-from vbv_lernwelt.course.models import Course
-from vbv_lernwelt.media_library.models import LibraryDocument
-from vbv_lernwelt.media_library.tests.media_library_factories import (
- LibraryDocumentFactory,
-)
-
-
-def create_default_collections():
- c = Collection.objects.all().delete()
-
- root, created = Collection.objects.get_or_create(name="Root", depth=0)
-
- for course in Course.objects.all():
- course_collection = root.add_child(name=course.title)
- for cat in course.coursecategory_set.all():
- cat_collection = course_collection.add_child(name=cat.title)
-
-
-def create_default_documents():
- LibraryDocument.objects.all().delete()
-
- path = os.path.join(
- os.path.dirname(os.path.abspath(__file__)), "../static/media/documents/"
- )
-
- collection = Collection.objects.get(name="Fahrzeug")
-
- filename = "SchweizerischesZivilgesetzbuch.pdf"
- document = LibraryDocumentFactory(
- title="V1 C25 ZGB CH",
- display_text="Schweizerisches Zivilgesetzbuch",
- description="Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.",
- link_display_text="Dokument laden",
- file=factory.django.FileField(
- from_path=os.path.join(path, filename), filename=filename
- ),
- collection=collection,
- )
-
- filename = "SmallPDF.pdf"
- document = LibraryDocumentFactory(
- title="V1 C25 ",
- display_text="Pdf showcase ",
- description="Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.",
- link_display_text="Dokument laden",
- file=factory.django.FileField(
- from_path=os.path.join(path, filename), filename=filename
- ),
- collection=collection,
- )
diff --git a/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py b/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py
index fc970baa..7c912559 100644
--- a/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py
+++ b/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py
@@ -106,19 +106,19 @@ class TestAssignmentCourseRemindersTest(TestCase):
notification = Notification.objects.get(
recipient__username=expected_recipient
)
- self.assertEquals(action_object, notification.action_object)
- self.assertEquals("ASSIGNMENT_REMINDER", notification.notification_trigger)
- self.assertEquals("INFORMATION", notification.notification_category)
- self.assertEquals(EXPECTED_MEMBER_VERB, notification.verb)
+ self.assertEqual(action_object, notification.action_object)
+ self.assertEqual("ASSIGNMENT_REMINDER", notification.notification_trigger)
+ self.assertEqual("INFORMATION", notification.notification_category)
+ self.assertEqual(EXPECTED_MEMBER_VERB, notification.verb)
template_data = notification.data["template_data"]
- self.assertEquals(
+ self.assertEqual(
action_object.learning_content.get_parent_circle().title,
template_data["circle"],
)
- self.assertEquals(
+ self.assertEqual(
action_object.learning_content.get_frontend_url(),
notification.target_url,
)
@@ -140,17 +140,17 @@ class TestAssignmentCourseRemindersTest(TestCase):
)
if assignment_type == AssignmentType.CASEWORK:
- self.assertEquals(
+ self.assertEqual(
EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER.name,
email_template,
)
elif assignment_type == AssignmentType.PREP_ASSIGNMENT:
- self.assertEquals(
+ self.assertEqual(
EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER.name,
email_template,
)
elif type(action_object) == CourseSessionEdoniqTest:
- self.assertEquals(
+ self.assertEqual(
EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER.name,
email_template,
)
@@ -176,7 +176,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
send_assignment_reminder_notifications()
# THEN
- self.assertEquals(3, len(Notification.objects.all()))
+ self.assertEqual(3, len(Notification.objects.all()))
self._assert_member_assignment_notifications(
action_object=should_be_sent,
expected_recipients=RECIPIENT_STUDENTS,
@@ -214,7 +214,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
send_assignment_reminder_notifications()
# THEN
- self.assertEquals(3, len(Notification.objects.all()))
+ self.assertEqual(3, len(Notification.objects.all()))
self._assert_member_assignment_notifications(
action_object=casework,
expected_recipients=RECIPIENT_STUDENTS,
@@ -236,16 +236,16 @@ class TestAssignmentCourseRemindersTest(TestCase):
send_assignment_reminder_notifications()
# THEN
- self.assertEquals(1, len(Notification.objects.all()))
+ self.assertEqual(1, len(Notification.objects.all()))
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
- self.assertEquals(casework, notification.action_object)
- self.assertEquals("INFORMATION", notification.notification_category)
- self.assertEquals(EXPECTED_EXPERT_VERB, notification.verb)
- self.assertEquals(
+ self.assertEqual(casework, notification.action_object)
+ self.assertEqual("INFORMATION", notification.notification_category)
+ self.assertEqual(EXPECTED_EXPERT_VERB, notification.verb)
+ self.assertEqual(
casework.evaluation_deadline.url_expert, notification.target_url
)
- self.assertEquals(
+ self.assertEqual(
"CASEWORK_EXPERT_EVALUATION_REMINDER", notification.notification_trigger
)
@@ -276,7 +276,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
send_assignment_reminder_notifications()
# THEN
- self.assertEquals(3, len(Notification.objects.all()))
+ self.assertEqual(3, len(Notification.objects.all()))
self._assert_member_assignment_notifications(
action_object=prep_assignment,
expected_recipients=RECIPIENT_STUDENTS,
diff --git a/server/vbv_lernwelt/notify/tests/test_attendance_reminders.py b/server/vbv_lernwelt/notify/tests/test_attendance_reminders.py
index 3a0a032c..44fdf084 100644
--- a/server/vbv_lernwelt/notify/tests/test_attendance_reminders.py
+++ b/server/vbv_lernwelt/notify/tests/test_attendance_reminders.py
@@ -65,52 +65,52 @@ class TestAttendanceCourseReminders(TestCase):
send_attendance_reminder_notifications()
- self.assertEquals(4, len(Notification.objects.all()))
+ self.assertEqual(4, len(Notification.objects.all()))
notification = Notification.objects.get(
recipient__username="test-student1@example.com"
)
- self.assertEquals(
+ self.assertEqual(
"Erinnerung: Bald findet ein Präsenzkurs statt",
notification.verb,
)
- self.assertEquals(
+ self.assertEqual(
"INFORMATION",
notification.notification_category,
)
- self.assertEquals(
+ self.assertEqual(
"ATTENDANCE_COURSE_REMINDER",
notification.notification_trigger,
)
- self.assertEquals(
+ self.assertEqual(
self.csac,
notification.action_object,
)
- self.assertEquals(
+ self.assertEqual(
self.csac.course_session,
notification.course_session,
)
- self.assertEquals(
+ self.assertEqual(
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
notification.target_url,
)
- self.assertEquals(
+ self.assertEqual(
self.csac.learning_content.title,
notification.data["template_data"]["attendance_course"],
)
- self.assertEquals(
+ self.assertEqual(
self.csac.location,
notification.data["template_data"]["location"],
)
- self.assertEquals(
+ self.assertEqual(
self.csac.trainer,
notification.data["template_data"]["trainer"],
)
- self.assertEquals(
+ self.assertEqual(
self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"),
notification.data["template_data"]["start"],
)
- self.assertEquals(
+ self.assertEqual(
self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"),
notification.data["template_data"]["end"],
)
diff --git a/server/vbv_lernwelt/static/icons/icon-handlungsfelder-overview.svg b/server/vbv_lernwelt/static/icons/icon-handlungsfelder-overview.svg
index 71716afc..1a59a6b1 100644
--- a/server/vbv_lernwelt/static/icons/icon-handlungsfelder-overview.svg
+++ b/server/vbv_lernwelt/static/icons/icon-handlungsfelder-overview.svg
@@ -1,3 +1,5 @@
diff --git a/trufflehog-allow.json b/trufflehog-allow.json
index 649d7e2f..af10024d 100644
--- a/trufflehog-allow.json
+++ b/trufflehog-allow.json
@@ -7,6 +7,7 @@
"ignore hash 6": "A035C8C19219BA821ECEA86B64E628F8D684696D",
"ignore hash 7": "96334b4eb6a7ae5b0d86abd7febcbcc67323bb94",
"ignore hash 8": "MTgwMTYwfEFQTXxBUFBMSUNBVElPTnwxMDQ5Njk0MDU0",
+ "ignore hash 9": "82ef9a8178dcb4df0b71540fa06d7da826ecb26e1977e230bdc8c9d6f9f1af84",
"json base64 content": "regex:\"content\": \"",
"img base64 content": "regex:data:image/png;base64,.*",
"sentry url": "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352",
diff --git a/trufflehog-exclude-patterns.txt b/trufflehog-exclude-patterns.txt
index bee94b37..7e7e012a 100644
--- a/trufflehog-exclude-patterns.txt
+++ b/trufflehog-exclude-patterns.txt
@@ -7,6 +7,7 @@ server/vbv_lernwelt/notify/email/email_services.py
server/vbv_lernwelt/static/
server/vbv_lernwelt/media/
server/vbv_lernwelt/edoniq_test/certificates/test.key
+server/vbv_lernwelt/shop/tests/test_create_signature.py
supabase.md
scripts/supabase/init.sql
ramon.wenger@iterativ.ch.gpg