Merged develop into feature/VBV-594-dashboard-feedback

This commit is contained in:
Christian Cueni 2023-12-18 08:51:47 +00:00
commit 2168feb74b
128 changed files with 4275 additions and 1277 deletions

View File

@ -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",

View File

@ -42,6 +42,7 @@ const dropdownSelected = computed<DropdownSelectable>({
border: !props.borderless, border: !props.borderless,
'font-bold': !props.borderless, 'font-bold': !props.borderless,
}" }"
data-cy="dropdown-select"
> >
<span v-if="dropdownSelected.iconName" class="mr-4"> <span v-if="dropdownSelected.iconName" class="mr-4">
<component :is="dropdownSelected.iconName"></component> <component :is="dropdownSelected.iconName"></component>
@ -75,6 +76,7 @@ const dropdownSelected = computed<DropdownSelectable>({
'relative cursor-default select-none py-2 pl-3 pr-9', 'relative cursor-default select-none py-2 pl-3 pr-9',
]" ]"
class="flex flex-row items-center" class="flex flex-row items-center"
:data-cy="`dropdown-select-option-${item.name}`"
> >
<span v-if="item.iconName" class="mr-4"> <span v-if="item.iconName" class="mr-4">
<component :is="item.iconName"></component> <component :is="item.iconName"></component>

View File

@ -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 }\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 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.
*/ */
@ -68,11 +68,11 @@ export function graphql(source: "\n query competenceCertificateQuery($courseSlu
/** /**
* 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 courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"]; export function graphql(source: "\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"): (typeof documents)["\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"];
/** /**
* 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 courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\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"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\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"]; export function graphql(source: "\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"): (typeof documents)["\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"];
/** /**
* 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

View File

@ -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
@ -235,6 +236,7 @@ type CourseObjectType {
title: String! title: String!
category_name: String! category_name: String!
slug: String! slug: String!
enable_circle_documents: Boolean!
learning_path: LearningPathObjectType! learning_path: LearningPathObjectType!
action_competences: [ActionCompetenceObjectType!]! action_competences: [ActionCompetenceObjectType!]!
} }
@ -455,7 +457,7 @@ type AssignmentObjectType implements CoursePageInterface {
assignment_type: AssignmentAssignmentAssignmentTypeChoices! assignment_type: AssignmentAssignmentAssignmentTypeChoices!
""" """
Muss der Auftrag durch eine Expertin oder einen Experten beurteilt werden? Muss der Auftrag durch eine/n Experten/in oder eine Lernbegleitung beurteilt werden?
""" """
needs_expert_evaluation: Boolean! needs_expert_evaluation: Boolean!
competence_certificate: CompetenceCertificateObjectType competence_certificate: CompetenceCertificateObjectType
@ -485,10 +487,14 @@ 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."""
enum AssignmentAssignmentAssignmentTypeChoices { enum AssignmentAssignmentAssignmentTypeChoices {
"""PRAXIS_ASSIGNMENT"""
PRAXIS_ASSIGNMENT
"""CASEWORK""" """CASEWORK"""
CASEWORK CASEWORK
@ -601,8 +607,20 @@ 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
"""CASEWORK""" """CASEWORK"""
CASEWORK CASEWORK
@ -701,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!
@ -827,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
} }

View File

@ -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";

View File

@ -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
} }
@ -117,6 +121,7 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
id id
title title
slug slug
enable_circle_documents
} }
users { users {
id id
@ -200,6 +205,7 @@ export const COURSE_QUERY = graphql(`
title title
slug slug
category_name category_name
enable_circle_documents
action_competences { action_competences {
competence_id competence_id
...CoursePageFields ...CoursePageFields

View File

@ -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",

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -67,7 +67,7 @@ const assignment = computed(
<div v-else-if="queryResult.error.value">{{ queryResult.error.value }}</div> <div v-else-if="queryResult.error.value">{{ queryResult.error.value }}</div>
<div v-else> <div v-else>
<header <header
class="relative flex h-12 w-full items-center justify-between border-b border-b-gray-400 bg-white px-4 lg:h-16 lg:px-8" class="relative flex h-12 w-full items-center justify-between border-b border-b-gray-400 bg-white px-4 md:h-16 md:px-8"
> >
<div class="flex items-center text-gray-900"> <div class="flex items-center text-gray-900">
<it-icon-assignment class="h-6 w-6"></it-icon-assignment> <it-icon-assignment class="h-6 w-6"></it-icon-assignment>
@ -88,7 +88,7 @@ const assignment = computed(
</button> </button>
</header> </header>
<div v-if="assignment && assignmentCompletion && assignmentUser" class="relative"> <div v-if="assignment && assignmentCompletion && assignmentUser" class="relative">
<div class="md:h-content flex flex-col md:flex-row"> <div class="flex flex-col md:h-[calc(100vh-64px)] md:flex-row">
<div <div
class="bg-white md:h-full md:overflow-y-auto" class="bg-white md:h-full md:overflow-y-auto"
:class="{ 'md:w-1/2': assignment.needs_expert_evaluation }" :class="{ 'md:w-1/2': assignment.needs_expert_evaluation }"
@ -119,7 +119,7 @@ const assignment = computed(
</div> </div>
<div <div
v-if="assignment.needs_expert_evaluation" v-if="assignment.needs_expert_evaluation"
class="order-first bg-gray-200 md:order-last md:w-1/2 md:overflow-y-auto" class="md:h-content order-first bg-gray-200 md:order-last md:w-1/2 md:overflow-y-auto"
> >
<EvaluationContainer <EvaluationContainer
:assignment-completion="assignmentCompletion" :assignment-completion="assignmentCompletion"

View File

@ -84,6 +84,22 @@ const taskExpertDataText = computed(() => {
return result; return result;
}); });
const text = computed(() => {
if (props.assignment.assignment_type === "CASEWORK") {
return {
evaluationFinish: "a.Bewertung abschliessen",
};
} else if (props.assignment.assignment_type === "PRAXIS_ASSIGNMENT") {
return {
evaluationFinish: "a.Feedback abschliessen",
};
} else {
return {
evaluationFinish: "UNKNOWN ASSIGNMENT TYPE",
};
}
});
function nextButtonEnabled() { function nextButtonEnabled() {
if (inEvaluationTask.value) { if (inEvaluationTask.value) {
return taskExpertDataText.value ?? false; return taskExpertDataText.value ?? false;
@ -159,7 +175,7 @@ function finishButtonEnabled() {
@click="emit('close')" @click="emit('close')"
> >
<span class="flex items-center"> <span class="flex items-center">
{{ $t("a.Bewertung abschliessen") }} {{ $t(text.evaluationFinish) }}
<it-icon-check class="ml-2 h-6 w-6"></it-icon-check> <it-icon-check class="ml-2 h-6 w-6"></it-icon-check>
</span> </span>
</button> </button>

View File

@ -3,6 +3,7 @@ import { useCurrentCourseSession } from "@/composables";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations"; import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import type { Assignment, AssignmentCompletion, CourseSessionUser } from "@/types"; import type { Assignment, AssignmentCompletion, CourseSessionUser } from "@/types";
import { useMutation } from "@urql/vue"; import { useMutation } from "@urql/vue";
import { computed } from "vue";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import * as log from "loglevel"; import * as log from "loglevel";
@ -17,6 +18,34 @@ const emit = defineEmits(["startEvaluation"]);
log.debug("EvaluationIntro setup"); log.debug("EvaluationIntro setup");
const text = computed(() => {
if (props.assignment.assignment_type === "CASEWORK") {
return {
evaluationTitle: "a.Bewertung",
evaluationInstruction: "assignment.evaluationInstrumentDescriptionText",
evaluationStart: "a.Bewertung starten",
evaluationContinue: "a.Bewertung fortsetzen",
evaluationView: "a.Bewertung ansehen",
};
} else if (props.assignment.assignment_type === "PRAXIS_ASSIGNMENT") {
return {
evaluationTitle: "Feedback",
evaluationInstruction: "a.assignment.evaluationInstrumentDescriptionTextFeedback",
evaluationStart: "a.Feedback geben",
evaluationContinue: "a.Feedback fortsetzen",
evaluationView: "a.Feedback ansehen",
};
} else {
return {
evaluationTitle: "UNKNOWN ASSIGNMENT TYPE",
evaluationInstruction: "UNKNOWN ASSIGNMENT TYPE",
evaluationStart: "UNKNOWN ASSIGNMENT TYPE",
evaluationContinue: "UNKNOWN ASSIGNMENT TYPE",
evaluationView: "UNKNOWN ASSIGNMENT TYPE",
};
}
});
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const upsertAssignmentCompletionMutation = useMutation( const upsertAssignmentCompletionMutation = useMutation(
@ -57,9 +86,9 @@ async function startEvaluation() {
}} }}
</div> </div>
<h3>{{ $t("a.Bewertung") }}</h3> <h3 data-cy="title">{{ $t(text.evaluationTitle) }}</h3>
<p v-if="props.dueDate" class="my-4"> <p v-if="props.dueDate" class="my-4" data-cy="evaluation-duedate">
{{ {{
$t( $t(
"assignment.Du musst die Bewertung bis am x um y Uhr abschliessen und freigeben", "assignment.Du musst die Bewertung bis am x um y Uhr abschliessen und freigeben",
@ -71,16 +100,25 @@ async function startEvaluation() {
}} }}
</p> </p>
<p class="my-4"> <p class="my-4" data-cy="instruction">
{{ $t("assignment.evaluationInstrumentDescriptionText") }} {{ $t(text.evaluationInstruction) }}
</p> </p>
<p class="my-4"> <p v-if="props.assignment.assignment_type === 'CASEWORK'" class="my-4">
<a :href="props.assignment.evaluation_document_url" class="link" target="_blank"> <a :href="props.assignment.evaluation_document_url" class="link" target="_blank">
{{ $t("a.Beurteilungsinstrument anzeigen") }} {{ $t("a.Beurteilungsinstrument anzeigen") }}
</a> </a>
</p> </p>
<p
v-if="props.assignment.solution_sample && props.assignment.solution_sample.url"
class="my-4"
>
<a :href="props.assignment.solution_sample.url" class="link" target="_blank">
{{ $t("assignment.submissionShowSampleSolution") }}
</a>
</p>
<div> <div>
<button <button
class="btn-primary text-large" class="btn-primary text-large"
@ -92,16 +130,16 @@ async function startEvaluation() {
props.assignmentCompletion.completion_status === 'EVALUATION_IN_PROGRESS' props.assignmentCompletion.completion_status === 'EVALUATION_IN_PROGRESS'
" "
> >
{{ $t("a.Bewertung fortsetzen") }} {{ $t(text.evaluationContinue) }}
</span> </span>
<span <span
v-else-if=" v-else-if="
props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED' props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED'
" "
> >
{{ $t("a.Bewertung ansehen") }} {{ $t(text.evaluationView) }}
</span> </span>
<span v-else>{{ $t("a.Bewertung starten") }}</span> <span v-else>{{ $t(text.evaluationStart) }}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -40,6 +40,37 @@ const upsertAssignmentCompletionMutation = useMutation(
UPSERT_ASSIGNMENT_COMPLETION_MUTATION UPSERT_ASSIGNMENT_COMPLETION_MUTATION
); );
const text = computed(() => {
if (props.assignment.assignment_type === "CASEWORK") {
return {
evaluationCriteria: "a.Beurteilungskriterium",
evaluationReason: "assignment.evaluationReason",
evaluationSubmit: "a.Bewertung freigeben",
evaluationSubmission: "a.Bewertung Freigabe",
evaluationFromUser: "a.Bewertung von x y",
evaluationSuccess: "a.Deine Bewertung für x y wurde freigegeben.",
};
} else if (props.assignment.assignment_type === "PRAXIS_ASSIGNMENT") {
return {
evaluationCriteria: "Feedback",
evaluationReason: "assignment.evaluationFeedback",
evaluationSubmit: "a.Feedback freigeben",
evaluationSubmission: "a.Feedback Freigabe",
evaluationFromUser: "a.Feedback von x y",
evaluationSuccess: "a.Dein Feedback für x y wurde freigegeben.",
};
} else {
return {
evaluationCriteria: "UNKNOWN ASSIGNMENT TYPE",
evaluationReason: "UNKNOWN ASSIGNMENT TYPE",
evaluationSubmit: "UNKNOWN ASSIGNMENT TYPE",
evaluationSubmission: "UNKNOWN ASSIGNMENT TYPE",
evaluationFromUser: "UNKNOWN ASSIGNMENT TYPE",
evaluationSuccess: "UNKNOWN ASSIGNMENT TYPE",
};
}
});
async function submitEvaluation() { async function submitEvaluation() {
upsertAssignmentCompletionMutation.executeMutation({ upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.assignment.id, assignmentId: props.assignment.id,
@ -58,7 +89,9 @@ async function submitEvaluation() {
} }
function subTaskByPoints(task: AssignmentEvaluationTask, points = 0) { function subTaskByPoints(task: AssignmentEvaluationTask, points = 0) {
return task.value.sub_tasks.find((subTask) => subTask.value.points === points); return task.value.sub_tasks !== undefined
? task.value.sub_tasks.find((subTask) => subTask.value.points === points)
: null;
} }
function evaluationForTask(task: AssignmentEvaluationTask) { function evaluationForTask(task: AssignmentEvaluationTask) {
@ -93,12 +126,19 @@ const evaluationUser = computed(() => {
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div> <div>
<h3 v-if="evaluationUser && props.showEvaluationUser" class="mb-6"> <h3 v-if="evaluationUser && props.showEvaluationUser" class="mb-6">
Bewertung von {{ evaluationUser.first_name }} {{ evaluationUser.last_name }} {{
$t(text.evaluationFromUser, {
x: evaluationUser?.first_name,
y: evaluationUser?.last_name,
})
}}
</h3> </h3>
<h3 v-else class="mb-6" data-cy="sub-title">{{ $t("a.Bewertung Freigabe") }}</h3> <h3 v-else class="mb-6" data-cy="sub-title">{{ $t(text.evaluationSubmission) }}</h3>
<section class="mb-6 border p-6" data-cy="result-section"> <section class="mb-6 border p-6" data-cy="result-section">
<section class="flex items-center"> <section
v-if="props.assignment.assignment_type === 'CASEWORK'"
class="flex items-center"
>
<div class="heading-1 py-4" data-cy="user-points"> <div class="heading-1 py-4" data-cy="user-points">
{{ userPoints }} {{ userPoints }}
</div> </div>
@ -116,6 +156,7 @@ const evaluationUser = computed(() => {
<div <div
v-if=" v-if="
props.assignment.assignment_type === 'CASEWORK' &&
props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED' && props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED' &&
!props.assignmentCompletion.evaluation_passed !props.assignmentCompletion.evaluation_passed
" "
@ -125,20 +166,29 @@ const evaluationUser = computed(() => {
</span> </span>
</div> </div>
<p class="my-4"> <div v-if="props.assignment.assignment_type === 'CASEWORK'">
{{ $t("assignment.evaluationInstrumentDescriptionText") }} <p class="my-4">
</p> {{ $t("assignment.evaluationInstrumentDescriptionText") }}
</p>
<p class="my-4">
<a
:href="props.assignment.evaluation_document_url"
class="link"
target="_blank"
>
{{ $t("a.Beurteilungsinstrument anzeigen") }}
</a>
</p>
</div>
<p class="my-4"> <p
<a v-if="
:href="props.assignment.evaluation_document_url" props.assignment.assignment_type === 'PRAXIS_ASSIGNMENT' &&
class="link" props.assignmentCompletion.completion_status !== 'EVALUATION_SUBMITTED'
target="_blank" "
> >
{{ $t("a.Beurteilungsinstrument anzeigen") }} {{ $t("a.assignment.evaluationFeedbackDescriptionText") }}
</a>
</p> </p>
<div <div
v-if="props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED'" v-if="props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED'"
> >
@ -156,13 +206,18 @@ const evaluationUser = computed(() => {
data-cy="submit-evaluation" data-cy="submit-evaluation"
@click="submitEvaluation()" @click="submitEvaluation()"
> >
{{ $t("a.Bewertung freigeben") }} {{ $t(text.evaluationSubmit) }}
</button> </button>
</div> </div>
<div v-if="state.showSuccessInfo" class="mt-4"> <div v-if="state.showSuccessInfo" class="mt-4">
<ItSuccessAlert <ItSuccessAlert
:text="`Deine Bewertung für ${props.assignmentUser.first_name} ${props.assignmentUser.last_name} wurde freigegeben.`" :text="
$t(text.evaluationSuccess, {
x: props.assignmentUser.first_name,
y: props.assignmentUser.last_name,
})
"
></ItSuccessAlert> ></ItSuccessAlert>
</div> </div>
</section> </section>
@ -172,7 +227,7 @@ const evaluationUser = computed(() => {
<article class="border-t py-4"> <article class="border-t py-4">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div class="mb-4 text-gray-900"> <div class="mb-4 text-gray-900">
{{ $t("a.Beurteilungskriterium") }} {{ index + 1 }}: {{ $t(text.evaluationCriteria) }} {{ index + 1 }}:
{{ task.value.title }} {{ task.value.title }}
</div> </div>
<div <div
@ -209,13 +264,16 @@ const evaluationUser = computed(() => {
open-links-in-new-tab open-links-in-new-tab
/> />
<div class="text-sm text-gray-800"> <div
v-if="assignment.assignment_type === 'CASEWORK'"
class="text-sm text-gray-800"
>
{{ evaluationForTask(task).points }} Punkte {{ evaluationForTask(task).points }} Punkte
</div> </div>
</section> </section>
<div> <div>
<span class="font-bold">{{ $t("a.Begründung") }}:</span> <span class="font-bold">{{ $t(text.evaluationReason) }}:</span>
{{ evaluationForTask(task).text }} {{ evaluationForTask(task).text }}
</div> </div>
</article> </article>

View File

@ -38,6 +38,28 @@ const expertData = computed(() => {
return data; return data;
}); });
const text = computed(() => {
if (props.assignment.assignment_type === "CASEWORK") {
return {
evaluationCriteria: "a.Beurteilungskriterium",
evaluationReason: "assignment.evaluationReason",
evaluationReasonPlaceholder: "assignment.justificationRequiredText",
};
} else if (props.assignment.assignment_type === "PRAXIS_ASSIGNMENT") {
return {
evaluationCriteria: "Feedback",
evaluationReason: "assignment.evaluationFeedback",
evaluationReasonPlaceholder: "assignment.feedbackRequiredText",
};
} else {
return {
evaluationCriteria: "UNKNOWN ASSIGNMENT TYPE",
evaluationReason: "UNKNOWN ASSIGNMENT TYPE",
evaluationReasonPlaceholder: "UNKNOWN ASSIGNMENT TYPE",
};
}
});
function changePoints(points: number) { function changePoints(points: number) {
log.debug("changePoints", points); log.debug("changePoints", points);
evaluateAssignmentCompletion({ evaluateAssignmentCompletion({
@ -88,7 +110,7 @@ const evaluateAssignmentCompletionDebounced = useDebounceFn(
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div data-cy="evaluation-task"> <div data-cy="evaluation-task">
<div class="text-bold mb-4 text-sm"> <div class="text-bold mb-4 text-sm">
{{ $t("a.Beurteilungskriterium") }} {{ taskIndex + 1 }} / {{ $t(text.evaluationCriteria) }} {{ taskIndex + 1 }} /
{{ props.assignment.evaluation_tasks.length }} {{ props.assignment.evaluation_tasks.length }}
{{ task.value.title }} {{ task.value.title }}
</div> </div>
@ -127,9 +149,9 @@ const evaluateAssignmentCompletionDebounced = useDebounceFn(
<ItTextarea <ItTextarea
class="mt-8" class="mt-8"
:model-value="expertData.text ?? ''" :model-value="expertData.text ?? ''"
:label="$t('a.Begründung')" :label="$t(text.evaluationReason)"
:disabled="!props.allowEdit" :disabled="!props.allowEdit"
:placeholder="$t('assignment.justificationRequiredText')" :placeholder="$t(text.evaluationReasonPlaceholder)"
data-cy="reason-text" data-cy="reason-text"
@update:model-value="onUpdateText($event)" @update:model-value="onUpdateText($event)"
></ItTextarea> ></ItTextarea>

View File

@ -38,6 +38,12 @@ const assignmentDetail = computed(() => {
return courseSessionDetailResult.findAssignment(props.learningContent.id); return courseSessionDetailResult.findAssignment(props.learningContent.id);
}); });
const isPraxisAssignment = computed(() => {
return (
props.learningContent.content_assignment.assignment_type === "PRAXIS_ASSIGNMENT"
);
});
onMounted(async () => { onMounted(async () => {
const { gradedUsers, assignmentSubmittedUsers } = const { gradedUsers, assignmentSubmittedUsers } =
await loadAssignmentCompletionStatusData( await loadAssignmentCompletionStatusData(
@ -130,7 +136,13 @@ function findUserPointsHtml(userId: string) {
> >
<it-icon-check class="h-4/5 w-4/5"></it-icon-check> <it-icon-check class="h-4/5 w-4/5"></it-icon-check>
</div> </div>
<div class="ml-2">{{ $t("a.Bewertung freigegeben") }}</div> <div class="ml-2">
{{
isPraxisAssignment
? $t("a.Feedback freigegeben")
: $t("a.Bewertung freigegeben")
}}
</div>
</div> </div>
<div <div
v-else-if="state.assignmentSubmittedUsers.includes(csu)" v-else-if="state.assignmentSubmittedUsers.includes(csu)"
@ -146,7 +158,7 @@ function findUserPointsHtml(userId: string) {
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div
v-if="findGradedUser(csu.user_id)" v-if="findGradedUser(csu.user_id) && !isPraxisAssignment"
v-html="findUserPointsHtml(csu.user_id)" v-html="findUserPointsHtml(csu.user_id)"
></div> ></div>
</section> </section>

View File

@ -61,6 +61,7 @@ const totalCount = (status: StatusCount) => {
const showEvaluationStatus = computed(() => { const showEvaluationStatus = computed(() => {
return ( return (
props.learningContent.content_assignment.assignment_type === "CASEWORK" || props.learningContent.content_assignment.assignment_type === "CASEWORK" ||
props.learningContent.content_assignment.assignment_type === "PRAXIS_ASSIGNMENT" ||
props.learningContent.content_assignment.assignment_type === "EDONIQ_TEST" props.learningContent.content_assignment.assignment_type === "EDONIQ_TEST"
); );
}); });

View File

@ -55,7 +55,11 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
</a> </a>
</div> </div>
</div> </div>
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0"> <div
v-if="courseSession.course.enable_circle_documents"
class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0"
data-cy="circle-documents"
>
<div> <div>
<h3 class="heading-3 mb-4 flex items-center gap-2"> <h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("a.Unterlagen für Teilnehmenden") }} {{ $t("a.Unterlagen für Teilnehmenden") }}

View File

@ -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) => {
@ -101,7 +105,11 @@ const getLearningContentType = (lc: LearningContent) => {
const getShowDetailsText = (lc: LearningContent) => { const getShowDetailsText = (lc: LearningContent) => {
if (isAssignment(lc)) { if (isAssignment(lc)) {
const assignmentType = (lc as LearningContentAssignment).assignment_type; const assignmentType = (lc as LearningContentAssignment).assignment_type;
if (assignmentType === "CASEWORK" || assignmentType === "REFLECTION") { if (
assignmentType === "PRAXIS_ASSIGNMENT" ||
assignmentType === "CASEWORK" ||
assignmentType === "REFLECTION"
) {
return t("a.Ergebnisse anschauen"); return t("a.Ergebnisse anschauen");
} else if ( } else if (
assignmentType === "PREP_ASSIGNMENT" || assignmentType === "PREP_ASSIGNMENT" ||
@ -132,6 +140,7 @@ const getIconName = (lc: LearningContent) => {
if ( if (
assignmentType === "PREP_ASSIGNMENT" || assignmentType === "PREP_ASSIGNMENT" ||
assignmentType === "CASEWORK" || assignmentType === "CASEWORK" ||
assignmentType === "PRAXIS_ASSIGNMENT" ||
assignmentType === "CONDITION_ACCEPTANCE" assignmentType === "CONDITION_ACCEPTANCE"
) { ) {
return "it-icon-assignment-large"; return "it-icon-assignment-large";

View File

@ -7,8 +7,8 @@ import CircleDiagram from "./CircleDiagram.vue";
import CircleOverview from "./CircleOverview.vue"; import CircleOverview from "./CircleOverview.vue";
import DocumentSection from "./DocumentSection.vue"; import DocumentSection from "./DocumentSection.vue";
import { import {
useCourseSessionDetailQuery,
useCourseDataWithCompletion, useCourseDataWithCompletion,
useCourseSessionDetailQuery,
} from "@/composables"; } from "@/composables";
import { stringifyParse } from "@/utils/utils"; import { stringifyParse } from "@/utils/utils";
import { useCircleStore } from "@/stores/circle"; import { useCircleStore } from "@/stores/circle";
@ -63,6 +63,10 @@ const showDuration = computed(() => {
return false; return false;
}); });
const showDocumentSection = computed(() => {
return lpQueryResult.course.value?.enable_circle_documents && !props.readonly;
});
watch( watch(
() => circle.value, () => circle.value,
() => { () => {
@ -189,7 +193,7 @@ watch(
{{ $t("circlePage.learnMore") }} {{ $t("circlePage.learnMore") }}
</button> </button>
</div> </div>
<DocumentSection v-if="!readonly" :circle="circle" /> <DocumentSection v-if="showDocumentSection" :circle="circle" />
<div v-if="!props.readonly" class="expert mt-8 border p-6"> <div v-if="!props.readonly" class="expert mt-8 border p-6">
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3> <h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
<div class="mt-4 leading-relaxed"> <div class="mt-4 leading-relaxed">

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mt-8 block border p-6"> <div class="mt-8 block border p-6" data-cy="circle-document-section">
<h3 class="text-blue-dark"> <h3 class="text-blue-dark">
{{ $t("circlePage.documents.title") }} {{ $t("circlePage.documents.title") }}
</h3> </h3>

View File

@ -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,

View File

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue"; import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import RichText from "@/components/ui/RichText.vue";
import type { Assignment } from "@/types"; import type { Assignment } from "@/types";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import log from "loglevel";
import dayjs from "dayjs"; import dayjs from "dayjs";
import RichText from "@/components/ui/RichText.vue"; import log from "loglevel";
interface Props { interface Props {
assignment: Assignment; assignment: Assignment;
@ -40,15 +40,13 @@ const step = useRouteQuery("step");
</li> </li>
</ul> </ul>
<h3 class="mb-4 mt-8">{{ $t("assignment.dueDateSubmission") }}</h3> <div v-if="submissionDeadlineStart" class="text-large">
<h3 class="mb-4 mt-8">{{ $t("assignment.dueDateSubmission") }}</h3>
<p v-if="submissionDeadlineStart" class="text-large">
{{ $t("assignment.dueDateIntroduction") }} {{ $t("assignment.dueDateIntroduction") }}
<DateEmbedding :single-date="dayjs(submissionDeadlineStart)"></DateEmbedding> <p>
</p> <DateEmbedding :single-date="dayjs(submissionDeadlineStart)"></DateEmbedding>
<p v-else class="text-large"> </p>
{{ $t("assignment.dueDateNotSet") }} </div>
</p>
<div v-if="props.assignment.effort_required"> <div v-if="props.assignment.effort_required">
<h3 class="mb-4 mt-8">{{ $t("assignment.effortTitle") }}</h3> <h3 class="mb-4 mt-8">{{ $t("assignment.effortTitle") }}</h3>

View File

@ -19,6 +19,7 @@ import { computed, reactive } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import eventBus from "@/utils/eventBus"; import eventBus from "@/utils/eventBus";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { AssignmentAssignmentAssignmentTypeChoices } from "@/gql/graphql";
const props = defineProps<{ const props = defineProps<{
assignment: Assignment; assignment: Assignment;
@ -76,14 +77,24 @@ const completionTaskData = computed(() => {
return props.assignmentCompletion?.task_completion_data ?? {}; return props.assignmentCompletion?.task_completion_data ?? {};
}); });
const canSubmit = computed(() => { const cannotSubmit = computed(() => {
return ( return (
!state.confirmInput || (!state.confirmInput && !isPraxisAssignment.value) ||
(props.assignment.assignment_type === "CASEWORK" && !state.confirmPerson) (props.assignment.assignment_type === "CASEWORK" && !state.confirmPerson)
); );
}); });
const isCasework = computed(() => props.assignment.assignment_type === "CASEWORK"); function checkAssignmentType(
assignmentType: AssignmentAssignmentAssignmentTypeChoices[]
) {
return assignmentType.includes(props.assignment.assignment_type);
}
const isCasework = computed(() => checkAssignmentType(["CASEWORK"]));
const mayBeEvaluated = computed(() =>
checkAssignmentType(["CASEWORK", "PRAXIS_ASSIGNMENT"])
);
const isPraxisAssignment = computed(() => checkAssignmentType(["PRAXIS_ASSIGNMENT"]));
const upsertAssignmentCompletionMutation = useMutation( const upsertAssignmentCompletionMutation = useMutation(
UPSERT_ASSIGNMENT_COMPLETION_MUTATION UPSERT_ASSIGNMENT_COMPLETION_MUTATION
@ -93,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({
@ -108,20 +127,24 @@ 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>
<div v-if="completionStatus === 'IN_PROGRESS'"> <div v-if="completionStatus === 'IN_PROGRESS'">
<ItCheckbox <ItCheckbox
v-if="!isPraxisAssignment"
class="w-full border-b border-gray-400 py-10 sm:py-6" class="w-full border-b border-gray-400 py-10 sm:py-6"
:checkbox-item="{ :checkbox-item="{
label: $t('assignment.confirmSubmitResults'), label: $t('assignment.confirmSubmitResults'),
@ -131,11 +154,14 @@ const onSubmit = async () => {
data-cy="confirm-submit-results" data-cy="confirm-submit-results"
@toggle="state.confirmInput = !state.confirmInput" @toggle="state.confirmInput = !state.confirmInput"
></ItCheckbox> ></ItCheckbox>
<div v-if="isCasework" class="w-full border-b border-gray-400"> <div v-if="mayBeEvaluated" class="w-full border-b border-gray-400">
<ItCheckbox <ItCheckbox
v-if="mayBeEvaluated"
class="py-6" class="py-6"
:checkbox-item="{ :checkbox-item="{
label: $t('assignment.confirmSubmitPerson'), label: isPraxisAssignment
? $t('a.confirmSubmitPersonPraxisAssignment')
: $t('assignment.confirmSubmitPerson'),
value: 'value', value: 'value',
checked: state.confirmPerson, checked: state.confirmPerson,
}" }"
@ -160,7 +186,7 @@ const onSubmit = async () => {
{{ $t("assignment.showAssessmentDocument") }} {{ $t("assignment.showAssessmentDocument") }}
</a> </a>
</div> </div>
<p v-if="isCasework && props.submissionDeadlineStart" class="pt-6"> <p v-if="mayBeEvaluated && props.submissionDeadlineStart" class="pt-6">
{{ $t("assignment.dueDateSubmission") }} {{ $t("assignment.dueDateSubmission") }}
<DateEmbedding <DateEmbedding
:single-date="dayjs(props.submissionDeadlineStart)" :single-date="dayjs(props.submissionDeadlineStart)"
@ -170,7 +196,7 @@ const onSubmit = async () => {
class="mt-6" class="mt-6"
variant="blue" variant="blue"
size="large" size="large"
:disabled="canSubmit" :disabled="cannotSubmit"
data-cy="submit-assignment" data-cy="submit-assignment"
@click="onSubmit" @click="onSubmit"
> >
@ -187,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

View File

@ -67,17 +67,17 @@ function handleDelete() {
id="upload" id="upload"
type="file" type="file"
class="absolute opacity-0" class="absolute opacity-0"
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.mov,.ppt,.pptx" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.mov,.ppt,.pptx,.mp4"
@change="fileSelected" @change="fileSelected"
/> />
<it-icon-document class="mr-1.5 h-7 w-7 text-blue-800" /> <it-icon-document class="mr-1.5 h-7 w-7 text-blue-800" />
{{ $t("a.Datei auswählen") }} {{ $t("a.Datei auswählen") }}
</div> </div>
<p class="text-sm text-gray-900"> <p class="text-sm text-gray-900">
{{ $t("a.Mögliche Formate") }}: .JPG, .PNG, .PDF, .DOC, .MOV, .PPT {{ $t("a.Mögliche Formate") }}: .JPG, .PNG, .PDF, .DOC, .MOV, .PPT, .MP4
</p> </p>
<p class="mb-8 text-sm text-gray-900"> <p class="mb-8 text-sm text-gray-900">
{{ $t("a.Maximale Dateigrösse") }}: 20 MB {{ $t("a.Maximale Dateigrösse") }}: 50 MB
</p> </p>
</template> </template>

View File

@ -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)"
/> />

View File

@ -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>

View File

@ -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>

View File

@ -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"]);

View File

@ -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
@ -188,6 +194,7 @@ export interface Course {
title: string; title: string;
category_name: string; category_name: string;
slug: string; slug: string;
enable_circle_documents: boolean;
} }
export interface CourseCategory { export interface CourseCategory {
@ -342,7 +349,7 @@ export interface AssignmentEvaluationTask {
title: string; title: string;
description: string; description: string;
max_points: number; max_points: number;
sub_tasks: AssignmentEvaluationSubTask[]; sub_tasks?: AssignmentEvaluationSubTask[];
}; };
} }
@ -559,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;
}

View File

@ -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 {

View File

@ -42,6 +42,8 @@ export function getAssignmentTypeTitle(assignmentType: AssignmentType): string {
switch (assignmentType) { switch (assignmentType) {
case "CASEWORK": case "CASEWORK":
return t("learningContentTypes.casework"); return t("learningContentTypes.casework");
case "PRAXIS_ASSIGNMENT":
return t("learningContentTypes.praxisAssignment");
case "PREP_ASSIGNMENT": case "PREP_ASSIGNMENT":
return t("learningContentTypes.prepAssignment"); return t("learningContentTypes.prepAssignment");
case "REFLECTION": case "REFLECTION":

View File

@ -1,6 +1,121 @@
import { TEST_STUDENT1_USER_ID } from "../../consts"; import { TEST_STUDENT1_USER_ID } from "../../consts";
import { login } from "../helpers"; import { login } from "../helpers";
function completePraxisAssignment(selectExpert = false) {
cy.visit("/course/test-lehrgang/learn/reisen/mein-kundenstamm");
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 1: Filtere nach Kundeneigenschaften"
);
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 1.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 1.2");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-3"]')
.clear()
.type("Hallo Teilaufgabe 1.3");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-4"]')
.clear()
.type("Hallo Teilaufgabe 1.4");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-5"]')
.clear()
.type("Hallo Teilaufgabe 1.5");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 2
cy.testLearningContentTitle("Teilaufgabe 2: Filtere nach Versicherungen");
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 2.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 2.2");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-3"]')
.clear()
.type("Hallo Teilaufgabe 2.3");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-4"]')
.clear()
.type("Hallo Teilaufgabe 2.4");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// check that results are stored on server
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion("assignment_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.completion_status).to.equal("IN_PROGRESS");
expect(JSON.stringify(ac.completion_data)).to.include(
"Hallo Teilaufgabe 2.1"
);
}
);
// step 3
cy.testLearningContentTitle(
"Teilaufgabe 3: Filtere nach besonderen Ereignissen"
);
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 3.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 3.2");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 4
cy.testLearningContentTitle("Teilaufgabe 4: Kundentelefonate");
cy.get('[data-cy="it-textarea-user-text-input-0"]')
.clear()
.type("Hallo Teilaufgabe 4.1");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 5
cy.testLearningContentTitle("Teilaufgabe 5: Kundentelefonate2");
cy.get('[data-cy="it-textarea-user-text-input-0"]')
.clear()
.type("Hallo Teilaufgabe 5.1");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="confirm-submit-results"]').should("not.exist");
cy.get('[data-cy="confirm-submit-person"]').should(
"contain",
"Folgende Person soll mir Feedback zu meinen Ergebnissen geben."
);
if (selectExpert) {
cy.get('[data-cy="confirm-submit-person"]').click();
}
cy.get('[data-cy="submit-assignment"]').click();
cy.get('[data-cy="success-text"]').should("exist");
// app goes back to circle view -> check if assignment is marked as completed
cy.url().should((url) => {
expect(url).to.match(/\/reisen#lu-reisen?$/);
});
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm-checkbox"]'
).should("have.class", "cy-checked");
}
describe("assignmentStudent.cy.js", () => { describe("assignmentStudent.cy.js", () => {
beforeEach(() => { beforeEach(() => {
cy.manageCommand("cypress_reset"); cy.manageCommand("cypress_reset");
@ -10,234 +125,255 @@ describe("assignmentStudent.cy.js", () => {
); );
}); });
it("can open assignment", () => { describe("Assignment", () => {
cy.testLearningContentTitle("Einleitung"); it("can open assignment", () => {
cy.testLearningContentSubtitle( cy.testLearningContentTitle("Einleitung");
"Überprüfen einer Motorfahrzeugs-Versicherungspolice" cy.testLearningContentSubtitle(
); "Überprüfen einer Motorfahrzeugs-Versicherungspolice"
});
it("can navigate through assignment", () => {
// 1 Step forward
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
);
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 2: Kundensituation und Ausgangslage"
);
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
cy.learningContentMultiLayoutPreviousStep();
cy.testLearningContentTitle(
"Teilaufgabe 2: Kundensituation und Ausgangslage"
);
});
it("can save confirmation", () => {
// 1 Step forward
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
);
// Click confirmation
cy.get('[data-cy="it-checkbox-confirmation-1"]').click();
cy.reload();
cy.get('[data-cy="it-checkbox-confirmation-1"]').should(
"have.class",
"cy-checked"
);
});
it("can save text", () => {
// 2 Steps forward
cy.learningContentMultiLayoutNextStep();
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 2: Kundensituation und Ausgangslage"
);
// Enter text
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallovelo");
// wait because of input debounce
cy.wait(550);
cy.reload();
cy.get('[data-cy="it-textarea-user-text-input-1"]').should(
"have.value",
"Hallovelo"
);
});
it("can visit sub step directly via url", () => {
cy.visit(
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice?step=3"
);
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
});
it("can visit sub step by clicking navigation bar", () => {
cy.get('[data-cy="nav-progress-step-4"]').click();
cy.testLearningContentTitle("Teilaufgabe 4: Deine Empfehlungen");
});
it("can submit assignment", () => {
cy.visit(
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice?step=7"
);
cy.get('[data-cy="confirm-submit-results"] label').click();
cy.get('[data-cy="confirm-submit-person"]').click();
cy.get('[data-cy="submit-assignment"]').click();
cy.get('[data-cy="success-text"]').should("exist");
// Check if trainer received notification
cy.clearLocalStorage();
cy.clearCookies();
cy.reload(true);
login("test-trainer1@example.com", "test");
cy.visit("/notifications");
cy.get(`[data-cy=notification-idx-0]`).within(() => {
cy.get('[data-cy="unread"]').should("exist");
cy.get('[data-cy="notification-target-idx-0-verb"]').contains(
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben."
);
});
});
it("can make complete assignment", () => {
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
);
// Click confirmation
cy.get('[data-cy="it-checkbox-confirmation-1"]').click();
cy.learningContentMultiLayoutNextStep();
// step 2
cy.testLearningContentTitle(
"Teilaufgabe 2: Kundensituation und Ausgangslage"
);
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 2");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// check that results are stored on server
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"assignment_user_id",
TEST_STUDENT1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("IN_PROGRESS");
expect(JSON.stringify(ac.completion_data)).to.include(
"Hallo Teilaufgabe 2"
); );
}); });
// step 3 it("can navigate through assignment", () => {
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung"); // 1 Step forward
cy.get('[data-cy="it-textarea-user-text-input-1"]') cy.learningContentMultiLayoutNextStep();
.clear() cy.testLearningContentTitle(
.type("Hallo Teilaufgabe 3"); "Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
// wait because of input debounce );
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 4 cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle("Teilaufgabe 4: Deine Empfehlungen"); cy.testLearningContentTitle(
cy.get('[data-cy="it-textarea-user-text-input-1"]') "Teilaufgabe 2: Kundensituation und Ausgangslage"
.clear() );
.type("Hallo Teilaufgabe 4.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 4.2");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-3"]')
.clear()
.type("Hallo Teilaufgabe 4.3");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 5 cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle("Teilaufgabe 5: Reflexion"); cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 5.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 5.2");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-3"]')
.clear()
.type("Hallo Teilaufgabe 5.3");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 6 cy.learningContentMultiLayoutPreviousStep();
cy.testLearningContentTitle("Teilaufgabe 6: Learnings");
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 6.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 6.2");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="confirm-submit-results"] label').click(); cy.testLearningContentTitle(
cy.get('[data-cy="confirm-submit-person"]').click(); "Teilaufgabe 2: Kundensituation und Ausgangslage"
cy.get('[data-cy="submit-assignment"]').click(); );
cy.get('[data-cy="success-text"]').should("exist");
// app goes back to circle view -> check if assignment is marked as completed
cy.url().should((url) => {
expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
}); });
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
).should("have.class", "cy-checked");
// reopening page should get directly to last step it("can save confirmation", () => {
cy.visit( // 1 Step forward
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice" cy.learningContentMultiLayoutNextStep();
); cy.testLearningContentTitle(
cy.url().should("include", "step=7"); "Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
);
// Click confirmation
cy.get('[data-cy="it-checkbox-confirmation-1"]').click();
// load AssignmentCompletion from DB and check cy.reload();
cy.loadAssignmentCompletion( cy.get('[data-cy="it-checkbox-confirmation-1"]').should(
"assignment_user_id", "have.class",
TEST_STUDENT1_USER_ID "cy-checked"
).then((ac) => { );
expect(ac.completion_status).to.equal("SUBMITTED");
expect(ac.evaluation_max_points).to.equal(24);
const completionString = JSON.stringify(ac.completion_data);
console.log(completionString);
expect(completionString).to.include("Hallo Teilaufgabe 2");
expect(completionString).to.include("Hallo Teilaufgabe 3");
expect(completionString).to.include("Hallo Teilaufgabe 4.1");
expect(completionString).to.include("Hallo Teilaufgabe 4.2");
expect(completionString).to.include("Hallo Teilaufgabe 4.3");
expect(completionString).to.include("Hallo Teilaufgabe 5.1");
expect(completionString).to.include("Hallo Teilaufgabe 5.2");
expect(completionString).to.include("Hallo Teilaufgabe 5.3");
expect(completionString).to.include("Hallo Teilaufgabe 6.1");
expect(completionString).to.include("Hallo Teilaufgabe 6.2");
}); });
it("can save text", () => {
// 2 Steps forward
cy.learningContentMultiLayoutNextStep();
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 2: Kundensituation und Ausgangslage"
);
// Enter text
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallovelo");
// wait because of input debounce
cy.wait(550);
cy.reload();
cy.get('[data-cy="it-textarea-user-text-input-1"]').should(
"have.value",
"Hallovelo"
);
});
it("can visit sub step directly via url", () => {
cy.visit(
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice?step=3"
);
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
});
it("can visit sub step by clicking navigation bar", () => {
cy.get('[data-cy="nav-progress-step-4"]').click();
cy.testLearningContentTitle("Teilaufgabe 4: Deine Empfehlungen");
});
it("can submit assignment", () => {
cy.visit(
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice?step=7"
);
cy.get('[data-cy="confirm-submit-results"] label').click();
cy.get('[data-cy="confirm-submit-person"]').click();
cy.get('[data-cy="submit-assignment"]').click();
cy.get('[data-cy="success-text"]').should("exist");
// Check if trainer received notification
cy.clearLocalStorage();
cy.clearCookies();
cy.reload(true);
login("test-trainer1@example.com", "test");
cy.visit("/notifications");
cy.get(`[data-cy=notification-idx-0]`).within(() => {
cy.get('[data-cy="unread"]').should("exist");
cy.get('[data-cy="notification-target-idx-0-verb"]').contains(
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben."
);
});
});
it("can make complete assignment", () => {
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
);
// Click confirmation
cy.get('[data-cy="it-checkbox-confirmation-1"]').click();
cy.learningContentMultiLayoutNextStep();
// step 2
cy.testLearningContentTitle(
"Teilaufgabe 2: Kundensituation und Ausgangslage"
);
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 2");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// check that results are stored on server
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"assignment_user_id",
TEST_STUDENT1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("IN_PROGRESS");
expect(JSON.stringify(ac.completion_data)).to.include(
"Hallo Teilaufgabe 2"
);
});
// step 3
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 3");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 4
cy.testLearningContentTitle("Teilaufgabe 4: Deine Empfehlungen");
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 4.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 4.2");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-3"]')
.clear()
.type("Hallo Teilaufgabe 4.3");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 5
cy.testLearningContentTitle("Teilaufgabe 5: Reflexion");
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 5.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 5.2");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-3"]')
.clear()
.type("Hallo Teilaufgabe 5.3");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
// step 6
cy.testLearningContentTitle("Teilaufgabe 6: Learnings");
cy.get('[data-cy="it-textarea-user-text-input-1"]')
.clear()
.type("Hallo Teilaufgabe 6.1");
cy.wait(550);
cy.get('[data-cy="it-textarea-user-text-input-2"]')
.clear()
.type("Hallo Teilaufgabe 6.2");
// wait because of input debounce
cy.wait(550);
cy.learningContentMultiLayoutNextStep();
cy.get('[data-cy="confirm-submit-person"]').should(
"contain",
"Ja, die folgende Person soll meine Ergebnisse bewerten."
);
cy.get('[data-cy="confirm-submit-results"] label').click();
cy.get('[data-cy="confirm-submit-person"]').click();
cy.get('[data-cy="submit-assignment"]').click();
cy.get('[data-cy="success-text"]').should("exist");
cy.get('[data-cy="confirm-container"]')
.find('[data-cy="show-sample-solution"]')
.then(($elements) => {
if ($elements.length > 0) {
// Ist die Musterlösung da?
cy.get('[data-cy="show-sample-solution"]').should("exist");
cy.get('[data-cy="show-sample-solution-button"]').should("exist");
}
});
cy.visit("/course/test-lehrgang/learn/fahrzeug/");
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
).should("have.class", "cy-checked");
//reopening page should get directly to last step
cy.visit(
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
);
cy.url().should("include", "step=7");
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"assignment_user_id",
TEST_STUDENT1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("SUBMITTED");
expect(ac.evaluation_max_points).to.equal(24);
const completionString = JSON.stringify(ac.completion_data);
console.log(completionString);
expect(completionString).to.include("Hallo Teilaufgabe 2");
expect(completionString).to.include("Hallo Teilaufgabe 3");
expect(completionString).to.include("Hallo Teilaufgabe 4.1");
expect(completionString).to.include("Hallo Teilaufgabe 4.2");
expect(completionString).to.include("Hallo Teilaufgabe 4.3");
expect(completionString).to.include("Hallo Teilaufgabe 5.1");
expect(completionString).to.include("Hallo Teilaufgabe 5.2");
expect(completionString).to.include("Hallo Teilaufgabe 5.3");
expect(completionString).to.include("Hallo Teilaufgabe 6.1");
expect(completionString).to.include("Hallo Teilaufgabe 6.2");
});
});
});
describe("Praxis Assignment", () => {
it("can make complete assignment without expert", () =>
completePraxisAssignment());
it("can make complete assignment with expert", () =>
completePraxisAssignment(true));
}); });
}); });

View File

@ -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", () => {
@ -7,191 +7,334 @@ describe("assignmentTrainer.cy.js", () => {
login("test-trainer1@example.com", "test"); login("test-trainer1@example.com", "test");
}); });
it("can open cockpit assignment page and open user assignment", () => { describe("Casework", () => {
cy.visit("/course/test-lehrgang/cockpit"); it("can open cockpit assignment page and open user assignment", () => {
cy.get( cy.visit("/course/test-lehrgang/cockpit");
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]' cy.get(
).click(); '[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="Student1"]').should("contain", "Ergebnisse abgegeben"); cy.get('[data-cy="Student1"]').should("contain", "Ergebnisse abgegeben");
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click(); cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="student-submission"]').should("contain", "Ergebnisse"); cy.get('[data-cy="student-submission"]').should("contain", "Ergebnisse");
cy.get('[data-cy="student-submission"]').should("contain", "Student1"); cy.get('[data-cy="student-submission"]').should("contain", "Student1");
});
it("can start evaluation and store evaluation results", () => {
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="start-evaluation"]').click();
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 1 / 5"
);
// without text input the button should be disabled
cy.get('[data-cy="next-step"]').should("be.disabled");
// with text you can continue
cy.get('[data-cy="subtask-4"]').click();
cy.get('[data-cy="reason-text"]').type("Gut gemacht!");
// wait for debounce
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 2 / 5"
);
cy.get('[data-cy="subtask-2"]').click();
cy.get('[data-cy="reason-text"]').type("Nicht so gut");
cy.wait(500);
// revisit step 1 will show stored data
cy.get('[data-cy="previous-step"]').click();
cy.get('[data-cy="reason-text"]')
.find("textarea")
.should("have.value", "Gut gemacht!");
// even after reload
cy.reload();
cy.get('[data-cy="reason-text"]')
.find("textarea")
.should("have.value", "Gut gemacht!");
// it can access step directly via url
cy.url().then((url) => {
const step2Url = url.replace("step=1", "step=2");
console.log(step2Url);
cy.visit(step2Url);
}); });
cy.get('[data-cy="reason-text"]') it("can start evaluation and store evaluation results", () => {
.find("textarea") cy.visit("/course/test-lehrgang/cockpit");
.should("have.value", "Nicht so gut"); cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
// load AssignmentCompletion from DB and check cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.loadAssignmentCompletion(
"assignment_user_id", cy.get('[data-cy="title"]').should("contain", "Bewertung");
TEST_STUDENT1_USER_ID cy.get('[data-cy="evaluation-duedate"]').should("exist");
).then((ac) => { cy.get('[data-cy="instruction"]').should(
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS"); "contain",
expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut"); "Die Gesamtpunktzahl und die daraus resultierende Note wird auf Grund des hinterlegeten Beurteilungsinstrument berechnet."
expect(Cypress._.values(ac.completion_data)).to.deep.include({ );
expert_data: { points: 2, text: "Nicht so gut" }, cy.get('[data-cy="start-evaluation"]').click();
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 1 / 5"
);
// without text input the button should be disabled
cy.get('[data-cy="next-step"]').should("be.disabled");
// with text you can continue
cy.get('[data-cy="subtask-4"]').click();
cy.get('[data-cy="reason-text"]').type("Gut gemacht!");
// wait for debounce
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 2 / 5"
);
cy.get('[data-cy="subtask-2"]').click();
cy.get('[data-cy="reason-text"]').type("Nicht so gut");
cy.wait(500);
// revisit step 1 will show stored data
cy.get('[data-cy="previous-step"]').click();
cy.get('[data-cy="reason-text"]')
.find("textarea")
.should("have.value", "Gut gemacht!");
// even after reload
cy.reload();
cy.get('[data-cy="reason-text"]')
.find("textarea")
.should("have.value", "Gut gemacht!");
// it can access step directly via url
cy.url().then((url) => {
const step2Url = url.replace("step=1", "step=2");
console.log(step2Url);
cy.visit(step2Url);
}); });
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 4, text: "Gut gemacht!" }, cy.get('[data-cy="reason-text"]')
.find("textarea")
.should("have.value", "Nicht so gut");
cy.wait(500);
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"evaluation_user_id",
TEST_TRAINER1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut");
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 2, text: "Nicht so gut" },
});
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 4, text: "Gut gemacht!" },
});
});
});
it("can make complete evaluation", () => {
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="start-evaluation"]').click();
// step 1
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 1 / 5"
);
cy.get('[data-cy="subtask-6"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 1");
// wait for debounce
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 2
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 2 / 5"
);
cy.get('[data-cy="subtask-4"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 2");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 3
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 3 / 5"
);
cy.get('[data-cy="subtask-2"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 3");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 4
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 4 / 5"
);
cy.get('[data-cy="subtask-3"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 4");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 5
cy.get('[data-cy="evaluation-task"]').should(
"contain",
"Beurteilungskriterium 5 / 5"
);
cy.get('[data-cy="subtask-2"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 5");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// freigabe
cy.get('[data-cy="sub-title"]').should("contain", "Bewertung Freigabe");
cy.get('[data-cy="user-points"]').should("contain", "17");
cy.get('[data-cy="total-points"]').should("contain", "24");
cy.get('[data-cy="submit-evaluation"]').click();
cy.get('[data-cy="result-section"]').should(
"contain",
"Deine Bewertung für Test Student1 wurde freigegeben"
);
// going back to cockpit should show points for student
cy.visit("/course/test-lehrgang/cockpit");
cy.reload();
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="Student1"]').should("contain", "Bewertung freigegeben");
cy.get('[data-cy="Student1"]').should("contain", "17 von 24 Punkte");
// clicking on results page will go to last step
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.url().should("include", "step=6");
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"evaluation_user_id",
TEST_TRAINER1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
expect(ac.evaluation_points).to.equal(17);
expect(ac.evaluation_max_points).to.equal(24);
expect(ac.evaluation_passed).to.equal(true);
const completionString = JSON.stringify(ac.completion_data);
expect(completionString).to.include("Begründung Schritt 1");
expect(completionString).to.include("Begründung Schritt 2");
expect(completionString).to.include("Begründung Schritt 3");
expect(completionString).to.include("Begründung Schritt 4");
expect(completionString).to.include("Begründung Schritt 5");
}); });
}); });
}); });
it("can make complete evaluation", () => { //Todo: Move tests to Lernbegleitung once it is implemented
cy.visit("/course/test-lehrgang/cockpit"); describe("Praxis Assignment", () => {
cy.get( it("can start evaluation and store evaluation results", () => {
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]' cy.visit("/course/test-lehrgang/cockpit");
).click(); cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]'
).click();
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click(); cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="start-evaluation"]').click(); cy.get('[data-cy="title"]').should("contain", "Feedback");
cy.get('[data-cy="evaluation-duedate]"').should("not.exist");
cy.get('[data-cy="instruction"]').should("contain", "Intro für Feedback");
cy.get('[data-cy="start-evaluation"]').click();
cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 1 / 5");
// step 1 // without text input the button should be disabled
cy.get('[data-cy="evaluation-task"]').should( cy.get('[data-cy="next-step"]').should("be.disabled");
"contain",
"Beurteilungskriterium 1 / 5"
);
cy.get('[data-cy="subtask-6"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 1");
// wait for debounce
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 2 // with text you can continue
cy.get('[data-cy="evaluation-task"]').should( cy.get('[data-cy="reason-text"]').type("Gut gemacht!");
"contain", // wait for debounce
"Beurteilungskriterium 2 / 5" cy.wait(500);
); cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="subtask-4"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 2");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 3 cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 2 / 5");
cy.get('[data-cy="evaluation-task"]').should( cy.get('[data-cy="reason-text"]').type("Nicht so gut");
"contain", cy.wait(1000);
"Beurteilungskriterium 3 / 5"
);
cy.get('[data-cy="subtask-2"]').click();
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 3");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// step 4 // load AssignmentCompletion from DB and check
cy.get('[data-cy="evaluation-task"]').should( cy.loadAssignmentCompletion(
"contain", "evaluation_user_id",
"Beurteilungskriterium 4 / 5" TEST_TRAINER1_USER_ID
); ).then((ac) => {
cy.get('[data-cy="subtask-3"]').click(); console.log(ac.completion_status);
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 4"); expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
cy.wait(500); expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut");
cy.get('[data-cy="next-step"]').click(); expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 0, text: "Nicht so gut" },
});
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 0, text: "Gut gemacht!" },
});
});
});
// step 5 it("can make complete evaluation", () => {
cy.get('[data-cy="evaluation-task"]').should( cy.visit("/course/test-lehrgang/cockpit");
"contain", cy.get('[data-cy="dropdown-select"]').click();
"Beurteilungskriterium 5 / 5" cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
); cy.get(
cy.get('[data-cy="subtask-2"]').click(); '[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]'
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 5"); ).click();
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// freigabe cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="sub-title"]').should("contain", "Bewertung Freigabe");
cy.get('[data-cy="user-points"]').should("contain", "17");
cy.get('[data-cy="total-points"]').should("contain", "24");
cy.get('[data-cy="submit-evaluation"]').click();
cy.get('[data-cy="result-section"]').should( cy.get('[data-cy="start-evaluation"]').click();
"contain",
"Deine Bewertung für Test Student1 wurde freigegeben"
);
// going back to cockpit should show points for student // step 1
cy.visit("/course/test-lehrgang/cockpit"); cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 1 / 5");
cy.reload(); cy.get('[data-cy="reason-text"]').type("Begründung Schritt 1");
cy.get( // wait for debounce
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]' cy.wait(500);
).click(); cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="Student1"]').should("contain", "Bewertung freigegeben"); // step 2
cy.get('[data-cy="Student1"]').should("contain", "17 von 24 Punkte"); cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 2 / 5");
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 2");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// clicking on results page will go to last step // step 3
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click(); cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 3 / 5");
cy.url().should("include", "step=6"); cy.get('[data-cy="reason-text"]').type("Begründung Schritt 3");
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
// load AssignmentCompletion from DB and check // step 4
cy.loadAssignmentCompletion( cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 4 / 5");
"assignment_user_id", cy.get('[data-cy="reason-text"]').type("Begründung Schritt 4");
TEST_STUDENT1_USER_ID cy.wait(500);
).then((ac) => { cy.get('[data-cy="next-step"]').click();
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
expect(ac.evaluation_points).to.equal(17); // step 5
expect(ac.evaluation_max_points).to.equal(24); cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 5 / 5");
expect(ac.evaluation_passed).to.equal(true); cy.get('[data-cy="reason-text"]').type("Begründung Schritt 5");
const completionString = JSON.stringify(ac.completion_data); cy.wait(500);
expect(completionString).to.include("Begründung Schritt 1"); cy.get('[data-cy="next-step"]').click();
expect(completionString).to.include("Begründung Schritt 2");
expect(completionString).to.include("Begründung Schritt 3"); // freigabe
expect(completionString).to.include("Begründung Schritt 4"); cy.get('[data-cy="sub-title"]').should("contain", "Feedback Freigabe");
expect(completionString).to.include("Begründung Schritt 5"); cy.get('[data-cy="total-points"]').should("not.exist");
cy.get('[data-cy="submit-evaluation"]').click();
cy.get('[data-cy="result-section"]').should(
"contain",
"Dein Feedback für Test Student1 wurde freigegeben"
);
// going back to cockpit should show points for student
cy.visit("/course/test-lehrgang/cockpit");
cy.reload();
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]'
).click();
cy.get('[data-cy="Student1"]').should("contain", "Feedback freigegeben");
cy.get('[data-cy="Student1"]').should("not.contain", "Punkte");
// clicking on results page will go to last step
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.url().should("include", "step=6");
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"evaluation_user_id",
TEST_TRAINER1_USER_ID
).then((ac) => {
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
expect(ac.evaluation_max_points).to.equal(0);
const completionString = JSON.stringify(ac.completion_data);
expect(completionString).to.include("Begründung Schritt 1");
expect(completionString).to.include("Begründung Schritt 2");
expect(completionString).to.include("Begründung Schritt 3");
expect(completionString).to.include("Begründung Schritt 4");
expect(completionString).to.include("Begründung Schritt 5");
});
}); });
}); });
}); });

View File

@ -118,6 +118,6 @@ describe("circle.cy.js", () => {
cy.visit("/course/test-lehrgang/learn/reisen"); cy.visit("/course/test-lehrgang/learn/reisen");
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3); cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
cy.get('[data-cy="lp-learning-content"]').should("have.length", 8); cy.get('[data-cy="lp-learning-content"]').should("have.length", 9);
}); });
}); });

View File

@ -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");

View File

@ -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",
});
}
);
});
}); });
}); });

View File

@ -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");
});
}); });
}); });

View File

@ -0,0 +1,40 @@
import { login } from "./helpers";
describe("settings.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
});
describe("with circle documents enabled", () => {
it("student can see circle documents", () => {
login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/learn/fahrzeug");
cy.get('[data-cy="circle-document-section"]').should("exist");
});
it("trainer can see circle documents", () => {
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.get('[data-cy="circle-documents"]').should("exist");
});
});
describe("with circle documents disabled", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset --no-enable-circle-documents");
});
it("student cannot see circle documents", () => {
login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/learn/fahrzeug");
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
cy.get('[data-cy="circle-document-section"]').should("not.exist");
});
it("trainer cannot see circle documents", () => {
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.get('[data-cy="circle-documents"]').should("not.exist");
});
});
});

View File

@ -60,6 +60,8 @@ Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
let pythonPaths = [ let pythonPaths = [
"/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/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

View File

@ -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).
![](./assets/files-presign.png)
- 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.

View File

@ -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()

View File

@ -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()

View File

@ -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():

View File

@ -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",
@ -212,23 +213,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")
@ -252,7 +243,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": {
@ -645,7 +648,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)
@ -654,18 +657,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",

View File

@ -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",
]

View File

@ -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

View File

@ -56,7 +56,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):
@ -88,7 +88,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
@ -132,6 +132,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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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`

View File

@ -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>
@ -4208,6 +4210,8 @@ def create_vv_gewinnen_casework(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID):
parent=assignment_list_page, parent=assignment_list_page,
title="Mein Kundenstamm", title="Mein Kundenstamm",
effort_required="60 bis 90 Minuten", effort_required="60 bis 90 Minuten",
needs_expert_evaluation=True,
assignment_type=AssignmentType.PRAXIS_ASSIGNMENT.name,
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
<h3>Thema</h3> <h3>Thema</h3>
@ -4255,6 +4259,51 @@ def create_vv_gewinnen_casework(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID):
) )
assignment.evaluation_tasks = [] assignment.evaluation_tasks = []
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 1",
max_points=0,
),
),
)
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 2",
max_points=0,
),
),
)
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 3",
max_points=0,
),
),
)
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 4",
max_points=0,
),
),
)
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 5",
max_points=0,
),
),
)
assignment.tasks = [] assignment.tasks = []
assignment.tasks.append( assignment.tasks.append(
@ -4476,89 +4525,20 @@ def create_vv_gewinnen_casework(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID):
), ),
) )
) )
assignment.tasks.append( assignment.tasks.append(
( (
"task", "task",
TaskBlockFactory( TaskBlockFactory(
title="Teilaufgabe 5: Kundenkontakte pflegen", title="Teilaufgabe 5: Kundentelefonate2",
content=StreamValue( content=StreamValue(
TaskContentStreamBlock(), TaskContentStreamBlock(),
stream_data=[ stream_data=[
(
"explanation",
ExplanationBlockFactory(
text=RichText(
replace_whitespace(
"""
<p>Dein bestehender Kundenstamm ist ein wertvoller Pool mit Potenzial, um weitere Versicherungslösungen anzubieten. Damit du die gute Beziehung zu deiner Kundschaft aufrechterhalten kannst, hast du nebst der Analyse des Kundenstamms noch weitere Optionen.</p>
"""
)
)
),
),
( (
"user_text_input", "user_text_input",
UserTextInputBlockFactory( UserTextInputBlockFactory(
text=RichText( text=RichText(
""" """
Welche weiteren Möglichkeiten hast du, um den Kontakt zu bestehenden Kundinnen/Kunden zu pflegen? <p>Notiere, welche hundert Kunden du nächste Woche für ein Beratungsgespräch telefonisch kontaktieren willst.</p>
"""
)
),
),
],
),
),
)
)
assignment.tasks.append(
(
"task",
TaskBlockFactory(
title="Teilaufgabe 6: Reflexion und Learnings",
content=StreamValue(
TaskContentStreamBlock(),
stream_data=[
(
"explanation",
ExplanationBlockFactory(
text=RichText(
replace_whitespace(
"""
Welche Erkenntnisse nimmst du aus diesem Praxisauftrag mit?
"""
)
)
),
),
(
"user_text_input",
UserTextInputBlockFactory(
text=RichText(
"""
Auf welche Weise wirst du deinen Kundenstamm durchforsten, um das Optimierungspotenzial deiner Kundinnen/Kunden zu erkennen?
"""
)
),
),
(
"user_text_input",
UserTextInputBlockFactory(
text=RichText(
"""
Frage zwei, drei Kolleginnen/Kollegen, wie sie ihren Kundenstamm pflegen.
"""
)
),
),
(
"user_text_input",
UserTextInputBlockFactory(
text=RichText(
"""
Wie wirst du deine bestehende Kundschaft pflegen?
""" """
) )
), ),
@ -4588,6 +4568,7 @@ def create_vv_einkommenssicherung_casework(
parent=assignment_list_page, parent=assignment_list_page,
title="Heirat: Was ändert sich", title="Heirat: Was ändert sich",
effort_required="45 bis 90 Minuten", effort_required="45 bis 90 Minuten",
assignment_type=AssignmentType.PRAXIS_ASSIGNMENT.name,
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
<h3>Thema</h3> <h3>Thema</h3>
@ -4639,6 +4620,43 @@ def create_vv_einkommenssicherung_casework(
assignment.evaluation_tasks = [] assignment.evaluation_tasks = []
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 1",
max_points=0,
),
),
)
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 2",
max_points=0,
),
),
)
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 3",
max_points=0,
),
),
)
assignment.evaluation_tasks.append(
(
"task",
EvaluationTaskBlockFactory(
title="Feedback zu Teilaufgabe 4",
max_points=0,
),
),
)
assignment.tasks = [] assignment.tasks = []
assignment.tasks.append( assignment.tasks.append(
( (
@ -4784,31 +4802,6 @@ def create_vv_einkommenssicherung_casework(
) )
) )
assignment.tasks.append(
(
"task",
TaskBlockFactory(
title="Teilaufgabe 5: Deine Meinung",
content=StreamValue(
TaskContentStreamBlock(),
stream_data=[
(
"user_text_input",
UserTextInputBlockFactory(
text=RichText(
"""
<p>Nachdem du nun die Auswirkungen einer Heirat unter die Lupe genommen hast, interessiert uns deine persönliche Meinung.</p>
<p>Heirat: Lohnt sich eine Heirat aus rein finanzieller Sicht? Begründe deine Ansicht.</p>
"""
)
),
),
],
),
),
)
)
assignment.save() assignment.save()
return assignment return assignment
@ -4826,6 +4819,7 @@ def create_vv_gesundheit_casework(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
parent=assignment_list_page, parent=assignment_list_page,
title="Krankenversicherung: Passt die Lösung noch?", title="Krankenversicherung: Passt die Lösung noch?",
effort_required="60 bis 90 Minuten", effort_required="60 bis 90 Minuten",
assignment_type=AssignmentType.PRAXIS_ASSIGNMENT.name,
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
<h3>Ausgangslage</h3> <h3>Ausgangslage</h3>

View File

@ -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 has_course_access, is_course_session_expert from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
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()

View File

@ -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",
),
),
]

View File

@ -111,6 +111,7 @@ class EvaluationTaskBlock(blocks.StructBlock):
class AssignmentType(Enum): class AssignmentType(Enum):
PRAXIS_ASSIGNMENT = "PRAXIS_ASSIGNMENT" # Praxisauftrag
CASEWORK = "CASEWORK" # Geleitete Fallarbeit CASEWORK = "CASEWORK" # Geleitete Fallarbeit
PREP_ASSIGNMENT = "PREP_ASSIGNMENT" # Vorbereitungsauftrag PREP_ASSIGNMENT = "PREP_ASSIGNMENT" # Vorbereitungsauftrag
REFLECTION = "REFLECTION" # Reflexion REFLECTION = "REFLECTION" # Reflexion
@ -143,7 +144,7 @@ class Assignment(CourseBasePage):
needs_expert_evaluation = models.BooleanField( needs_expert_evaluation = models.BooleanField(
default=False, default=False,
help_text="Muss der Auftrag durch eine Expertin oder einen Experten beurteilt werden?", help_text="Muss der Auftrag durch eine/n Experten/in oder eine Lernbegleitung beurteilt werden?",
) )
competence_certificate = models.ForeignKey( competence_certificate = models.ForeignKey(
@ -199,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"),

View File

@ -144,7 +144,7 @@ def update_assignment_completion(
ac.evaluation_points = evaluation_points ac.evaluation_points = evaluation_points
# if no evaluation_passed is provided, we calculate it from the points # if no evaluation_passed is provided, we calculate it from the points
if evaluation_passed is None: if evaluation_passed is None and ac.evaluation_max_points > 0:
if evaluation_points is not None and ac.evaluation_max_points is not None: if evaluation_points is not None and ac.evaluation_max_points is not None:
# if more or equal than 60% of the points are reached, the assignment is passed # if more or equal than 60% of the points are reached, the assignment is passed
ac.evaluation_passed = (evaluation_points / ac.evaluation_max_points) >= 0.6 ac.evaluation_passed = (evaluation_points / ac.evaluation_max_points) >= 0.6

View File

@ -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",
) )

View File

@ -13,6 +13,7 @@ from vbv_lernwelt.core.constants import (
TEST_TRAINER1_USER_ID, TEST_TRAINER1_USER_ID,
) )
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.consts import COURSE_TEST_ID
from vbv_lernwelt.course.creators.test_course import ( from vbv_lernwelt.course.creators.test_course import (
create_edoniq_test_result_data, create_edoniq_test_result_data,
create_feedback_response_data, create_feedback_response_data,
@ -20,6 +21,7 @@ from vbv_lernwelt.course.creators.test_course import (
create_test_assignment_submitted_data, create_test_assignment_submitted_data,
) )
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import (
Course,
CourseCompletion, CourseCompletion,
CourseCompletionStatus, CourseCompletionStatus,
CourseSession, CourseSession,
@ -30,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
@ -73,6 +76,11 @@ from vbv_lernwelt.notify.models import Notification
default=False, default=False,
help="will create attendance days data", help="will create attendance days data",
) )
@click.option(
"--enable-circle-documents/--no-enable-circle-documents",
default=True,
help="will enable circle documents for test course",
)
def command( def command(
create_assignment_completion, create_assignment_completion,
create_assignment_evaluation, create_assignment_evaluation,
@ -81,6 +89,7 @@ def command(
create_feedback_responses, create_feedback_responses,
create_course_completion_performance_criteria, create_course_completion_performance_criteria,
create_attendance_days, create_attendance_days,
enable_circle_documents,
): ):
print("cypress reset data") print("cypress reset data")
CourseCompletion.objects.all().delete() CourseCompletion.objects.all().delete()
@ -101,6 +110,13 @@ def command(
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID), course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID),
user=User.objects.get(id=TEST_STUDENT1_USER_ID), user=User.objects.get(id=TEST_STUDENT1_USER_ID),
) )
create_test_assignment_submitted_data(
assignment=Assignment.objects.get(
slug="test-lehrgang-assignment-mein-kundenstamm"
),
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID),
user=User.objects.get(id=TEST_STUDENT1_USER_ID),
)
if create_assignment_evaluation: if create_assignment_evaluation:
if not assignment_evaluation_scores: if not assignment_evaluation_scores:
assignment_evaluation_scores = [6, 6, 6, 3, 3] assignment_evaluation_scores = [6, 6, 6, 3, 3]
@ -140,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(
@ -159,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",
}, },
) )
@ -178,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",
}, },
) )
@ -197,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",
}, },
) )
@ -243,3 +319,7 @@ def command(
datetime(year=2000, month=10, day=31, hour=11) datetime(year=2000, month=10, day=31, hour=11)
) )
attendance_course.save() attendance_course.save()
course = Course.objects.get(id=COURSE_TEST_ID)
course.enable_circle_documents = enable_circle_documents
course.save()

View File

@ -10,6 +10,7 @@ from vbv_lernwelt.assignment.creators.create_assignments import (
create_uk_fahrzeug_casework, create_uk_fahrzeug_casework,
create_uk_fahrzeug_prep_assignment, create_uk_fahrzeug_prep_assignment,
create_uk_reflection, create_uk_reflection,
create_vv_gewinnen_casework,
) )
from vbv_lernwelt.assignment.models import ( from vbv_lernwelt.assignment.models import (
Assignment, Assignment,
@ -69,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,
@ -81,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,
@ -91,14 +99,21 @@ 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()
# assignments create assignments parent page
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
_assignment_list_page = AssignmentListPageFactory(
parent=course_page,
)
if include_uk: if include_uk:
# assignments create assignments parent page
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
_assignment_list_page = AssignmentListPageFactory(
parent=course_page,
)
create_uk_fahrzeug_casework( create_uk_fahrzeug_casework(
course_id=COURSE_TEST_ID, competence_certificate=competence_certificate course_id=COURSE_TEST_ID, competence_certificate=competence_certificate
) )
@ -111,6 +126,9 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
competence_certificate=competence_certificate, competence_certificate=competence_certificate,
) )
if include_vv:
create_vv_gewinnen_casework(course_id=COURSE_TEST_ID)
create_test_learning_path(include_uk=include_uk, include_vv=include_vv) create_test_learning_path(include_uk=include_uk, include_vv=include_vv)
create_test_media_library() create_test_media_library()
@ -190,6 +208,14 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
) )
cset.deadline.save() cset.deadline.save()
if include_vv:
_csa = CourseSessionAssignment.objects.create(
course_session=cs_bern,
learning_content=LearningContentAssignment.objects.get(
slug=f"test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"
),
)
cs_zurich = CourseSession.objects.create( cs_zurich = CourseSession.objects.create(
course_id=COURSE_TEST_ID, course_id=COURSE_TEST_ID,
title="Test Zürich 2022 a", title="Test Zürich 2022 a",
@ -213,6 +239,8 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
role=CourseSessionUser.Role.EXPERT, role=CourseSessionUser.Role.EXPERT,
) )
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")) csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
if include_vv:
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-reisen"))
trainer2 = User.objects.get(email="test-trainer2@example.com") trainer2 = User.objects.get(email="test-trainer2@example.com")
csu = CourseSessionUser.objects.create( csu = CourseSessionUser.objects.create(
@ -344,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(
@ -507,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,
@ -514,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,
) )
@ -555,6 +593,15 @@ def create_test_circle_reisen(lp):
content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/emma-und-ayla-campen-durch-amerika-analyse-xapi-FZoZOP9y/index.html", content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/emma-und-ayla-campen-durch-amerika-analyse-xapi-FZoZOP9y/index.html",
) )
LearningContentAssignmentFactory(
title="Mein Kundenstamm",
assignment_type="PRAXIS_ASSIGNMENT",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-mein-kundenstamm"
),
),
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="Y1"), parent=ActionCompetence.objects.get(competence_id="Y1"),
competence_id=f"Y1.1", competence_id=f"Y1.1",
@ -589,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,
) )

View File

@ -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,
) )

View File

@ -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
) )

View File

@ -94,7 +94,7 @@ class CourseObjectType(DjangoObjectType):
class Meta: class Meta:
model = Course model = Course
fields = ("id", "title", "category_name", "slug") fields = ("id", "title", "category_name", "slug", "enable_circle_documents")
@staticmethod @staticmethod
def resolve_learning_path(root: Course, info): def resolve_learning_path(root: Course, info):

View File

@ -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)

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.20 on 2023-11-23 10:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0004_auto_20230823_1744"),
]
operations = [
migrations.AddField(
model_name="course",
name="enable_circle_documents",
field=models.BooleanField(
default=True, verbose_name="Trainer Dokumente in Circles"
),
),
]

View File

@ -23,6 +23,9 @@ class Course(models.Model):
slug = models.SlugField( slug = models.SlugField(
_("Slug"), max_length=255, unique=True, blank=True, allow_unicode=True _("Slug"), max_length=255, unique=True, blank=True, allow_unicode=True
) )
enable_circle_documents = models.BooleanField(
_("Trainer Dokumente in Circles"), default=True
)
def get_course_url(self): def get_course_url(self):
return f"/course/{self.slug}" return f"/course/{self.slug}"

View File

@ -17,7 +17,7 @@ class CourseSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Course model = Course
fields = ["id", "title", "category_name", "slug"] fields = ["id", "title", "category_name", "slug", "enable_circle_documents"]
class CourseCategorySerializer(serializers.ModelSerializer): class CourseCategorySerializer(serializers.ModelSerializer):

View File

@ -74,12 +74,6 @@ class CourseSessionAttendanceCourseAdmin(admin.ModelAdmin):
@admin.register(CourseSessionAssignment) @admin.register(CourseSessionAssignment)
class CourseSessionAssignmentAdmin(admin.ModelAdmin): class CourseSessionAssignmentAdmin(admin.ModelAdmin):
readonly_fields = [
"course_session",
"learning_content",
"submission_deadline",
"evaluation_deadline",
]
list_display = [ list_display = [
"course_session", "course_session",
"circle", "circle",
@ -88,6 +82,11 @@ class CourseSessionAssignmentAdmin(admin.ModelAdmin):
"evaluation_date", "evaluation_date",
] ]
list_filter = ["course_session__course", "course_session"] list_filter = ["course_session__course", "course_session"]
raw_id_fields = [
"course_session",
"submission_deadline",
"evaluation_deadline",
]
def submission_date(self, obj): def submission_date(self, obj):
if obj.submission_deadline: if obj.submission_deadline:
@ -124,7 +123,7 @@ class CourseSessionAssignmentAdmin(admin.ModelAdmin):
readonly_fields = super(CourseSessionAssignmentAdmin, self).get_readonly_fields( readonly_fields = super(CourseSessionAssignmentAdmin, self).get_readonly_fields(
request, obj request, obj
) )
return readonly_fields + ["circle_display"] return ["circle_display"]
# Override get_form to include circle_display # Override get_form to include circle_display
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):

View File

@ -109,6 +109,7 @@ class CourseSessionAssignment(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="assignment_submission_deadline", related_name="assignment_submission_deadline",
null=True, null=True,
blank=True,
) )
evaluation_deadline = models.OneToOneField( evaluation_deadline = models.OneToOneField(
@ -116,6 +117,7 @@ class CourseSessionAssignment(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="assignment_evaluation_deadline", related_name="assignment_evaluation_deadline",
null=True, null=True,
blank=True,
) )
class Meta: class Meta:
@ -131,6 +133,7 @@ class CourseSessionAssignment(models.Model):
assignment_type = self.learning_content.assignment_type assignment_type = self.learning_content.assignment_type
assignment_type_translation_keys = { assignment_type_translation_keys = {
AssignmentType.CASEWORK.value: "learningContentTypes.casework", AssignmentType.CASEWORK.value: "learningContentTypes.casework",
AssignmentType.PRAXIS_ASSIGNMENT.value: "learningContentTypes.praxisAssignment",
AssignmentType.PREP_ASSIGNMENT.value: "learningContentTypes.prepAssignment", AssignmentType.PREP_ASSIGNMENT.value: "learningContentTypes.prepAssignment",
AssignmentType.REFLECTION.value: "learningContentTypes.reflection", AssignmentType.REFLECTION.value: "learningContentTypes.reflection",
} }

View File

@ -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
), ),
) )

View File

@ -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)

View File

@ -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"]),
} }
) )

View File

@ -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(

View File

@ -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),
]

View File

@ -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

View File

@ -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 {}

View File

@ -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)

View File

@ -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)

View File

@ -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] = []

View File

@ -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
)

View File

@ -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()

View File

@ -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,
) )

View File

@ -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,

View File

@ -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,
),
),
]

View File

@ -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)
@ -441,6 +448,9 @@ class LearningContentAssignment(LearningContent):
self.assignment_type = self.content_assignment.assignment_type self.assignment_type = self.content_assignment.assignment_type
super().save(**kwargs) super().save(**kwargs)
def __str__(self):
return f"{self.id} - {self.title}"
@classmethod @classmethod
def get_serializer_class(cls): def get_serializer_class(cls):
from vbv_lernwelt.learnpath.serializers import ( from vbv_lernwelt.learnpath.serializers import (

View File

@ -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):

View File

@ -28,7 +28,7 @@ class TestRetrieveLearingPathContents(APITestCase):
# topics and circles # topics and circles
self.assertEqual(4, len(data["children"])) self.assertEqual(4, len(data["children"]))
# circle "analyse" contents # circle "analyse" contents
self.assertEqual(14, len(data["children"][3]["children"])) self.assertEqual(15, len(data["children"][3]["children"]))
def test_normalUser_withoutCourseSession_cannotAccess(self): def test_normalUser_withoutCourseSession_cannotAccess(self):
self.user = User.objects.get(username="student") self.user = User.objects.get(username="student")

View File

@ -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",
)

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MediaLibraryConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "vbv_lernwelt.media_files"

View File

@ -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()

View File

@ -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()

Some files were not shown because too many files have changed in this diff Show More