Merge branch 'develop' into feature/VBV-597-umsetzung-cockpit-lernbegleitung
This commit is contained in:
commit
e5ad3f08d2
|
|
@ -87,10 +87,14 @@ def main(app_name, image_name, environment_file):
|
||||||
"IT_DJANGO_SECRET_KEY": env.str(
|
"IT_DJANGO_SECRET_KEY": env.str(
|
||||||
"IT_DJANGO_SECRET_KEY", generate_random_string(63)
|
"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_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""),
|
||||||
"AWS_S3_REGION_NAME": "eu-central-1",
|
"AWS_S3_REGION_NAME": env.str("AWS_S3_REGION_NAME", "eu-central-1"),
|
||||||
"AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch",
|
"AWS_STORAGE_BUCKET_NAME": env.str(
|
||||||
|
"AWS_STORAGE_BUCKET_NAME", "myvbv-dev.iterativ.ch"
|
||||||
|
),
|
||||||
"FILE_UPLOAD_STORAGE": "s3",
|
"FILE_UPLOAD_STORAGE": "s3",
|
||||||
"IT_DJANGO_DEBUG": "false",
|
"IT_DJANGO_DEBUG": "false",
|
||||||
"IT_SERVE_VUE": "false",
|
"IT_SERVE_VUE": "false",
|
||||||
|
|
|
||||||
|
|
@ -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 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 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 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 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 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 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 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 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 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.
|
* 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.
|
* 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.
|
* 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) {
|
export function graphql(source: string) {
|
||||||
return (documents as any)[source] ?? {};
|
return (documents as any)[source] ?? {};
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -9,7 +9,8 @@ type Query {
|
||||||
learning_content_media_library: LearningContentMediaLibraryObjectType
|
learning_content_media_library: LearningContentMediaLibraryObjectType
|
||||||
learning_content_assignment: LearningContentAssignmentObjectType
|
learning_content_assignment: LearningContentAssignmentObjectType
|
||||||
learning_content_attendance_course: LearningContentAttendanceCourseObjectType
|
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_learning_module: LearningContentLearningModuleObjectType
|
||||||
learning_content_knowledge_assessment: LearningContentKnowledgeAssessmentObjectType
|
learning_content_knowledge_assessment: LearningContentKnowledgeAssessmentObjectType
|
||||||
learning_content_placeholder: LearningContentPlaceholderObjectType
|
learning_content_placeholder: LearningContentPlaceholderObjectType
|
||||||
|
|
@ -486,6 +487,7 @@ type AssignmentObjectType implements CoursePageInterface {
|
||||||
max_points: Int
|
max_points: Int
|
||||||
learning_content: LearningContentInterface
|
learning_content: LearningContentInterface
|
||||||
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
|
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
|
||||||
|
solution_sample: ContentDocumentObjectType
|
||||||
}
|
}
|
||||||
|
|
||||||
"""An enumeration."""
|
"""An enumeration."""
|
||||||
|
|
@ -605,6 +607,15 @@ schema (one of the key benefits of GraphQL).
|
||||||
"""
|
"""
|
||||||
scalar JSONString
|
scalar JSONString
|
||||||
|
|
||||||
|
type ContentDocumentObjectType {
|
||||||
|
id: ID!
|
||||||
|
display_text: String!
|
||||||
|
description: String!
|
||||||
|
link_display_text: String!
|
||||||
|
thumbnail: String!
|
||||||
|
url: String
|
||||||
|
}
|
||||||
|
|
||||||
"""An enumeration."""
|
"""An enumeration."""
|
||||||
enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
|
enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
|
||||||
"""PRAXIS_ASSIGNMENT"""
|
"""PRAXIS_ASSIGNMENT"""
|
||||||
|
|
@ -708,7 +719,23 @@ type LearningContentMediaLibraryObjectType implements CoursePageInterface & Lear
|
||||||
circle: CircleLightObjectType
|
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!
|
id: ID!
|
||||||
title: String!
|
title: String!
|
||||||
slug: String!
|
slug: String!
|
||||||
|
|
@ -834,7 +861,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
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
|
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
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"
|
||||||
export const CompetencePerformanceStatisticsSummaryType = "CompetencePerformanceStatisticsSummaryType";
|
export const CompetencePerformanceStatisticsSummaryType = "CompetencePerformanceStatisticsSummaryType";
|
||||||
export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
|
export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
|
||||||
export const CompetencesStatisticsType = "CompetencesStatisticsType";
|
export const CompetencesStatisticsType = "CompetencesStatisticsType";
|
||||||
|
export const ContentDocumentObjectType = "ContentDocumentObjectType";
|
||||||
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
||||||
export const CourseObjectType = "CourseObjectType";
|
export const CourseObjectType = "CourseObjectType";
|
||||||
export const CoursePageInterface = "CoursePageInterface";
|
export const CoursePageInterface = "CoursePageInterface";
|
||||||
|
|
@ -54,7 +55,8 @@ export const LearningContentAssignmentObjectType = "LearningContentAssignmentObj
|
||||||
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
|
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
|
||||||
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
|
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
|
||||||
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
|
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
|
||||||
export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType";
|
export const LearningContentFeedbackUKObjectType = "LearningContentFeedbackUKObjectType";
|
||||||
|
export const LearningContentFeedbackVVObjectType = "LearningContentFeedbackVVObjectType";
|
||||||
export const LearningContentInterface = "LearningContentInterface";
|
export const LearningContentInterface = "LearningContentInterface";
|
||||||
export const LearningContentKnowledgeAssessmentObjectType = "LearningContentKnowledgeAssessmentObjectType";
|
export const LearningContentKnowledgeAssessmentObjectType = "LearningContentKnowledgeAssessmentObjectType";
|
||||||
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";
|
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
|
||||||
tasks
|
tasks
|
||||||
title
|
title
|
||||||
translation_key
|
translation_key
|
||||||
|
solution_sample {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
competence_certificate {
|
competence_certificate {
|
||||||
...CoursePageFields
|
...CoursePageFields
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@
|
||||||
"performanceObjectivesTitle": "Leistungsziele",
|
"performanceObjectivesTitle": "Leistungsziele",
|
||||||
"showAssessmentDocument": "Bewertungsinstrument anzeigen",
|
"showAssessmentDocument": "Bewertungsinstrument anzeigen",
|
||||||
"submissionNotificationDisclaimer": "{{name}} wird deine Ergebnisse bewerten. Du wirst per Benachrichtigung informiert, sobald die Bewertung für dich freigegeben wurde.",
|
"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",
|
"submitAssignment": "Ergebnisse abgeben",
|
||||||
"taskDefinition": "Bearbeite die Teilaufgaben und dokumentiere deine Ergebnisse.",
|
"taskDefinition": "Bearbeite die Teilaufgaben und dokumentiere deine Ergebnisse.",
|
||||||
"taskDefinitionTitle": "Aufgabenstellung",
|
"taskDefinitionTitle": "Aufgabenstellung",
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ const appointments = computed(() => {
|
||||||
.allDueDates()
|
.allDueDates()
|
||||||
.filter(
|
.filter(
|
||||||
(dueDate) =>
|
(dueDate) =>
|
||||||
|
hasDueDate(dueDate) &&
|
||||||
isMatchingCourse(dueDate) &&
|
isMatchingCourse(dueDate) &&
|
||||||
isMatchingSession(dueDate) &&
|
isMatchingSession(dueDate) &&
|
||||||
isMatchingCircle(dueDate)
|
isMatchingCircle(dueDate)
|
||||||
|
|
@ -108,6 +109,10 @@ const isMatchingCircle = (dueDate: DueDate) =>
|
||||||
const isMatchingCourse = (dueDate: DueDate) =>
|
const isMatchingCourse = (dueDate: DueDate) =>
|
||||||
courseSessions.value.map((cs) => cs.id).includes(dueDate.course_session_id);
|
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 numAppointmentsToShow = ref(7);
|
||||||
const canLoadMore = computed(() => {
|
const canLoadMore = computed(() => {
|
||||||
return numAppointmentsToShow.value < appointments.value.length;
|
return numAppointmentsToShow.value < appointments.value.length;
|
||||||
|
|
|
||||||
|
|
@ -18,70 +18,24 @@
|
||||||
</span>
|
</span>
|
||||||
{{ $t("feedback.feedbackPageInfo") }}
|
{{ $t("feedback.feedbackPageInfo") }}
|
||||||
</p>
|
</p>
|
||||||
<ol v-if="feedbackData.amount > 0">
|
<FeedbackPageVV v-if="feedbackType === 'vv'" :feedback-data="feedbackData" />
|
||||||
<li
|
<FeedbackPageUK
|
||||||
v-for="(question, i) in orderedQuestions"
|
v-else-if="feedbackType === 'uk'"
|
||||||
:key="i"
|
:feedback-data="feedbackData"
|
||||||
:data-cy="`question-${i + 1}`"
|
/>
|
||||||
>
|
|
||||||
<RatingScale
|
|
||||||
v-if="ratingKeys.includes(question.key)"
|
|
||||||
class="mb-8 bg-white"
|
|
||||||
:ratings="feedbackData.questions[question.key]"
|
|
||||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
|
||||||
:text="question.question"
|
|
||||||
/>
|
|
||||||
<VerticalBarChart
|
|
||||||
v-else-if="verticalChartKyes.includes(question.key)"
|
|
||||||
class="mb-8 bg-white"
|
|
||||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
|
||||||
:ratings="feedbackData.questions[question.key]"
|
|
||||||
:text="question.question"
|
|
||||||
:ratio="0.2"
|
|
||||||
/>
|
|
||||||
<OpenFeedback
|
|
||||||
v-else-if="
|
|
||||||
openKeys.includes(question.key) && feedbackData.questions[question.key]
|
|
||||||
"
|
|
||||||
class="mb-8 bg-white"
|
|
||||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
|
||||||
:text="question.question"
|
|
||||||
:answers="feedbackData.questions[question.key].filter((a: string) => a !== '')"
|
|
||||||
></OpenFeedback>
|
|
||||||
<HorizontalBarChart
|
|
||||||
v-else-if="
|
|
||||||
horizontalChartKeys.includes(question.key) &&
|
|
||||||
feedbackData.questions[question.key]
|
|
||||||
"
|
|
||||||
class="mb-8 bg-white"
|
|
||||||
:title="`${$t('feedback.questionTitle')} ${i}`"
|
|
||||||
:text="question.question"
|
|
||||||
:items="feedbackData.questions[question.key].map((a: string) => `${a}%`)"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
|
|
||||||
import OpenFeedback from "@/components/ui/OpenFeedback.vue";
|
|
||||||
import RatingScale from "@/components/ui/RatingScale.vue";
|
|
||||||
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
|
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
import { itGet } from "@/fetchHelpers";
|
import { itGet } from "@/fetchHelpers";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useTranslation } from "i18next-vue";
|
import type { FeedbackData, FeedbackType } from "@/types";
|
||||||
|
import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue";
|
||||||
interface FeedbackData {
|
import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue";
|
||||||
amount: number;
|
|
||||||
questions: {
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
@ -91,72 +45,21 @@ const props = defineProps<{
|
||||||
log.debug("FeedbackPage created", props.circleId);
|
log.debug("FeedbackPage created", props.circleId);
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const orderedQuestions = [
|
|
||||||
{
|
|
||||||
key: "satisfaction",
|
|
||||||
question: t("feedback.satisfactionLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "goal_attainment",
|
|
||||||
question: t("feedback.goalAttainmentLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "proficiency",
|
|
||||||
question: t("feedback.proficiencyLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "preparation_task_clarity",
|
|
||||||
question: t("feedback.preparationTaskClarityLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "instructor_competence",
|
|
||||||
question: t("feedback.instructorCompetenceLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "instructor_respect",
|
|
||||||
question: t("feedback.instructorRespectLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "instructor_open_feedback",
|
|
||||||
question: t("feedback.instructorOpenFeedbackLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "would_recommend",
|
|
||||||
question: t("feedback.recommendLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "course_negative_feedback",
|
|
||||||
question: t("feedback.courseNegativeFeedbackLabel"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "course_positive_feedback",
|
|
||||||
question: t("feedback.coursePositiveFeedbackLabel"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ratingKeys = [
|
|
||||||
"satisfaction",
|
|
||||||
"goal_attainment",
|
|
||||||
"instructor_competence",
|
|
||||||
"instructor_respect",
|
|
||||||
];
|
|
||||||
const verticalChartKyes = ["preparation_task_clarity", "would_recommend"];
|
|
||||||
const horizontalChartKeys = ["proficiency"];
|
|
||||||
const openKeys = [
|
|
||||||
"course_negative_feedback",
|
|
||||||
"course_positive_feedback",
|
|
||||||
"instructor_open_feedback",
|
|
||||||
];
|
|
||||||
|
|
||||||
const feedbackData = ref<FeedbackData | undefined>(undefined);
|
const feedbackData = ref<FeedbackData | undefined>(undefined);
|
||||||
|
const feedbackType = ref<FeedbackType | undefined>(undefined);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
log.debug("FeedbackPage mounted");
|
log.debug("FeedbackPage mounted");
|
||||||
feedbackData.value = await itGet(
|
feedbackData.value = await itGet(
|
||||||
`/api/core/feedback/${courseSession.value.id}/${props.circleId}`
|
`/api/core/feedback/${courseSession.value.id}/${props.circleId}`
|
||||||
);
|
);
|
||||||
|
log.debug("FeedbackPage feedbackData", feedbackData.value);
|
||||||
|
if (
|
||||||
|
feedbackData.value &&
|
||||||
|
["uk", "vv"].includes(feedbackData.value?.feedbackType ?? "")
|
||||||
|
) {
|
||||||
|
feedbackType.value = feedbackData.value.feedbackType;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<FeedbackResults
|
||||||
|
:ordered-questions="orderedQuestions"
|
||||||
|
:feedback-data="feedbackData"
|
||||||
|
:rating-keys="ratingKeys"
|
||||||
|
:vertical-chart-keys="verticalChartKeys"
|
||||||
|
:horizontal-chart-keys="horizontalChartKeys"
|
||||||
|
:open-keys="openKeys"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FeedbackResults from "@/pages/cockpit/FeedbackResults.vue";
|
||||||
|
import type { FeedbackData } from "@/types";
|
||||||
|
import * as log from "loglevel";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
feedbackData: FeedbackData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
log.debug("FeedbackPageUK created");
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const orderedQuestions = [
|
||||||
|
{
|
||||||
|
key: "satisfaction",
|
||||||
|
question: t("feedback.satisfactionLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "goal_attainment",
|
||||||
|
question: t("feedback.goalAttainmentLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "proficiency",
|
||||||
|
question: t("feedback.proficiencyLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "preparation_task_clarity",
|
||||||
|
question: t("feedback.preparationTaskClarityLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "instructor_competence",
|
||||||
|
question: t("feedback.instructorCompetenceLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "instructor_respect",
|
||||||
|
question: t("feedback.instructorRespectLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "instructor_open_feedback",
|
||||||
|
question: t("feedback.instructorOpenFeedbackLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "would_recommend",
|
||||||
|
question: t("feedback.recommendLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "course_negative_feedback",
|
||||||
|
question: t("feedback.courseNegativeFeedbackLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "course_positive_feedback",
|
||||||
|
question: t("feedback.coursePositiveFeedbackLabel"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ratingKeys = [
|
||||||
|
"satisfaction",
|
||||||
|
"goal_attainment",
|
||||||
|
"instructor_competence",
|
||||||
|
"instructor_respect",
|
||||||
|
];
|
||||||
|
const verticalChartKeys = ["preparation_task_clarity", "would_recommend"];
|
||||||
|
const horizontalChartKeys = ["proficiency"];
|
||||||
|
const openKeys = [
|
||||||
|
"course_negative_feedback",
|
||||||
|
"course_positive_feedback",
|
||||||
|
"instructor_open_feedback",
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<FeedbackResults
|
||||||
|
:ordered-questions="orderedQuestions"
|
||||||
|
:feedback-data="feedbackData"
|
||||||
|
:rating-keys="ratingKeys"
|
||||||
|
:vertical-chart-keys="verticalChartKeys"
|
||||||
|
:horizontal-chart-keys="horizontalChartKeys"
|
||||||
|
:open-keys="openKeys"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FeedbackResults from "@/pages/cockpit/FeedbackResults.vue";
|
||||||
|
import type { FeedbackData } from "@/types";
|
||||||
|
import * as log from "loglevel";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
feedbackData: FeedbackData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
log.debug("FeedbackPageVV created");
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const orderedQuestions = [
|
||||||
|
{
|
||||||
|
key: "satisfaction",
|
||||||
|
question: t("feedback.satisfactionLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "goal_attainment",
|
||||||
|
question: t("feedback.goalAttainmentLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "proficiency",
|
||||||
|
question: t("feedback.proficiencyLabelVV"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "preparation_task_clarity",
|
||||||
|
question: t("feedback.praxisAssignmentClarity"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "would_recommend",
|
||||||
|
question: t("feedback.recommendLabelVV"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "course_negative_feedback",
|
||||||
|
question: t("feedback.courseNegativeFeedbackLabel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "course_positive_feedback",
|
||||||
|
question: t("feedback.coursePositiveFeedbackLabel"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ratingKeys = ["satisfaction", "goal_attainment"];
|
||||||
|
const verticalChartKeys = ["preparation_task_clarity", "would_recommend"];
|
||||||
|
const horizontalChartKeys = ["proficiency"];
|
||||||
|
const openKeys = ["course_negative_feedback", "course_positive_feedback"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<ol v-if="feedbackData.amount > 0">
|
||||||
|
<li
|
||||||
|
v-for="(question, i) in orderedQuestions"
|
||||||
|
:key="i"
|
||||||
|
:data-cy="`question-${i + 1}`"
|
||||||
|
>
|
||||||
|
<RatingScale
|
||||||
|
v-if="ratingKeys.includes(question.key)"
|
||||||
|
class="mb-8 bg-white"
|
||||||
|
:ratings="feedbackData.questions[question.key]"
|
||||||
|
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||||
|
:text="question.question"
|
||||||
|
/>
|
||||||
|
<VerticalBarChart
|
||||||
|
v-else-if="verticalChartKeys.includes(question.key)"
|
||||||
|
class="mb-8 bg-white"
|
||||||
|
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||||
|
:ratings="feedbackData.questions[question.key]"
|
||||||
|
:text="question.question"
|
||||||
|
:ratio="0.2"
|
||||||
|
/>
|
||||||
|
<OpenFeedback
|
||||||
|
v-else-if="
|
||||||
|
openKeys.includes(question.key) && feedbackData.questions[question.key]
|
||||||
|
"
|
||||||
|
class="mb-8 bg-white"
|
||||||
|
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||||
|
:text="question.question"
|
||||||
|
:answers="feedbackData.questions[question.key].filter((a: string) => a !== '')"
|
||||||
|
></OpenFeedback>
|
||||||
|
<HorizontalBarChart
|
||||||
|
v-else-if="
|
||||||
|
horizontalChartKeys.includes(question.key) &&
|
||||||
|
feedbackData.questions[question.key]
|
||||||
|
"
|
||||||
|
class="mb-8 bg-white"
|
||||||
|
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||||
|
:text="question.question"
|
||||||
|
:items="feedbackData.questions[question.key].map((a: string) => `${a}%`)"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
|
||||||
|
import OpenFeedback from "@/components/ui/OpenFeedback.vue";
|
||||||
|
import RatingScale from "@/components/ui/RatingScale.vue";
|
||||||
|
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
|
||||||
|
import type { FeedbackData } from "@/types";
|
||||||
|
import * as log from "loglevel";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orderedQuestions?: {
|
||||||
|
key: string;
|
||||||
|
question: string;
|
||||||
|
}[];
|
||||||
|
feedbackData: FeedbackData;
|
||||||
|
ratingKeys?: string[];
|
||||||
|
verticalChartKeys?: string[];
|
||||||
|
horizontalChartKeys?: string[];
|
||||||
|
openKeys?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
orderedQuestions: () => [],
|
||||||
|
ratingKeys: () => [],
|
||||||
|
verticalChartKeys: () => [],
|
||||||
|
horizontalChartKeys: () => [],
|
||||||
|
openKeys: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
log.debug("FeedbackBasePage created");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -52,7 +52,8 @@ const submittables = computed(() => {
|
||||||
const learningContents = circleFlatLearningContents(circle).filter(
|
const learningContents = circleFlatLearningContents(circle).filter(
|
||||||
(lc) =>
|
(lc) =>
|
||||||
lc.content_type === "learnpath.LearningContentAssignment" ||
|
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"
|
lc.content_type === "learnpath.LearningContentEdoniqTest"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -72,7 +73,10 @@ const submittables = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isFeedback = (lc: LearningContent) => {
|
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) => {
|
const isAssignment = (lc: LearningContent) => {
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,13 @@ import type { Component } from "vue";
|
||||||
import { computed, onUnmounted } from "vue";
|
import { computed, onUnmounted } from "vue";
|
||||||
import AssignmentBlock from "./blocks/AssignmentBlock.vue";
|
import AssignmentBlock from "./blocks/AssignmentBlock.vue";
|
||||||
import AttendanceCourseBlock from "./blocks/AttendanceCourseBlock.vue";
|
import AttendanceCourseBlock from "./blocks/AttendanceCourseBlock.vue";
|
||||||
import FeedbackBlock from "./feedback/FeedbackBlock.vue";
|
|
||||||
import IframeBlock from "./blocks/IframeBlock.vue";
|
import IframeBlock from "./blocks/IframeBlock.vue";
|
||||||
import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue";
|
import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue";
|
||||||
import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
|
import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
|
||||||
import RichTextBlock from "./blocks/RichTextBlock.vue";
|
import RichTextBlock from "./blocks/RichTextBlock.vue";
|
||||||
import VideoBlock from "./blocks/VideoBlock.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 { getPreviousRoute } from "@/router/history";
|
||||||
import { stringifyParse } from "@/utils/utils";
|
import { stringifyParse } from "@/utils/utils";
|
||||||
import { useCourseDataWithCompletion } from "@/composables";
|
import { useCourseDataWithCompletion } from "@/composables";
|
||||||
|
|
@ -42,7 +43,8 @@ const COMPONENTS: Record<LearningContentContentType, Component> = {
|
||||||
"learnpath.LearningContentAssignment": AssignmentBlock,
|
"learnpath.LearningContentAssignment": AssignmentBlock,
|
||||||
"learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock,
|
"learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock,
|
||||||
"learnpath.LearningContentDocumentList": DocumentListBlock,
|
"learnpath.LearningContentDocumentList": DocumentListBlock,
|
||||||
"learnpath.LearningContentFeedback": FeedbackBlock,
|
"learnpath.LearningContentFeedbackUK": FeedbackBlockUK,
|
||||||
|
"learnpath.LearningContentFeedbackVV": FeedbackBlockVV,
|
||||||
"learnpath.LearningContentLearningModule": IframeBlock,
|
"learnpath.LearningContentLearningModule": IframeBlock,
|
||||||
"learnpath.LearningContentKnowledgeAssessment": IframeBlock,
|
"learnpath.LearningContentKnowledgeAssessment": IframeBlock,
|
||||||
"learnpath.LearningContentMediaLibrary": MediaLibraryBlock,
|
"learnpath.LearningContentMediaLibrary": MediaLibraryBlock,
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,14 @@ const onEditTask = (task: AssignmentTask) => {
|
||||||
emit("editTask", task);
|
emit("editTask", task);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openSolutionSample = () => {
|
||||||
|
const url = props.assignment.solution_sample?.url ?? "";
|
||||||
|
|
||||||
|
if (props.assignment.solution_sample) {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await upsertAssignmentCompletionMutation.executeMutation({
|
await upsertAssignmentCompletionMutation.executeMutation({
|
||||||
|
|
@ -119,14 +127,17 @@ const onSubmit = async () => {
|
||||||
bustItGetCache(
|
bustItGetCache(
|
||||||
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
|
`/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) {
|
} catch (error) {
|
||||||
log.error("Could not submit assignment", error);
|
log.error("Could not submit assignment", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full border border-gray-400 p-8">
|
<div class="w-full border border-gray-400 p-8" data-cy="confirm-container">
|
||||||
<h3 class="heading-3 border-b border-gray-400 pb-6">
|
<h3 class="heading-3 border-b border-gray-400 pb-6">
|
||||||
{{ $t("assignment.submitAssignment") }}
|
{{ $t("assignment.submitAssignment") }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -202,6 +213,26 @@ const onSubmit = async () => {
|
||||||
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
|
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="props.assignment.solution_sample"
|
||||||
|
class="pt-2"
|
||||||
|
data-cy="show-sample-solution"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{ $t("assignment.submissionShowSampleSolutionText") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ItButton
|
||||||
|
class="mt-6"
|
||||||
|
variant="primary"
|
||||||
|
size="normal"
|
||||||
|
:disabled="false"
|
||||||
|
data-cy="show-sample-solution-button"
|
||||||
|
@click="openSolutionSample"
|
||||||
|
>
|
||||||
|
<p>{{ $t("assignment.submissionShowSampleSolution") }}</p>
|
||||||
|
</ItButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AssignmentSubmissionResponses
|
<AssignmentSubmissionResponses
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,32 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
|
||||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
|
||||||
import { graphql } from "@/gql";
|
import { graphql } from "@/gql";
|
||||||
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
|
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 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 { useMutation } from "@urql/vue";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { useTranslation } from "i18next-vue";
|
|
||||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||||
|
import { bustItGetCache } from "@/fetchHelpers";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
const props = defineProps<{
|
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 courseSession = useCurrentCourseSession();
|
||||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||||
|
|
||||||
const title = computed(
|
const title = computed(() => `«${props.content.circle?.title}»: ${props.title}`);
|
||||||
() => `«${props.content.circle?.title}»: ${t("feedback.areYouSatisfied")}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const circleExperts = computed(() => {
|
const circleExperts = computed(() => {
|
||||||
if (props.content?.circle?.slug) {
|
if (props.content?.circle?.slug) {
|
||||||
|
|
@ -38,34 +35,29 @@ const circleExperts = computed(() => {
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const stepLabels = [
|
const localStepLabels = ref(props.stepLabels);
|
||||||
t("general.introduction"),
|
const localQuestionData = ref(props.questionData);
|
||||||
t("feedback.satisfactionLabel"),
|
const feedbackData: FeedbackData = reactive(feedbackDataFactory());
|
||||||
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 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
|
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
|
||||||
const sendFeedbackMutation = graphql(`
|
const sendFeedbackMutation = graphql(`
|
||||||
mutation SendFeedbackMutation(
|
mutation SendFeedbackMutation(
|
||||||
$courseSessionId: ID!
|
$courseSessionId: ID!
|
||||||
$learningContentId: ID!
|
$learningContentId: ID!
|
||||||
|
$learningContentType: String!
|
||||||
$data: GenericScalar!
|
$data: GenericScalar!
|
||||||
$submitted: Boolean
|
$submitted: Boolean
|
||||||
) {
|
) {
|
||||||
send_feedback(
|
send_feedback(
|
||||||
course_session_id: $courseSessionId
|
course_session_id: $courseSessionId
|
||||||
learning_content_page_id: $learningContentId
|
learning_content_page_id: $learningContentId
|
||||||
|
learning_content_type: $learningContentType
|
||||||
data: $data
|
data: $data
|
||||||
submitted: $submitted
|
submitted: $submitted
|
||||||
) {
|
) {
|
||||||
|
|
@ -90,69 +82,6 @@ interface FeedbackData {
|
||||||
[key: string]: number | string | null;
|
[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 = () => {
|
const previousStep = () => {
|
||||||
if (stepNo.value > 0) {
|
if (stepNo.value > 0) {
|
||||||
stepNo.value -= 1;
|
stepNo.value -= 1;
|
||||||
|
|
@ -160,23 +89,17 @@ const previousStep = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextStep = () => {
|
const nextStep = () => {
|
||||||
if (stepNo.value < numSteps && hasStepValidInput(stepNo.value)) {
|
if (stepNo.value < numSteps.value && hasStepValidInput(stepNo.value)) {
|
||||||
stepNo.value += 1;
|
stepNo.value += 1;
|
||||||
}
|
}
|
||||||
log.debug(`next step ${stepNo.value} of ${numSteps}`);
|
log.debug(`next step ${stepNo.value} of ${numSteps.value}`);
|
||||||
mutateFeedback(feedbackData);
|
mutateFeedback(feedbackData);
|
||||||
};
|
};
|
||||||
|
|
||||||
function hasStepValidInput(stepNumber: number) {
|
function hasStepValidInput(stepNumber: number) {
|
||||||
const question = questionData[stepNumber - 1];
|
const question = localQuestionData.value[stepNumber - 1];
|
||||||
if (question) {
|
if (question) {
|
||||||
if (
|
if (textQuestionKeys.value.includes(question.modelKey)) {
|
||||||
[
|
|
||||||
"instructor_open_feedback",
|
|
||||||
"course_negative_feedback",
|
|
||||||
"course_positive_feedback",
|
|
||||||
].includes(question.modelKey)
|
|
||||||
) {
|
|
||||||
// text response questions need to have a "truthy" value (not "" or null)
|
// text response questions need to have a "truthy" value (not "" or null)
|
||||||
return feedbackData[question.modelKey];
|
return feedbackData[question.modelKey];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -192,6 +115,7 @@ function mutateFeedback(data: FeedbackData, submit = false) {
|
||||||
return executeMutation({
|
return executeMutation({
|
||||||
courseSessionId: courseSession.value.id,
|
courseSessionId: courseSession.value.id,
|
||||||
learningContentId: props.content.id,
|
learningContentId: props.content.id,
|
||||||
|
learningContentType: props.content.content_type,
|
||||||
data: data,
|
data: data,
|
||||||
submitted: submit,
|
submitted: submit,
|
||||||
})
|
})
|
||||||
|
|
@ -199,29 +123,40 @@ function mutateFeedback(data: FeedbackData, submit = false) {
|
||||||
log.debug("feedback mutation result", result);
|
log.debug("feedback mutation result", result);
|
||||||
if (result.data?.send_feedback?.feedback_response?.data) {
|
if (result.data?.send_feedback?.feedback_response?.data) {
|
||||||
const responseData = result.data.send_feedback.feedback_response.data;
|
const responseData = result.data.send_feedback.feedback_response.data;
|
||||||
if (!responseData.instructor_open_feedback) {
|
textQuestionKeys.value.map((key) => {
|
||||||
responseData.instructor_open_feedback = "";
|
if (!responseData[key]) {
|
||||||
}
|
responseData[key] = "";
|
||||||
if (!responseData.course_negative_feedback) {
|
}
|
||||||
responseData.course_negative_feedback = "";
|
});
|
||||||
}
|
|
||||||
if (!responseData.course_positive_feedback) {
|
|
||||||
responseData.course_positive_feedback = "";
|
|
||||||
}
|
|
||||||
Object.assign(feedbackData, responseData);
|
Object.assign(feedbackData, responseData);
|
||||||
log.debug("feedback data", feedbackData);
|
log.debug("feedback data", feedbackData);
|
||||||
feedbackSubmitted.value =
|
feedbackSubmitted.value =
|
||||||
result.data?.send_feedback?.feedback_response?.submitted || false;
|
result.data?.send_feedback?.feedback_response?.submitted || false;
|
||||||
}
|
}
|
||||||
|
bustItGetCache(
|
||||||
|
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((e) => log.error(e));
|
.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 () => {
|
onMounted(async () => {
|
||||||
log.debug("Feedback mounted");
|
log.debug("Feedback mounted");
|
||||||
await mutateFeedback({});
|
await mutateFeedback({});
|
||||||
if (feedbackSubmitted.value) {
|
if (feedbackSubmitted.value) {
|
||||||
stepNo.value = numSteps - 1;
|
stepNo.value = numSteps.value - 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -246,22 +181,22 @@ onMounted(async () => {
|
||||||
@next="nextStep()"
|
@next="nextStep()"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p v-if="stepNo === 0" class="mt-10">
|
<p v-if="stepNo === 0" class="mt-10" data-cy="introduction">
|
||||||
{{
|
{{ introduction }}
|
||||||
$t("feedback.intro", {
|
|
||||||
name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2">
|
<p
|
||||||
{{ stepLabels[stepNo] }}
|
v-if="stepNo > 0 && stepNo + 1 < numSteps"
|
||||||
|
class="pb-2"
|
||||||
|
:data-cy="`question-${stepNo}`"
|
||||||
|
>
|
||||||
|
{{ localStepLabels[stepNo] }}
|
||||||
</p>
|
</p>
|
||||||
<div v-for="(question, index) in questionData" :key="index">
|
<div v-for="(question, index) in questionData" :key="index">
|
||||||
<!-- eslint-disable -->
|
<!-- eslint-disable -->
|
||||||
<!-- eslint does not like the dynamic v-model... -->
|
<!-- eslint does not like the dynamic v-model... -->
|
||||||
<component
|
<component
|
||||||
:is="question.component"
|
:is="question.component"
|
||||||
v-if="index + 1 === stepNo"
|
v-if="index + 1 === stepNo && feedbackData != undefined"
|
||||||
v-model="feedbackData[question.modelKey] as any"
|
v-model="feedbackData[question.modelKey] as any"
|
||||||
:items="question['items']"
|
:items="question['items']"
|
||||||
:cy-key="question.modelKey"
|
:cy-key="question.modelKey"
|
||||||
|
|
@ -269,14 +204,11 @@ onMounted(async () => {
|
||||||
<!-- eslint-enable -->
|
<!-- eslint-enable -->
|
||||||
</div>
|
</div>
|
||||||
<FeedbackCompletition
|
<FeedbackCompletition
|
||||||
v-if="stepNo === 11"
|
v-if="stepNo === numSteps - 1"
|
||||||
:avatar-url="circleExperts[0].avatar_url"
|
:avatar-url="avatarUrl"
|
||||||
:title="
|
:show-avatar="showAvatar"
|
||||||
$t('feedback.completionTitle', {
|
:title="completionTitle"
|
||||||
name: `${circleExperts[0].first_name} ${circleExperts[0].last_name}`,
|
:description="completionDescription"
|
||||||
})
|
|
||||||
"
|
|
||||||
:description="$t('feedback.completionDescription')"
|
|
||||||
:feedback-sent="feedbackSubmitted"
|
:feedback-sent="feedbackSubmitted"
|
||||||
@send-feedback="mutateFeedback(feedbackData, true)"
|
@send-feedback="mutateFeedback(feedbackData, true)"
|
||||||
/>
|
/>
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||||
|
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||||
|
import {
|
||||||
|
PERCENTAGES,
|
||||||
|
RATINGS,
|
||||||
|
YES_NO,
|
||||||
|
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
||||||
|
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import { useCourseSessionDetailQuery } from "@/composables";
|
||||||
|
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
|
||||||
|
}>();
|
||||||
|
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const circleExperts = computed(() => {
|
||||||
|
if (props.content?.circle?.slug) {
|
||||||
|
return courseSessionDetailResult.filterCircleExperts(props.content.circle.slug);
|
||||||
|
}
|
||||||
|
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 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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FeedbackBase
|
||||||
|
:step-labels="stepLabels"
|
||||||
|
:question-data="questionData"
|
||||||
|
:content="props.content"
|
||||||
|
:introduction="
|
||||||
|
$t('feedback.intro', {
|
||||||
|
name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:title="$t('feedback.areYouSatisfied')"
|
||||||
|
:completion-title="
|
||||||
|
$t('feedback.completionTitle', {
|
||||||
|
name: `${circleExperts[0].first_name} ${circleExperts[0].last_name}`,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:completion-description="$t('feedback.completionDescription')"
|
||||||
|
:show-avatar="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||||
|
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||||
|
import {
|
||||||
|
PERCENTAGES,
|
||||||
|
RATINGS,
|
||||||
|
YES_NO,
|
||||||
|
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
||||||
|
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
|
||||||
|
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const stepLabels = [
|
||||||
|
t("general.introduction"),
|
||||||
|
t("feedback.satisfactionLabel"),
|
||||||
|
t("feedback.goalAttainmentLabel"),
|
||||||
|
t("feedback.proficiencyLabelVV"),
|
||||||
|
t("feedback.praxisAssignmentClarity"),
|
||||||
|
t("feedback.recommendLabelVV"),
|
||||||
|
t("feedback.coursePositiveFeedbackLabel"),
|
||||||
|
t("feedback.courseNegativeFeedbackLabel"),
|
||||||
|
t("general.submission"),
|
||||||
|
];
|
||||||
|
|
||||||
|
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: "would_recommend",
|
||||||
|
items: YES_NO,
|
||||||
|
component: ItRadioGroup,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelKey: "course_positive_feedback",
|
||||||
|
component: ItTextarea,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelKey: "course_negative_feedback",
|
||||||
|
component: ItTextarea,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FeedbackBase
|
||||||
|
:step-labels="stepLabels"
|
||||||
|
:question-data="questionData"
|
||||||
|
:content="props.content"
|
||||||
|
:introduction="$t('a.feedback.introductionVV')"
|
||||||
|
:title="$t('Feedback')"
|
||||||
|
:completion-title="$t('feedback.completionDescriptionVV')"
|
||||||
|
:completion-description="$t('feedback.completionDescriptionVV')"
|
||||||
|
:show-avatar="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
@ -4,7 +4,11 @@
|
||||||
<div
|
<div
|
||||||
class="b-0 flex flex-col lg:flex-row lg:items-center lg:border lg:border-gray-400 lg:p-8"
|
class="b-0 flex flex-col lg:flex-row lg:items-center lg:border lg:border-gray-400 lg:p-8"
|
||||||
>
|
>
|
||||||
<img :src="avatarUrl" class="mb-6 h-16 w-16 rounded-full lg:mr-12" />
|
<img
|
||||||
|
v-if="showAvatar"
|
||||||
|
:src="avatarUrl"
|
||||||
|
class="mb-6 h-16 w-16 rounded-full lg:mr-12"
|
||||||
|
/>
|
||||||
<h2 class="mb-8 block lg:hidden">{{ title }}</h2>
|
<h2 class="mb-8 block lg:hidden">{{ title }}</h2>
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-6">{{ description }}</p>
|
<p class="mb-6">{{ description }}</p>
|
||||||
|
|
@ -33,6 +37,7 @@ interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
feedbackSent?: boolean;
|
feedbackSent?: boolean;
|
||||||
|
showAvatar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -40,6 +45,7 @@ withDefaults(defineProps<Props>(), {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
feedbackSent: false,
|
feedbackSent: false,
|
||||||
|
showAvatar: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["sendFeedback"]);
|
defineEmits(["sendFeedback"]);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ import type {
|
||||||
LearningContentAttendanceCourseObjectType,
|
LearningContentAttendanceCourseObjectType,
|
||||||
LearningContentDocumentListObjectType,
|
LearningContentDocumentListObjectType,
|
||||||
LearningContentEdoniqTestObjectType,
|
LearningContentEdoniqTestObjectType,
|
||||||
LearningContentFeedbackObjectType,
|
LearningContentFeedbackUkObjectType,
|
||||||
|
LearningContentFeedbackVvObjectType,
|
||||||
LearningContentKnowledgeAssessmentObjectType,
|
LearningContentKnowledgeAssessmentObjectType,
|
||||||
LearningContentLearningModuleObjectType,
|
LearningContentLearningModuleObjectType,
|
||||||
LearningContentMediaLibraryObjectType,
|
LearningContentMediaLibraryObjectType,
|
||||||
|
|
@ -68,8 +69,12 @@ export type LearningContentEdoniqTest = LearningContentEdoniqTestObjectType & {
|
||||||
readonly content_type: "learnpath.LearningContentEdoniqTest";
|
readonly content_type: "learnpath.LearningContentEdoniqTest";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LearningContentFeedback = LearningContentFeedbackObjectType & {
|
export type LearningContentFeedbackVV = LearningContentFeedbackVvObjectType & {
|
||||||
readonly content_type: "learnpath.LearningContentFeedback";
|
readonly content_type: "learnpath.LearningContentFeedbackVV";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LearningContentFeedbackUK = LearningContentFeedbackUkObjectType & {
|
||||||
|
readonly content_type: "learnpath.LearningContentFeedbackUK";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
|
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
|
||||||
|
|
@ -102,7 +107,8 @@ export type LearningContent =
|
||||||
| LearningContentAttendanceCourse
|
| LearningContentAttendanceCourse
|
||||||
| LearningContentDocumentList
|
| LearningContentDocumentList
|
||||||
| LearningContentEdoniqTest
|
| LearningContentEdoniqTest
|
||||||
| LearningContentFeedback
|
| LearningContentFeedbackUK
|
||||||
|
| LearningContentFeedbackVV
|
||||||
| LearningContentLearningModule
|
| LearningContentLearningModule
|
||||||
| LearningContentKnowledgeAssessment
|
| LearningContentKnowledgeAssessment
|
||||||
| LearningContentMediaLibrary
|
| LearningContentMediaLibrary
|
||||||
|
|
@ -560,3 +566,13 @@ export type DueDate = SimpleDueDate & {
|
||||||
course_session_id: string;
|
course_session_id: string;
|
||||||
circle: CircleLight | null;
|
circle: CircleLight | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FeedbackType = "uk" | "vv";
|
||||||
|
|
||||||
|
export interface FeedbackData {
|
||||||
|
amount: number;
|
||||||
|
questions: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
feedbackType: FeedbackType;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ export function learningContentTypeData(
|
||||||
return { title: t("learningContentTypes.test"), icon: "it-icon-lc-test" };
|
return { title: t("learningContentTypes.test"), icon: "it-icon-lc-test" };
|
||||||
case "learnpath.LearningContentRichText":
|
case "learnpath.LearningContentRichText":
|
||||||
return { title: t("learningContentTypes.text"), icon: "it-icon-lc-resource" };
|
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" };
|
return { title: t("learningContentTypes.feedback"), icon: "it-icon-lc-feedback" };
|
||||||
case "learnpath.LearningContentPlaceholder":
|
case "learnpath.LearningContentPlaceholder":
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
||||||
import { login } from "../helpers";
|
import { login } from "../helpers";
|
||||||
|
|
||||||
// Daniel: without this comment, my tool will reformat the login import out...
|
|
||||||
|
|
||||||
function completePraxisAssignment(selectExpert = false) {
|
function completePraxisAssignment(selectExpert = false) {
|
||||||
cy.visit("/course/test-lehrgang/learn/reisen/mein-kundenstamm");
|
cy.visit("/course/test-lehrgang/learn/reisen/mein-kundenstamm");
|
||||||
cy.learningContentMultiLayoutNextStep();
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
|
@ -326,16 +324,23 @@ describe("assignmentStudent.cy.js", () => {
|
||||||
cy.get('[data-cy="submit-assignment"]').click();
|
cy.get('[data-cy="submit-assignment"]').click();
|
||||||
cy.get('[data-cy="success-text"]').should("exist");
|
cy.get('[data-cy="success-text"]').should("exist");
|
||||||
|
|
||||||
// app goes back to circle view -> check if assignment is marked as completed
|
cy.get('[data-cy="confirm-container"]')
|
||||||
cy.url().should((url) => {
|
.find('[data-cy="show-sample-solution"]')
|
||||||
expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
|
.then(($elements) => {
|
||||||
});
|
if ($elements.length > 0) {
|
||||||
cy.reload();
|
// 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(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
|
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
|
|
||||||
// reopening page should get directly to last step
|
//reopening page should get directly to last step
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
import { TEST_TRAINER1_USER_ID } from "../../consts";
|
||||||
import { login } from "../helpers";
|
import { login } from "../helpers";
|
||||||
|
|
||||||
describe("assignmentTrainer.cy.js", () => {
|
describe("assignmentTrainer.cy.js", () => {
|
||||||
|
|
@ -85,8 +85,8 @@ describe("assignmentTrainer.cy.js", () => {
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
// load AssignmentCompletion from DB and check
|
// load AssignmentCompletion from DB and check
|
||||||
cy.loadAssignmentCompletion(
|
cy.loadAssignmentCompletion(
|
||||||
"assignment_user_id",
|
"evaluation_user_id",
|
||||||
TEST_STUDENT1_USER_ID
|
TEST_TRAINER1_USER_ID
|
||||||
).then((ac) => {
|
).then((ac) => {
|
||||||
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
|
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
|
||||||
expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut");
|
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
|
// load AssignmentCompletion from DB and check
|
||||||
cy.loadAssignmentCompletion(
|
cy.loadAssignmentCompletion(
|
||||||
"assignment_user_id",
|
"evaluation_user_id",
|
||||||
TEST_STUDENT1_USER_ID
|
TEST_TRAINER1_USER_ID
|
||||||
).then((ac) => {
|
).then((ac) => {
|
||||||
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
|
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
|
||||||
expect(ac.evaluation_points).to.equal(17);
|
expect(ac.evaluation_points).to.equal(17);
|
||||||
|
|
@ -237,8 +237,8 @@ describe("assignmentTrainer.cy.js", () => {
|
||||||
|
|
||||||
// load AssignmentCompletion from DB and check
|
// load AssignmentCompletion from DB and check
|
||||||
cy.loadAssignmentCompletion(
|
cy.loadAssignmentCompletion(
|
||||||
"assignment_user_id",
|
"evaluation_user_id",
|
||||||
TEST_STUDENT1_USER_ID
|
TEST_TRAINER1_USER_ID
|
||||||
).then((ac) => {
|
).then((ac) => {
|
||||||
console.log(ac.completion_status);
|
console.log(ac.completion_status);
|
||||||
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
|
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
|
||||||
|
|
@ -323,8 +323,8 @@ describe("assignmentTrainer.cy.js", () => {
|
||||||
|
|
||||||
// load AssignmentCompletion from DB and check
|
// load AssignmentCompletion from DB and check
|
||||||
cy.loadAssignmentCompletion(
|
cy.loadAssignmentCompletion(
|
||||||
"assignment_user_id",
|
"evaluation_user_id",
|
||||||
TEST_STUDENT1_USER_ID
|
TEST_TRAINER1_USER_ID
|
||||||
).then((ac) => {
|
).then((ac) => {
|
||||||
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
|
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
|
||||||
expect(ac.evaluation_max_points).to.equal(0);
|
expect(ac.evaluation_max_points).to.equal(0);
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ describe("dashboardSupervisor.cy.js", () => {
|
||||||
describe("feedback summary box", () => {
|
describe("feedback summary box", () => {
|
||||||
it("contains correct numbers", () => {
|
it("contains correct numbers", () => {
|
||||||
getDashboardStatistics("feedback.average").should("have.text", "3.3");
|
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", () => {
|
it("contains correct details link", () => {
|
||||||
clickOnDetailsLink("feedback");
|
clickOnDetailsLink("feedback");
|
||||||
|
|
|
||||||
|
|
@ -5,153 +5,359 @@ describe("feedbackStudent.cy.js", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.manageCommand("cypress_reset");
|
cy.manageCommand("cypress_reset");
|
||||||
login("test-student1@example.com", "test");
|
login("test-student1@example.com", "test");
|
||||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open feedback page", () => {
|
describe("Feedback UK", () => {
|
||||||
cy.testLearningContentTitle("Kursfeedback");
|
beforeEach(() => {
|
||||||
cy.testLearningContentSubtitle("Feedback");
|
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", () => {
|
describe("Feedback VV", () => {
|
||||||
// initial wait for step 0 (or none with step==0) is required for pipelines
|
beforeEach(() => {
|
||||||
cy.url().should((url) => {
|
cy.visit("/course/test-lehrgang/learn/reisen/feedback");
|
||||||
expect(url).to.match(/\/fahrzeug\/feedback(\?step=0)?$/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.wait(200);
|
it("can open feedback page", () => {
|
||||||
cy.learningContentMultiLayoutNextStep();
|
cy.testLearningContentTitle("Feedback");
|
||||||
cy.wait(200);
|
cy.testLearningContentSubtitle("Feedback");
|
||||||
|
|
||||||
// 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?$/);
|
|
||||||
});
|
});
|
||||||
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
|
it("can create feedback by giving answers to all steps", () => {
|
||||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
|
// initial wait for step 0 (or none with step==0) is required for pipelines
|
||||||
cy.url().should("include", "step=11");
|
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.wait(200);
|
||||||
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
|
cy.learningContentMultiLayoutNextStep();
|
||||||
(ac) => {
|
cy.wait(200);
|
||||||
expect(ac.submitted).to.be.true;
|
|
||||||
expect(ac.data).to.deep.equal({
|
// fill feedback form
|
||||||
course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
|
// step 1
|
||||||
course_positive_feedback: "Ich bin zufrieden mit den meisten Dingen.",
|
cy.url().should("include", "step=1");
|
||||||
goal_attainment: 3,
|
cy.get('[data-cy="question-1"]').should(
|
||||||
instructor_competence: 2,
|
"contain",
|
||||||
instructor_open_feedback: "Der Kursleiter ist eigentlich ganz nett.",
|
"Zufriedenheit insgesamt"
|
||||||
instructor_respect: 1,
|
);
|
||||||
preparation_task_clarity: false,
|
cy.get('[data-cy="next-step"]').should("be.disabled");
|
||||||
proficiency: 80,
|
cy.get('[data-cy="radio-4"]').click();
|
||||||
satisfaction: 4,
|
cy.wait(200);
|
||||||
would_recommend: true,
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,77 +16,216 @@ describe("feedbackTrainer.cy.js", () => {
|
||||||
cy.get('[data-cy="feedback-data-amount"]').should("contain", "0");
|
cy.get('[data-cy="feedback-data-amount"]').should("contain", "0");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can open feedback results page with results", () => {
|
describe("FeedbackUK", function () {
|
||||||
cy.manageCommand("cypress_reset --create-feedback-responses");
|
it("can open feedback results page with results", () => {
|
||||||
login("test-trainer1@example.com", "test");
|
cy.manageCommand("cypress_reset --create-feedback-responses");
|
||||||
cy.visit("/course/test-lehrgang/cockpit");
|
login("test-trainer1@example.com", "test");
|
||||||
cy.get(
|
cy.visit("/course/test-lehrgang/cockpit");
|
||||||
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
|
cy.get(
|
||||||
).click();
|
'[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"]')
|
// check titles of questions
|
||||||
.find('[data-cy="rating-scale-average"]')
|
cy.get('[data-cy="question-1"]').should(
|
||||||
.should("contain", "3.3");
|
"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"]')
|
cy.get('[data-cy="question-1"]')
|
||||||
.find('[data-cy="rating-scale-average"]')
|
.find('[data-cy="rating-scale-average"]')
|
||||||
.should("contain", "3.0");
|
.should("contain", "3.3");
|
||||||
|
|
||||||
cy.get('[data-cy="question-3"]')
|
cy.get('[data-cy="question-2"]')
|
||||||
.find('[data-cy="percentage-value-40%"]')
|
.find('[data-cy="rating-scale-average"]')
|
||||||
.should("contain", "33.3");
|
.should("contain", "3.0");
|
||||||
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"]')
|
cy.get('[data-cy="question-3"]')
|
||||||
.find('[data-cy="popover-yes"]')
|
.find('[data-cy="percentage-value-40%"]')
|
||||||
.click()
|
.should("contain", "33.3");
|
||||||
.find('[data-cy="num-yes"]')
|
cy.get('[data-cy="question-3"]')
|
||||||
.should("contain", "3");
|
.find('[data-cy="percentage-value-80%"]')
|
||||||
cy.get('[data-cy="question-4"]')
|
.should("contain", "33.3");
|
||||||
.find('[data-cy="popover-no"]')
|
cy.get('[data-cy="question-3"]')
|
||||||
.click()
|
.find('[data-cy="percentage-value-100%"]')
|
||||||
.find('[data-cy="num-no"]')
|
.should("contain", "33.3");
|
||||||
.should("contain", "0");
|
|
||||||
|
|
||||||
cy.get('[data-cy="question-5"]')
|
cy.get('[data-cy="question-4"]')
|
||||||
.find('[data-cy="rating-scale-average"]')
|
.find('[data-cy="popover-yes"]')
|
||||||
.should("contain", "2.7");
|
.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"]')
|
cy.get('[data-cy="question-5"]')
|
||||||
.find('[data-cy="rating-scale-average"]')
|
.find('[data-cy="rating-scale-average"]')
|
||||||
.should("contain", "3.0");
|
.should("contain", "2.7");
|
||||||
|
|
||||||
cy.get('[data-cy="question-7"]')
|
cy.get('[data-cy="question-6"]')
|
||||||
.should("contain", "Super Kurs!")
|
.find('[data-cy="rating-scale-average"]')
|
||||||
.should("contain", "Super, bin begeistert")
|
.should("contain", "3.0");
|
||||||
.should("contain", "Ok, entspricht den Erwartungen");
|
|
||||||
|
|
||||||
cy.get('[data-cy="question-8"]')
|
cy.get('[data-cy="question-7"]')
|
||||||
.find('[data-cy="popover-yes"]')
|
.should("contain", "Super Kurs!")
|
||||||
.click()
|
.should("contain", "Super, bin begeistert")
|
||||||
.find('[data-cy="num-yes"]')
|
.should("contain", "Ok, entspricht den Erwartungen");
|
||||||
.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-9"]')
|
cy.get('[data-cy="question-8"]')
|
||||||
.should("contain", "Nichts Schlechtes")
|
.find('[data-cy="popover-yes"]')
|
||||||
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
|
.click()
|
||||||
.should("contain", "Mehr Videos wären schön.");
|
.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"]')
|
cy.get('[data-cy="question-9"]')
|
||||||
.should("contain", "Nur Gutes.")
|
.should("contain", "Nichts Schlechtes")
|
||||||
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
|
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
|
||||||
.should("contain", "Die Präsentation war super");
|
.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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
|
||||||
"/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
"/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
||||||
"/Users/eliabieri/iterativ/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/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}`;
|
let bashCommand = `PATH=${pythonPaths.join(":")}:$PATH && ${execCommand}`;
|
||||||
return cy
|
return cy
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
|
|
@ -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/<document-id> 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/<document-id>
|
||||||
|
|
||||||
|
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/<file-id>
|
||||||
|
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
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -18,8 +18,7 @@ def main():
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
settings.DEBUG = True
|
settings.DEBUG = True
|
||||||
from django.db import connection
|
from django.db import connection, reset_queries
|
||||||
from django.db import reset_queries
|
|
||||||
|
|
||||||
reset_queries()
|
reset_queries()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,15 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
from vbv_lernwelt.core.schema import Query
|
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.core.schema import Query
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
settings.DEBUG = True
|
settings.DEBUG = True
|
||||||
from django.db import connection
|
from django.db import connection, reset_queries
|
||||||
from django.db import reset_queries
|
|
||||||
|
|
||||||
reset_queries()
|
reset_queries()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
from vbv_lernwelt.notify.email.email_services import (
|
from vbv_lernwelt.notify.email.email_services import (
|
||||||
|
create_template_data_from_course_session_attendance_course,
|
||||||
EmailTemplate,
|
EmailTemplate,
|
||||||
send_email,
|
send_email,
|
||||||
create_template_data_from_course_session_attendance_course,
|
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ THIRD_PARTY_APPS = [
|
||||||
|
|
||||||
LOCAL_APPS = [
|
LOCAL_APPS = [
|
||||||
"vbv_lernwelt.core",
|
"vbv_lernwelt.core",
|
||||||
|
"vbv_lernwelt.media_files",
|
||||||
"vbv_lernwelt.sso",
|
"vbv_lernwelt.sso",
|
||||||
"vbv_lernwelt.course",
|
"vbv_lernwelt.course",
|
||||||
"vbv_lernwelt.learnpath",
|
"vbv_lernwelt.learnpath",
|
||||||
|
|
@ -213,23 +214,13 @@ STATICFILES_FINDERS = [
|
||||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
"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
|
# MEDIA
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
||||||
MEDIA_ROOT = str(APPS_DIR / "media")
|
MEDIA_ROOT = str(APPS_DIR / "media")
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
||||||
if USE_AWS:
|
|
||||||
# https://wagtail.org/blog/amazon-s3-for-media-files/
|
MEDIA_URL = "/server/media/"
|
||||||
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
|
|
||||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
|
||||||
else:
|
|
||||||
MEDIA_URL = "/server/media/"
|
|
||||||
|
|
||||||
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
|
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
|
||||||
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
|
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_ENABLE_WHATS_NEW_BANNER = False
|
||||||
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES
|
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 = {
|
WAGTAILADMIN_RICH_TEXT_EDITORS = {
|
||||||
"default": {
|
"default": {
|
||||||
|
|
@ -646,7 +649,7 @@ NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
|
||||||
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
||||||
|
|
||||||
# S3 BUCKET CONFIGURATION
|
# 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":
|
if FILE_UPLOAD_STORAGE == "local":
|
||||||
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=5242880)
|
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=5242880)
|
||||||
|
|
@ -655,18 +658,19 @@ if FILE_UPLOAD_STORAGE == "s3":
|
||||||
# Using django-storages
|
# Using django-storages
|
||||||
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html
|
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html
|
||||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||||
|
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID", default="AKIAZJLREPUVWNBTJ5VY")
|
||||||
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID")
|
|
||||||
AWS_S3_SECRET_ACCESS_KEY = env("AWS_S3_SECRET_ACCESS_KEY")
|
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", "eu-central-1")
|
||||||
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME")
|
|
||||||
AWS_S3_SIGNATURE_VERSION = env("AWS_S3_SIGNATURE_VERSION", default="s3v4")
|
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
|
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=20971520) # 20MB
|
||||||
|
|
||||||
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||||
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="private")
|
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="private")
|
||||||
|
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=7200) # seconds
|
||||||
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=300) # seconds
|
|
||||||
|
|
||||||
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = (
|
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = (
|
||||||
"jpg",
|
"jpg",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||||
|
|
||||||
from .base import * # noqa
|
from .base import * # noqa
|
||||||
|
|
@ -8,6 +9,7 @@ from .base import * # noqa
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
|
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
|
||||||
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||||
|
|
||||||
|
# Select faster password hasher during tests
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
|
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
|
||||||
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
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"
|
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||||
|
|
||||||
WHITENOISE_MANIFEST_STRICT = False
|
WHITENOISE_MANIFEST_STRICT = False
|
||||||
|
AWS_S3_FILE_OVERWRITE = True
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
class DisableMigrations(dict):
|
class DisableMigrations(dict):
|
||||||
|
|
@ -36,8 +29,3 @@ class DisableMigrations(dict):
|
||||||
|
|
||||||
|
|
||||||
MIGRATION_MODULES = DisableMigrations()
|
MIGRATION_MODULES = DisableMigrations()
|
||||||
|
|
||||||
# Select faster password hasher during tests
|
|
||||||
PASSWORD_HASHERS = [
|
|
||||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
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
|
from .base import * # noqa
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ from vbv_lernwelt.importer.views import (
|
||||||
from vbv_lernwelt.notify.views import email_notification_settings
|
from vbv_lernwelt.notify.views import email_notification_settings
|
||||||
from wagtail import urls as wagtail_urls
|
from wagtail import urls as wagtail_urls
|
||||||
from wagtail.admin import urls as wagtailadmin_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):
|
class SignedIntConverter(IntConverter):
|
||||||
|
|
@ -89,7 +89,7 @@ urlpatterns = [
|
||||||
|
|
||||||
# wagtail urls
|
# wagtail urls
|
||||||
path('server/cms/', include(wagtailadmin_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)),
|
path('server/pages/', include(wagtail_urls)),
|
||||||
|
|
||||||
# core
|
# core
|
||||||
|
|
@ -138,6 +138,7 @@ urlpatterns = [
|
||||||
name="request_assignment_completion_status"),
|
name="request_assignment_completion_status"),
|
||||||
|
|
||||||
# documents
|
# documents
|
||||||
|
# TODO: remfactor to files app
|
||||||
path(r'api/core/document/start/', document_upload_start,
|
path(r'api/core/document/start/', document_upload_start,
|
||||||
name='file_upload_start'),
|
name='file_upload_start'),
|
||||||
path(r'api/core/document/<str:document_id>/', document_delete,
|
path(r'api/core/document/<str:document_id>/', document_delete,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ mypy # https://github.com/python/mypy
|
||||||
django-stubs # https://github.com/typeddjango/django-stubs
|
django-stubs # https://github.com/typeddjango/django-stubs
|
||||||
pytest # https://github.com/pytest-dev/pytest
|
pytest # https://github.com/pytest-dev/pytest
|
||||||
pytest-sugar # https://github.com/Frozenball/pytest-sugar
|
pytest-sugar # https://github.com/Frozenball/pytest-sugar
|
||||||
|
pytest-xdist #
|
||||||
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs
|
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,7 @@ azure-core==1.29.1
|
||||||
azure-identity==1.14.0
|
azure-identity==1.14.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
azure-storage-blob==12.17.0
|
azure-storage-blob==12.17.0
|
||||||
# via
|
# via -r requirements.in
|
||||||
# -r requirements.in
|
|
||||||
# django-storages
|
|
||||||
backcall==0.2.0
|
backcall==0.2.0
|
||||||
# via ipython
|
# via ipython
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
|
|
@ -176,7 +174,7 @@ django-ratelimit==4.1.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-redis==5.3.0
|
django-redis==5.3.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-storages[azure]==1.13.2
|
django-storages==1.13.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-stubs==4.2.3
|
django-stubs==4.2.3
|
||||||
# via
|
# via
|
||||||
|
|
@ -209,6 +207,8 @@ exceptiongroup==1.1.2
|
||||||
# via
|
# via
|
||||||
# anyio
|
# anyio
|
||||||
# pytest
|
# pytest
|
||||||
|
execnet==2.0.2
|
||||||
|
# via pytest-xdist
|
||||||
executing==1.2.0
|
executing==1.2.0
|
||||||
# via stack-data
|
# via stack-data
|
||||||
factory-boy==3.3.0
|
factory-boy==3.3.0
|
||||||
|
|
@ -397,7 +397,9 @@ pyflakes==3.1.0
|
||||||
pygments==2.16.1
|
pygments==2.16.1
|
||||||
# via ipython
|
# via ipython
|
||||||
pyjwt[crypto]==2.8.0
|
pyjwt[crypto]==2.8.0
|
||||||
# via msal
|
# via
|
||||||
|
# msal
|
||||||
|
# pyjwt
|
||||||
pylint==2.17.5
|
pylint==2.17.5
|
||||||
# via
|
# via
|
||||||
# pylint-django
|
# pylint-django
|
||||||
|
|
@ -415,10 +417,13 @@ pytest==7.4.0
|
||||||
# -r requirements-dev.in
|
# -r requirements-dev.in
|
||||||
# pytest-django
|
# pytest-django
|
||||||
# pytest-sugar
|
# pytest-sugar
|
||||||
|
# pytest-xdist
|
||||||
pytest-django==4.5.2
|
pytest-django==4.5.2
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
pytest-sugar==0.9.7
|
pytest-sugar==0.9.7
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
|
pytest-xdist==3.5.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
|
|
@ -616,7 +621,9 @@ wheel==0.41.1
|
||||||
whitenoise[brotli]==6.5.0
|
whitenoise[brotli]==6.5.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
willow[heif]==1.6.1
|
willow[heif]==1.6.1
|
||||||
# via wagtail
|
# via
|
||||||
|
# wagtail
|
||||||
|
# willow
|
||||||
wrapt==1.15.0
|
wrapt==1.15.0
|
||||||
# via astroid
|
# via astroid
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
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
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
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}'`
|
coverage_python=`coverage report -m | tail -n1 | awk '{print $4}'`
|
||||||
commit=`git rev-parse HEAD`
|
commit=`git rev-parse HEAD`
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from vbv_lernwelt.course.consts import (
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.models import CoursePage
|
from vbv_lernwelt.course.models import CoursePage
|
||||||
|
from vbv_lernwelt.media_files.models import ContentDocument
|
||||||
from wagtail.blocks import StreamValue
|
from wagtail.blocks import StreamValue
|
||||||
from wagtail.blocks.list_block import ListBlock, ListValue
|
from wagtail.blocks.list_block import ListBlock, ListValue
|
||||||
from wagtail.rich_text import RichText
|
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,
|
needs_expert_evaluation=True,
|
||||||
competence_certificate=competence_certificate,
|
competence_certificate=competence_certificate,
|
||||||
effort_required="ca. 5 Stunden",
|
effort_required="ca. 5 Stunden",
|
||||||
|
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
|
||||||
intro_text=replace_whitespace(
|
intro_text=replace_whitespace(
|
||||||
"""
|
"""
|
||||||
<h3>Ausgangslage</h3>
|
<h3>Ausgangslage</h3>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
||||||
from vbv_lernwelt.course.models import CourseSession
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
from vbv_lernwelt.iam.permissions import can_evaluate_assignments, has_course_access
|
from vbv_lernwelt.iam.permissions import can_evaluate_assignments, has_course_access
|
||||||
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
||||||
|
from vbv_lernwelt.media_files.graphql.types import ContentDocumentObjectType
|
||||||
|
|
||||||
|
|
||||||
class AssignmentCompletionObjectType(DjangoObjectType):
|
class AssignmentCompletionObjectType(DjangoObjectType):
|
||||||
|
|
@ -52,6 +53,7 @@ class AssignmentObjectType(DjangoObjectType):
|
||||||
learning_content_page_id=graphene.ID(required=False),
|
learning_content_page_id=graphene.ID(required=False),
|
||||||
assignment_user_id=graphene.UUID(required=False),
|
assignment_user_id=graphene.UUID(required=False),
|
||||||
)
|
)
|
||||||
|
solution_sample = graphene.Field(ContentDocumentObjectType)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Assignment
|
model = Assignment
|
||||||
|
|
@ -67,6 +69,9 @@ class AssignmentObjectType(DjangoObjectType):
|
||||||
"competence_certificate",
|
"competence_certificate",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def resolve_solution_sample(self, info):
|
||||||
|
return self.solution_sample
|
||||||
|
|
||||||
def resolve_max_points(self, info):
|
def resolve_max_points(self, info):
|
||||||
return self.get_max_points()
|
return self.get_max_points()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -200,12 +200,22 @@ class Assignment(CourseBasePage):
|
||||||
help_text="Beurteilungsschritte",
|
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 + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("assignment_type"),
|
FieldPanel("assignment_type"),
|
||||||
FieldPanel("needs_expert_evaluation"),
|
FieldPanel("needs_expert_evaluation"),
|
||||||
PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"),
|
PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"),
|
||||||
FieldPanel("intro_text"),
|
FieldPanel("intro_text"),
|
||||||
FieldPanel("effort_required"),
|
FieldPanel("effort_required"),
|
||||||
|
FieldPanel("solution_sample"),
|
||||||
FieldPanel("performance_objectives"),
|
FieldPanel("performance_objectives"),
|
||||||
FieldPanel("tasks"),
|
FieldPanel("tasks"),
|
||||||
FieldPanel("evaluation_description"),
|
FieldPanel("evaluation_description"),
|
||||||
|
|
|
||||||
|
|
@ -106,14 +106,14 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
task_data = data["task_completion_data"][task_id]
|
task_data = data["task_completion_data"][task_id]
|
||||||
self.assertDictEqual(
|
self.maxDiff = None
|
||||||
task_data,
|
self.assertEqual(task_data["user_data"]["fileId"], file_id)
|
||||||
{
|
self.assertEqual(task_data["user_data"]["fileInfo"]["id"], file_id)
|
||||||
"user_data": {
|
self.assertEqual(task_data["user_data"]["fileInfo"]["name"], "file.txt")
|
||||||
"fileId": file_id,
|
self.assertTrue(
|
||||||
"fileInfo": {"id": file_id, "name": "file.txt", "url": file_url},
|
task_data["user_data"]["fileInfo"]["url"].startswith(
|
||||||
}
|
"https://s3.eu-central-1.amazonaws.com/myvbv-dev.iterativ.ch"
|
||||||
},
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# check DB data
|
# check DB data
|
||||||
|
|
@ -194,31 +194,31 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
||||||
# check notification
|
# check notification
|
||||||
self.assertEqual(Notification.objects.count(), 1)
|
self.assertEqual(Notification.objects.count(), 1)
|
||||||
notification = Notification.objects.first()
|
notification = Notification.objects.first()
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben.",
|
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben.",
|
||||||
notification.verb,
|
notification.verb,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"test-trainer1@example.com",
|
"test-trainer1@example.com",
|
||||||
notification.recipient.email,
|
notification.recipient.email,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"test-student1@example.com",
|
"test-student1@example.com",
|
||||||
notification.actor.email,
|
notification.actor.email,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"USER_INTERACTION",
|
"USER_INTERACTION",
|
||||||
notification.notification_category,
|
notification.notification_category,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"CASEWORK_SUBMITTED",
|
"CASEWORK_SUBMITTED",
|
||||||
notification.notification_trigger,
|
notification.notification_trigger,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.action_object,
|
notification.action_object,
|
||||||
db_entry,
|
db_entry,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.course_session,
|
notification.course_session,
|
||||||
self.course_session,
|
self.course_session,
|
||||||
)
|
)
|
||||||
|
|
@ -422,35 +422,35 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
||||||
# check notification
|
# check notification
|
||||||
self.assertEqual(Notification.objects.count(), 1)
|
self.assertEqual(Notification.objects.count(), 1)
|
||||||
notification = Notification.objects.first()
|
notification = Notification.objects.first()
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"Test Trainer1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» bewertet.",
|
"Test Trainer1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» bewertet.",
|
||||||
notification.verb,
|
notification.verb,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"test-student1@example.com",
|
"test-student1@example.com",
|
||||||
notification.recipient.email,
|
notification.recipient.email,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"test-trainer1@example.com",
|
"test-trainer1@example.com",
|
||||||
notification.actor.email,
|
notification.actor.email,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"USER_INTERACTION",
|
"USER_INTERACTION",
|
||||||
notification.notification_category,
|
notification.notification_category,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"CASEWORK_EVALUATED",
|
"CASEWORK_EVALUATED",
|
||||||
notification.notification_trigger,
|
notification.notification_trigger,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.action_object,
|
notification.action_object,
|
||||||
db_entry,
|
db_entry,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.course_session,
|
notification.course_session,
|
||||||
self.course_session,
|
self.course_session,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.target_url,
|
notification.target_url,
|
||||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice",
|
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
||||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
from vbv_lernwelt.learnpath.models import (
|
from vbv_lernwelt.learnpath.models import (
|
||||||
LearningContentAttendanceCourse,
|
LearningContentAttendanceCourse,
|
||||||
LearningContentFeedback,
|
LearningContentFeedbackUK,
|
||||||
|
LearningContentFeedbackVV,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.notify.models import Notification
|
from vbv_lernwelt.notify.models import Notification
|
||||||
|
|
||||||
|
|
@ -155,7 +156,9 @@ def command(
|
||||||
if create_feedback_responses:
|
if create_feedback_responses:
|
||||||
print("create_feedback_responses")
|
print("create_feedback_responses")
|
||||||
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
|
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"
|
slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback"
|
||||||
)
|
)
|
||||||
create_feedback_response_data(
|
create_feedback_response_data(
|
||||||
|
|
@ -174,6 +177,7 @@ def command(
|
||||||
"would_recommend": True,
|
"would_recommend": True,
|
||||||
"course_negative_feedback": "Nichts Schlechtes",
|
"course_negative_feedback": "Nichts Schlechtes",
|
||||||
"course_positive_feedback": "Nur Gutes.",
|
"course_positive_feedback": "Nur Gutes.",
|
||||||
|
"feedback_type": "uk",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -193,6 +197,7 @@ def command(
|
||||||
"would_recommend": True,
|
"would_recommend": True,
|
||||||
"course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
|
"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!",
|
"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,
|
"would_recommend": False,
|
||||||
"course_negative_feedback": "Mehr Videos wären schön.",
|
"course_negative_feedback": "Mehr Videos wären schön.",
|
||||||
"course_positive_feedback": "Die Präsentation war super",
|
"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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
LearningContentAssignmentFactory,
|
LearningContentAssignmentFactory,
|
||||||
LearningContentAttendanceCourseFactory,
|
LearningContentAttendanceCourseFactory,
|
||||||
LearningContentEdoniqTestFactory,
|
LearningContentEdoniqTestFactory,
|
||||||
LearningContentFeedbackFactory,
|
LearningContentFeedbackUKFactory,
|
||||||
|
LearningContentFeedbackVVFactory,
|
||||||
LearningContentKnowledgeAssessmentFactory,
|
LearningContentKnowledgeAssessmentFactory,
|
||||||
LearningContentLearningModuleFactory,
|
LearningContentLearningModuleFactory,
|
||||||
LearningContentMediaLibraryFactory,
|
LearningContentMediaLibraryFactory,
|
||||||
|
|
@ -82,6 +83,12 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
LearningUnitFactory,
|
LearningUnitFactory,
|
||||||
TopicFactory,
|
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 (
|
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||||
MediaLibraryCategoryPageFactory,
|
MediaLibraryCategoryPageFactory,
|
||||||
MediaLibraryContentPageFactory,
|
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):
|
def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
# create_locales_for_wagtail()
|
# 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()
|
course = create_test_course_with_categories()
|
||||||
competence_certificate = create_test_competence_navi()
|
competence_certificate = create_test_competence_navi()
|
||||||
|
|
||||||
|
|
@ -360,6 +372,7 @@ def create_feedback_response_data(
|
||||||
"would_recommend": True,
|
"would_recommend": True,
|
||||||
"course_negative_feedback": "Nichts Schlechtes",
|
"course_negative_feedback": "Nichts Schlechtes",
|
||||||
"course_positive_feedback": "Nur Gutes.",
|
"course_positive_feedback": "Nur Gutes.",
|
||||||
|
"feedback_type": "uk",
|
||||||
}
|
}
|
||||||
|
|
||||||
return update_feedback_response(
|
return update_feedback_response(
|
||||||
|
|
@ -523,6 +536,14 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
||||||
slug__startswith=f"test-lehrgang-assignment-reflexion"
|
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(
|
LearningContentAssignmentFactory(
|
||||||
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
|
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
|
||||||
parent=circle,
|
parent=circle,
|
||||||
|
|
@ -530,7 +551,8 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
||||||
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
|
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
|
title="Feedback",
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -614,7 +636,8 @@ def create_test_circle_reisen(lp):
|
||||||
title="Reflexion",
|
title="Reflexion",
|
||||||
parent=parent,
|
parent=parent,
|
||||||
)
|
)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
|
title="Feedback",
|
||||||
parent=parent,
|
parent=parent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
LearningContentAttendanceCourseFactory,
|
LearningContentAttendanceCourseFactory,
|
||||||
LearningContentDocumentListFactory,
|
LearningContentDocumentListFactory,
|
||||||
LearningContentEdoniqTestFactory,
|
LearningContentEdoniqTestFactory,
|
||||||
LearningContentFeedbackFactory,
|
LearningContentFeedbackUKFactory,
|
||||||
LearningContentMediaLibraryFactory,
|
LearningContentMediaLibraryFactory,
|
||||||
LearningContentPlaceholderFactory,
|
LearningContentPlaceholderFactory,
|
||||||
LearningPathFactory,
|
LearningPathFactory,
|
||||||
|
|
@ -30,6 +30,11 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
LearningUnitFactory,
|
LearningUnitFactory,
|
||||||
TopicFactory,
|
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 (
|
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||||
LearnMediaBlockFactory,
|
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")
|
user = User.objects.get(username="info@iterativ.ch")
|
||||||
|
|
||||||
course_page = CoursePage.objects.get(course_id=course_id)
|
course_page = CoursePage.objects.get(course_id=course_id)
|
||||||
|
|
||||||
lp = LearningPathFactory(
|
lp = LearningPathFactory(
|
||||||
title="Lernpfad",
|
title="Lernpfad",
|
||||||
parent=course_page,
|
parent=course_page,
|
||||||
|
|
@ -254,7 +260,7 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
||||||
title="Unterlagen für den Unterricht",
|
title="Unterlagen für den Unterricht",
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
|
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",
|
# 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)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
|
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",
|
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)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end")
|
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",
|
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)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end")
|
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)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
|
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)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end")
|
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)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end")
|
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)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1192,7 +1198,7 @@ def create_uk_fr_circle_fahrzeug(lp, title="Véhicule"):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1330,7 +1336,7 @@ def create_uk_it_circle_fahrzeug(lp, title="Veicolo"):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ from vbv_lernwelt.learnpath.graphql.types import (
|
||||||
LearningContentAttendanceCourseObjectType,
|
LearningContentAttendanceCourseObjectType,
|
||||||
LearningContentDocumentListObjectType,
|
LearningContentDocumentListObjectType,
|
||||||
LearningContentEdoniqTestObjectType,
|
LearningContentEdoniqTestObjectType,
|
||||||
LearningContentFeedbackObjectType,
|
LearningContentFeedbackUKObjectType,
|
||||||
|
LearningContentFeedbackVVObjectType,
|
||||||
LearningContentKnowledgeAssessmentObjectType,
|
LearningContentKnowledgeAssessmentObjectType,
|
||||||
LearningContentLearningModuleObjectType,
|
LearningContentLearningModuleObjectType,
|
||||||
LearningContentMediaLibraryObjectType,
|
LearningContentMediaLibraryObjectType,
|
||||||
|
|
@ -50,7 +51,8 @@ class CourseQuery(graphene.ObjectType):
|
||||||
learning_content_attendance_course = graphene.Field(
|
learning_content_attendance_course = graphene.Field(
|
||||||
LearningContentAttendanceCourseObjectType
|
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(
|
learning_content_learning_module = graphene.Field(
|
||||||
LearningContentLearningModuleObjectType
|
LearningContentLearningModuleObjectType
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,12 @@ from vbv_lernwelt.learnpath.models import (
|
||||||
LearningContentAssignment,
|
LearningContentAssignment,
|
||||||
LearningContentAttendanceCourse,
|
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 (
|
from vbv_lernwelt.media_library.create_default_media_library import (
|
||||||
create_default_media_library,
|
create_default_media_library,
|
||||||
)
|
)
|
||||||
|
|
@ -128,6 +134,11 @@ ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
|
||||||
def command(course):
|
def command(course):
|
||||||
print("Creating default courses", 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:
|
if COURSE_VERSICHERUNGSVERMITTLERIN_ID in course:
|
||||||
create_versicherungsvermittlerin_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_uk_competence_profile(course_id=course_id)
|
||||||
create_default_media_library(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():
|
def create_course_uk_de_course_sessions():
|
||||||
course = Course.objects.get(id=COURSE_UK)
|
course = Course.objects.get(id=COURSE_UK)
|
||||||
|
|
|
||||||
|
|
@ -148,8 +148,8 @@ class DashboardQuery(graphene.ObjectType):
|
||||||
assignment=ProgressDashboardAssignmentType( # noqa
|
assignment=ProgressDashboardAssignmentType( # noqa
|
||||||
_id=course_id, # noqa
|
_id=course_id, # noqa
|
||||||
total_count=len(evaluation_results), # noqa
|
total_count=len(evaluation_results), # noqa
|
||||||
points_max_count=points_max_count, # noqa
|
points_max_count=int(points_max_count), # noqa
|
||||||
points_achieved_count=points_achieved_count, # noqa
|
points_achieved_count=int(points_achieved_count), # noqa
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from wagtail.models import Page
|
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.duedate.models import DueDate
|
||||||
from vbv_lernwelt.learnpath.models import (
|
from vbv_lernwelt.learnpath.models import (
|
||||||
Circle,
|
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)
|
@admin.register(DueDate)
|
||||||
class DueDateAdmin(admin.ModelAdmin):
|
class DueDateAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "start"
|
date_hierarchy = "start"
|
||||||
|
|
@ -23,6 +37,7 @@ class DueDateAdmin(admin.ModelAdmin):
|
||||||
]
|
]
|
||||||
list_filter = ["course_session__course", "course_session"]
|
list_filter = ["course_session__course", "course_session"]
|
||||||
readonly_fields = ["course_session", "page"]
|
readonly_fields = ["course_session", "page"]
|
||||||
|
actions = [sync_wagtail_due_date_url]
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
default_readonly = super(DueDateAdmin, self).get_readonly_fields(request, obj)
|
default_readonly = super(DueDateAdmin, self).get_readonly_fields(request, obj)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class FeedbackResponseFactory(DjangoModelFactory):
|
||||||
"Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
|
"Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
"feedback_type": FuzzyChoice(["uk", "vv"]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,16 @@ from vbv_lernwelt.course.models import CourseSession
|
||||||
from vbv_lernwelt.feedback.graphql.types import (
|
from vbv_lernwelt.feedback.graphql.types import (
|
||||||
FeedbackResponseObjectType as FeedbackResponseType,
|
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.feedback.services import update_feedback_response
|
||||||
from vbv_lernwelt.iam.permissions import has_course_session_access
|
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__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -25,6 +31,7 @@ class SendFeedbackMutation(graphene.Mutation):
|
||||||
class Arguments:
|
class Arguments:
|
||||||
course_session_id = graphene.ID(required=True)
|
course_session_id = graphene.ID(required=True)
|
||||||
learning_content_page_id = graphene.ID(required=True)
|
learning_content_page_id = graphene.ID(required=True)
|
||||||
|
learning_content_type = graphene.String(required=True)
|
||||||
data = GenericScalar()
|
data = GenericScalar()
|
||||||
submitted = graphene.Boolean(required=False, default_value=False)
|
submitted = graphene.Boolean(required=False, default_value=False)
|
||||||
|
|
||||||
|
|
@ -35,11 +42,29 @@ class SendFeedbackMutation(graphene.Mutation):
|
||||||
info,
|
info,
|
||||||
course_session_id,
|
course_session_id,
|
||||||
learning_content_page_id,
|
learning_content_page_id,
|
||||||
|
learning_content_type,
|
||||||
data,
|
data,
|
||||||
submitted,
|
submitted,
|
||||||
):
|
):
|
||||||
feedback_user_id = info.context.user.id
|
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
|
id=learning_content_page_id
|
||||||
)
|
)
|
||||||
circle = learning_content.get_circle()
|
circle = learning_content.get_circle()
|
||||||
|
|
@ -65,7 +90,7 @@ class SendFeedbackMutation(graphene.Mutation):
|
||||||
course_session_id=course_session_id,
|
course_session_id=course_session_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = CourseFeedbackSerializer(data=data)
|
serializer = serializerClass(data=data)
|
||||||
|
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
]
|
||||||
|
|
@ -5,6 +5,11 @@ from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
FEEDBACK_TYPES = (
|
||||||
|
("uk", "Feedback UK"),
|
||||||
|
("vv", "Feedback VV"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FeedbackIntegerField(serializers.IntegerField):
|
class FeedbackIntegerField(serializers.IntegerField):
|
||||||
def __init__(self, **kwargs):
|
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()
|
satisfaction = FeedbackIntegerField()
|
||||||
goal_attainment = FeedbackIntegerField()
|
goal_attainment = FeedbackIntegerField()
|
||||||
proficiency = serializers.IntegerField(required=False, allow_null=True)
|
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 CypressFeedbackResponseSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FeedbackResponse
|
model = FeedbackResponse
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
|
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
|
||||||
from vbv_lernwelt.course.services import mark_course_completion
|
from vbv_lernwelt.course.services import mark_course_completion
|
||||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
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__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -12,7 +17,9 @@ logger = structlog.get_logger(__name__)
|
||||||
def update_feedback_response(
|
def update_feedback_response(
|
||||||
feedback_user: User,
|
feedback_user: User,
|
||||||
course_session: CourseSession,
|
course_session: CourseSession,
|
||||||
learning_content_feedback_page: LearningContentFeedback,
|
learning_content_feedback_page: Union[
|
||||||
|
LearningContentFeedbackUK, LearningContentFeedbackVV
|
||||||
|
],
|
||||||
submitted: bool,
|
submitted: bool,
|
||||||
validated_data: dict,
|
validated_data: dict,
|
||||||
):
|
):
|
||||||
|
|
@ -26,18 +33,7 @@ def update_feedback_response(
|
||||||
|
|
||||||
original_data = feedback_response.data
|
original_data = feedback_response.data
|
||||||
updated_data = validated_data
|
updated_data = validated_data
|
||||||
initial_data = {
|
initial_data = initial_data_for_feedback_page(learning_content_feedback_page)
|
||||||
"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": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
merged_data = initial_data | {
|
merged_data = initial_data | {
|
||||||
key: updated_data[key]
|
key: updated_data[key]
|
||||||
|
|
@ -71,3 +67,36 @@ def update_feedback_response(
|
||||||
)
|
)
|
||||||
|
|
||||||
return 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 {}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ class FeedbackRestApiTestCase(FeedbackBaseTestCase):
|
||||||
"course_negative_feedback": self.feedback_data[
|
"course_negative_feedback": self.feedback_data[
|
||||||
"course_negative_feedback"
|
"course_negative_feedback"
|
||||||
][i],
|
][i],
|
||||||
|
"feedback_type": "uk",
|
||||||
},
|
},
|
||||||
feedback_user=self.feedback_users[i],
|
feedback_user=self.feedback_users[i],
|
||||||
submitted=True,
|
submitted=True,
|
||||||
|
|
@ -129,6 +130,7 @@ class FeedbackRestApiTestCase(FeedbackBaseTestCase):
|
||||||
expected = {
|
expected = {
|
||||||
"amount": 3,
|
"amount": 3,
|
||||||
"questions": self.feedback_data,
|
"questions": self.feedback_data,
|
||||||
|
"feedbackType": "uk",
|
||||||
}
|
}
|
||||||
print(response.data)
|
print(response.data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -61,12 +61,13 @@ def get_feedback_for_circle(request, course_session_id, circle_id):
|
||||||
feedback_user__in=feedback_users(course_session_id),
|
feedback_user__in=feedback_users(course_session_id),
|
||||||
).order_by("created_at")
|
).order_by("created_at")
|
||||||
|
|
||||||
# I guess this is ok for the üK case
|
feedback_data = {"amount": len(feedbacks), "questions": {}, "feedbackType": None}
|
||||||
feedback_data = {"amount": len(feedbacks), "questions": {}}
|
|
||||||
|
|
||||||
if feedback_data["amount"] == 0:
|
if feedback_data["amount"] == 0:
|
||||||
return Response(status=200, data=feedback_data)
|
return Response(status=200, data=feedback_data)
|
||||||
|
|
||||||
|
feedback_data["feedbackType"] = feedbacks[0].data.get("feedback_type", None)
|
||||||
|
|
||||||
for field in FEEDBACK_FIELDS:
|
for field in FEEDBACK_FIELDS:
|
||||||
feedback_data["questions"][field] = []
|
feedback_data["questions"][field] = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -13,7 +13,7 @@ from vbv_lernwelt.course.models import CourseCategory, CoursePage
|
||||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
CircleFactory,
|
CircleFactory,
|
||||||
LearningContentAssignmentFactory,
|
LearningContentAssignmentFactory,
|
||||||
LearningContentFeedbackFactory,
|
LearningContentFeedbackVVFactory,
|
||||||
LearningContentLearningModuleFactory,
|
LearningContentLearningModuleFactory,
|
||||||
LearningContentMediaLibraryFactory,
|
LearningContentMediaLibraryFactory,
|
||||||
LearningContentPlaceholderFactory,
|
LearningContentPlaceholderFactory,
|
||||||
|
|
@ -201,7 +201,7 @@ def create_circle_basis(lp, title="Basis"):
|
||||||
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
|
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -278,7 +278,7 @@ def create_circle_gewinnen(lp, title="Gewinnen"):
|
||||||
slug__startswith=f"{course_slug}-assignment-reflexion"
|
slug__startswith=f"{course_slug}-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -368,7 +368,7 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
|
||||||
# slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
# slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||||
# ),
|
# ),
|
||||||
# ),
|
# ),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -554,7 +554,7 @@ def create_circle_reisen(lp, title="Reisen"):
|
||||||
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -647,7 +647,7 @@ def create_circle_einkommenssicherung(lp, title="Einkommenssicherung"):
|
||||||
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -700,7 +700,7 @@ def create_circle_wohneigentum(lp, title="Wohneigentum"):
|
||||||
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -782,7 +782,7 @@ def create_circle_pensionierung(lp, title="Pensionierung"):
|
||||||
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -839,7 +839,7 @@ def create_circle_erben(lp, title="Erben/Vererben"):
|
||||||
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -929,7 +929,7 @@ def create_circle_gesundheit(lp, title="Gesundheit"):
|
||||||
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1352,7 +1352,7 @@ def create_learning_sequence_transfer(parent, title, lc_praxis_title=None):
|
||||||
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
|
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
LearningContentFeedbackFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
parent=parent,
|
parent=parent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ from vbv_lernwelt.learnpath.models import (
|
||||||
LearningContentAttendanceCourse,
|
LearningContentAttendanceCourse,
|
||||||
LearningContentDocumentList,
|
LearningContentDocumentList,
|
||||||
LearningContentEdoniqTest,
|
LearningContentEdoniqTest,
|
||||||
LearningContentFeedback,
|
LearningContentFeedbackUK,
|
||||||
|
LearningContentFeedbackVV,
|
||||||
LearningContentKnowledgeAssessment,
|
LearningContentKnowledgeAssessment,
|
||||||
LearningContentLearningModule,
|
LearningContentLearningModule,
|
||||||
LearningContentMediaLibrary,
|
LearningContentMediaLibrary,
|
||||||
|
|
@ -49,8 +50,10 @@ class LearningContentInterface(CoursePageInterface):
|
||||||
return LearningContentAssignmentObjectType
|
return LearningContentAssignmentObjectType
|
||||||
elif isinstance(instance, LearningContentAttendanceCourse):
|
elif isinstance(instance, LearningContentAttendanceCourse):
|
||||||
return LearningContentAttendanceCourseObjectType
|
return LearningContentAttendanceCourseObjectType
|
||||||
elif isinstance(instance, LearningContentFeedback):
|
elif isinstance(instance, LearningContentFeedbackUK):
|
||||||
return LearningContentFeedbackObjectType
|
return LearningContentFeedbackUKObjectType
|
||||||
|
elif isinstance(instance, LearningContentFeedbackVV):
|
||||||
|
return LearningContentFeedbackVVObjectType
|
||||||
elif isinstance(instance, LearningContentLearningModule):
|
elif isinstance(instance, LearningContentLearningModule):
|
||||||
return LearningContentLearningModuleObjectType
|
return LearningContentLearningModuleObjectType
|
||||||
elif isinstance(instance, LearningContentKnowledgeAssessment):
|
elif isinstance(instance, LearningContentKnowledgeAssessment):
|
||||||
|
|
@ -105,9 +108,19 @@ class LearningContentPlaceholderObjectType(DjangoObjectType):
|
||||||
fields = []
|
fields = []
|
||||||
|
|
||||||
|
|
||||||
class LearningContentFeedbackObjectType(DjangoObjectType):
|
class LearningContentFeedbackUKObjectType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LearningContentFeedback
|
model = LearningContentFeedbackUK
|
||||||
|
interfaces = (
|
||||||
|
CoursePageInterface,
|
||||||
|
LearningContentInterface,
|
||||||
|
)
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
|
||||||
|
class LearningContentFeedbackVVObjectType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = LearningContentFeedbackVV
|
||||||
interfaces = (
|
interfaces = (
|
||||||
CoursePageInterface,
|
CoursePageInterface,
|
||||||
LearningContentInterface,
|
LearningContentInterface,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -72,7 +72,8 @@ class Circle(CourseBasePage):
|
||||||
"learnpath.LearningUnit",
|
"learnpath.LearningUnit",
|
||||||
"learnpath.LearningContentAssignment",
|
"learnpath.LearningContentAssignment",
|
||||||
"learnpath.LearningContentAttendanceCourse",
|
"learnpath.LearningContentAttendanceCourse",
|
||||||
"learnpath.LearningContentFeedback",
|
"learnpath.LearningContentFeedbackUK",
|
||||||
|
"learnpath.LearningContentFeedbackVV",
|
||||||
"learnpath.LearningContentLearningModule",
|
"learnpath.LearningContentLearningModule",
|
||||||
"learnpath.LearningContentKnowledgeAssessment",
|
"learnpath.LearningContentKnowledgeAssessment",
|
||||||
"learnpath.LearningContentMediaLibrary",
|
"learnpath.LearningContentMediaLibrary",
|
||||||
|
|
@ -318,7 +319,13 @@ class LearningContentPlaceholder(LearningContent):
|
||||||
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
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"]
|
parent_page_types = ["learnpath.Circle"]
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
can_user_self_toggle_course_completion = models.BooleanField(default=False)
|
can_user_self_toggle_course_completion = models.BooleanField(default=False)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ from vbv_lernwelt.learnpath.models import (
|
||||||
LearningContentAttendanceCourse,
|
LearningContentAttendanceCourse,
|
||||||
LearningContentDocumentList,
|
LearningContentDocumentList,
|
||||||
LearningContentEdoniqTest,
|
LearningContentEdoniqTest,
|
||||||
LearningContentFeedback,
|
LearningContentFeedbackUK,
|
||||||
|
LearningContentFeedbackVV,
|
||||||
LearningContentKnowledgeAssessment,
|
LearningContentKnowledgeAssessment,
|
||||||
LearningContentLearningModule,
|
LearningContentLearningModule,
|
||||||
LearningContentMediaLibrary,
|
LearningContentMediaLibrary,
|
||||||
|
|
@ -120,14 +121,24 @@ class LearningContentPlaceholderFactory(wagtail_factories.PageFactory):
|
||||||
model = LearningContentPlaceholder
|
model = LearningContentPlaceholder
|
||||||
|
|
||||||
|
|
||||||
class LearningContentFeedbackFactory(wagtail_factories.PageFactory):
|
class LearningContentFeedbackVVFactory(wagtail_factories.PageFactory):
|
||||||
title = "Feedback"
|
title = "FeedbackVV"
|
||||||
minutes = 0
|
minutes = 0
|
||||||
content_url = ""
|
content_url = ""
|
||||||
description = RichText("")
|
description = RichText("")
|
||||||
|
|
||||||
class Meta:
|
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):
|
class LearningContentLearningModuleFactory(wagtail_factories.PageFactory):
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MediaLibraryConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "vbv_lernwelt.media_files"
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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"),)
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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",)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 627 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 638 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
@ -106,19 +106,19 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
recipient__username=expected_recipient
|
recipient__username=expected_recipient
|
||||||
)
|
)
|
||||||
self.assertEquals(action_object, notification.action_object)
|
self.assertEqual(action_object, notification.action_object)
|
||||||
self.assertEquals("ASSIGNMENT_REMINDER", notification.notification_trigger)
|
self.assertEqual("ASSIGNMENT_REMINDER", notification.notification_trigger)
|
||||||
self.assertEquals("INFORMATION", notification.notification_category)
|
self.assertEqual("INFORMATION", notification.notification_category)
|
||||||
self.assertEquals(EXPECTED_MEMBER_VERB, notification.verb)
|
self.assertEqual(EXPECTED_MEMBER_VERB, notification.verb)
|
||||||
|
|
||||||
template_data = notification.data["template_data"]
|
template_data = notification.data["template_data"]
|
||||||
|
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
action_object.learning_content.get_parent_circle().title,
|
action_object.learning_content.get_parent_circle().title,
|
||||||
template_data["circle"],
|
template_data["circle"],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
action_object.learning_content.get_frontend_url(),
|
action_object.learning_content.get_frontend_url(),
|
||||||
notification.target_url,
|
notification.target_url,
|
||||||
)
|
)
|
||||||
|
|
@ -140,17 +140,17 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
if assignment_type == AssignmentType.CASEWORK:
|
if assignment_type == AssignmentType.CASEWORK:
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER.name,
|
EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER.name,
|
||||||
email_template,
|
email_template,
|
||||||
)
|
)
|
||||||
elif assignment_type == AssignmentType.PREP_ASSIGNMENT:
|
elif assignment_type == AssignmentType.PREP_ASSIGNMENT:
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER.name,
|
EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER.name,
|
||||||
email_template,
|
email_template,
|
||||||
)
|
)
|
||||||
elif type(action_object) == CourseSessionEdoniqTest:
|
elif type(action_object) == CourseSessionEdoniqTest:
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER.name,
|
EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER.name,
|
||||||
email_template,
|
email_template,
|
||||||
)
|
)
|
||||||
|
|
@ -176,7 +176,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
send_assignment_reminder_notifications()
|
send_assignment_reminder_notifications()
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEquals(3, len(Notification.objects.all()))
|
self.assertEqual(3, len(Notification.objects.all()))
|
||||||
self._assert_member_assignment_notifications(
|
self._assert_member_assignment_notifications(
|
||||||
action_object=should_be_sent,
|
action_object=should_be_sent,
|
||||||
expected_recipients=RECIPIENT_STUDENTS,
|
expected_recipients=RECIPIENT_STUDENTS,
|
||||||
|
|
@ -214,7 +214,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
send_assignment_reminder_notifications()
|
send_assignment_reminder_notifications()
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEquals(3, len(Notification.objects.all()))
|
self.assertEqual(3, len(Notification.objects.all()))
|
||||||
self._assert_member_assignment_notifications(
|
self._assert_member_assignment_notifications(
|
||||||
action_object=casework,
|
action_object=casework,
|
||||||
expected_recipients=RECIPIENT_STUDENTS,
|
expected_recipients=RECIPIENT_STUDENTS,
|
||||||
|
|
@ -236,16 +236,16 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
send_assignment_reminder_notifications()
|
send_assignment_reminder_notifications()
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEquals(1, len(Notification.objects.all()))
|
self.assertEqual(1, len(Notification.objects.all()))
|
||||||
|
|
||||||
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
||||||
self.assertEquals(casework, notification.action_object)
|
self.assertEqual(casework, notification.action_object)
|
||||||
self.assertEquals("INFORMATION", notification.notification_category)
|
self.assertEqual("INFORMATION", notification.notification_category)
|
||||||
self.assertEquals(EXPECTED_EXPERT_VERB, notification.verb)
|
self.assertEqual(EXPECTED_EXPERT_VERB, notification.verb)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
casework.evaluation_deadline.url_expert, notification.target_url
|
casework.evaluation_deadline.url_expert, notification.target_url
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"CASEWORK_EXPERT_EVALUATION_REMINDER", notification.notification_trigger
|
"CASEWORK_EXPERT_EVALUATION_REMINDER", notification.notification_trigger
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -276,7 +276,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
send_assignment_reminder_notifications()
|
send_assignment_reminder_notifications()
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEquals(3, len(Notification.objects.all()))
|
self.assertEqual(3, len(Notification.objects.all()))
|
||||||
self._assert_member_assignment_notifications(
|
self._assert_member_assignment_notifications(
|
||||||
action_object=prep_assignment,
|
action_object=prep_assignment,
|
||||||
expected_recipients=RECIPIENT_STUDENTS,
|
expected_recipients=RECIPIENT_STUDENTS,
|
||||||
|
|
|
||||||
|
|
@ -65,52 +65,52 @@ class TestAttendanceCourseReminders(TestCase):
|
||||||
|
|
||||||
send_attendance_reminder_notifications()
|
send_attendance_reminder_notifications()
|
||||||
|
|
||||||
self.assertEquals(4, len(Notification.objects.all()))
|
self.assertEqual(4, len(Notification.objects.all()))
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
recipient__username="test-student1@example.com"
|
recipient__username="test-student1@example.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"Erinnerung: Bald findet ein Präsenzkurs statt",
|
"Erinnerung: Bald findet ein Präsenzkurs statt",
|
||||||
notification.verb,
|
notification.verb,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"INFORMATION",
|
"INFORMATION",
|
||||||
notification.notification_category,
|
notification.notification_category,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"ATTENDANCE_COURSE_REMINDER",
|
"ATTENDANCE_COURSE_REMINDER",
|
||||||
notification.notification_trigger,
|
notification.notification_trigger,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac,
|
self.csac,
|
||||||
notification.action_object,
|
notification.action_object,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.course_session,
|
self.csac.course_session,
|
||||||
notification.course_session,
|
notification.course_session,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
|
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
|
||||||
notification.target_url,
|
notification.target_url,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.learning_content.title,
|
self.csac.learning_content.title,
|
||||||
notification.data["template_data"]["attendance_course"],
|
notification.data["template_data"]["attendance_course"],
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.location,
|
self.csac.location,
|
||||||
notification.data["template_data"]["location"],
|
notification.data["template_data"]["location"],
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.trainer,
|
self.csac.trainer,
|
||||||
notification.data["template_data"]["trainer"],
|
notification.data["template_data"]["trainer"],
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"),
|
self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"),
|
||||||
notification.data["template_data"]["start"],
|
notification.data["template_data"]["start"],
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"),
|
self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"),
|
||||||
notification.data["template_data"]["end"],
|
notification.data["template_data"]["end"],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue