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", generate_random_string(63)
),
"AWS_S3_ACCESS_KEY_ID": env.str("AWS_S3_ACCESS_KEY_ID", ""),
"AWS_S3_ACCESS_KEY_ID": env.str(
"AWS_S3_ACCESS_KEY_ID", "AKIAZJLREPUVWNBTJ5VY"
),
"AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""),
"AWS_S3_REGION_NAME": "eu-central-1",
"AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch",
"AWS_S3_REGION_NAME": env.str("AWS_S3_REGION_NAME", "eu-central-1"),
"AWS_STORAGE_BUCKET_NAME": env.str(
"AWS_STORAGE_BUCKET_NAME", "myvbv-dev.iterativ.ch"
),
"FILE_UPLOAD_STORAGE": "s3",
"IT_DJANGO_DEBUG": "false",
"IT_SERVE_VUE": "false",

View File

@ -42,6 +42,7 @@ const dropdownSelected = computed<DropdownSelectable>({
border: !props.borderless,
'font-bold': !props.borderless,
}"
data-cy="dropdown-select"
>
<span v-if="dropdownSelected.iconName" class="mr-4">
<component :is="dropdownSelected.iconName"></component>
@ -75,6 +76,7 @@ const dropdownSelected = computed<DropdownSelectable>({
'relative cursor-default select-none py-2 pl-3 pr-9',
]"
class="flex flex-row items-center"
:data-cy="`dropdown-select-option-${item.name}`"
>
<span v-if="item.iconName" class="mr-4">
<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 fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\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 courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
};
/**
@ -60,7 +60,7 @@ export function graphql(source: "\n query attendanceCheckQuery($courseSessionId
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"): (typeof documents)["\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"];
export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"): (typeof documents)["\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@ -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.
*/
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.
*/
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.
*/
@ -88,7 +88,7 @@ export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};

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_assignment: LearningContentAssignmentObjectType
learning_content_attendance_course: LearningContentAttendanceCourseObjectType
learning_content_feedback: LearningContentFeedbackObjectType
learning_content_feedback_uk: LearningContentFeedbackUKObjectType
learning_content_feedback_vv: LearningContentFeedbackVVObjectType
learning_content_learning_module: LearningContentLearningModuleObjectType
learning_content_knowledge_assessment: LearningContentKnowledgeAssessmentObjectType
learning_content_placeholder: LearningContentPlaceholderObjectType
@ -235,6 +236,7 @@ type CourseObjectType {
title: String!
category_name: String!
slug: String!
enable_circle_documents: Boolean!
learning_path: LearningPathObjectType!
action_competences: [ActionCompetenceObjectType!]!
}
@ -455,7 +457,7 @@ type AssignmentObjectType implements CoursePageInterface {
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!
competence_certificate: CompetenceCertificateObjectType
@ -485,10 +487,14 @@ type AssignmentObjectType implements CoursePageInterface {
max_points: Int
learning_content: LearningContentInterface
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
solution_sample: ContentDocumentObjectType
}
"""An enumeration."""
enum AssignmentAssignmentAssignmentTypeChoices {
"""PRAXIS_ASSIGNMENT"""
PRAXIS_ASSIGNMENT
"""CASEWORK"""
CASEWORK
@ -601,8 +607,20 @@ schema (one of the key benefits of GraphQL).
"""
scalar JSONString
type ContentDocumentObjectType {
id: ID!
display_text: String!
description: String!
link_display_text: String!
thumbnail: String!
url: String
}
"""An enumeration."""
enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
"""PRAXIS_ASSIGNMENT"""
PRAXIS_ASSIGNMENT
"""CASEWORK"""
CASEWORK
@ -701,7 +719,23 @@ type LearningContentMediaLibraryObjectType implements CoursePageInterface & Lear
circle: CircleLightObjectType
}
type LearningContentFeedbackObjectType implements CoursePageInterface & LearningContentInterface {
type LearningContentFeedbackUKObjectType implements CoursePageInterface & LearningContentInterface {
id: ID!
title: String!
slug: String!
content_type: String!
live: Boolean!
translation_key: String!
frontend_url: String!
course: CourseObjectType
minutes: Int
description: String!
content_url: String!
can_user_self_toggle_course_completion: Boolean!
circle: CircleLightObjectType
}
type LearningContentFeedbackVVObjectType implements CoursePageInterface & LearningContentInterface {
id: ID!
title: String!
slug: String!
@ -827,7 +861,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
}
type Mutation {
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, submitted: Boolean = false): SendFeedbackMutation
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
}
@ -869,4 +903,4 @@ enum AssignmentCompletionStatus {
SUBMITTED
EVALUATION_IN_PROGRESS
EVALUATION_SUBMITTED
}
}

View File

@ -23,6 +23,7 @@ export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"
export const CompetencePerformanceStatisticsSummaryType = "CompetencePerformanceStatisticsSummaryType";
export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
export const CompetencesStatisticsType = "CompetencesStatisticsType";
export const ContentDocumentObjectType = "ContentDocumentObjectType";
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
export const CourseObjectType = "CourseObjectType";
export const CoursePageInterface = "CoursePageInterface";
@ -54,7 +55,8 @@ export const LearningContentAssignmentObjectType = "LearningContentAssignmentObj
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
export const LearningContentEdoniqTestObjectType = "LearningContentEdoniqTestObjectType";
export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType";
export const LearningContentFeedbackUKObjectType = "LearningContentFeedbackUKObjectType";
export const LearningContentFeedbackVVObjectType = "LearningContentFeedbackVVObjectType";
export const LearningContentInterface = "LearningContentInterface";
export const LearningContentKnowledgeAssessmentObjectType = "LearningContentKnowledgeAssessmentObjectType";
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";

View File

@ -46,6 +46,10 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
tasks
title
translation_key
solution_sample {
id
url
}
competence_certificate {
...CoursePageFields
}
@ -117,6 +121,7 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
id
title
slug
enable_circle_documents
}
users {
id
@ -200,6 +205,7 @@ export const COURSE_QUERY = graphql(`
title
slug
category_name
enable_circle_documents
action_competences {
competence_id
...CoursePageFields

View File

@ -87,6 +87,8 @@
"performanceObjectivesTitle": "Leistungsziele",
"showAssessmentDocument": "Bewertungsinstrument anzeigen",
"submissionNotificationDisclaimer": "{{name}} wird deine Ergebnisse bewerten. Du wirst per Benachrichtigung informiert, sobald die Bewertung für dich freigegeben wurde.",
"submissionShowSampleSolution": "Musterlösung anzeigen",
"submissionShowSampleSolutionText": "Hier findest du eine mögliche Lösung zu deinen Aufgaben. Vorgehen und Prozesse in deiner Organisation können von dieser Lösung abweichen.",
"submitAssignment": "Ergebnisse abgeben",
"taskDefinition": "Bearbeite die Teilaufgaben und dokumentiere deine Ergebnisse.",
"taskDefinitionTitle": "Aufgabenstellung",

View File

@ -91,6 +91,7 @@ const appointments = computed(() => {
.allDueDates()
.filter(
(dueDate) =>
hasDueDate(dueDate) &&
isMatchingCourse(dueDate) &&
isMatchingSession(dueDate) &&
isMatchingCircle(dueDate)
@ -108,6 +109,10 @@ const isMatchingCircle = (dueDate: DueDate) =>
const isMatchingCourse = (dueDate: DueDate) =>
courseSessions.value.map((cs) => cs.id).includes(dueDate.course_session_id);
const hasDueDate = (dueDate: DueDate) => {
return dueDate.start || dueDate.end;
};
const numAppointmentsToShow = ref(7);
const canLoadMore = computed(() => {
return numAppointmentsToShow.value < appointments.value.length;

View File

@ -18,70 +18,24 @@
</span>
{{ $t("feedback.feedbackPageInfo") }}
</p>
<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="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>
<FeedbackPageVV v-if="feedbackType === 'vv'" :feedback-data="feedbackData" />
<FeedbackPageUK
v-else-if="feedbackType === 'uk'"
:feedback-data="feedbackData"
/>
</main>
</div>
</div>
</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 { useCurrentCourseSession } from "@/composables";
import { itGet } from "@/fetchHelpers";
import * as log from "loglevel";
import { onMounted, ref } from "vue";
import { useTranslation } from "i18next-vue";
interface FeedbackData {
amount: number;
questions: {
[key: string]: any;
};
}
import type { FeedbackData, FeedbackType } from "@/types";
import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue";
import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue";
const props = defineProps<{
courseSlug: string;
@ -91,72 +45,21 @@ const props = defineProps<{
log.debug("FeedbackPage created", props.circleId);
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 feedbackType = ref<FeedbackType | undefined>(undefined);
onMounted(async () => {
log.debug("FeedbackPage mounted");
feedbackData.value = await itGet(
`/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>

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>
<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">
<it-icon-assignment class="h-6 w-6"></it-icon-assignment>
@ -88,7 +88,7 @@ const assignment = computed(
</button>
</header>
<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
class="bg-white md:h-full md:overflow-y-auto"
:class="{ 'md:w-1/2': assignment.needs_expert_evaluation }"
@ -119,7 +119,7 @@ const assignment = computed(
</div>
<div
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
:assignment-completion="assignmentCompletion"

View File

@ -84,6 +84,22 @@ const taskExpertDataText = computed(() => {
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() {
if (inEvaluationTask.value) {
return taskExpertDataText.value ?? false;
@ -159,7 +175,7 @@ function finishButtonEnabled() {
@click="emit('close')"
>
<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>
</span>
</button>

View File

@ -3,6 +3,7 @@ import { useCurrentCourseSession } from "@/composables";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import type { Assignment, AssignmentCompletion, CourseSessionUser } from "@/types";
import { useMutation } from "@urql/vue";
import { computed } from "vue";
import dayjs, { Dayjs } from "dayjs";
import * as log from "loglevel";
@ -17,6 +18,34 @@ const emit = defineEmits(["startEvaluation"]);
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 upsertAssignmentCompletionMutation = useMutation(
@ -57,9 +86,9 @@ async function startEvaluation() {
}}
</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(
"assignment.Du musst die Bewertung bis am x um y Uhr abschliessen und freigeben",
@ -71,16 +100,25 @@ async function startEvaluation() {
}}
</p>
<p class="my-4">
{{ $t("assignment.evaluationInstrumentDescriptionText") }}
<p class="my-4" data-cy="instruction">
{{ $t(text.evaluationInstruction) }}
</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">
{{ $t("a.Beurteilungsinstrument anzeigen") }}
</a>
</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>
<button
class="btn-primary text-large"
@ -92,16 +130,16 @@ async function startEvaluation() {
props.assignmentCompletion.completion_status === 'EVALUATION_IN_PROGRESS'
"
>
{{ $t("a.Bewertung fortsetzen") }}
{{ $t(text.evaluationContinue) }}
</span>
<span
v-else-if="
props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED'
"
>
{{ $t("a.Bewertung ansehen") }}
{{ $t(text.evaluationView) }}
</span>
<span v-else>{{ $t("a.Bewertung starten") }}</span>
<span v-else>{{ $t(text.evaluationStart) }}</span>
</button>
</div>
</div>

View File

@ -40,6 +40,37 @@ const upsertAssignmentCompletionMutation = useMutation(
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() {
upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.assignment.id,
@ -58,7 +89,9 @@ async function submitEvaluation() {
}
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) {
@ -93,12 +126,19 @@ const evaluationUser = computed(() => {
<!-- eslint-disable vue/no-v-html -->
<div>
<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 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="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">
{{ userPoints }}
</div>
@ -116,6 +156,7 @@ const evaluationUser = computed(() => {
<div
v-if="
props.assignment.assignment_type === 'CASEWORK' &&
props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED' &&
!props.assignmentCompletion.evaluation_passed
"
@ -125,20 +166,29 @@ const evaluationUser = computed(() => {
</span>
</div>
<p class="my-4">
{{ $t("assignment.evaluationInstrumentDescriptionText") }}
</p>
<div v-if="props.assignment.assignment_type === 'CASEWORK'">
<p class="my-4">
{{ $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">
<a
:href="props.assignment.evaluation_document_url"
class="link"
target="_blank"
>
{{ $t("a.Beurteilungsinstrument anzeigen") }}
</a>
<p
v-if="
props.assignment.assignment_type === 'PRAXIS_ASSIGNMENT' &&
props.assignmentCompletion.completion_status !== 'EVALUATION_SUBMITTED'
"
>
{{ $t("a.assignment.evaluationFeedbackDescriptionText") }}
</p>
<div
v-if="props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED'"
>
@ -156,13 +206,18 @@ const evaluationUser = computed(() => {
data-cy="submit-evaluation"
@click="submitEvaluation()"
>
{{ $t("a.Bewertung freigeben") }}
{{ $t(text.evaluationSubmit) }}
</button>
</div>
<div v-if="state.showSuccessInfo" class="mt-4">
<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>
</div>
</section>
@ -172,7 +227,7 @@ const evaluationUser = computed(() => {
<article class="border-t py-4">
<div class="flex flex-row justify-between">
<div class="mb-4 text-gray-900">
{{ $t("a.Beurteilungskriterium") }} {{ index + 1 }}:
{{ $t(text.evaluationCriteria) }} {{ index + 1 }}:
{{ task.value.title }}
</div>
<div
@ -209,13 +264,16 @@ const evaluationUser = computed(() => {
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
</div>
</section>
<div>
<span class="font-bold">{{ $t("a.Begründung") }}:</span>
<span class="font-bold">{{ $t(text.evaluationReason) }}:</span>
{{ evaluationForTask(task).text }}
</div>
</article>

View File

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

View File

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

View File

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

View File

@ -55,7 +55,11 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
</a>
</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>
<h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("a.Unterlagen für Teilnehmenden") }}

View File

@ -52,7 +52,8 @@ const submittables = computed(() => {
const learningContents = circleFlatLearningContents(circle).filter(
(lc) =>
lc.content_type === "learnpath.LearningContentAssignment" ||
lc.content_type === "learnpath.LearningContentFeedback" ||
lc.content_type === "learnpath.LearningContentFeedbackUK" ||
lc.content_type === "learnpath.LearningContentFeedbackVV" ||
lc.content_type === "learnpath.LearningContentEdoniqTest"
);
@ -72,7 +73,10 @@ const submittables = computed(() => {
});
const isFeedback = (lc: LearningContent) => {
return lc.content_type === "learnpath.LearningContentFeedback";
return (
lc.content_type === "learnpath.LearningContentFeedbackUK" ||
lc.content_type === "learnpath.LearningContentFeedbackVV"
);
};
const isAssignment = (lc: LearningContent) => {
@ -101,7 +105,11 @@ const getLearningContentType = (lc: LearningContent) => {
const getShowDetailsText = (lc: LearningContent) => {
if (isAssignment(lc)) {
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");
} else if (
assignmentType === "PREP_ASSIGNMENT" ||
@ -132,6 +140,7 @@ const getIconName = (lc: LearningContent) => {
if (
assignmentType === "PREP_ASSIGNMENT" ||
assignmentType === "CASEWORK" ||
assignmentType === "PRAXIS_ASSIGNMENT" ||
assignmentType === "CONDITION_ACCEPTANCE"
) {
return "it-icon-assignment-large";

View File

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

View File

@ -1,5 +1,5 @@
<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">
{{ $t("circlePage.documents.title") }}
</h3>

View File

@ -14,12 +14,13 @@ import type { Component } from "vue";
import { computed, onUnmounted } from "vue";
import AssignmentBlock from "./blocks/AssignmentBlock.vue";
import AttendanceCourseBlock from "./blocks/AttendanceCourseBlock.vue";
import FeedbackBlock from "./feedback/FeedbackBlock.vue";
import IframeBlock from "./blocks/IframeBlock.vue";
import MediaLibraryBlock from "./blocks/MediaLibraryBlock.vue";
import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
import RichTextBlock from "./blocks/RichTextBlock.vue";
import VideoBlock from "./blocks/VideoBlock.vue";
import FeedbackBlockUK from "./feedback/FeedbackBlockUK.vue";
import FeedbackBlockVV from "./feedback/FeedbackBlockVV.vue";
import { getPreviousRoute } from "@/router/history";
import { stringifyParse } from "@/utils/utils";
import { useCourseDataWithCompletion } from "@/composables";
@ -42,7 +43,8 @@ const COMPONENTS: Record<LearningContentContentType, Component> = {
"learnpath.LearningContentAssignment": AssignmentBlock,
"learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock,
"learnpath.LearningContentDocumentList": DocumentListBlock,
"learnpath.LearningContentFeedback": FeedbackBlock,
"learnpath.LearningContentFeedbackUK": FeedbackBlockUK,
"learnpath.LearningContentFeedbackVV": FeedbackBlockVV,
"learnpath.LearningContentLearningModule": IframeBlock,
"learnpath.LearningContentKnowledgeAssessment": IframeBlock,
"learnpath.LearningContentMediaLibrary": MediaLibraryBlock,

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import RichText from "@/components/ui/RichText.vue";
import type { Assignment } from "@/types";
import { useRouteQuery } from "@vueuse/router";
import log from "loglevel";
import dayjs from "dayjs";
import RichText from "@/components/ui/RichText.vue";
import log from "loglevel";
interface Props {
assignment: Assignment;
@ -40,15 +40,13 @@ const step = useRouteQuery("step");
</li>
</ul>
<h3 class="mb-4 mt-8">{{ $t("assignment.dueDateSubmission") }}</h3>
<p v-if="submissionDeadlineStart" class="text-large">
<div v-if="submissionDeadlineStart" class="text-large">
<h3 class="mb-4 mt-8">{{ $t("assignment.dueDateSubmission") }}</h3>
{{ $t("assignment.dueDateIntroduction") }}
<DateEmbedding :single-date="dayjs(submissionDeadlineStart)"></DateEmbedding>
</p>
<p v-else class="text-large">
{{ $t("assignment.dueDateNotSet") }}
</p>
<p>
<DateEmbedding :single-date="dayjs(submissionDeadlineStart)"></DateEmbedding>
</p>
</div>
<div v-if="props.assignment.effort_required">
<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 eventBus from "@/utils/eventBus";
import dayjs from "dayjs";
import type { AssignmentAssignmentAssignmentTypeChoices } from "@/gql/graphql";
const props = defineProps<{
assignment: Assignment;
@ -76,14 +77,24 @@ const completionTaskData = computed(() => {
return props.assignmentCompletion?.task_completion_data ?? {};
});
const canSubmit = computed(() => {
const cannotSubmit = computed(() => {
return (
!state.confirmInput ||
(!state.confirmInput && !isPraxisAssignment.value) ||
(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(
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
@ -93,6 +104,14 @@ const onEditTask = (task: AssignmentTask) => {
emit("editTask", task);
};
const openSolutionSample = () => {
const url = props.assignment.solution_sample?.url ?? "";
if (props.assignment.solution_sample) {
window.open(url, "_blank");
}
};
const onSubmit = async () => {
try {
await upsertAssignmentCompletionMutation.executeMutation({
@ -108,20 +127,24 @@ const onSubmit = async () => {
bustItGetCache(
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
);
eventBus.emit("finishedLearningContent", true);
// if solution sample is available, do not close the assigment automatically
if (!props.assignment.solution_sample) {
eventBus.emit("finishedLearningContent", true);
}
} catch (error) {
log.error("Could not submit assignment", error);
}
};
</script>
<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">
{{ $t("assignment.submitAssignment") }}
</h3>
<div v-if="completionStatus === 'IN_PROGRESS'">
<ItCheckbox
v-if="!isPraxisAssignment"
class="w-full border-b border-gray-400 py-10 sm:py-6"
:checkbox-item="{
label: $t('assignment.confirmSubmitResults'),
@ -131,11 +154,14 @@ const onSubmit = async () => {
data-cy="confirm-submit-results"
@toggle="state.confirmInput = !state.confirmInput"
></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
v-if="mayBeEvaluated"
class="py-6"
:checkbox-item="{
label: $t('assignment.confirmSubmitPerson'),
label: isPraxisAssignment
? $t('a.confirmSubmitPersonPraxisAssignment')
: $t('assignment.confirmSubmitPerson'),
value: 'value',
checked: state.confirmPerson,
}"
@ -160,7 +186,7 @@ const onSubmit = async () => {
{{ $t("assignment.showAssessmentDocument") }}
</a>
</div>
<p v-if="isCasework && props.submissionDeadlineStart" class="pt-6">
<p v-if="mayBeEvaluated && props.submissionDeadlineStart" class="pt-6">
{{ $t("assignment.dueDateSubmission") }}
<DateEmbedding
:single-date="dayjs(props.submissionDeadlineStart)"
@ -170,7 +196,7 @@ const onSubmit = async () => {
class="mt-6"
variant="blue"
size="large"
:disabled="canSubmit"
:disabled="cannotSubmit"
data-cy="submit-assignment"
@click="onSubmit"
>
@ -187,6 +213,26 @@ const onSubmit = async () => {
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
}}
</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>
<AssignmentSubmissionResponses

View File

@ -67,17 +67,17 @@ function handleDelete() {
id="upload"
type="file"
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"
/>
<it-icon-document class="mr-1.5 h-7 w-7 text-blue-800" />
{{ $t("a.Datei auswählen") }}
</div>
<p class="text-sm text-gray-900">
{{ $t("a.Mögliche Formate") }}: .JPG, .PNG, .PDF, .DOC, .MOV, .PPT
{{ $t("a.Mögliche Formate") }}: .JPG, .PNG, .PDF, .DOC, .MOV, .PPT, .MP4
</p>
<p class="mb-8 text-sm text-gray-900">
{{ $t("a.Maximale Dateigrösse") }}: 20 MB
{{ $t("a.Maximale Dateigrösse") }}: 50 MB
</p>
</template>

View File

@ -1,35 +1,32 @@
<script setup lang="ts">
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue";
import { graphql } from "@/gql";
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
import {
PERCENTAGES,
RATINGS,
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import type { LearningContentFeedback } from "@/types";
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
import { useMutation } from "@urql/vue";
import { useRouteQuery } from "@vueuse/router";
import log from "loglevel";
import { computed, onMounted, reactive, ref } from "vue";
import { useTranslation } from "i18next-vue";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import { bustItGetCache } from "@/fetchHelpers";
import { useUserStore } from "@/stores/user";
const props = defineProps<{
content: LearningContentFeedback;
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
stepLabels: string[];
questionData: any[];
introduction: string;
title: string;
completionTitle: string;
completionDescription: string;
showAvatar: boolean;
}>();
const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const { t } = useTranslation();
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
const title = computed(
() => `«${props.content.circle?.title}»: ${t("feedback.areYouSatisfied")}`
);
const title = computed(() => `«${props.content.circle?.title}»: ${props.title}`);
const circleExperts = computed(() => {
if (props.content?.circle?.slug) {
@ -38,34 +35,29 @@ const circleExperts = computed(() => {
return [];
});
const stepLabels = [
t("general.introduction"),
t("feedback.satisfactionLabel"),
t("feedback.goalAttainmentLabel"),
t("feedback.proficiencyLabel"),
t("feedback.preparationTaskClarityLabel"),
t("feedback.instructorCompetenceLabel"),
t("feedback.instructorRespectLabel"),
t("feedback.instructorOpenFeedbackLabel"),
t("feedback.recommendLabel"),
t("feedback.coursePositiveFeedbackLabel"),
t("feedback.courseNegativeFeedbackLabel"),
t("general.submission"),
];
const localStepLabels = ref(props.stepLabels);
const localQuestionData = ref(props.questionData);
const feedbackData: FeedbackData = reactive(feedbackDataFactory());
const numSteps = stepLabels.length;
const numSteps = computed(() => localStepLabels.value.length);
const textQuestionKeys = computed(() =>
props.questionData.filter((item) => isTextNode(item)).map((item) => item.modelKey)
);
const avatarUrl = computed(() => circleExperts.value[0]?.avatar_url);
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
const sendFeedbackMutation = graphql(`
mutation SendFeedbackMutation(
$courseSessionId: ID!
$learningContentId: ID!
$learningContentType: String!
$data: GenericScalar!
$submitted: Boolean
) {
send_feedback(
course_session_id: $courseSessionId
learning_content_page_id: $learningContentId
learning_content_type: $learningContentType
data: $data
submitted: $submitted
) {
@ -90,69 +82,6 @@ interface FeedbackData {
[key: string]: number | string | null;
}
const feedbackData: FeedbackData = reactive({
satisfaction: null,
goal_attainment: null,
proficiency: null,
preparation_task_clarity: null,
instructor_competence: null,
instructor_respect: null,
instructor_open_feedback: "",
would_recommend: null,
course_positive_feedback: "",
course_negative_feedback: "",
});
const questionData = [
{
modelKey: "satisfaction",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "goal_attainment",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "proficiency",
items: PERCENTAGES,
component: ItRadioGroup,
},
{
modelKey: "preparation_task_clarity",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "instructor_competence",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "instructor_respect",
items: RATINGS,
component: ItRadioGroup,
},
{
modelKey: "instructor_open_feedback",
component: ItTextarea,
},
{
modelKey: "would_recommend",
items: YES_NO,
component: ItRadioGroup,
},
{
modelKey: "course_positive_feedback",
component: ItTextarea,
},
{
modelKey: "course_negative_feedback",
component: ItTextarea,
},
];
const previousStep = () => {
if (stepNo.value > 0) {
stepNo.value -= 1;
@ -160,23 +89,17 @@ const previousStep = () => {
};
const nextStep = () => {
if (stepNo.value < numSteps && hasStepValidInput(stepNo.value)) {
if (stepNo.value < numSteps.value && hasStepValidInput(stepNo.value)) {
stepNo.value += 1;
}
log.debug(`next step ${stepNo.value} of ${numSteps}`);
log.debug(`next step ${stepNo.value} of ${numSteps.value}`);
mutateFeedback(feedbackData);
};
function hasStepValidInput(stepNumber: number) {
const question = questionData[stepNumber - 1];
const question = localQuestionData.value[stepNumber - 1];
if (question) {
if (
[
"instructor_open_feedback",
"course_negative_feedback",
"course_positive_feedback",
].includes(question.modelKey)
) {
if (textQuestionKeys.value.includes(question.modelKey)) {
// text response questions need to have a "truthy" value (not "" or null)
return feedbackData[question.modelKey];
} else {
@ -192,6 +115,7 @@ function mutateFeedback(data: FeedbackData, submit = false) {
return executeMutation({
courseSessionId: courseSession.value.id,
learningContentId: props.content.id,
learningContentType: props.content.content_type,
data: data,
submitted: submit,
})
@ -199,29 +123,40 @@ function mutateFeedback(data: FeedbackData, submit = false) {
log.debug("feedback mutation result", result);
if (result.data?.send_feedback?.feedback_response?.data) {
const responseData = result.data.send_feedback.feedback_response.data;
if (!responseData.instructor_open_feedback) {
responseData.instructor_open_feedback = "";
}
if (!responseData.course_negative_feedback) {
responseData.course_negative_feedback = "";
}
if (!responseData.course_positive_feedback) {
responseData.course_positive_feedback = "";
}
textQuestionKeys.value.map((key) => {
if (!responseData[key]) {
responseData[key] = "";
}
});
Object.assign(feedbackData, responseData);
log.debug("feedback data", feedbackData);
feedbackSubmitted.value =
result.data?.send_feedback?.feedback_response?.submitted || false;
}
bustItGetCache(
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
);
})
.catch((e) => log.error(e));
}
function feedbackDataFactory() {
const data: FeedbackData = {};
localQuestionData.value.map((item) => {
data[item.modelKey] = isTextNode(item) ? "" : null;
});
return data;
}
function isTextNode(item: any) {
return item.component.props.placeholder;
}
onMounted(async () => {
log.debug("Feedback mounted");
await mutateFeedback({});
if (feedbackSubmitted.value) {
stepNo.value = numSteps - 1;
stepNo.value = numSteps.value - 1;
}
});
</script>
@ -246,22 +181,22 @@ onMounted(async () => {
@next="nextStep()"
>
<div>
<p v-if="stepNo === 0" class="mt-10">
{{
$t("feedback.intro", {
name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
})
}}
<p v-if="stepNo === 0" class="mt-10" data-cy="introduction">
{{ introduction }}
</p>
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2">
{{ stepLabels[stepNo] }}
<p
v-if="stepNo > 0 && stepNo + 1 < numSteps"
class="pb-2"
:data-cy="`question-${stepNo}`"
>
{{ localStepLabels[stepNo] }}
</p>
<div v-for="(question, index) in questionData" :key="index">
<!-- eslint-disable -->
<!-- eslint does not like the dynamic v-model... -->
<component
:is="question.component"
v-if="index + 1 === stepNo"
v-if="index + 1 === stepNo && feedbackData != undefined"
v-model="feedbackData[question.modelKey] as any"
:items="question['items']"
:cy-key="question.modelKey"
@ -269,14 +204,11 @@ onMounted(async () => {
<!-- eslint-enable -->
</div>
<FeedbackCompletition
v-if="stepNo === 11"
:avatar-url="circleExperts[0].avatar_url"
:title="
$t('feedback.completionTitle', {
name: `${circleExperts[0].first_name} ${circleExperts[0].last_name}`,
})
"
:description="$t('feedback.completionDescription')"
v-if="stepNo === numSteps - 1"
:avatar-url="avatarUrl"
:show-avatar="showAvatar"
:title="completionTitle"
:description="completionDescription"
:feedback-sent="feedbackSubmitted"
@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
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>
<div>
<p class="mb-6">{{ description }}</p>
@ -33,6 +37,7 @@ interface Props {
title?: string;
description?: string;
feedbackSent?: boolean;
showAvatar?: boolean;
}
withDefaults(defineProps<Props>(), {
@ -40,6 +45,7 @@ withDefaults(defineProps<Props>(), {
title: "",
description: "",
feedbackSent: false,
showAvatar: true,
});
defineEmits(["sendFeedback"]);

View File

@ -11,7 +11,8 @@ import type {
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType,
LearningContentFeedbackUkObjectType,
LearningContentFeedbackVvObjectType,
LearningContentKnowledgeAssessmentObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
@ -68,8 +69,12 @@ export type LearningContentEdoniqTest = LearningContentEdoniqTestObjectType & {
readonly content_type: "learnpath.LearningContentEdoniqTest";
};
export type LearningContentFeedback = LearningContentFeedbackObjectType & {
readonly content_type: "learnpath.LearningContentFeedback";
export type LearningContentFeedbackVV = LearningContentFeedbackVvObjectType & {
readonly content_type: "learnpath.LearningContentFeedbackVV";
};
export type LearningContentFeedbackUK = LearningContentFeedbackUkObjectType & {
readonly content_type: "learnpath.LearningContentFeedbackUK";
};
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
@ -102,7 +107,8 @@ export type LearningContent =
| LearningContentAttendanceCourse
| LearningContentDocumentList
| LearningContentEdoniqTest
| LearningContentFeedback
| LearningContentFeedbackUK
| LearningContentFeedbackVV
| LearningContentLearningModule
| LearningContentKnowledgeAssessment
| LearningContentMediaLibrary
@ -188,6 +194,7 @@ export interface Course {
title: string;
category_name: string;
slug: string;
enable_circle_documents: boolean;
}
export interface CourseCategory {
@ -342,7 +349,7 @@ export interface AssignmentEvaluationTask {
title: string;
description: string;
max_points: number;
sub_tasks: AssignmentEvaluationSubTask[];
sub_tasks?: AssignmentEvaluationSubTask[];
};
}
@ -559,3 +566,13 @@ export type DueDate = SimpleDueDate & {
course_session_id: string;
circle: CircleLight | null;
};
export type FeedbackType = "uk" | "vv";
export interface FeedbackData {
amount: number;
questions: {
[key: string]: any;
};
feedbackType: FeedbackType;
}

View File

@ -46,7 +46,8 @@ export function learningContentTypeData(
return { title: t("learningContentTypes.test"), icon: "it-icon-lc-test" };
case "learnpath.LearningContentRichText":
return { title: t("learningContentTypes.text"), icon: "it-icon-lc-resource" };
case "learnpath.LearningContentFeedback":
case "learnpath.LearningContentFeedbackUK":
case "learnpath.LearningContentFeedbackVV":
return { title: t("learningContentTypes.feedback"), icon: "it-icon-lc-feedback" };
case "learnpath.LearningContentPlaceholder":
return {

View File

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

View File

@ -1,6 +1,121 @@
import { TEST_STUDENT1_USER_ID } from "../../consts";
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", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
@ -10,234 +125,255 @@ describe("assignmentStudent.cy.js", () => {
);
});
it("can open assignment", () => {
cy.testLearningContentTitle("Einleitung");
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"
describe("Assignment", () => {
it("can open assignment", () => {
cy.testLearningContentTitle("Einleitung");
cy.testLearningContentSubtitle(
"Überprüfen einer Motorfahrzeugs-Versicherungspolice"
);
});
// 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();
it("can navigate through assignment", () => {
// 1 Step forward
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 1: Beispiel einer Versicherungspolice finden"
);
// 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();
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle(
"Teilaufgabe 2: Kundensituation und Ausgangslage"
);
// 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();
cy.learningContentMultiLayoutNextStep();
cy.testLearningContentTitle("Teilaufgabe 3: Aktuelle Versicherung");
// 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.learningContentMultiLayoutPreviousStep();
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");
// 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.testLearningContentTitle(
"Teilaufgabe 2: Kundensituation und Ausgangslage"
);
});
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
cy.visit(
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
);
cy.url().should("include", "step=7");
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();
// 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");
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
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";
describe("assignmentTrainer.cy.js", () => {
@ -7,191 +7,334 @@ describe("assignmentTrainer.cy.js", () => {
login("test-trainer1@example.com", "test");
});
it("can open cockpit assignment page and open user assignment", () => {
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();
describe("Casework", () => {
it("can open cockpit assignment page and open user assignment", () => {
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"]').should("contain", "Ergebnisse abgegeben");
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="Student1"]').should("contain", "Ergebnisse abgegeben");
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", "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="student-submission"]').should("contain", "Ergebnisse");
cy.get('[data-cy="student-submission"]').should("contain", "Student1");
});
cy.get('[data-cy="reason-text"]')
.find("textarea")
.should("have.value", "Nicht so gut");
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();
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"assignment_user_id",
TEST_STUDENT1_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" },
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="title"]').should("contain", "Bewertung");
cy.get('[data-cy="evaluation-duedate"]').should("exist");
cy.get('[data-cy="instruction"]').should(
"contain",
"Die Gesamtpunktzahl und die daraus resultierende Note wird auf Grund des hinterlegeten Beurteilungsinstrument berechnet."
);
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", () => {
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();
//Todo: Move tests to Lernbegleitung once it is implemented
describe("Praxis Assignment", () => {
it("can start evaluation and store evaluation results", () => {
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-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
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();
// without text input the button should be disabled
cy.get('[data-cy="next-step"]').should("be.disabled");
// 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();
// with text you can continue
cy.get('[data-cy="reason-text"]').type("Gut gemacht!");
// wait for debounce
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();
cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 2 / 5");
cy.get('[data-cy="reason-text"]').type("Nicht so gut");
cy.wait(1000);
// 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();
// load AssignmentCompletion from DB and check
cy.loadAssignmentCompletion(
"evaluation_user_id",
TEST_TRAINER1_USER_ID
).then((ac) => {
console.log(ac.completion_status);
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: 0, text: "Nicht so gut" },
});
expect(Cypress._.values(ac.completion_data)).to.deep.include({
expert_data: { points: 0, text: "Gut gemacht!" },
});
});
});
// 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();
it("can make complete evaluation", () => {
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-details-btn-test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"]'
).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="Student1"]').find('[data-cy="show-results"]').click();
cy.get('[data-cy="result-section"]').should(
"contain",
"Deine Bewertung für Test Student1 wurde freigegeben"
);
cy.get('[data-cy="start-evaluation"]').click();
// 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();
// step 1
cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 1 / 5");
cy.get('[data-cy="reason-text"]').type("Begründung Schritt 1");
// wait for debounce
cy.wait(500);
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="Student1"]').should("contain", "Bewertung freigegeben");
cy.get('[data-cy="Student1"]').should("contain", "17 von 24 Punkte");
// step 2
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
cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
cy.url().should("include", "step=6");
// step 3
cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 3 / 5");
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
cy.loadAssignmentCompletion(
"assignment_user_id",
TEST_STUDENT1_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");
// step 4
cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 4 / 5");
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", "Feedback 5 / 5");
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", "Feedback Freigabe");
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.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", () => {
it("contains correct numbers", () => {
getDashboardStatistics("feedback.average").should("have.text", "3.3");
getDashboardStatistics("feedback.count").should("have.text", "3");
getDashboardStatistics("feedback.count").should("have.text", "6");
});
it("contains correct details link", () => {
clickOnDetailsLink("feedback");

View File

@ -5,153 +5,359 @@ describe("feedbackStudent.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
});
it("can open feedback page", () => {
cy.testLearningContentTitle("Kursfeedback");
cy.testLearningContentSubtitle("Feedback");
describe("Feedback UK", () => {
beforeEach(() => {
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
});
it("can open feedback page", () => {
cy.testLearningContentTitle("Kursfeedback");
cy.testLearningContentSubtitle("Feedback");
});
it("can create feedback by giving answers to all steps", () => {
// initial wait for step 0 (or none with step==0) is required for pipelines
cy.url().should((url) => {
expect(url).to.match(/\/fahrzeug\/feedback(\?step=0)?$/);
});
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// fill feedback form
// step 1
cy.url().should("include", "step=1");
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-4"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 2
cy.url().should("include", "step=2");
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.instructor_competence).to.equal(null);
}
);
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Vorbereitungsaufträge klar und verständlich?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get('[data-cy="question-5"]').should(
"contain",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-2"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get('[data-cy="question-6"]').should(
"contain",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-1"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 7
cy.url().should("include", "step=7");
cy.get('[data-cy="question-7"]').should(
"contain",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-instructor_open_feedback"]').type(
"Der Kursleiter ist eigentlich ganz nett."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 8
cy.url().should("include", "step=8");
cy.get('[data-cy="question-8"]').should(
"contain",
"Würdest du den Kurs weiterempfehlen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-true"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 9
cy.url().should("include", "step=9");
cy.get('[data-cy="question-9"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Ich bin zufrieden mit den meisten Dingen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 10
cy.url().should("include", "step=10");
cy.get('[data-cy="question-10"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=11");
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
});
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-feedback-checkbox"]'
).should("have.class", "cy-checked");
// reopening page should get directly to last step
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
cy.url().should("include", "step=11");
// check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.true;
expect(ac.data).to.deep.equal({
course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
course_positive_feedback:
"Ich bin zufrieden mit den meisten Dingen.",
goal_attainment: 3,
instructor_competence: 2,
instructor_open_feedback:
"Der Kursleiter ist eigentlich ganz nett.",
instructor_respect: 1,
preparation_task_clarity: false,
proficiency: 80,
satisfaction: 4,
would_recommend: true,
feedback_type: "uk",
});
}
);
});
});
it("can create feedback by giving answers to all steps", () => {
// initial wait for step 0 (or none with step==0) is required for pipelines
cy.url().should((url) => {
expect(url).to.match(/\/fahrzeug\/feedback(\?step=0)?$/);
describe("Feedback VV", () => {
beforeEach(() => {
cy.visit("/course/test-lehrgang/learn/reisen/feedback");
});
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// fill feedback form
// step 1
cy.url().should("include", "step=1");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-4"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 2
cy.url().should("include", "step=2");
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.instructor_competence).to.equal(null);
}
);
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-2"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-1"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 7
cy.url().should("include", "step=7");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-instructor_open_feedback"]').type(
"Der Kursleiter ist eigentlich ganz nett."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 8
cy.url().should("include", "step=8");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-true"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 9
cy.url().should("include", "step=9");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Ich bin zufrieden mit den meisten Dingen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 10
cy.url().should("include", "step=10");
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=11");
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
it("can open feedback page", () => {
cy.testLearningContentTitle("Feedback");
cy.testLearningContentSubtitle("Feedback");
});
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-feedback-checkbox"]'
).should("have.class", "cy-checked");
// reopening page should get directly to last step
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
cy.url().should("include", "step=11");
it("can create feedback by giving answers to all steps", () => {
// initial wait for step 0 (or none with step==0) is required for pipelines
cy.url().should((url) => {
expect(url).to.match(/\/reisen\/feedback(\?step=0)?$/);
});
cy.get('[data-cy="introduction"]').contains(
"Wir bitten dich um dein Feedback. Es hilft uns, damit wir deine Lernerlebnisse verbessern können."
);
// check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.true;
expect(ac.data).to.deep.equal({
course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
course_positive_feedback: "Ich bin zufrieden mit den meisten Dingen.",
goal_attainment: 3,
instructor_competence: 2,
instructor_open_feedback: "Der Kursleiter ist eigentlich ganz nett.",
instructor_respect: 1,
preparation_task_clarity: false,
proficiency: 80,
satisfaction: 4,
would_recommend: true,
});
}
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// fill feedback form
// step 1
cy.url().should("include", "step=1");
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-4"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 2
cy.url().should("include", "step=2");
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
// the system should store after every step -> check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.false;
expect(ac.data.satisfaction).to.equal(4);
expect(ac.data.course_positive_feedback).to.equal(null);
}
);
cy.get('[data-cy="radio-3"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 3
cy.url().should("include", "step=3");
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-80"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 4
cy.url().should("include", "step=4");
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Praxisaufträge klar und verständlich?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 5
cy.url().should("include", "step=5");
cy.get('[data-cy="question-5"]').should(
"contain",
"Würdest du den Circle weiterempfehlen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="radio-false"]').click();
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 6
cy.url().should("include", "step=6");
cy.get('[data-cy="question-6"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_positive_feedback"]').type(
"Der Circle ist eigentlich ganz nett."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
// step 7
cy.url().should("include", "step=7");
cy.get('[data-cy="question-7"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
);
cy.get('[data-cy="next-step"]').should("be.disabled");
cy.get('[data-cy="it-textarea-course_negative_feedback"]').type(
"Ich bin unzufrieden mit einigen Sachen."
);
cy.wait(200);
cy.learningContentMultiLayoutNextStep();
cy.wait(200);
cy.url().should("include", "step=8");
cy.get('[data-cy="sendFeedbackButton"]').click();
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
// marked complete in circle
cy.url().should((url) => {
expect(url).to.match(/\/reisen#lu-transfer-reflexion-feedback?$/);
});
cy.reload();
cy.get(
'[data-cy="test-lehrgang-lp-circle-reisen-lc-feedback-checkbox"]'
).should("have.class", "cy-checked");
// reopening page should get directly to last step
cy.visit("/course/test-lehrgang/learn/reisen/feedback");
cy.url().should("include", "step=8");
// check stored data
cy.loadFeedbackResponse("feedback_user_id", TEST_STUDENT1_USER_ID).then(
(ac) => {
expect(ac.submitted).to.be.true;
expect(ac.data).to.deep.equal({
course_negative_feedback: "Ich bin unzufrieden mit einigen Sachen.",
course_positive_feedback: "Der Circle ist eigentlich ganz nett.",
goal_attainment: 3,
preparation_task_clarity: false,
proficiency: 80,
satisfaction: 4,
would_recommend: false,
feedback_type: "vv",
});
}
);
});
});
});

View File

@ -16,77 +16,216 @@ describe("feedbackTrainer.cy.js", () => {
cy.get('[data-cy="feedback-data-amount"]').should("contain", "0");
});
it("can open feedback results page with results", () => {
cy.manageCommand("cypress_reset --create-feedback-responses");
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
).click();
describe("FeedbackUK", function () {
it("can open feedback results page with results", () => {
cy.manageCommand("cypress_reset --create-feedback-responses");
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-fahrzeug-lc-feedback"]'
).click();
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
cy.get('[data-cy="question-1"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.3");
// check titles of questions
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
);
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
);
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?"
);
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Vorbereitungsaufträge klar und verständlich?"
);
cy.get('[data-cy="question-5"]').should(
"contain",
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?"
);
cy.get('[data-cy="question-6"]').should(
"contain",
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?"
);
cy.get('[data-cy="question-7"]').should(
"contain",
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?"
);
cy.get('[data-cy="question-8"]').should(
"contain",
"Würdest du den Kurs weiterempfehlen?"
);
cy.get('[data-cy="question-9"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
);
cy.get('[data-cy="question-10"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
);
cy.get('[data-cy="question-2"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.0");
cy.get('[data-cy="question-1"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-40%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-80%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-100%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-2"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.0");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "3");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "0");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-40%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-80%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-100%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-5"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "2.7");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "3");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "0");
cy.get('[data-cy="question-6"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.0");
cy.get('[data-cy="question-5"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "2.7");
cy.get('[data-cy="question-7"]')
.should("contain", "Super Kurs!")
.should("contain", "Super, bin begeistert")
.should("contain", "Ok, entspricht den Erwartungen");
cy.get('[data-cy="question-6"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.0");
cy.get('[data-cy="question-8"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "2");
cy.get('[data-cy="question-8"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "1");
cy.get('[data-cy="question-7"]')
.should("contain", "Super Kurs!")
.should("contain", "Super, bin begeistert")
.should("contain", "Ok, entspricht den Erwartungen");
cy.get('[data-cy="question-9"]')
.should("contain", "Nichts Schlechtes")
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
.should("contain", "Mehr Videos wären schön.");
cy.get('[data-cy="question-8"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "2");
cy.get('[data-cy="question-8"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "1");
cy.get('[data-cy="question-10"]')
.should("contain", "Nur Gutes.")
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
.should("contain", "Die Präsentation war super");
cy.get('[data-cy="question-9"]')
.should("contain", "Nichts Schlechtes")
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
.should("contain", "Mehr Videos wären schön.");
cy.get('[data-cy="question-10"]')
.should("contain", "Nur Gutes.")
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
.should("contain", "Die Präsentation war super");
});
});
describe("FeedbackVV", function () {
it("can open feedback results page with results", () => {
cy.manageCommand("cypress_reset --create-feedback-responses");
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
cy.get(
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-reisen-lc-feedback"]'
).click();
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
// check titles of questions
cy.get('[data-cy="question-1"]').should(
"contain",
"Zufriedenheit insgesamt"
);
cy.get('[data-cy="question-2"]').should(
"contain",
"Zielerreichung insgesamt"
);
cy.get('[data-cy="question-3"]').should(
"contain",
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
);
cy.get('[data-cy="question-4"]').should(
"contain",
"Waren die Praxisaufträge klar und verständlich?"
);
cy.get('[data-cy="question-5"]').should(
"contain",
"Würdest du den Circle weiterempfehlen?"
);
cy.get('[data-cy="question-6"]').should(
"contain",
"Wo siehst du Verbesserungspotential?"
);
cy.get('[data-cy="question-7"]').should(
"contain",
"Was hat dir besonders gut gefallen?"
);
cy.get('[data-cy="question-1"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.3");
cy.get('[data-cy="question-2"]')
.find('[data-cy="rating-scale-average"]')
.should("contain", "3.0");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-40%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-80%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-3"]')
.find('[data-cy="percentage-value-100%"]')
.should("contain", "33.3");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "3");
cy.get('[data-cy="question-4"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "0");
cy.get('[data-cy="question-5"]')
.find('[data-cy="popover-yes"]')
.click()
.find('[data-cy="num-yes"]')
.should("contain", "2");
cy.get('[data-cy="question-5"]')
.find('[data-cy="popover-no"]')
.click()
.find('[data-cy="num-no"]')
.should("contain", "1");
cy.get('[data-cy="question-6"]')
.should("contain", "Nichts Schlechtes")
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
.should("contain", "Mehr Videos wären schön.");
cy.get('[data-cy="question-7"]')
.should("contain", "Nur Gutes.")
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
.should("contain", "Die Präsentation war super");
});
});
});

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 = [
"/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
"/Users/eliabieri/iterativ/vbv_lernwelt/.direnv/python-3.10.6/bin",
"/Users/christiancueni/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
"/Users/renzo/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
];
let bashCommand = `PATH=${pythonPaths.join(":")}:$PATH && ${execCommand}`;
return cy

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
settings.DEBUG = True
from django.db import connection
from django.db import reset_queries
from django.db import connection, 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")
django.setup()
from vbv_lernwelt.core.schema import Query
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.schema import Query
def main():
from django.conf import settings
settings.DEBUG = True
from django.db import connection
from django.db import reset_queries
from django.db import connection, reset_queries
reset_queries()

View File

@ -10,12 +10,12 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
django.setup()
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.notify.email.email_services import (
create_template_data_from_course_session_attendance_course,
EmailTemplate,
send_email,
create_template_data_from_course_session_attendance_course,
)
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
def main():

View File

@ -116,6 +116,7 @@ THIRD_PARTY_APPS = [
LOCAL_APPS = [
"vbv_lernwelt.core",
"vbv_lernwelt.media_files",
"vbv_lernwelt.sso",
"vbv_lernwelt.course",
"vbv_lernwelt.learnpath",
@ -212,23 +213,13 @@ STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
USE_AWS = env("USE_AWS", False)
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", "")
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", "")
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", "")
AWS_S3_CUSTOM_DOMAIN = "%s.s3.amazonaws.com" % AWS_STORAGE_BUCKET_NAME
# MEDIA
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR / "media")
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
if USE_AWS:
# https://wagtail.org/blog/amazon-s3-for-media-files/
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
else:
MEDIA_URL = "/server/media/"
MEDIA_URL = "/server/media/"
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
@ -252,7 +243,19 @@ WAGTAIL_ENABLE_UPDATE_CHECK = False
WAGTAIL_ENABLE_WHATS_NEW_BANNER = False
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES
WAGTAILDOCS_DOCUMENT_MODEL = "media_library.LibraryDocument"
WAGTAILDOCS_DOCUMENT_MODEL = "media_files.ContentDocument"
WAGTAILIMAGES_IMAGE_MODEL = "media_files.ContentImage"
# this setting makes that the document is served by django, and the url is the django url.
# https://docs.wagtail.org/en/stable/reference/settings.html#wagtaildocs-serve-method
# The file is served by django as streaming response. If it should be serverd by nginx, then install django sendfile
WAGTAILDOCS_SERVE_METHOD = "serve_view"
# WAGTAILDOCS_INLINE_CONTENT_TYPES = ['application/pdf', 'text/plain']
WAGTAILIMAGES_MAX_UPLOAD_SIZE = 20 * 1024 * 1024 # 20MB
# WAGTAILIMAGES_RENDITION_STORAGE = 'myapp.backends.MyCustomStorage'
WAGTAILADMIN_RICH_TEXT_EDITORS = {
"default": {
@ -645,7 +648,7 @@ NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
# S3 BUCKET CONFIGURATION
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="local") # local | s3
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="s3") # local | s3
if FILE_UPLOAD_STORAGE == "local":
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=5242880)
@ -654,18 +657,19 @@ if FILE_UPLOAD_STORAGE == "s3":
# Using django-storages
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID")
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID", default="AKIAZJLREPUVWNBTJ5VY")
AWS_S3_SECRET_ACCESS_KEY = env("AWS_S3_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME")
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", "eu-central-1")
AWS_S3_SIGNATURE_VERSION = env("AWS_S3_SIGNATURE_VERSION", default="s3v4")
AWS_STORAGE_BUCKET_NAME = env(
"AWS_STORAGE_BUCKET_NAME", default="myvbv-dev.iterativ.ch"
)
AWS_S3_FILE_OVERWRITE = env("AWS_S3_FILE_OVERWRITE", False)
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=20971520) # 20MB
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="private")
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=300) # seconds
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=7200) # seconds
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = (
"jpg",

View File

@ -1,6 +1,7 @@
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
import os
os.environ["IT_APP_ENVIRONMENT"] = "local"
from .base import * # noqa
@ -8,6 +9,7 @@ from .base import * # noqa
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
TEST_RUNNER = "django.test.runner.DiscoverRunner"
# Select faster password hasher during tests
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
@ -15,16 +17,7 @@ PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
WHITENOISE_MANIFEST_STRICT = False
# Dummy data
AWS_S3_ACCESS_KEY_ID = "SOMEKEY"
AWS_S3_SECRET_ACCESS_KEY = "SOMEACCESSKEY"
AWS_STORAGE_BUCKET_NAME = "myvbv-dev.iterativ.ch"
AWS_S3_REGION_NAME = "eu-central-1"
AWS_S3_SIGNATURE_VERSION = "s3v4"
FILE_MAX_SIZE = 20971520 # 20MB
AWS_DEFAULT_ACL = "private"
AWS_PRESIGNED_EXPIRY = 300
AWS_S3_FILE_OVERWRITE = True
class DisableMigrations(dict):
@ -36,8 +29,3 @@ class DisableMigrations(dict):
MIGRATION_MODULES = DisableMigrations()
# Select faster password hasher during tests
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.MD5PasswordHasher",
]

View File

@ -2,6 +2,10 @@
import os
os.environ["IT_APP_ENVIRONMENT"] = "local"
os.environ["AWS_S3_SECRET_ACCESS_KEY"] = os.environ.get(
"AWS_S3_SECRET_ACCESS_KEY",
"!!!default_for_quieting_cypress_within_pycharm!!!",
)
from .base import * # noqa

View File

@ -56,7 +56,7 @@ from vbv_lernwelt.importer.views import (
from vbv_lernwelt.notify.views import email_notification_settings
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
from wagtail.documents import urls as media_library_urls
class SignedIntConverter(IntConverter):
@ -88,7 +88,7 @@ urlpatterns = [
# wagtail urls
path('server/cms/', include(wagtailadmin_urls)),
path('server/documents/', include(wagtaildocs_urls)),
path('server/documents/', include(media_library_urls)),
path('server/pages/', include(wagtail_urls)),
# core
@ -132,6 +132,7 @@ urlpatterns = [
name="request_assignment_completion_status"),
# documents
# TODO: remfactor to files app
path(r'api/core/document/start/', document_upload_start,
name='file_upload_start'),
path(r'api/core/document/<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
pytest # https://github.com/pytest-dev/pytest
pytest-sugar # https://github.com/Frozenball/pytest-sugar
pytest-xdist #
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs

View File

@ -38,9 +38,7 @@ azure-core==1.29.1
azure-identity==1.14.0
# via -r requirements.in
azure-storage-blob==12.17.0
# via
# -r requirements.in
# django-storages
# via -r requirements.in
backcall==0.2.0
# via ipython
bcrypt==4.0.1
@ -176,7 +174,7 @@ django-ratelimit==4.1.0
# via -r requirements.in
django-redis==5.3.0
# via -r requirements.in
django-storages[azure]==1.13.2
django-storages==1.13.2
# via -r requirements.in
django-stubs==4.2.3
# via
@ -209,6 +207,8 @@ exceptiongroup==1.1.2
# via
# anyio
# pytest
execnet==2.0.2
# via pytest-xdist
executing==1.2.0
# via stack-data
factory-boy==3.3.0
@ -397,7 +397,9 @@ pyflakes==3.1.0
pygments==2.16.1
# via ipython
pyjwt[crypto]==2.8.0
# via msal
# via
# msal
# pyjwt
pylint==2.17.5
# via
# pylint-django
@ -415,10 +417,13 @@ pytest==7.4.0
# -r requirements-dev.in
# pytest-django
# pytest-sugar
# pytest-xdist
pytest-django==4.5.2
# via -r requirements-dev.in
pytest-sugar==0.9.7
# via -r requirements-dev.in
pytest-xdist==3.5.0
# via -r requirements-dev.in
python-dateutil==2.8.2
# via
# -r requirements.in
@ -616,7 +621,9 @@ wheel==0.41.1
whitenoise[brotli]==6.5.0
# via -r requirements.in
willow[heif]==1.6.1
# via wagtail
# via
# wagtail
# willow
wrapt==1.15.0
# via astroid

View File

@ -1,4 +1,6 @@
#!/bin/bash
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
pytest --junitxml=../test-reports/coverage.xml
# limit test to 6 parallel processes, otherwise ratelimit of s3 could be hit
pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml

View File

@ -3,7 +3,7 @@
set -e
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
coverage run -m pytest --junitxml=../test-reports/coverage.xml $1
coverage run -m pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml $1
coverage_python=`coverage report -m | tail -n1 | awk '{print $4}'`
commit=`git rev-parse HEAD`

View File

@ -20,6 +20,7 @@ from vbv_lernwelt.course.consts import (
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
)
from vbv_lernwelt.course.models import CoursePage
from vbv_lernwelt.media_files.models import ContentDocument
from wagtail.blocks import StreamValue
from wagtail.blocks.list_block import ListBlock, ListValue
from wagtail.rich_text import RichText
@ -39,6 +40,7 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
needs_expert_evaluation=True,
competence_certificate=competence_certificate,
effort_required="ca. 5 Stunden",
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
intro_text=replace_whitespace(
"""
<h3>Ausgangslage</h3>
@ -4208,6 +4210,8 @@ def create_vv_gewinnen_casework(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID):
parent=assignment_list_page,
title="Mein Kundenstamm",
effort_required="60 bis 90 Minuten",
needs_expert_evaluation=True,
assignment_type=AssignmentType.PRAXIS_ASSIGNMENT.name,
intro_text=replace_whitespace(
"""
<h3>Thema</h3>
@ -4255,6 +4259,51 @@ def create_vv_gewinnen_casework(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID):
)
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.append(
@ -4476,89 +4525,20 @@ def create_vv_gewinnen_casework(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID):
),
)
)
assignment.tasks.append(
(
"task",
TaskBlockFactory(
title="Teilaufgabe 5: Kundenkontakte pflegen",
title="Teilaufgabe 5: Kundentelefonate2",
content=StreamValue(
TaskContentStreamBlock(),
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",
UserTextInputBlockFactory(
text=RichText(
"""
Welche weiteren Möglichkeiten hast du, um den Kontakt zu bestehenden Kundinnen/Kunden zu pflegen?
"""
)
),
),
],
),
),
)
)
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?
<p>Notiere, welche hundert Kunden du nächste Woche für ein Beratungsgespräch telefonisch kontaktieren willst.</p>
"""
)
),
@ -4588,6 +4568,7 @@ def create_vv_einkommenssicherung_casework(
parent=assignment_list_page,
title="Heirat: Was ändert sich",
effort_required="45 bis 90 Minuten",
assignment_type=AssignmentType.PRAXIS_ASSIGNMENT.name,
intro_text=replace_whitespace(
"""
<h3>Thema</h3>
@ -4639,6 +4620,43 @@ def create_vv_einkommenssicherung_casework(
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.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()
return assignment
@ -4826,6 +4819,7 @@ def create_vv_gesundheit_casework(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
parent=assignment_list_page,
title="Krankenversicherung: Passt die Lösung noch?",
effort_required="60 bis 90 Minuten",
assignment_type=AssignmentType.PRAXIS_ASSIGNMENT.name,
intro_text=replace_whitespace(
"""
<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.iam.permissions import has_course_access, is_course_session_expert
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
from vbv_lernwelt.media_files.graphql.types import ContentDocumentObjectType
class AssignmentCompletionObjectType(DjangoObjectType):
@ -52,6 +53,7 @@ class AssignmentObjectType(DjangoObjectType):
learning_content_page_id=graphene.ID(required=False),
assignment_user_id=graphene.UUID(required=False),
)
solution_sample = graphene.Field(ContentDocumentObjectType)
class Meta:
model = Assignment
@ -67,6 +69,9 @@ class AssignmentObjectType(DjangoObjectType):
"competence_certificate",
)
def resolve_solution_sample(self, info):
return self.solution_sample
def resolve_max_points(self, info):
return self.get_max_points()

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):
PRAXIS_ASSIGNMENT = "PRAXIS_ASSIGNMENT" # Praxisauftrag
CASEWORK = "CASEWORK" # Geleitete Fallarbeit
PREP_ASSIGNMENT = "PREP_ASSIGNMENT" # Vorbereitungsauftrag
REFLECTION = "REFLECTION" # Reflexion
@ -143,7 +144,7 @@ class Assignment(CourseBasePage):
needs_expert_evaluation = models.BooleanField(
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(
@ -199,12 +200,22 @@ class Assignment(CourseBasePage):
help_text="Beurteilungsschritte",
)
solution_sample = models.ForeignKey(
"media_files.ContentDocument",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
help_text="Musterlösung",
)
content_panels = Page.content_panels + [
FieldPanel("assignment_type"),
FieldPanel("needs_expert_evaluation"),
PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"),
FieldPanel("intro_text"),
FieldPanel("effort_required"),
FieldPanel("solution_sample"),
FieldPanel("performance_objectives"),
FieldPanel("tasks"),
FieldPanel("evaluation_description"),

View File

@ -144,7 +144,7 @@ def update_assignment_completion(
ac.evaluation_points = evaluation_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 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

View File

@ -106,14 +106,14 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
)
task_data = data["task_completion_data"][task_id]
self.assertDictEqual(
task_data,
{
"user_data": {
"fileId": file_id,
"fileInfo": {"id": file_id, "name": "file.txt", "url": file_url},
}
},
self.maxDiff = None
self.assertEqual(task_data["user_data"]["fileId"], file_id)
self.assertEqual(task_data["user_data"]["fileInfo"]["id"], file_id)
self.assertEqual(task_data["user_data"]["fileInfo"]["name"], "file.txt")
self.assertTrue(
task_data["user_data"]["fileInfo"]["url"].startswith(
"https://s3.eu-central-1.amazonaws.com/myvbv-dev.iterativ.ch"
)
)
# check DB data
@ -194,31 +194,31 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
# check notification
self.assertEqual(Notification.objects.count(), 1)
notification = Notification.objects.first()
self.assertEquals(
self.assertEqual(
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben.",
notification.verb,
)
self.assertEquals(
self.assertEqual(
"test-trainer1@example.com",
notification.recipient.email,
)
self.assertEquals(
self.assertEqual(
"test-student1@example.com",
notification.actor.email,
)
self.assertEquals(
self.assertEqual(
"USER_INTERACTION",
notification.notification_category,
)
self.assertEquals(
self.assertEqual(
"CASEWORK_SUBMITTED",
notification.notification_trigger,
)
self.assertEquals(
self.assertEqual(
notification.action_object,
db_entry,
)
self.assertEquals(
self.assertEqual(
notification.course_session,
self.course_session,
)
@ -422,35 +422,35 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
# check notification
self.assertEqual(Notification.objects.count(), 1)
notification = Notification.objects.first()
self.assertEquals(
self.assertEqual(
"Test Trainer1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» bewertet.",
notification.verb,
)
self.assertEquals(
self.assertEqual(
"test-student1@example.com",
notification.recipient.email,
)
self.assertEquals(
self.assertEqual(
"test-trainer1@example.com",
notification.actor.email,
)
self.assertEquals(
self.assertEqual(
"USER_INTERACTION",
notification.notification_category,
)
self.assertEquals(
self.assertEqual(
"CASEWORK_EVALUATED",
notification.notification_trigger,
)
self.assertEquals(
self.assertEqual(
notification.action_object,
db_entry,
)
self.assertEquals(
self.assertEqual(
notification.course_session,
self.course_session,
)
self.assertEquals(
self.assertEqual(
notification.target_url,
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice",
)

View File

@ -13,6 +13,7 @@ from vbv_lernwelt.core.constants import (
TEST_TRAINER1_USER_ID,
)
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_edoniq_test_result_data,
create_feedback_response_data,
@ -20,6 +21,7 @@ from vbv_lernwelt.course.creators.test_course import (
create_test_assignment_submitted_data,
)
from vbv_lernwelt.course.models import (
Course,
CourseCompletion,
CourseCompletionStatus,
CourseSession,
@ -30,7 +32,8 @@ from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
LearningContentFeedback,
LearningContentFeedbackUK,
LearningContentFeedbackVV,
)
from vbv_lernwelt.notify.models import Notification
@ -73,6 +76,11 @@ from vbv_lernwelt.notify.models import Notification
default=False,
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(
create_assignment_completion,
create_assignment_evaluation,
@ -81,6 +89,7 @@ def command(
create_feedback_responses,
create_course_completion_performance_criteria,
create_attendance_days,
enable_circle_documents,
):
print("cypress reset data")
CourseCompletion.objects.all().delete()
@ -101,6 +110,13 @@ def command(
course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_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 not assignment_evaluation_scores:
assignment_evaluation_scores = [6, 6, 6, 3, 3]
@ -140,7 +156,9 @@ def command(
if create_feedback_responses:
print("create_feedback_responses")
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
learning_content_feedback_page = LearningContentFeedback.objects.get(
# feedback fahrzeug
learning_content_feedback_page = LearningContentFeedbackUK.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback"
)
create_feedback_response_data(
@ -159,6 +177,7 @@ def command(
"would_recommend": True,
"course_negative_feedback": "Nichts Schlechtes",
"course_positive_feedback": "Nur Gutes.",
"feedback_type": "uk",
},
)
@ -178,6 +197,7 @@ def command(
"would_recommend": True,
"course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
"course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
"feedback_type": "uk",
},
)
@ -197,6 +217,62 @@ def command(
"would_recommend": False,
"course_negative_feedback": "Mehr Videos wären schön.",
"course_positive_feedback": "Die Präsentation war super",
"feedback_type": "uk",
},
)
# feedback reisen
learning_content_feedback_page = LearningContentFeedbackVV.objects.get(
slug="test-lehrgang-lp-circle-reisen-lc-feedback"
)
create_feedback_response_data(
feedback_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
course_session=course_session,
learning_content_feedback_page=learning_content_feedback_page,
submitted=True,
feedback_data={
"satisfaction": 4,
"goal_attainment": 3,
"proficiency": 80,
"preparation_task_clarity": True,
"would_recommend": True,
"course_negative_feedback": "Nichts Schlechtes",
"course_positive_feedback": "Nur Gutes.",
"feedback_type": "vv",
},
)
create_feedback_response_data(
feedback_user=User.objects.get(id=TEST_STUDENT2_USER_ID),
course_session=course_session,
learning_content_feedback_page=learning_content_feedback_page,
submitted=True,
feedback_data={
"satisfaction": 4,
"goal_attainment": 4,
"proficiency": 100,
"preparation_task_clarity": True,
"would_recommend": True,
"course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
"course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
"feedback_type": "vv",
},
)
create_feedback_response_data(
feedback_user=User.objects.get(id=TEST_STUDENT3_USER_ID),
course_session=course_session,
learning_content_feedback_page=learning_content_feedback_page,
submitted=True,
feedback_data={
"satisfaction": 2,
"goal_attainment": 2,
"proficiency": 40,
"preparation_task_clarity": True,
"would_recommend": False,
"course_negative_feedback": "Mehr Videos wären schön.",
"course_positive_feedback": "Die Präsentation war super",
"feedback_type": "vv",
},
)
@ -243,3 +319,7 @@ def command(
datetime(year=2000, month=10, day=31, hour=11)
)
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_prep_assignment,
create_uk_reflection,
create_vv_gewinnen_casework,
)
from vbv_lernwelt.assignment.models import (
Assignment,
@ -69,7 +70,8 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningContentAssignmentFactory,
LearningContentAttendanceCourseFactory,
LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory,
LearningContentFeedbackUKFactory,
LearningContentFeedbackVVFactory,
LearningContentKnowledgeAssessmentFactory,
LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory,
@ -81,6 +83,12 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningUnitFactory,
TopicFactory,
)
from vbv_lernwelt.media_files.create_default_documents import (
create_default_collections,
create_default_content_documents,
)
from vbv_lernwelt.media_files.create_default_images import create_default_images
from vbv_lernwelt.media_files.models import ContentDocument, ContentImage, UserImage
from vbv_lernwelt.media_library.tests.media_library_factories import (
MediaLibraryCategoryPageFactory,
MediaLibraryContentPageFactory,
@ -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):
# create_locales_for_wagtail()
create_default_collections()
create_default_content_documents()
if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
create_default_images()
course = create_test_course_with_categories()
competence_certificate = create_test_competence_navi()
# 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:
# 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(
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,
)
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_media_library()
@ -190,6 +208,14 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
)
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(
course_id=COURSE_TEST_ID,
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,
)
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")
csu = CourseSessionUser.objects.create(
@ -344,6 +372,7 @@ def create_feedback_response_data(
"would_recommend": True,
"course_negative_feedback": "Nichts Schlechtes",
"course_positive_feedback": "Nur Gutes.",
"feedback_type": "uk",
}
return update_feedback_response(
@ -507,6 +536,14 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
slug__startswith=f"test-lehrgang-assignment-reflexion"
),
),
assignment = Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
)
assignment.solution_sample = ContentDocument.objects.get(
title="Musterlösung Fahrzeug"
)
assignment.save()
LearningContentAssignmentFactory(
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
parent=circle,
@ -514,7 +551,8 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
),
)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
title="Feedback",
parent=circle,
)
@ -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",
)
LearningContentAssignmentFactory(
title="Mein Kundenstamm",
assignment_type="PRAXIS_ASSIGNMENT",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-mein-kundenstamm"
),
),
PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="Y1"),
competence_id=f"Y1.1",
@ -589,7 +636,8 @@ def create_test_circle_reisen(lp):
title="Reflexion",
parent=parent,
)
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
title="Feedback",
parent=parent,
)

View File

@ -22,7 +22,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningContentAttendanceCourseFactory,
LearningContentDocumentListFactory,
LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory,
LearningContentFeedbackUKFactory,
LearningContentMediaLibraryFactory,
LearningContentPlaceholderFactory,
LearningPathFactory,
@ -30,6 +30,11 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
LearningUnitFactory,
TopicFactory,
)
from vbv_lernwelt.media_files.create_default_documents import (
create_default_collections,
create_default_content_documents,
)
from vbv_lernwelt.media_files.create_default_images import create_default_images
from vbv_lernwelt.media_library.tests.media_library_factories import (
LearnMediaBlockFactory,
)
@ -40,6 +45,7 @@ def create_uk_learning_path(course_id=COURSE_UK, user=None, skip_locales=True):
user = User.objects.get(username="info@iterativ.ch")
course_page = CoursePage.objects.get(course_id=course_id)
lp = LearningPathFactory(
title="Lernpfad",
parent=course_page,
@ -254,7 +260,7 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
title="Unterlagen für den Unterricht",
parent=circle,
)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@ -364,7 +370,7 @@ In diesem Circle erfährst du wie die überbetrieblichen Kurse aufgebaut sind. Z
# test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
# )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@ -479,7 +485,7 @@ Dans ce cercle, tu apprendras comment les cours interentreprises sont structuré
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end")
@ -594,7 +600,7 @@ In questo Circle imparerai come sono strutturati i corsi interaziendali. Imparer
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end")
@ -699,7 +705,7 @@ In diesem Circle lernst du die wichtigsten Grundlagen bezüglich Versicherungswi
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
@ -809,7 +815,7 @@ Dans ce cercle, tu apprends les bases les plus importantes en matière d'assuran
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Transfert", parent=circle, icon="it-icon-ls-end")
@ -918,7 +924,7 @@ In questo Circle imparerai le basi più importanti del settore assicurativo e de
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
LearningSequenceFactory(title="Trasferimento", parent=circle, icon="it-icon-ls-end")
@ -1058,7 +1064,7 @@ def create_uk_circle_fahrzeug(lp, title="Fahrzeug"):
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
@ -1192,7 +1198,7 @@ def create_uk_fr_circle_fahrzeug(lp, title="Véhicule"):
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)
@ -1330,7 +1336,7 @@ def create_uk_it_circle_fahrzeug(lp, title="Veicolo"):
],
)
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
LearningContentFeedbackUKFactory(
parent=circle,
)

View File

@ -10,7 +10,8 @@ from vbv_lernwelt.learnpath.graphql.types import (
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType,
LearningContentFeedbackUKObjectType,
LearningContentFeedbackVVObjectType,
LearningContentKnowledgeAssessmentObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
@ -50,7 +51,8 @@ class CourseQuery(graphene.ObjectType):
learning_content_attendance_course = graphene.Field(
LearningContentAttendanceCourseObjectType
)
learning_content_feedback = graphene.Field(LearningContentFeedbackObjectType)
learning_content_feedback_uk = graphene.Field(LearningContentFeedbackUKObjectType)
learning_content_feedback_vv = graphene.Field(LearningContentFeedbackVVObjectType)
learning_content_learning_module = graphene.Field(
LearningContentLearningModuleObjectType
)

View File

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

View File

@ -100,6 +100,12 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAssignment,
LearningContentAttendanceCourse,
)
from vbv_lernwelt.media_files.create_default_documents import (
create_default_collections,
create_default_content_documents,
create_default_user_documents,
)
from vbv_lernwelt.media_files.create_default_images import create_default_images
from vbv_lernwelt.media_library.create_default_media_library import (
create_default_media_library,
)
@ -128,6 +134,11 @@ ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
def command(course):
print("Creating default courses", course)
create_default_collections()
create_default_content_documents()
create_default_user_documents()
create_default_images()
if COURSE_VERSICHERUNGSVERMITTLERIN_ID in course:
create_versicherungsvermittlerin_course()
@ -285,6 +296,9 @@ def create_course_uk_de(course_id=COURSE_UK, lang="de"):
create_uk_competence_profile(course_id=course_id)
create_default_media_library(course_id=course_id)
create_default_collections()
create_default_content_documents()
def create_course_uk_de_course_sessions():
course = Course.objects.get(id=COURSE_UK)

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"), 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):
return f"/course/{self.slug}"

View File

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

View File

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

View File

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

View File

@ -148,8 +148,8 @@ class DashboardQuery(graphene.ObjectType):
assignment=ProgressDashboardAssignmentType( # noqa
_id=course_id, # noqa
total_count=len(evaluation_results), # noqa
points_max_count=points_max_count, # noqa
points_achieved_count=points_achieved_count, # noqa
points_max_count=int(points_max_count), # noqa
points_achieved_count=int(points_achieved_count), # noqa
),
)

View File

@ -1,6 +1,11 @@
from django.contrib import admin
from wagtail.models import Page
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.learnpath.models import (
Circle,
@ -9,7 +14,16 @@ from vbv_lernwelt.learnpath.models import (
)
# Register your models here.
@admin.action(description="Re-sync URLs from LearningContent")
def sync_wagtail_due_date_url(modeladmin, request, queryset):
for assignment in CourseSessionAssignment.objects.all():
assignment.save()
for edoniq_test in CourseSessionEdoniqTest.objects.all():
edoniq_test.save()
for attendance in CourseSessionAttendanceCourse.objects.all():
attendance.save()
@admin.register(DueDate)
class DueDateAdmin(admin.ModelAdmin):
date_hierarchy = "start"
@ -23,6 +37,7 @@ class DueDateAdmin(admin.ModelAdmin):
]
list_filter = ["course_session__course", "course_session"]
readonly_fields = ["course_session", "page"]
actions = [sync_wagtail_due_date_url]
def get_readonly_fields(self, request, obj=None):
default_readonly = super(DueDateAdmin, self).get_readonly_fields(request, obj)

View File

@ -34,6 +34,7 @@ class FeedbackResponseFactory(DjangoModelFactory):
"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 (
FeedbackResponseObjectType as FeedbackResponseType,
)
from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer
from vbv_lernwelt.feedback.serializers import (
CourseFeedbackSerializerUK,
CourseFeedbackSerializerVV,
)
from vbv_lernwelt.feedback.services import update_feedback_response
from vbv_lernwelt.iam.permissions import has_course_session_access
from vbv_lernwelt.learnpath.models import LearningContentFeedback
from vbv_lernwelt.learnpath.models import (
LearningContentFeedbackUK,
LearningContentFeedbackVV,
)
logger = structlog.get_logger(__name__)
@ -25,6 +31,7 @@ class SendFeedbackMutation(graphene.Mutation):
class Arguments:
course_session_id = graphene.ID(required=True)
learning_content_page_id = graphene.ID(required=True)
learning_content_type = graphene.String(required=True)
data = GenericScalar()
submitted = graphene.Boolean(required=False, default_value=False)
@ -35,11 +42,29 @@ class SendFeedbackMutation(graphene.Mutation):
info,
course_session_id,
learning_content_page_id,
learning_content_type,
data,
submitted,
):
feedback_user_id = info.context.user.id
learning_content = LearningContentFeedback.objects.get(
if learning_content_type == "learnpath.LearningContentFeedbackVV":
learningContentFeedbackModel = LearningContentFeedbackVV
serializerClass = CourseFeedbackSerializerVV
data["feedback_type"] = "vv"
elif learning_content_type == "learnpath.LearningContentFeedbackUK":
learningContentFeedbackModel = LearningContentFeedbackUK
serializerClass = CourseFeedbackSerializerUK
data["feedback_type"] = "uk"
else:
errors = [
ErrorType(
field="learningContentType", messages="Invalid learningContentType"
)
]
return SendFeedbackMutation(errors=errors)
learning_content = learningContentFeedbackModel.objects.get(
id=learning_content_page_id
)
circle = learning_content.get_circle()
@ -65,7 +90,7 @@ class SendFeedbackMutation(graphene.Mutation):
course_session_id=course_session_id,
)
serializer = CourseFeedbackSerializer(data=data)
serializer = serializerClass(data=data)
if not serializer.is_valid():
logger.error(

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__)
FEEDBACK_TYPES = (
("uk", "Feedback UK"),
("vv", "Feedback VV"),
)
class FeedbackIntegerField(serializers.IntegerField):
def __init__(self, **kwargs):
@ -13,7 +18,8 @@ class FeedbackIntegerField(serializers.IntegerField):
)
class CourseFeedbackSerializer(serializers.Serializer):
class CourseFeedbackSerializerUK(serializers.Serializer):
feedback_type = serializers.ChoiceField(choices=FEEDBACK_TYPES)
satisfaction = FeedbackIntegerField()
goal_attainment = FeedbackIntegerField()
proficiency = serializers.IntegerField(required=False, allow_null=True)
@ -33,6 +39,22 @@ class CourseFeedbackSerializer(serializers.Serializer):
)
class CourseFeedbackSerializerVV(serializers.Serializer):
feedback_type = serializers.ChoiceField(choices=FEEDBACK_TYPES)
satisfaction = FeedbackIntegerField()
goal_attainment = FeedbackIntegerField()
proficiency = serializers.IntegerField(required=False, allow_null=True)
preparation_task_clarity = serializers.BooleanField(required=False, allow_null=True)
materials_rating = FeedbackIntegerField()
would_recommend = serializers.BooleanField(required=False, allow_null=True)
course_positive_feedback = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
course_negative_feedback = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
class CypressFeedbackResponseSerializer(serializers.ModelSerializer):
class Meta:
model = FeedbackResponse

View File

@ -1,10 +1,15 @@
from typing import Union
import structlog
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import LearningContentFeedback
from vbv_lernwelt.learnpath.models import (
LearningContentFeedbackUK,
LearningContentFeedbackVV,
)
logger = structlog.get_logger(__name__)
@ -12,7 +17,9 @@ logger = structlog.get_logger(__name__)
def update_feedback_response(
feedback_user: User,
course_session: CourseSession,
learning_content_feedback_page: LearningContentFeedback,
learning_content_feedback_page: Union[
LearningContentFeedbackUK, LearningContentFeedbackVV
],
submitted: bool,
validated_data: dict,
):
@ -26,18 +33,7 @@ def update_feedback_response(
original_data = feedback_response.data
updated_data = validated_data
initial_data = {
"satisfaction": None,
"goal_attainment": None,
"proficiency": None,
"preparation_task_clarity": None,
"instructor_competence": None,
"instructor_respect": None,
"instructor_open_feedback": "",
"would_recommend": None,
"course_negative_feedback": "",
"course_positive_feedback": "",
}
initial_data = initial_data_for_feedback_page(learning_content_feedback_page)
merged_data = initial_data | {
key: updated_data[key]
@ -71,3 +67,36 @@ def update_feedback_response(
)
return feedback_response
def initial_data_for_feedback_page(
learning_content_feedback_page: Union[
LearningContentFeedbackUK, LearningContentFeedbackVV
]
):
if hasattr(learning_content_feedback_page, "learningcontentfeedbackuk"):
return {
"satisfaction": None,
"goal_attainment": None,
"proficiency": None,
"preparation_task_clarity": None,
"instructor_competence": None,
"instructor_respect": None,
"instructor_open_feedback": "",
"would_recommend": None,
"course_negative_feedback": "",
"course_positive_feedback": "",
"feedback_type": "uk",
}
if hasattr(learning_content_feedback_page, "learningcontentfeedbackvv"):
return {
"satisfaction": None,
"goal_attainment": None,
"proficiency": None,
"preparation_task_clarity": None,
"would_recommend": None,
"course_negative_feedback": "",
"course_positive_feedback": "",
"feedback_type": "vv",
}
return {}

View File

@ -114,6 +114,7 @@ class FeedbackRestApiTestCase(FeedbackBaseTestCase):
"course_negative_feedback": self.feedback_data[
"course_negative_feedback"
][i],
"feedback_type": "uk",
},
feedback_user=self.feedback_users[i],
submitted=True,
@ -129,6 +130,7 @@ class FeedbackRestApiTestCase(FeedbackBaseTestCase):
expected = {
"amount": 3,
"questions": self.feedback_data,
"feedbackType": "uk",
}
print(response.data)

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),
).order_by("created_at")
# I guess this is ok for the üK case
feedback_data = {"amount": len(feedbacks), "questions": {}}
feedback_data = {"amount": len(feedbacks), "questions": {}, "feedbackType": None}
if feedback_data["amount"] == 0:
return Response(status=200, data=feedback_data)
feedback_data["feedbackType"] = feedbacks[0].data.get("feedback_type", None)
for field in FEEDBACK_FIELDS:
feedback_data["questions"][field] = []

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 (
CircleFactory,
LearningContentAssignmentFactory,
LearningContentFeedbackFactory,
LearningContentFeedbackVVFactory,
LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory,
LearningContentPlaceholderFactory,
@ -201,7 +201,7 @@ def create_circle_basis(lp, title="Basis"):
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -278,7 +278,7 @@ def create_circle_gewinnen(lp, title="Gewinnen"):
slug__startswith=f"{course_slug}-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -368,7 +368,7 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
# slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
# ),
# ),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -554,7 +554,7 @@ def create_circle_reisen(lp, title="Reisen"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -647,7 +647,7 @@ def create_circle_einkommenssicherung(lp, title="Einkommenssicherung"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -700,7 +700,7 @@ def create_circle_wohneigentum(lp, title="Wohneigentum"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -782,7 +782,7 @@ def create_circle_pensionierung(lp, title="Pensionierung"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -839,7 +839,7 @@ def create_circle_erben(lp, title="Erben/Vererben"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -929,7 +929,7 @@ def create_circle_gesundheit(lp, title="Gesundheit"):
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=circle,
)
@ -1352,7 +1352,7 @@ def create_learning_sequence_transfer(parent, title, lc_praxis_title=None):
slug__startswith=f"versicherungsvermittler-in-assignment-reflexion"
),
),
LearningContentFeedbackFactory(
LearningContentFeedbackVVFactory(
parent=parent,
)

View File

@ -10,7 +10,8 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
LearningContentDocumentList,
LearningContentEdoniqTest,
LearningContentFeedback,
LearningContentFeedbackUK,
LearningContentFeedbackVV,
LearningContentKnowledgeAssessment,
LearningContentLearningModule,
LearningContentMediaLibrary,
@ -49,8 +50,10 @@ class LearningContentInterface(CoursePageInterface):
return LearningContentAssignmentObjectType
elif isinstance(instance, LearningContentAttendanceCourse):
return LearningContentAttendanceCourseObjectType
elif isinstance(instance, LearningContentFeedback):
return LearningContentFeedbackObjectType
elif isinstance(instance, LearningContentFeedbackUK):
return LearningContentFeedbackUKObjectType
elif isinstance(instance, LearningContentFeedbackVV):
return LearningContentFeedbackVVObjectType
elif isinstance(instance, LearningContentLearningModule):
return LearningContentLearningModuleObjectType
elif isinstance(instance, LearningContentKnowledgeAssessment):
@ -105,9 +108,19 @@ class LearningContentPlaceholderObjectType(DjangoObjectType):
fields = []
class LearningContentFeedbackObjectType(DjangoObjectType):
class LearningContentFeedbackUKObjectType(DjangoObjectType):
class Meta:
model = LearningContentFeedback
model = LearningContentFeedbackUK
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningContentFeedbackVVObjectType(DjangoObjectType):
class Meta:
model = LearningContentFeedbackVV
interfaces = (
CoursePageInterface,
LearningContentInterface,

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.LearningContentAssignment",
"learnpath.LearningContentAttendanceCourse",
"learnpath.LearningContentFeedback",
"learnpath.LearningContentFeedbackUK",
"learnpath.LearningContentFeedbackVV",
"learnpath.LearningContentLearningModule",
"learnpath.LearningContentKnowledgeAssessment",
"learnpath.LearningContentMediaLibrary",
@ -318,7 +319,13 @@ class LearningContentPlaceholder(LearningContent):
can_user_self_toggle_course_completion = models.BooleanField(default=True)
class LearningContentFeedback(LearningContent):
class LearningContentFeedbackUK(LearningContent):
parent_page_types = ["learnpath.Circle"]
subpage_types = []
can_user_self_toggle_course_completion = models.BooleanField(default=False)
class LearningContentFeedbackVV(LearningContent):
parent_page_types = ["learnpath.Circle"]
subpage_types = []
can_user_self_toggle_course_completion = models.BooleanField(default=False)
@ -441,6 +448,9 @@ class LearningContentAssignment(LearningContent):
self.assignment_type = self.content_assignment.assignment_type
super().save(**kwargs)
def __str__(self):
return f"{self.id} - {self.title}"
@classmethod
def get_serializer_class(cls):
from vbv_lernwelt.learnpath.serializers import (

View File

@ -7,7 +7,8 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
LearningContentDocumentList,
LearningContentEdoniqTest,
LearningContentFeedback,
LearningContentFeedbackUK,
LearningContentFeedbackVV,
LearningContentKnowledgeAssessment,
LearningContentLearningModule,
LearningContentMediaLibrary,
@ -120,14 +121,24 @@ class LearningContentPlaceholderFactory(wagtail_factories.PageFactory):
model = LearningContentPlaceholder
class LearningContentFeedbackFactory(wagtail_factories.PageFactory):
title = "Feedback"
class LearningContentFeedbackVVFactory(wagtail_factories.PageFactory):
title = "FeedbackVV"
minutes = 0
content_url = ""
description = RichText("")
class Meta:
model = LearningContentFeedback
model = LearningContentFeedbackVV
class LearningContentFeedbackUKFactory(wagtail_factories.PageFactory):
title = "FeedbackUK"
minutes = 0
content_url = ""
description = RichText("")
class Meta:
model = LearningContentFeedbackUK
class LearningContentLearningModuleFactory(wagtail_factories.PageFactory):

View File

@ -28,7 +28,7 @@ class TestRetrieveLearingPathContents(APITestCase):
# topics and circles
self.assertEqual(4, len(data["children"]))
# 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):
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