Merge branch 'develop' into feature/VBV-597-umsetzung-cockpit-lernbegleitung
This commit is contained in:
commit
e5ad3f08d2
|
|
@ -87,10 +87,14 @@ def main(app_name, image_name, environment_file):
|
|||
"IT_DJANGO_SECRET_KEY": env.str(
|
||||
"IT_DJANGO_SECRET_KEY", 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",
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ const documents = {
|
|||
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
|
||||
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
|
||||
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
|
||||
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
||||
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
|
||||
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
|
||||
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
|
||||
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
|
||||
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -60,7 +60,7 @@ export function graphql(source: "\n query attendanceCheckQuery($courseSessionId
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"): (typeof documents)["\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"];
|
||||
export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"): (typeof documents)["\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
@ -88,7 +88,7 @@ export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -9,7 +9,8 @@ type Query {
|
|||
learning_content_media_library: LearningContentMediaLibraryObjectType
|
||||
learning_content_assignment: LearningContentAssignmentObjectType
|
||||
learning_content_attendance_course: LearningContentAttendanceCourseObjectType
|
||||
learning_content_feedback: LearningContentFeedbackObjectType
|
||||
learning_content_feedback_uk: LearningContentFeedbackUKObjectType
|
||||
learning_content_feedback_vv: LearningContentFeedbackVVObjectType
|
||||
learning_content_learning_module: LearningContentLearningModuleObjectType
|
||||
learning_content_knowledge_assessment: LearningContentKnowledgeAssessmentObjectType
|
||||
learning_content_placeholder: LearningContentPlaceholderObjectType
|
||||
|
|
@ -486,6 +487,7 @@ type AssignmentObjectType implements CoursePageInterface {
|
|||
max_points: Int
|
||||
learning_content: LearningContentInterface
|
||||
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
|
||||
solution_sample: ContentDocumentObjectType
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
|
|
@ -605,6 +607,15 @@ schema (one of the key benefits of GraphQL).
|
|||
"""
|
||||
scalar JSONString
|
||||
|
||||
type ContentDocumentObjectType {
|
||||
id: ID!
|
||||
display_text: String!
|
||||
description: String!
|
||||
link_display_text: String!
|
||||
thumbnail: String!
|
||||
url: String
|
||||
}
|
||||
|
||||
"""An enumeration."""
|
||||
enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
|
||||
"""PRAXIS_ASSIGNMENT"""
|
||||
|
|
@ -708,7 +719,23 @@ type LearningContentMediaLibraryObjectType implements CoursePageInterface & Lear
|
|||
circle: CircleLightObjectType
|
||||
}
|
||||
|
||||
type LearningContentFeedbackObjectType implements CoursePageInterface & LearningContentInterface {
|
||||
type LearningContentFeedbackUKObjectType implements CoursePageInterface & LearningContentInterface {
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
content_type: String!
|
||||
live: Boolean!
|
||||
translation_key: String!
|
||||
frontend_url: String!
|
||||
course: CourseObjectType
|
||||
minutes: Int
|
||||
description: String!
|
||||
content_url: String!
|
||||
can_user_self_toggle_course_completion: Boolean!
|
||||
circle: CircleLightObjectType
|
||||
}
|
||||
|
||||
type LearningContentFeedbackVVObjectType implements CoursePageInterface & LearningContentInterface {
|
||||
id: ID!
|
||||
title: String!
|
||||
slug: String!
|
||||
|
|
@ -834,7 +861,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
|
|||
}
|
||||
|
||||
type Mutation {
|
||||
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, submitted: Boolean = false): SendFeedbackMutation
|
||||
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
|
||||
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
|
||||
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
|
|||
tasks
|
||||
title
|
||||
translation_key
|
||||
solution_sample {
|
||||
id
|
||||
url
|
||||
}
|
||||
competence_certificate {
|
||||
...CoursePageFields
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
<FeedbackPageVV v-if="feedbackType === 'vv'" :feedback-data="feedbackData" />
|
||||
<FeedbackPageUK
|
||||
v-else-if="feedbackType === 'uk'"
|
||||
:feedback-data="feedbackData"
|
||||
/>
|
||||
<VerticalBarChart
|
||||
v-else-if="verticalChartKyes.includes(question.key)"
|
||||
class="mb-8 bg-white"
|
||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||
:ratings="feedbackData.questions[question.key]"
|
||||
:text="question.question"
|
||||
:ratio="0.2"
|
||||
/>
|
||||
<OpenFeedback
|
||||
v-else-if="
|
||||
openKeys.includes(question.key) && feedbackData.questions[question.key]
|
||||
"
|
||||
class="mb-8 bg-white"
|
||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||
:text="question.question"
|
||||
:answers="feedbackData.questions[question.key].filter((a: string) => a !== '')"
|
||||
></OpenFeedback>
|
||||
<HorizontalBarChart
|
||||
v-else-if="
|
||||
horizontalChartKeys.includes(question.key) &&
|
||||
feedbackData.questions[question.key]
|
||||
"
|
||||
class="mb-8 bg-white"
|
||||
:title="`${$t('feedback.questionTitle')} ${i}`"
|
||||
:text="question.question"
|
||||
:items="feedbackData.questions[question.key].map((a: string) => `${a}%`)"
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</main>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<FeedbackResults
|
||||
:ordered-questions="orderedQuestions"
|
||||
:feedback-data="feedbackData"
|
||||
:rating-keys="ratingKeys"
|
||||
:vertical-chart-keys="verticalChartKeys"
|
||||
:horizontal-chart-keys="horizontalChartKeys"
|
||||
:open-keys="openKeys"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FeedbackResults from "@/pages/cockpit/FeedbackResults.vue";
|
||||
import type { FeedbackData } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
|
||||
defineProps<{
|
||||
feedbackData: FeedbackData;
|
||||
}>();
|
||||
|
||||
log.debug("FeedbackPageUK created");
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const orderedQuestions = [
|
||||
{
|
||||
key: "satisfaction",
|
||||
question: t("feedback.satisfactionLabel"),
|
||||
},
|
||||
{
|
||||
key: "goal_attainment",
|
||||
question: t("feedback.goalAttainmentLabel"),
|
||||
},
|
||||
{
|
||||
key: "proficiency",
|
||||
question: t("feedback.proficiencyLabel"),
|
||||
},
|
||||
{
|
||||
key: "preparation_task_clarity",
|
||||
question: t("feedback.preparationTaskClarityLabel"),
|
||||
},
|
||||
{
|
||||
key: "instructor_competence",
|
||||
question: t("feedback.instructorCompetenceLabel"),
|
||||
},
|
||||
{
|
||||
key: "instructor_respect",
|
||||
question: t("feedback.instructorRespectLabel"),
|
||||
},
|
||||
{
|
||||
key: "instructor_open_feedback",
|
||||
question: t("feedback.instructorOpenFeedbackLabel"),
|
||||
},
|
||||
{
|
||||
key: "would_recommend",
|
||||
question: t("feedback.recommendLabel"),
|
||||
},
|
||||
{
|
||||
key: "course_negative_feedback",
|
||||
question: t("feedback.courseNegativeFeedbackLabel"),
|
||||
},
|
||||
{
|
||||
key: "course_positive_feedback",
|
||||
question: t("feedback.coursePositiveFeedbackLabel"),
|
||||
},
|
||||
];
|
||||
|
||||
const ratingKeys = [
|
||||
"satisfaction",
|
||||
"goal_attainment",
|
||||
"instructor_competence",
|
||||
"instructor_respect",
|
||||
];
|
||||
const verticalChartKeys = ["preparation_task_clarity", "would_recommend"];
|
||||
const horizontalChartKeys = ["proficiency"];
|
||||
const openKeys = [
|
||||
"course_negative_feedback",
|
||||
"course_positive_feedback",
|
||||
"instructor_open_feedback",
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<FeedbackResults
|
||||
:ordered-questions="orderedQuestions"
|
||||
:feedback-data="feedbackData"
|
||||
:rating-keys="ratingKeys"
|
||||
:vertical-chart-keys="verticalChartKeys"
|
||||
:horizontal-chart-keys="horizontalChartKeys"
|
||||
:open-keys="openKeys"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FeedbackResults from "@/pages/cockpit/FeedbackResults.vue";
|
||||
import type { FeedbackData } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
|
||||
defineProps<{
|
||||
feedbackData: FeedbackData;
|
||||
}>();
|
||||
|
||||
log.debug("FeedbackPageVV created");
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const orderedQuestions = [
|
||||
{
|
||||
key: "satisfaction",
|
||||
question: t("feedback.satisfactionLabel"),
|
||||
},
|
||||
{
|
||||
key: "goal_attainment",
|
||||
question: t("feedback.goalAttainmentLabel"),
|
||||
},
|
||||
{
|
||||
key: "proficiency",
|
||||
question: t("feedback.proficiencyLabelVV"),
|
||||
},
|
||||
{
|
||||
key: "preparation_task_clarity",
|
||||
question: t("feedback.praxisAssignmentClarity"),
|
||||
},
|
||||
{
|
||||
key: "would_recommend",
|
||||
question: t("feedback.recommendLabelVV"),
|
||||
},
|
||||
{
|
||||
key: "course_negative_feedback",
|
||||
question: t("feedback.courseNegativeFeedbackLabel"),
|
||||
},
|
||||
{
|
||||
key: "course_positive_feedback",
|
||||
question: t("feedback.coursePositiveFeedbackLabel"),
|
||||
},
|
||||
];
|
||||
|
||||
const ratingKeys = ["satisfaction", "goal_attainment"];
|
||||
const verticalChartKeys = ["preparation_task_clarity", "would_recommend"];
|
||||
const horizontalChartKeys = ["proficiency"];
|
||||
const openKeys = ["course_negative_feedback", "course_positive_feedback"];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<ol v-if="feedbackData.amount > 0">
|
||||
<li
|
||||
v-for="(question, i) in orderedQuestions"
|
||||
:key="i"
|
||||
:data-cy="`question-${i + 1}`"
|
||||
>
|
||||
<RatingScale
|
||||
v-if="ratingKeys.includes(question.key)"
|
||||
class="mb-8 bg-white"
|
||||
:ratings="feedbackData.questions[question.key]"
|
||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||
:text="question.question"
|
||||
/>
|
||||
<VerticalBarChart
|
||||
v-else-if="verticalChartKeys.includes(question.key)"
|
||||
class="mb-8 bg-white"
|
||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||
:ratings="feedbackData.questions[question.key]"
|
||||
:text="question.question"
|
||||
:ratio="0.2"
|
||||
/>
|
||||
<OpenFeedback
|
||||
v-else-if="
|
||||
openKeys.includes(question.key) && feedbackData.questions[question.key]
|
||||
"
|
||||
class="mb-8 bg-white"
|
||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||
:text="question.question"
|
||||
:answers="feedbackData.questions[question.key].filter((a: string) => a !== '')"
|
||||
></OpenFeedback>
|
||||
<HorizontalBarChart
|
||||
v-else-if="
|
||||
horizontalChartKeys.includes(question.key) &&
|
||||
feedbackData.questions[question.key]
|
||||
"
|
||||
class="mb-8 bg-white"
|
||||
:title="`${$t('feedback.questionTitle')} ${i + 1}`"
|
||||
:text="question.question"
|
||||
:items="feedbackData.questions[question.key].map((a: string) => `${a}%`)"
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
|
||||
import OpenFeedback from "@/components/ui/OpenFeedback.vue";
|
||||
import RatingScale from "@/components/ui/RatingScale.vue";
|
||||
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
|
||||
import type { FeedbackData } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
||||
interface Props {
|
||||
orderedQuestions?: {
|
||||
key: string;
|
||||
question: string;
|
||||
}[];
|
||||
feedbackData: FeedbackData;
|
||||
ratingKeys?: string[];
|
||||
verticalChartKeys?: string[];
|
||||
horizontalChartKeys?: string[];
|
||||
openKeys?: string[];
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
orderedQuestions: () => [],
|
||||
ratingKeys: () => [],
|
||||
verticalChartKeys: () => [],
|
||||
horizontalChartKeys: () => [],
|
||||
openKeys: () => [],
|
||||
});
|
||||
|
||||
log.debug("FeedbackBasePage created");
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -52,7 +52,8 @@ const submittables = computed(() => {
|
|||
const learningContents = circleFlatLearningContents(circle).filter(
|
||||
(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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -104,6 +104,14 @@ const onEditTask = (task: AssignmentTask) => {
|
|||
emit("editTask", task);
|
||||
};
|
||||
|
||||
const openSolutionSample = () => {
|
||||
const url = props.assignment.solution_sample?.url ?? "";
|
||||
|
||||
if (props.assignment.solution_sample) {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
|
|
@ -119,14 +127,17 @@ const onSubmit = async () => {
|
|||
bustItGetCache(
|
||||
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
|
||||
);
|
||||
// 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>
|
||||
|
|
@ -202,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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
/>
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
<script setup lang="ts">
|
||||
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||
import {
|
||||
PERCENTAGES,
|
||||
RATINGS,
|
||||
YES_NO,
|
||||
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
||||
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
|
||||
import { computed } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
|
||||
}>();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
if (props.content?.circle?.slug) {
|
||||
return courseSessionDetailResult.filterCircleExperts(props.content.circle.slug);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const stepLabels = [
|
||||
t("general.introduction"),
|
||||
t("feedback.satisfactionLabel"),
|
||||
t("feedback.goalAttainmentLabel"),
|
||||
t("feedback.proficiencyLabel"),
|
||||
t("feedback.preparationTaskClarityLabel"),
|
||||
t("feedback.instructorCompetenceLabel"),
|
||||
t("feedback.instructorRespectLabel"),
|
||||
t("feedback.instructorOpenFeedbackLabel"),
|
||||
t("feedback.recommendLabel"),
|
||||
t("feedback.coursePositiveFeedbackLabel"),
|
||||
t("feedback.courseNegativeFeedbackLabel"),
|
||||
t("general.submission"),
|
||||
];
|
||||
|
||||
const questionData = [
|
||||
{
|
||||
modelKey: "satisfaction",
|
||||
items: RATINGS,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "goal_attainment",
|
||||
items: RATINGS,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "proficiency",
|
||||
items: PERCENTAGES,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "preparation_task_clarity",
|
||||
items: YES_NO,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "instructor_competence",
|
||||
items: RATINGS,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "instructor_respect",
|
||||
items: RATINGS,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "instructor_open_feedback",
|
||||
component: ItTextarea,
|
||||
},
|
||||
{
|
||||
modelKey: "would_recommend",
|
||||
items: YES_NO,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "course_positive_feedback",
|
||||
component: ItTextarea,
|
||||
},
|
||||
{
|
||||
modelKey: "course_negative_feedback",
|
||||
component: ItTextarea,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FeedbackBase
|
||||
:step-labels="stepLabels"
|
||||
:question-data="questionData"
|
||||
:content="props.content"
|
||||
:introduction="
|
||||
$t('feedback.intro', {
|
||||
name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
|
||||
})
|
||||
"
|
||||
:title="$t('feedback.areYouSatisfied')"
|
||||
:completion-title="
|
||||
$t('feedback.completionTitle', {
|
||||
name: `${circleExperts[0].first_name} ${circleExperts[0].last_name}`,
|
||||
})
|
||||
"
|
||||
:completion-description="$t('feedback.completionDescription')"
|
||||
:show-avatar="true"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<script setup lang="ts">
|
||||
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||
import {
|
||||
PERCENTAGES,
|
||||
RATINGS,
|
||||
YES_NO,
|
||||
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
||||
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } from "@/types";
|
||||
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
content: LearningContentFeedbackVV | LearningContentFeedbackUK;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const stepLabels = [
|
||||
t("general.introduction"),
|
||||
t("feedback.satisfactionLabel"),
|
||||
t("feedback.goalAttainmentLabel"),
|
||||
t("feedback.proficiencyLabelVV"),
|
||||
t("feedback.praxisAssignmentClarity"),
|
||||
t("feedback.recommendLabelVV"),
|
||||
t("feedback.coursePositiveFeedbackLabel"),
|
||||
t("feedback.courseNegativeFeedbackLabel"),
|
||||
t("general.submission"),
|
||||
];
|
||||
|
||||
const questionData = [
|
||||
{
|
||||
modelKey: "satisfaction",
|
||||
items: RATINGS,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "goal_attainment",
|
||||
items: RATINGS,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "proficiency",
|
||||
items: PERCENTAGES,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "preparation_task_clarity",
|
||||
items: YES_NO,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "would_recommend",
|
||||
items: YES_NO,
|
||||
component: ItRadioGroup,
|
||||
},
|
||||
{
|
||||
modelKey: "course_positive_feedback",
|
||||
component: ItTextarea,
|
||||
},
|
||||
{
|
||||
modelKey: "course_negative_feedback",
|
||||
component: ItTextarea,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FeedbackBase
|
||||
:step-labels="stepLabels"
|
||||
:question-data="questionData"
|
||||
:content="props.content"
|
||||
:introduction="$t('a.feedback.introductionVV')"
|
||||
:title="$t('Feedback')"
|
||||
:completion-title="$t('feedback.completionDescriptionVV')"
|
||||
:completion-description="$t('feedback.completionDescriptionVV')"
|
||||
:show-avatar="false"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -4,7 +4,11 @@
|
|||
<div
|
||||
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"]);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import type {
|
|||
LearningContentAttendanceCourseObjectType,
|
||||
LearningContentDocumentListObjectType,
|
||||
LearningContentEdoniqTestObjectType,
|
||||
LearningContentFeedbackObjectType,
|
||||
LearningContentFeedbackUkObjectType,
|
||||
LearningContentFeedbackVvObjectType,
|
||||
LearningContentKnowledgeAssessmentObjectType,
|
||||
LearningContentLearningModuleObjectType,
|
||||
LearningContentMediaLibraryObjectType,
|
||||
|
|
@ -68,8 +69,12 @@ export type LearningContentEdoniqTest = LearningContentEdoniqTestObjectType & {
|
|||
readonly content_type: "learnpath.LearningContentEdoniqTest";
|
||||
};
|
||||
|
||||
export type LearningContentFeedback = LearningContentFeedbackObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentFeedback";
|
||||
export type LearningContentFeedbackVV = LearningContentFeedbackVvObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentFeedbackVV";
|
||||
};
|
||||
|
||||
export type LearningContentFeedbackUK = LearningContentFeedbackUkObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentFeedbackUK";
|
||||
};
|
||||
|
||||
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
|
||||
|
|
@ -102,7 +107,8 @@ export type LearningContent =
|
|||
| LearningContentAttendanceCourse
|
||||
| LearningContentDocumentList
|
||||
| LearningContentEdoniqTest
|
||||
| LearningContentFeedback
|
||||
| LearningContentFeedbackUK
|
||||
| LearningContentFeedbackVV
|
||||
| LearningContentLearningModule
|
||||
| LearningContentKnowledgeAssessment
|
||||
| LearningContentMediaLibrary
|
||||
|
|
@ -560,3 +566,13 @@ export type DueDate = SimpleDueDate & {
|
|||
course_session_id: string;
|
||||
circle: CircleLight | null;
|
||||
};
|
||||
|
||||
export type FeedbackType = "uk" | "vv";
|
||||
|
||||
export interface FeedbackData {
|
||||
amount: number;
|
||||
questions: {
|
||||
[key: string]: any;
|
||||
};
|
||||
feedbackType: FeedbackType;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
||||
import { login } from "../helpers";
|
||||
|
||||
// Daniel: without this comment, my tool will reformat the login import out...
|
||||
|
||||
function completePraxisAssignment(selectExpert = false) {
|
||||
cy.visit("/course/test-lehrgang/learn/reisen/mein-kundenstamm");
|
||||
cy.learningContentMultiLayoutNextStep();
|
||||
|
|
@ -326,16 +324,23 @@ describe("assignmentStudent.cy.js", () => {
|
|||
cy.get('[data-cy="submit-assignment"]').click();
|
||||
cy.get('[data-cy="success-text"]').should("exist");
|
||||
|
||||
// app goes back to circle view -> check if assignment is marked as completed
|
||||
cy.url().should((url) => {
|
||||
expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
|
||||
cy.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.reload();
|
||||
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/");
|
||||
|
||||
cy.get(
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
|
||||
).should("have.class", "cy-checked");
|
||||
|
||||
// reopening page should get directly to last step
|
||||
//reopening page should get directly to last step
|
||||
cy.visit(
|
||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
||||
import { TEST_TRAINER1_USER_ID } from "../../consts";
|
||||
import { login } from "../helpers";
|
||||
|
||||
describe("assignmentTrainer.cy.js", () => {
|
||||
|
|
@ -85,8 +85,8 @@ describe("assignmentTrainer.cy.js", () => {
|
|||
cy.wait(500);
|
||||
// load AssignmentCompletion from DB and check
|
||||
cy.loadAssignmentCompletion(
|
||||
"assignment_user_id",
|
||||
TEST_STUDENT1_USER_ID
|
||||
"evaluation_user_id",
|
||||
TEST_TRAINER1_USER_ID
|
||||
).then((ac) => {
|
||||
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
|
||||
expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut");
|
||||
|
|
@ -187,8 +187,8 @@ describe("assignmentTrainer.cy.js", () => {
|
|||
|
||||
// load AssignmentCompletion from DB and check
|
||||
cy.loadAssignmentCompletion(
|
||||
"assignment_user_id",
|
||||
TEST_STUDENT1_USER_ID
|
||||
"evaluation_user_id",
|
||||
TEST_TRAINER1_USER_ID
|
||||
).then((ac) => {
|
||||
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
|
||||
expect(ac.evaluation_points).to.equal(17);
|
||||
|
|
@ -237,8 +237,8 @@ describe("assignmentTrainer.cy.js", () => {
|
|||
|
||||
// load AssignmentCompletion from DB and check
|
||||
cy.loadAssignmentCompletion(
|
||||
"assignment_user_id",
|
||||
TEST_STUDENT1_USER_ID
|
||||
"evaluation_user_id",
|
||||
TEST_TRAINER1_USER_ID
|
||||
).then((ac) => {
|
||||
console.log(ac.completion_status);
|
||||
expect(ac.completion_status).to.equal("EVALUATION_IN_PROGRESS");
|
||||
|
|
@ -323,8 +323,8 @@ describe("assignmentTrainer.cy.js", () => {
|
|||
|
||||
// load AssignmentCompletion from DB and check
|
||||
cy.loadAssignmentCompletion(
|
||||
"assignment_user_id",
|
||||
TEST_STUDENT1_USER_ID
|
||||
"evaluation_user_id",
|
||||
TEST_TRAINER1_USER_ID
|
||||
).then((ac) => {
|
||||
expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
|
||||
expect(ac.evaluation_max_points).to.equal(0);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
beforeEach(() => {
|
||||
cy.manageCommand("cypress_reset");
|
||||
login("test-student1@example.com", "test");
|
||||
});
|
||||
|
||||
describe("Feedback UK", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/course/test-lehrgang/learn/fahrzeug/feedback");
|
||||
});
|
||||
|
||||
|
|
@ -26,6 +30,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
// 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);
|
||||
|
|
@ -34,6 +42,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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(
|
||||
|
|
@ -50,6 +62,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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);
|
||||
|
|
@ -58,6 +74,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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);
|
||||
|
|
@ -66,6 +86,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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);
|
||||
|
|
@ -74,6 +98,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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);
|
||||
|
|
@ -82,6 +110,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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."
|
||||
|
|
@ -92,6 +124,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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);
|
||||
|
|
@ -100,6 +136,11 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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."
|
||||
|
|
@ -110,6 +151,10 @@ describe("feedbackStudent.cy.js", () => {
|
|||
|
||||
// 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."
|
||||
|
|
@ -141,17 +186,178 @@ describe("feedbackStudent.cy.js", () => {
|
|||
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.",
|
||||
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_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",
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Feedback VV", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/course/test-lehrgang/learn/reisen/feedback");
|
||||
});
|
||||
|
||||
it("can open feedback page", () => {
|
||||
cy.testLearningContentTitle("Feedback");
|
||||
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(/\/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."
|
||||
);
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ describe("feedbackTrainer.cy.js", () => {
|
|||
cy.get('[data-cy="feedback-data-amount"]').should("contain", "0");
|
||||
});
|
||||
|
||||
describe("FeedbackUK", function () {
|
||||
it("can open feedback results page with results", () => {
|
||||
cy.manageCommand("cypress_reset --create-feedback-responses");
|
||||
login("test-trainer1@example.com", "test");
|
||||
|
|
@ -26,6 +27,48 @@ describe("feedbackTrainer.cy.js", () => {
|
|||
|
||||
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 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-1"]')
|
||||
.find('[data-cy="rating-scale-average"]')
|
||||
.should("contain", "3.3");
|
||||
|
|
@ -89,4 +132,100 @@ describe("feedbackTrainer.cy.js", () => {
|
|||
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
|
||||
.should("contain", "Die Präsentation war super");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FeedbackVV", function () {
|
||||
it("can open feedback results page with results", () => {
|
||||
cy.manageCommand("cypress_reset --create-feedback-responses");
|
||||
login("test-trainer1@example.com", "test");
|
||||
cy.visit("/course/test-lehrgang/cockpit");
|
||||
cy.get('[data-cy="dropdown-select"]').click();
|
||||
cy.get('[data-cy="dropdown-select-option-Reisen"]').click();
|
||||
cy.get(
|
||||
'[data-cy="show-feedback-btn-test-lehrgang-lp-circle-reisen-lc-feedback"]'
|
||||
).click();
|
||||
|
||||
cy.get('[data-cy="feedback-data-amount"]').should("contain", "3");
|
||||
|
||||
// check titles of questions
|
||||
cy.get('[data-cy="question-1"]').should(
|
||||
"contain",
|
||||
"Zufriedenheit insgesamt"
|
||||
);
|
||||
cy.get('[data-cy="question-2"]').should(
|
||||
"contain",
|
||||
"Zielerreichung insgesamt"
|
||||
);
|
||||
cy.get('[data-cy="question-3"]').should(
|
||||
"contain",
|
||||
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?"
|
||||
);
|
||||
cy.get('[data-cy="question-4"]').should(
|
||||
"contain",
|
||||
"Waren die Praxisaufträge klar und verständlich?"
|
||||
);
|
||||
cy.get('[data-cy="question-5"]').should(
|
||||
"contain",
|
||||
"Würdest du den Circle weiterempfehlen?"
|
||||
);
|
||||
cy.get('[data-cy="question-6"]').should(
|
||||
"contain",
|
||||
"Wo siehst du Verbesserungspotential?"
|
||||
);
|
||||
cy.get('[data-cy="question-7"]').should(
|
||||
"contain",
|
||||
"Was hat dir besonders gut gefallen?"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="question-1"]')
|
||||
.find('[data-cy="rating-scale-average"]')
|
||||
.should("contain", "3.3");
|
||||
|
||||
cy.get('[data-cy="question-2"]')
|
||||
.find('[data-cy="rating-scale-average"]')
|
||||
.should("contain", "3.0");
|
||||
|
||||
cy.get('[data-cy="question-3"]')
|
||||
.find('[data-cy="percentage-value-40%"]')
|
||||
.should("contain", "33.3");
|
||||
cy.get('[data-cy="question-3"]')
|
||||
.find('[data-cy="percentage-value-80%"]')
|
||||
.should("contain", "33.3");
|
||||
cy.get('[data-cy="question-3"]')
|
||||
.find('[data-cy="percentage-value-100%"]')
|
||||
.should("contain", "33.3");
|
||||
|
||||
cy.get('[data-cy="question-4"]')
|
||||
.find('[data-cy="popover-yes"]')
|
||||
.click()
|
||||
.find('[data-cy="num-yes"]')
|
||||
.should("contain", "3");
|
||||
cy.get('[data-cy="question-4"]')
|
||||
.find('[data-cy="popover-no"]')
|
||||
.click()
|
||||
.find('[data-cy="num-no"]')
|
||||
.should("contain", "0");
|
||||
|
||||
cy.get('[data-cy="question-5"]')
|
||||
.find('[data-cy="popover-yes"]')
|
||||
.click()
|
||||
.find('[data-cy="num-yes"]')
|
||||
.should("contain", "2");
|
||||
cy.get('[data-cy="question-5"]')
|
||||
.find('[data-cy="popover-no"]')
|
||||
.click()
|
||||
.find('[data-cy="num-no"]')
|
||||
.should("contain", "1");
|
||||
|
||||
cy.get('[data-cy="question-6"]')
|
||||
.should("contain", "Nichts Schlechtes")
|
||||
.should("contain", "Es wäre praktisch, Zugang zu einer FAQ zu haben.")
|
||||
.should("contain", "Mehr Videos wären schön.");
|
||||
|
||||
cy.get('[data-cy="question-7"]')
|
||||
.should("contain", "Nur Gutes.")
|
||||
.should("contain", "Das Beispiel mit der Katze fand ich sehr gut")
|
||||
.should("contain", "Die Präsentation war super");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
|
|||
"/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
||||
"/Users/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 |
|
|
@ -0,0 +1,73 @@
|
|||
# Files handling
|
||||
|
||||
This document describes how files are handled in this appication.
|
||||
|
||||
# Types of files
|
||||
|
||||
static files: files that are not changed by the application, e.g. images, fonts, etc.¨
|
||||
|
||||
### content documents:
|
||||
|
||||
Files that belong to the content and are managed by the content editors in the CMS (pdf, excel, word, etc.)
|
||||
|
||||
### user documents:
|
||||
|
||||
Files that are uploaded by the users (pdf, etc.). Therefore not visible in the CMS.
|
||||
Images are handled seprately from documents since images require additional processing (resizing, cropping, etc.).
|
||||
Visible in the django admin.
|
||||
|
||||
### content images:
|
||||
|
||||
Images that belong to the content and are managed by the content editors in the CMS.
|
||||
|
||||
### user images:
|
||||
|
||||
Images that are uploaded by the users. Therefore not visible in the CMS. Visible in the django admin.
|
||||
|
||||
## Static files
|
||||
|
||||
These files are publicly served on S3.
|
||||
|
||||
## Content documents
|
||||
|
||||
These files are part of the content. Such as a pdf thas cointains additional information to a course.
|
||||
These files are not publicly available. The content files are uploaded by the editors in the wagtail cms.
|
||||
|
||||
https://www.hacksoft.io/blog/direct-to-s3-file-upload-with-django
|
||||
|
||||
Django handles the permissions to these files. Via a view django checks if the user has permissions to access the file,
|
||||
and gerates a temporary url that is valid for a limited time. Still the documents are served by django. This done for
|
||||
usability reasons. The user sees the url mydomain.com/media/documents/<document-id> and not a url to S3. Therefore the
|
||||
user can share the url with other users. (still they need to login and have the permissions to access the file)
|
||||
|
||||
The downside of this is that the django server processes these files. (could be circumvented by django-sendfile).
|
||||
|
||||

|
||||
|
||||
- These Files are handled stored as wagtail documents. As a model and the file itself is stored in S3.
|
||||
|
||||
### Frontend access to content documents
|
||||
|
||||
For the frontend django generates a fixed url per file /media/documents/<document-id>
|
||||
|
||||
When the frontend requests this file, django checks if the user has permissions to access the file.
|
||||
If so, django generates a temporary url that is valid for a limited time. Then sends a redirect to the frontend.
|
||||
|
||||
In this waz the frontend does not need to know about the permissions. Content grapql can be cached if needed and urls
|
||||
can be shared by the users.
|
||||
|
||||
content_documents
|
||||
user_documents
|
||||
|
||||
public files
|
||||
|
||||
## User documents
|
||||
|
||||
- User uploaded files are stored in S3. but the permissions is handled by django. Same process as content files.
|
||||
|
||||
Same process as content files. But the url is /media/user-uploads/<file-id>
|
||||
And the files are not managed by Wagtail. Due to another model, they are not visible to the user in the CMS.
|
||||
|
||||
## Content images
|
||||
|
||||
Content Images are served directly from S3. The permissions are handled by dja
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -18,8 +18,7 @@ def main():
|
|||
from django.conf import settings
|
||||
|
||||
settings.DEBUG = True
|
||||
from django.db import connection
|
||||
from django.db import reset_queries
|
||||
from django.db import connection, reset_queries
|
||||
|
||||
reset_queries()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ THIRD_PARTY_APPS = [
|
|||
|
||||
LOCAL_APPS = [
|
||||
"vbv_lernwelt.core",
|
||||
"vbv_lernwelt.media_files",
|
||||
"vbv_lernwelt.sso",
|
||||
"vbv_lernwelt.course",
|
||||
"vbv_lernwelt.learnpath",
|
||||
|
|
@ -213,23 +214,13 @@ STATICFILES_FINDERS = [
|
|||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
]
|
||||
|
||||
USE_AWS = env("USE_AWS", False)
|
||||
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", "")
|
||||
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", "")
|
||||
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", "")
|
||||
AWS_S3_CUSTOM_DOMAIN = "%s.s3.amazonaws.com" % AWS_STORAGE_BUCKET_NAME
|
||||
|
||||
# MEDIA
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
||||
MEDIA_ROOT = str(APPS_DIR / "media")
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
||||
if USE_AWS:
|
||||
# https://wagtail.org/blog/amazon-s3-for-media-files/
|
||||
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
|
||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||
else:
|
||||
MEDIA_URL = "/server/media/"
|
||||
|
||||
MEDIA_URL = "/server/media/"
|
||||
|
||||
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
|
||||
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
|
||||
|
|
@ -253,7 +244,19 @@ WAGTAIL_ENABLE_UPDATE_CHECK = False
|
|||
WAGTAIL_ENABLE_WHATS_NEW_BANNER = False
|
||||
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES
|
||||
|
||||
WAGTAILDOCS_DOCUMENT_MODEL = "media_library.LibraryDocument"
|
||||
WAGTAILDOCS_DOCUMENT_MODEL = "media_files.ContentDocument"
|
||||
WAGTAILIMAGES_IMAGE_MODEL = "media_files.ContentImage"
|
||||
|
||||
# this setting makes that the document is served by django, and the url is the django url.
|
||||
# https://docs.wagtail.org/en/stable/reference/settings.html#wagtaildocs-serve-method
|
||||
# The file is served by django as streaming response. If it should be serverd by nginx, then install django sendfile
|
||||
WAGTAILDOCS_SERVE_METHOD = "serve_view"
|
||||
# WAGTAILDOCS_INLINE_CONTENT_TYPES = ['application/pdf', 'text/plain']
|
||||
|
||||
|
||||
WAGTAILIMAGES_MAX_UPLOAD_SIZE = 20 * 1024 * 1024 # 20MB
|
||||
# WAGTAILIMAGES_RENDITION_STORAGE = 'myapp.backends.MyCustomStorage'
|
||||
|
||||
|
||||
WAGTAILADMIN_RICH_TEXT_EDITORS = {
|
||||
"default": {
|
||||
|
|
@ -646,7 +649,7 @@ NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
|
|||
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
||||
|
||||
# S3 BUCKET CONFIGURATION
|
||||
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="local") # local | s3
|
||||
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="s3") # local | s3
|
||||
|
||||
if FILE_UPLOAD_STORAGE == "local":
|
||||
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=5242880)
|
||||
|
|
@ -655,18 +658,19 @@ if FILE_UPLOAD_STORAGE == "s3":
|
|||
# Using django-storages
|
||||
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html
|
||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||
|
||||
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID")
|
||||
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID", default="AKIAZJLREPUVWNBTJ5VY")
|
||||
AWS_S3_SECRET_ACCESS_KEY = env("AWS_S3_SECRET_ACCESS_KEY")
|
||||
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
|
||||
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME")
|
||||
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", "eu-central-1")
|
||||
AWS_S3_SIGNATURE_VERSION = env("AWS_S3_SIGNATURE_VERSION", default="s3v4")
|
||||
AWS_STORAGE_BUCKET_NAME = env(
|
||||
"AWS_STORAGE_BUCKET_NAME", default="myvbv-dev.iterativ.ch"
|
||||
)
|
||||
AWS_S3_FILE_OVERWRITE = env("AWS_S3_FILE_OVERWRITE", False)
|
||||
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=20971520) # 20MB
|
||||
|
||||
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="private")
|
||||
|
||||
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=300) # seconds
|
||||
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=7200) # seconds
|
||||
|
||||
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = (
|
||||
"jpg",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ from vbv_lernwelt.importer.views import (
|
|||
from vbv_lernwelt.notify.views import email_notification_settings
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
from wagtail.documents import urls as media_library_urls
|
||||
|
||||
|
||||
class SignedIntConverter(IntConverter):
|
||||
|
|
@ -89,7 +89,7 @@ urlpatterns = [
|
|||
|
||||
# wagtail urls
|
||||
path('server/cms/', include(wagtailadmin_urls)),
|
||||
path('server/documents/', include(wagtaildocs_urls)),
|
||||
path('server/documents/', include(media_library_urls)),
|
||||
path('server/pages/', include(wagtail_urls)),
|
||||
|
||||
# core
|
||||
|
|
@ -138,6 +138,7 @@ urlpatterns = [
|
|||
name="request_assignment_completion_status"),
|
||||
|
||||
# documents
|
||||
# TODO: remfactor to files app
|
||||
path(r'api/core/document/start/', document_upload_start,
|
||||
name='file_upload_start'),
|
||||
path(r'api/core/document/<str:document_id>/', document_delete,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
|||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.iam.permissions import can_evaluate_assignments, has_course_access
|
||||
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
||||
from vbv_lernwelt.media_files.graphql.types import ContentDocumentObjectType
|
||||
|
||||
|
||||
class AssignmentCompletionObjectType(DjangoObjectType):
|
||||
|
|
@ -52,6 +53,7 @@ class AssignmentObjectType(DjangoObjectType):
|
|||
learning_content_page_id=graphene.ID(required=False),
|
||||
assignment_user_id=graphene.UUID(required=False),
|
||||
)
|
||||
solution_sample = graphene.Field(ContentDocumentObjectType)
|
||||
|
||||
class Meta:
|
||||
model = Assignment
|
||||
|
|
@ -67,6 +69,9 @@ class AssignmentObjectType(DjangoObjectType):
|
|||
"competence_certificate",
|
||||
)
|
||||
|
||||
def resolve_solution_sample(self, info):
|
||||
return self.solution_sample
|
||||
|
||||
def resolve_max_points(self, info):
|
||||
return self.get_max_points()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-05 16:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("media_files", "0001_initial"),
|
||||
("assignment", "0010_assignmentcompletion_edoniq_extended_time_flag"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="assignment",
|
||||
name="solution_sample",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Musterlösung",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="media_files.contentdocument",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -200,12 +200,22 @@ class Assignment(CourseBasePage):
|
|||
help_text="Beurteilungsschritte",
|
||||
)
|
||||
|
||||
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"),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
|||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
from vbv_lernwelt.learnpath.models import (
|
||||
LearningContentAttendanceCourse,
|
||||
LearningContentFeedback,
|
||||
LearningContentFeedbackUK,
|
||||
LearningContentFeedbackVV,
|
||||
)
|
||||
from vbv_lernwelt.notify.models import Notification
|
||||
|
||||
|
|
@ -155,7 +156,9 @@ def command(
|
|||
if create_feedback_responses:
|
||||
print("create_feedback_responses")
|
||||
course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
|
||||
learning_content_feedback_page = LearningContentFeedback.objects.get(
|
||||
|
||||
# feedback fahrzeug
|
||||
learning_content_feedback_page = LearningContentFeedbackUK.objects.get(
|
||||
slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback"
|
||||
)
|
||||
create_feedback_response_data(
|
||||
|
|
@ -174,6 +177,7 @@ def command(
|
|||
"would_recommend": True,
|
||||
"course_negative_feedback": "Nichts Schlechtes",
|
||||
"course_positive_feedback": "Nur Gutes.",
|
||||
"feedback_type": "uk",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -193,6 +197,7 @@ def command(
|
|||
"would_recommend": True,
|
||||
"course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
|
||||
"course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
|
||||
"feedback_type": "uk",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -212,6 +217,62 @@ def command(
|
|||
"would_recommend": False,
|
||||
"course_negative_feedback": "Mehr Videos wären schön.",
|
||||
"course_positive_feedback": "Die Präsentation war super",
|
||||
"feedback_type": "uk",
|
||||
},
|
||||
)
|
||||
|
||||
# feedback reisen
|
||||
learning_content_feedback_page = LearningContentFeedbackVV.objects.get(
|
||||
slug="test-lehrgang-lp-circle-reisen-lc-feedback"
|
||||
)
|
||||
create_feedback_response_data(
|
||||
feedback_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
|
||||
course_session=course_session,
|
||||
learning_content_feedback_page=learning_content_feedback_page,
|
||||
submitted=True,
|
||||
feedback_data={
|
||||
"satisfaction": 4,
|
||||
"goal_attainment": 3,
|
||||
"proficiency": 80,
|
||||
"preparation_task_clarity": True,
|
||||
"would_recommend": True,
|
||||
"course_negative_feedback": "Nichts Schlechtes",
|
||||
"course_positive_feedback": "Nur Gutes.",
|
||||
"feedback_type": "vv",
|
||||
},
|
||||
)
|
||||
|
||||
create_feedback_response_data(
|
||||
feedback_user=User.objects.get(id=TEST_STUDENT2_USER_ID),
|
||||
course_session=course_session,
|
||||
learning_content_feedback_page=learning_content_feedback_page,
|
||||
submitted=True,
|
||||
feedback_data={
|
||||
"satisfaction": 4,
|
||||
"goal_attainment": 4,
|
||||
"proficiency": 100,
|
||||
"preparation_task_clarity": True,
|
||||
"would_recommend": True,
|
||||
"course_negative_feedback": "Es wäre praktisch, Zugang zu einer FAQ zu haben.",
|
||||
"course_positive_feedback": "Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
|
||||
"feedback_type": "vv",
|
||||
},
|
||||
)
|
||||
|
||||
create_feedback_response_data(
|
||||
feedback_user=User.objects.get(id=TEST_STUDENT3_USER_ID),
|
||||
course_session=course_session,
|
||||
learning_content_feedback_page=learning_content_feedback_page,
|
||||
submitted=True,
|
||||
feedback_data={
|
||||
"satisfaction": 2,
|
||||
"goal_attainment": 2,
|
||||
"proficiency": 40,
|
||||
"preparation_task_clarity": True,
|
||||
"would_recommend": False,
|
||||
"course_negative_feedback": "Mehr Videos wären schön.",
|
||||
"course_positive_feedback": "Die Präsentation war super",
|
||||
"feedback_type": "vv",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
|||
LearningContentAssignmentFactory,
|
||||
LearningContentAttendanceCourseFactory,
|
||||
LearningContentEdoniqTestFactory,
|
||||
LearningContentFeedbackFactory,
|
||||
LearningContentFeedbackUKFactory,
|
||||
LearningContentFeedbackVVFactory,
|
||||
LearningContentKnowledgeAssessmentFactory,
|
||||
LearningContentLearningModuleFactory,
|
||||
LearningContentMediaLibraryFactory,
|
||||
|
|
@ -82,6 +83,12 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
|||
LearningUnitFactory,
|
||||
TopicFactory,
|
||||
)
|
||||
from vbv_lernwelt.media_files.create_default_documents import (
|
||||
create_default_collections,
|
||||
create_default_content_documents,
|
||||
)
|
||||
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||
from vbv_lernwelt.media_files.models import ContentDocument, ContentImage, UserImage
|
||||
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||
MediaLibraryCategoryPageFactory,
|
||||
MediaLibraryContentPageFactory,
|
||||
|
|
@ -92,6 +99,11 @@ from vbv_lernwelt.media_library.tests.media_library_factories import (
|
|||
|
||||
def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||
# create_locales_for_wagtail()
|
||||
create_default_collections()
|
||||
create_default_content_documents()
|
||||
if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
|
||||
create_default_images()
|
||||
|
||||
course = create_test_course_with_categories()
|
||||
competence_certificate = create_test_competence_navi()
|
||||
|
||||
|
|
@ -360,6 +372,7 @@ def create_feedback_response_data(
|
|||
"would_recommend": True,
|
||||
"course_negative_feedback": "Nichts Schlechtes",
|
||||
"course_positive_feedback": "Nur Gutes.",
|
||||
"feedback_type": "uk",
|
||||
}
|
||||
|
||||
return update_feedback_response(
|
||||
|
|
@ -523,6 +536,14 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
|||
slug__startswith=f"test-lehrgang-assignment-reflexion"
|
||||
),
|
||||
),
|
||||
|
||||
assignment = Assignment.objects.get(
|
||||
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
|
||||
)
|
||||
assignment.solution_sample = ContentDocument.objects.get(
|
||||
title="Musterlösung Fahrzeug"
|
||||
)
|
||||
assignment.save()
|
||||
LearningContentAssignmentFactory(
|
||||
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
|
||||
parent=circle,
|
||||
|
|
@ -530,7 +551,8 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
|||
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
|
||||
),
|
||||
)
|
||||
LearningContentFeedbackFactory(
|
||||
LearningContentFeedbackUKFactory(
|
||||
title="Feedback",
|
||||
parent=circle,
|
||||
)
|
||||
|
||||
|
|
@ -614,7 +636,8 @@ def create_test_circle_reisen(lp):
|
|||
title="Reflexion",
|
||||
parent=parent,
|
||||
)
|
||||
LearningContentFeedbackFactory(
|
||||
LearningContentFeedbackVVFactory(
|
||||
title="Feedback",
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class FeedbackResponseFactory(DjangoModelFactory):
|
|||
"Das Beispiel mit der Katze fand ich sehr gut veranschaulicht!",
|
||||
]
|
||||
),
|
||||
"feedback_type": FuzzyChoice(["uk", "vv"]),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,16 @@ from vbv_lernwelt.course.models import CourseSession
|
|||
from vbv_lernwelt.feedback.graphql.types import (
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-07 14:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def add_field_to_json(apps, _schema_editor):
|
||||
FeedbackResponse = apps.get_model("feedback", "FeedbackResponse")
|
||||
for instance in FeedbackResponse.objects.all():
|
||||
if instance.data is None:
|
||||
instance.data = {}
|
||||
instance.data["feedback_type"] = "uk" # Set the default value
|
||||
instance.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("feedback", "0006_auto_20230922_1131"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_field_to_json),
|
||||
]
|
||||
|
|
@ -5,6 +5,11 @@ from vbv_lernwelt.feedback.models import FeedbackResponse
|
|||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import json
|
||||
|
||||
from graphene_django.utils.testing import GraphQLTestCase
|
||||
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.consts import COURSE_TEST_ID
|
||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
from vbv_lernwelt.learnpath.models import LearningContentFeedbackUK
|
||||
|
||||
|
||||
class FeedbackMutationTestCase(GraphQLTestCase):
|
||||
GRAPHQL_URL = "/server/graphql/"
|
||||
|
||||
def setUp(self):
|
||||
create_default_users()
|
||||
create_test_course(include_vv=False, with_sessions=True)
|
||||
self.course_session = CourseSession.objects.get(title="Test Bern 2022 a")
|
||||
self.learning_content_feedback_page = LearningContentFeedbackUK.objects.get(
|
||||
slug="test-lehrgang-lp-circle-fahrzeug-lc-feedback"
|
||||
)
|
||||
self.student = User.objects.get(username="test-student1@example.com")
|
||||
self.client.force_login(self.student)
|
||||
|
||||
def test_creates_response(self):
|
||||
data = {
|
||||
"course_negative_feedback": "schlecht",
|
||||
"course_positive_feedback": "gut",
|
||||
"feedback_type": "uk",
|
||||
"goal_attainment": 3,
|
||||
"preparation_task_clarity": False,
|
||||
"proficiency": 100,
|
||||
"satisfaction": 3,
|
||||
"would_recommend": False,
|
||||
"instructor_competence": None,
|
||||
"instructor_respect": None,
|
||||
"instructor_open_feedback": None,
|
||||
}
|
||||
|
||||
response = self.query(
|
||||
f"""
|
||||
mutation {{
|
||||
send_feedback(
|
||||
course_session_id: "{COURSE_TEST_ID}"
|
||||
learning_content_page_id: "{self.learning_content_feedback_page.id}"
|
||||
learning_content_type: "learnpath.LearningContentFeedbackUK"
|
||||
data: {{
|
||||
course_negative_feedback: "{data['course_negative_feedback']}",
|
||||
course_positive_feedback: "{data['course_positive_feedback']}",
|
||||
feedback_type: null,
|
||||
goal_attainment: {data['goal_attainment']},
|
||||
preparation_task_clarity: {str(data['preparation_task_clarity']).lower()},
|
||||
proficiency: {data['proficiency']},
|
||||
satisfaction: {data['satisfaction']},
|
||||
would_recommend: {str(data['would_recommend']).lower()},
|
||||
instructor_competence: null,
|
||||
instructor_respect: null,
|
||||
instructor_open_feedback: null,
|
||||
}},
|
||||
submitted: false
|
||||
) {{
|
||||
feedback_response {{
|
||||
id
|
||||
data
|
||||
submitted
|
||||
__typename
|
||||
}}
|
||||
errors {{
|
||||
field
|
||||
messages
|
||||
__typename
|
||||
}}
|
||||
__typename
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
self.assertResponseNoErrors(response)
|
||||
self.assertDictEqual(
|
||||
content["data"]["send_feedback"]["feedback_response"]["data"], data
|
||||
)
|
||||
|
||||
feedback = FeedbackResponse.objects.first()
|
||||
self.assertEqual(feedback.data, data)
|
||||
self.assertEqual(feedback.submitted, False)
|
||||
self.assertEqual(feedback.feedback_user, self.student)
|
||||
|
|
@ -61,12 +61,13 @@ def get_feedback_for_circle(request, course_session_id, circle_id):
|
|||
feedback_user__in=feedback_users(course_session_id),
|
||||
).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] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import datetime
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.files.integrations import s3_get_client
|
||||
from vbv_lernwelt.files.models import UploadFile
|
||||
|
||||
|
||||
class UploadFileIntegrationTest(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.s3_client = s3_get_client()
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(username="testuser")
|
||||
# Creating a dummy file for upload
|
||||
self.dummy_file = SimpleUploadedFile(
|
||||
"testfile.txt", b"these are the file contents!"
|
||||
)
|
||||
self.upload_file = UploadFile.objects.create(
|
||||
original_file_name="testfile.txt",
|
||||
file_name="testfile123.txt",
|
||||
file_type="text/plain",
|
||||
uploaded_by=self.user,
|
||||
file=self.dummy_file,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.upload_file.delete_file()
|
||||
|
||||
def test_upload_to_s3(self):
|
||||
# Verify if file is uploaded to S3
|
||||
response = self.s3_client.get_object(
|
||||
Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=str(self.upload_file.file)
|
||||
)
|
||||
self.assertEqual(response["Body"].read(), b"these are the file contents!")
|
||||
|
||||
def test_url_property(self):
|
||||
self.upload_file.upload_finished_at = datetime.datetime.now()
|
||||
self.upload_file.save()
|
||||
url = self.upload_file.url
|
||||
response = requests.get(url)
|
||||
# Assert that the URL is a valid presigned S3 URL and accessible
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content, b"these are the file contents!")
|
||||
|
||||
def test_delete_file_method(self):
|
||||
file_path = str(self.upload_file.file)
|
||||
self.upload_file.delete_file()
|
||||
# Assert that the file is deleted from S3
|
||||
with self.assertRaises(self.s3_client.exceptions.NoSuchKey):
|
||||
self.s3_client.get_object(
|
||||
Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_path
|
||||
)
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import os
|
||||
|
||||
import boto3
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.files.integrations import (
|
||||
s3_delete_file,
|
||||
s3_generate_presigned_post,
|
||||
s3_generate_presigned_url,
|
||||
s3_get_client,
|
||||
)
|
||||
|
||||
|
||||
class TestIntegrationsIntegrationTest(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.s3_client = s3_get_client()
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(username="testuser")
|
||||
|
||||
# Creating a dummy file for upload
|
||||
self.dummy_file = SimpleUploadedFile(
|
||||
"testfile.txt", b"these are the file contents!"
|
||||
)
|
||||
|
||||
def test_s3_generate_presigned_post(self):
|
||||
# Test generating a presigned POST for file upload
|
||||
presigned_post_data = s3_generate_presigned_post(
|
||||
file_path=f"{self.user.id}/testfile.txt",
|
||||
file_type="text/plain",
|
||||
file_name="testfile.txt",
|
||||
)
|
||||
self.assertIn("url", presigned_post_data)
|
||||
self.assertIn("fields", presigned_post_data)
|
||||
|
||||
# Upload file using the presigned URL
|
||||
files = {"file": self.dummy_file}
|
||||
response = requests.post(
|
||||
presigned_post_data["url"], data=presigned_post_data["fields"], files=files
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_s3_generate_presigned_url(self):
|
||||
# First, manually upload a file to S3 for testing
|
||||
self.s3_client.upload_fileobj
|
||||
|
||||
self.s3_client.upload_fileobj(
|
||||
self.dummy_file, settings.AWS_STORAGE_BUCKET_NAME, "testfile.txt"
|
||||
)
|
||||
|
||||
# Test generating a presigned URL for the uploaded file
|
||||
presigned_url = s3_generate_presigned_url(file_path="testfile.txt")
|
||||
response = requests.get(presigned_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content, b"these are the file contents!")
|
||||
|
||||
def test_s3_delete_file(self):
|
||||
# Upload a file to S3 for testing
|
||||
self.s3_client.upload_fileobj(
|
||||
self.dummy_file, settings.AWS_STORAGE_BUCKET_NAME, "testfile.txt"
|
||||
)
|
||||
|
||||
# Test deleting the file
|
||||
s3_delete_file(file_path="testfile.txt")
|
||||
# Assert that the file no longer exists
|
||||
with self.assertRaises(boto3.exceptions.botocore.client.ClientError):
|
||||
self.s3_client.head_object(
|
||||
Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key="testfile.txt"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Clean up any remaining files in the S3 bucket
|
||||
s3_delete_file(file_path="testfile.txt")
|
||||
super().tearDownClass()
|
||||
|
|
@ -13,7 +13,7 @@ from vbv_lernwelt.course.models import CourseCategory, CoursePage
|
|||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||
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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
# Generated by Django 3.2.20 on 2023-11-29 07:27
|
||||
|
||||
import django.db.models.deletion
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("wagtailcore", "0089_log_entry_data_json_null_to_object"),
|
||||
("learnpath", "0011_learningcontentknowledgeassessment"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel("LearningContentFeedback", "LearningContentFeedbackUK"),
|
||||
migrations.CreateModel(
|
||||
name="LearningContentFeedbackVV",
|
||||
fields=[
|
||||
(
|
||||
"page_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="wagtailcore.page",
|
||||
),
|
||||
),
|
||||
("minutes", models.PositiveIntegerField(default=15)),
|
||||
("description", wagtail.fields.RichTextField(blank=True)),
|
||||
("content_url", models.TextField(blank=True)),
|
||||
("has_course_completion_status", models.BooleanField(default=True)),
|
||||
(
|
||||
"can_user_self_toggle_course_completion",
|
||||
models.BooleanField(default=False),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="learningcontentassignment",
|
||||
name="assignment_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("VOLUNTARY_CASEWORK", "VOLUNTARY_CASEWORK"),
|
||||
("MANDATORY_CASEWORK", "MANDATORY_CASEWORK"),
|
||||
("PREP_ASSIGNMENT", "PREP_ASSIGNMENT"),
|
||||
("REFLECTION", "REFLECTION"),
|
||||
("CONDITION_ACCEPTANCE", "CONDITION_ACCEPTANCE"),
|
||||
("EDONIQ_TEST", "EDONIQ_TEST"),
|
||||
],
|
||||
default="MANDATORY_CASEWORK",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -72,7 +72,8 @@ class Circle(CourseBasePage):
|
|||
"learnpath.LearningUnit",
|
||||
"learnpath.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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from vbv_lernwelt.media_files.models import UserDocument, UserImage
|
||||
|
||||
|
||||
@admin.register(UserDocument)
|
||||
class UserDocumentAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"title",
|
||||
"file",
|
||||
"created_at",
|
||||
"uploaded_by_user",
|
||||
"file_size",
|
||||
"file_hash",
|
||||
)
|
||||
search_fields = ("title", "uploaded_by_user__username", "tags__name")
|
||||
list_filter = ("created_at", "uploaded_by_user")
|
||||
autocomplete_fields = ["uploaded_by_user"]
|
||||
date_hierarchy = "created_at"
|
||||
readonly_fields = (
|
||||
"file_size",
|
||||
"file_hash",
|
||||
"created_at",
|
||||
"uploaded_by_user",
|
||||
"file",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(UserImage)
|
||||
class UserImageAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"title",
|
||||
"file",
|
||||
"created_at",
|
||||
"uploaded_by_user",
|
||||
"file_size",
|
||||
)
|
||||
search_fields = ("title", "uploaded_by_user__username")
|
||||
list_filter = ("created_at", "uploaded_by_user")
|
||||
autocomplete_fields = ["uploaded_by_user"]
|
||||
date_hierarchy = "created_at"
|
||||
readonly_fields = (
|
||||
"file_size",
|
||||
"file_hash",
|
||||
"created_at",
|
||||
"uploaded_by_user",
|
||||
"file",
|
||||
"tags",
|
||||
"title",
|
||||
"focal_point_x",
|
||||
"focal_point_y",
|
||||
"focal_point_width",
|
||||
"focal_point_height",
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MediaLibraryConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "vbv_lernwelt.media_files"
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import os
|
||||
|
||||
import factory
|
||||
from django.conf import settings
|
||||
from wagtail.models import Collection
|
||||
|
||||
from vbv_lernwelt.media_files.models import ContentDocument, UserDocument
|
||||
from vbv_lernwelt.media_files.tests.media_library_factories import (
|
||||
ContentDocumentFactory,
|
||||
UserDocumentFactory,
|
||||
)
|
||||
|
||||
|
||||
def delete_default_documents():
|
||||
"""deletes all documents"""
|
||||
if "prod" in settings.APP_ENVIRONMENT:
|
||||
raise Exception("This command must not be used in production environment")
|
||||
|
||||
ContentDocument.objects.all().delete()
|
||||
UserDocument.objects.all().delete()
|
||||
|
||||
|
||||
def create_default_collections():
|
||||
root, created = Collection.objects.get_or_create(name="Root", depth=0)
|
||||
|
||||
|
||||
def create_default_content_documents():
|
||||
"""creates a default document for testing purposes"""
|
||||
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "./tests/test_documents/"
|
||||
)
|
||||
|
||||
filename = "Vermittler_Motorfahrzeug_Versicherung_Musterlösung.pdf"
|
||||
document = ContentDocumentFactory(
|
||||
title="Musterlösung Fahrzeug",
|
||||
display_text="Musterlösung Fahrzeug",
|
||||
description="Musterlösung für den Auftrag Fahrzeug",
|
||||
link_display_text="Dokument laden",
|
||||
file=factory.django.FileField(
|
||||
from_path=os.path.join(path, filename), filename=filename
|
||||
),
|
||||
)
|
||||
document.tags.set(("Fahrzeug", "Musterlösung", "Vermittler"))
|
||||
document.save()
|
||||
|
||||
filename = "TestExcelSheet.xlsx"
|
||||
document = ContentDocumentFactory(
|
||||
title="Mustertabelle",
|
||||
display_text="Mustertabelle",
|
||||
link_display_text="Dokument laden",
|
||||
file=factory.django.FileField(
|
||||
from_path=os.path.join(path, filename), filename=filename
|
||||
),
|
||||
)
|
||||
document.tags.set(("Vermittler"))
|
||||
document.save()
|
||||
|
||||
|
||||
def create_default_user_documents():
|
||||
"""creates a default document for testing purposes"""
|
||||
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "./tests/test_documents/"
|
||||
)
|
||||
|
||||
filename = "FallanalyseTeststudent.pdf"
|
||||
document = UserDocumentFactory(
|
||||
title="Lösung Fallanalyse",
|
||||
file=factory.django.FileField(
|
||||
from_path=os.path.join(path, filename), filename=filename
|
||||
),
|
||||
)
|
||||
document.save()
|
||||
|
||||
filename = "FallanalyseTeststudent.docx"
|
||||
document = UserDocumentFactory(
|
||||
title="Lösung Fallanalyse",
|
||||
file=factory.django.FileField(
|
||||
from_path=os.path.join(path, filename), filename=filename
|
||||
),
|
||||
)
|
||||
document.save()
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
|
||||
from vbv_lernwelt.media_files.models import ContentImage, UserImage
|
||||
|
||||
|
||||
def delete_default_images():
|
||||
"""deletes all images"""
|
||||
if "prod" in settings.APP_ENVIRONMENT:
|
||||
raise Exception("This command must not be used in production environment")
|
||||
ContentImage.objects.all().delete()
|
||||
UserImage.objects.all().delete()
|
||||
|
||||
|
||||
def create_default_images():
|
||||
create_default_content_images()
|
||||
create_default_user_images()
|
||||
|
||||
|
||||
def create_default_content_images():
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "./tests/test_images/"
|
||||
)
|
||||
|
||||
images = [
|
||||
("bike_accident.jpg", "Bike Accident"),
|
||||
("car_accident.jpg", "Car Accident"),
|
||||
]
|
||||
|
||||
for filename, title in images:
|
||||
file_path = os.path.join(path, filename)
|
||||
with open(file_path, "rb") as f:
|
||||
image, _ = ContentImage.objects.get_or_create(
|
||||
title=title,
|
||||
file=File(f, name=filename),
|
||||
focal_point_x=600,
|
||||
focal_point_y=600,
|
||||
focal_point_width=300,
|
||||
focal_point_height=300,
|
||||
)
|
||||
image.tags.set(("Fahrzeug", "Unfall", "Vermittler"))
|
||||
image.save()
|
||||
|
||||
|
||||
def create_default_user_images():
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "./tests/test_images/"
|
||||
)
|
||||
filename, title = ("user1_profile.jpg", "User1 Profile")
|
||||
file_path = os.path.join(path, filename)
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
image, _ = UserImage.objects.get_or_create(
|
||||
title=title,
|
||||
file=File(f, name=filename),
|
||||
focal_point_x=600,
|
||||
focal_point_y=600,
|
||||
focal_point_width=300,
|
||||
focal_point_height=300,
|
||||
)
|
||||
image.save()
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import graphene
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from vbv_lernwelt.media_files.models import ContentDocument
|
||||
|
||||
|
||||
class ContentDocumentObjectType(DjangoObjectType):
|
||||
url = graphene.String(source="url")
|
||||
|
||||
class Meta:
|
||||
model = ContentDocument
|
||||
fields = (
|
||||
"id",
|
||||
"display_text",
|
||||
"description",
|
||||
"link_display_text",
|
||||
"thumbnail",
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import djclick as click
|
||||
|
||||
from vbv_lernwelt.media_files.create_default_documents import (
|
||||
create_default_collections,
|
||||
create_default_content_documents,
|
||||
create_default_user_documents,
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
def command():
|
||||
print("Creating default documents...")
|
||||
create_default_collections()
|
||||
create_default_content_documents()
|
||||
create_default_user_documents()
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import djclick as click
|
||||
|
||||
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||
|
||||
|
||||
@click.command()
|
||||
def command():
|
||||
print("Creating default images...")
|
||||
create_default_images()
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import djclick as click
|
||||
|
||||
from vbv_lernwelt.media_files.create_default_documents import delete_default_documents
|
||||
from vbv_lernwelt.media_files.create_default_images import delete_default_images
|
||||
|
||||
|
||||
@click.command()
|
||||
def command():
|
||||
print("Deleting all images...")
|
||||
delete_default_images()
|
||||
print("Deleting all documents...")
|
||||
delete_default_documents()
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-05 16:11
|
||||
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import wagtail.images.models
|
||||
import wagtail.models.collections
|
||||
import wagtail.search.index
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import vbv_lernwelt.media_files.storage_backends
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("taggit", "0005_auto_20220424_2025"),
|
||||
("wagtailcore", "0089_log_entry_data_json_null_to_object"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ContentImage",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=255, verbose_name="title")),
|
||||
("width", models.IntegerField(editable=False, verbose_name="width")),
|
||||
("height", models.IntegerField(editable=False, verbose_name="height")),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, db_index=True, verbose_name="created at"
|
||||
),
|
||||
),
|
||||
("focal_point_x", models.PositiveIntegerField(blank=True, null=True)),
|
||||
("focal_point_y", models.PositiveIntegerField(blank=True, null=True)),
|
||||
(
|
||||
"focal_point_width",
|
||||
models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"focal_point_height",
|
||||
models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
("file_size", models.PositiveIntegerField(editable=False, null=True)),
|
||||
(
|
||||
"file_hash",
|
||||
models.CharField(
|
||||
blank=True, db_index=True, editable=False, max_length=40
|
||||
),
|
||||
),
|
||||
(
|
||||
"file",
|
||||
wagtail.images.models.WagtailImageField(
|
||||
height_field="height",
|
||||
storage=vbv_lernwelt.media_files.storage_backends.ContentImagesStorage,
|
||||
upload_to=wagtail.images.models.get_upload_to,
|
||||
verbose_name="file",
|
||||
width_field="width",
|
||||
),
|
||||
),
|
||||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
verbose_name="collection",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tags",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text=None,
|
||||
through="taggit.TaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="tags",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uploaded_by_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="uploaded by user",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=(
|
||||
wagtail.images.models.ImageFileMixin,
|
||||
wagtail.search.index.Indexed,
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserImage",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=255, verbose_name="title")),
|
||||
("width", models.IntegerField(editable=False, verbose_name="width")),
|
||||
("height", models.IntegerField(editable=False, verbose_name="height")),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, db_index=True, verbose_name="created at"
|
||||
),
|
||||
),
|
||||
("focal_point_x", models.PositiveIntegerField(blank=True, null=True)),
|
||||
("focal_point_y", models.PositiveIntegerField(blank=True, null=True)),
|
||||
(
|
||||
"focal_point_width",
|
||||
models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"focal_point_height",
|
||||
models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
("file_size", models.PositiveIntegerField(editable=False, null=True)),
|
||||
(
|
||||
"file_hash",
|
||||
models.CharField(
|
||||
blank=True, db_index=True, editable=False, max_length=40
|
||||
),
|
||||
),
|
||||
(
|
||||
"file",
|
||||
wagtail.images.models.WagtailImageField(
|
||||
height_field="height",
|
||||
storage=vbv_lernwelt.media_files.storage_backends.UserImagesStorage,
|
||||
upload_to=wagtail.images.models.get_upload_to,
|
||||
verbose_name="file",
|
||||
width_field="width",
|
||||
),
|
||||
),
|
||||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
verbose_name="collection",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tags",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text=None,
|
||||
through="taggit.TaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="tags",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uploaded_by_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="uploaded by user",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=(
|
||||
wagtail.images.models.ImageFileMixin,
|
||||
wagtail.search.index.Indexed,
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserDocument",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=255, verbose_name="title")),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
|
||||
),
|
||||
("file_size", models.PositiveIntegerField(editable=False, null=True)),
|
||||
(
|
||||
"file_hash",
|
||||
models.CharField(blank=True, editable=False, max_length=40),
|
||||
),
|
||||
(
|
||||
"file",
|
||||
models.FileField(
|
||||
storage=vbv_lernwelt.media_files.storage_backends.UserDocumentsStorage,
|
||||
upload_to="documents",
|
||||
verbose_name="file",
|
||||
),
|
||||
),
|
||||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
verbose_name="collection",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tags",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text=None,
|
||||
through="taggit.TaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="tags",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uploaded_by_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="uploaded by user",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "document",
|
||||
"verbose_name_plural": "documents",
|
||||
"abstract": False,
|
||||
},
|
||||
bases=(wagtail.search.index.Indexed, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ContentDocument",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=255, verbose_name="title")),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
|
||||
),
|
||||
("file_size", models.PositiveIntegerField(editable=False, null=True)),
|
||||
(
|
||||
"file_hash",
|
||||
models.CharField(blank=True, editable=False, max_length=40),
|
||||
),
|
||||
(
|
||||
"file",
|
||||
models.FileField(
|
||||
storage=vbv_lernwelt.media_files.storage_backends.ContentDocumentsStorage,
|
||||
upload_to="documents",
|
||||
verbose_name="file",
|
||||
),
|
||||
),
|
||||
("display_text", models.CharField(default="", max_length=1024)),
|
||||
("description", models.TextField(blank=True, default="")),
|
||||
(
|
||||
"link_display_text",
|
||||
models.CharField(blank=True, default="", max_length=1024),
|
||||
),
|
||||
(
|
||||
"thumbnail",
|
||||
models.CharField(blank=True, default="", max_length=1024),
|
||||
),
|
||||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
verbose_name="collection",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tags",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text=None,
|
||||
through="taggit.TaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="tags",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uploaded_by_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="uploaded by user",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "document",
|
||||
"verbose_name_plural": "documents",
|
||||
"abstract": False,
|
||||
},
|
||||
bases=(wagtail.search.index.Indexed, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserImageRendition",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("filter_spec", models.CharField(db_index=True, max_length=255)),
|
||||
(
|
||||
"file",
|
||||
wagtail.images.models.WagtailImageField(
|
||||
height_field="height",
|
||||
upload_to=wagtail.images.models.get_rendition_upload_to,
|
||||
width_field="width",
|
||||
),
|
||||
),
|
||||
("width", models.IntegerField(editable=False)),
|
||||
("height", models.IntegerField(editable=False)),
|
||||
(
|
||||
"focal_point_key",
|
||||
models.CharField(
|
||||
blank=True, default="", editable=False, max_length=16
|
||||
),
|
||||
),
|
||||
(
|
||||
"image",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="renditions",
|
||||
to="media_files.userimage",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("image", "filter_spec", "focal_point_key")},
|
||||
},
|
||||
bases=(wagtail.images.models.ImageFileMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ContentImageRendition",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("filter_spec", models.CharField(db_index=True, max_length=255)),
|
||||
(
|
||||
"file",
|
||||
wagtail.images.models.WagtailImageField(
|
||||
height_field="height",
|
||||
upload_to=wagtail.images.models.get_rendition_upload_to,
|
||||
width_field="width",
|
||||
),
|
||||
),
|
||||
("width", models.IntegerField(editable=False)),
|
||||
("height", models.IntegerField(editable=False)),
|
||||
(
|
||||
"focal_point_key",
|
||||
models.CharField(
|
||||
blank=True, default="", editable=False, max_length=16
|
||||
),
|
||||
),
|
||||
(
|
||||
"image",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="renditions",
|
||||
to="media_files.contentimage",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("image", "filter_spec", "focal_point_key")},
|
||||
},
|
||||
bases=(wagtail.images.models.ImageFileMixin, models.Model),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail.documents.models import AbstractDocument, Document
|
||||
from wagtail.images.models import (
|
||||
AbstractImage,
|
||||
AbstractRendition,
|
||||
get_upload_to,
|
||||
Image,
|
||||
WagtailImageField,
|
||||
)
|
||||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.media_files.storage_backends import (
|
||||
ContentDocumentsStorage,
|
||||
ContentImagesStorage,
|
||||
UserDocumentsStorage,
|
||||
UserImagesStorage,
|
||||
)
|
||||
|
||||
|
||||
class ContentDocument(AbstractDocument):
|
||||
"""
|
||||
Content documents are documents that are handled by the CMS.
|
||||
"""
|
||||
|
||||
file = models.FileField(
|
||||
upload_to="documents", verbose_name=_("file"), storage=ContentDocumentsStorage
|
||||
)
|
||||
display_text = models.CharField(max_length=1024, default="")
|
||||
description = models.TextField(default="", blank=True)
|
||||
link_display_text = models.CharField(max_length=1024, default="", blank=True)
|
||||
thumbnail = models.CharField(default="", max_length=1024, blank=True)
|
||||
|
||||
admin_form_fields = Document.admin_form_fields + (
|
||||
"display_text",
|
||||
"description",
|
||||
"link_display_text",
|
||||
"thumbnail",
|
||||
)
|
||||
|
||||
def has_permission(self, user: User):
|
||||
# TODO: 20-11-2023 Renzo: add more advanced permission handling
|
||||
if user.is_authenticated:
|
||||
return True
|
||||
|
||||
|
||||
class UserDocument(AbstractDocument):
|
||||
"""
|
||||
Documents that are uploaded by the user and not visible in the CMS.
|
||||
Still they are inherited from the Wagtail Document model.
|
||||
"""
|
||||
|
||||
file = models.FileField(
|
||||
upload_to="documents", verbose_name=_("file"), storage=UserDocumentsStorage
|
||||
)
|
||||
|
||||
|
||||
class ContentImage(AbstractImage):
|
||||
"""
|
||||
Content images are images that are handled by the CMS.
|
||||
"""
|
||||
|
||||
file = WagtailImageField(
|
||||
verbose_name=_("file"),
|
||||
upload_to=get_upload_to,
|
||||
width_field="width",
|
||||
height_field="height",
|
||||
storage=ContentImagesStorage,
|
||||
)
|
||||
admin_form_fields = Image.admin_form_fields + (
|
||||
# Then add the field names here to make them appear in the form:
|
||||
# 'caption',
|
||||
)
|
||||
|
||||
|
||||
class UserImage(AbstractImage):
|
||||
"""
|
||||
User images are images that are uploaded by the user and not visible in the CMS.
|
||||
Still they are inherited from the Wagtail Image model.
|
||||
"""
|
||||
|
||||
file = WagtailImageField(
|
||||
verbose_name=_("file"),
|
||||
upload_to=get_upload_to,
|
||||
width_field="width",
|
||||
height_field="height",
|
||||
storage=UserImagesStorage,
|
||||
)
|
||||
|
||||
|
||||
class ContentImageRendition(AbstractRendition):
|
||||
image = models.ForeignKey(
|
||||
ContentImage, on_delete=models.CASCADE, related_name="renditions"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("image", "filter_spec", "focal_point_key"),)
|
||||
|
||||
|
||||
class UserImageRendition(AbstractRendition):
|
||||
image = models.ForeignKey(
|
||||
UserImage, on_delete=models.CASCADE, related_name="renditions"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("image", "filter_spec", "focal_point_key"),)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
from storages.backends.s3boto3 import S3Boto3Storage
|
||||
|
||||
|
||||
# inspired by https://theyashshahs.medium.com/aws-s3-signed-urls-in-django-d9e66853a42f
|
||||
|
||||
|
||||
class ContentDocumentsStorage(S3Boto3Storage):
|
||||
location = "media/content_documents"
|
||||
default_acl = "private"
|
||||
|
||||
|
||||
class ContentImagesStorage(S3Boto3Storage):
|
||||
location = "media/content_images"
|
||||
default_acl = "private"
|
||||
|
||||
|
||||
class UserDocumentsStorage(S3Boto3Storage):
|
||||
location = "media/user_documents"
|
||||
default_acl = "private"
|
||||
|
||||
|
||||
class UserImagesStorage(S3Boto3Storage):
|
||||
location = "media/user_images"
|
||||
default_acl = "private"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Iterativ GmbH
|
||||
# http://www.iterativ.ch/
|
||||
#
|
||||
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
|
||||
#
|
||||
# Created on 2022-08-16
|
||||
# @author: lorenz.padberg@iterativ.ch
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import wagtail_factories
|
||||
|
||||
from vbv_lernwelt.media_files.models import ContentDocument, UserDocument
|
||||
|
||||
|
||||
class ContentDocumentFactory(wagtail_factories.DocumentFactory):
|
||||
link_display_text = "Dokument herunter laden"
|
||||
description = ""
|
||||
|
||||
class Meta:
|
||||
model = ContentDocument
|
||||
django_get_or_create = ("title", "description")
|
||||
|
||||
|
||||
class UserDocumentFactory(wagtail_factories.DocumentFactory):
|
||||
class Meta:
|
||||
model = UserDocument
|
||||
django_get_or_create = ("title",)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import datetime
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from wagtail.models import Collection
|
||||
|
||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.media_files.models import ContentDocument
|
||||
|
||||
TITLE = "Musterlösung Fahrzeug"
|
||||
|
||||
|
||||
class TestContentDocumentServing(TestCase):
|
||||
def setUp(self):
|
||||
create_default_users()
|
||||
now_str = str(datetime.datetime.now().strftime("%d-%m-%Y-%H-%M"))
|
||||
collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
|
||||
document = ContentDocument.objects.create(
|
||||
title=TITLE,
|
||||
display_text="Musterlösung Fahrzeug",
|
||||
description="Musterlösung für den Auftrag Fahrzeug",
|
||||
link_display_text="Dokument laden",
|
||||
file=SimpleUploadedFile(
|
||||
f"testdocument_{now_str}.txt", b"these are the file contents!"
|
||||
),
|
||||
collection=collection,
|
||||
)
|
||||
document.save()
|
||||
|
||||
def test_download_document_from_wagtail_logged_in_user_200(self):
|
||||
self.user = User.objects.get(username="admin")
|
||||
self.client.login(username="admin", password="test")
|
||||
document = ContentDocument.objects.get(title=TITLE)
|
||||
client = self.client
|
||||
|
||||
self.assertEqual(
|
||||
document.url, f"/server/documents/{document.id}/{document.filename}"
|
||||
)
|
||||
|
||||
response = client.get(document.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_download_document_from_wagtail_anonymous_user_redirect_to_login(self):
|
||||
document = ContentDocument.objects.get(title=TITLE)
|
||||
self.client.logout()
|
||||
response = self.client.get(document.url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue("login" in response.url)
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import datetime
|
||||
from unittest import skipIf
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import override_settings, TestCase
|
||||
from wagtail.models import Collection
|
||||
|
||||
from vbv_lernwelt.media_files.models import ContentDocument
|
||||
|
||||
TITLE = "Musterlösung Fahrzeug"
|
||||
|
||||
|
||||
class TestContentDocumentStorage(TestCase):
|
||||
@override_settings(FILE_UPLOAD_STORAGE="s3")
|
||||
def setUp(self):
|
||||
now_str = str(datetime.datetime.now().strftime("%d-%m-%Y-%H"))
|
||||
collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
|
||||
document = ContentDocument.objects.create(
|
||||
title=TITLE,
|
||||
display_text="Musterlösung Fahrzeug",
|
||||
description="Musterlösung für den Auftrag Fahrzeug",
|
||||
link_display_text="Dokument laden",
|
||||
file=SimpleUploadedFile(
|
||||
f"testdocument_{now_str}.txt", b"these are the file contents!"
|
||||
),
|
||||
collection=collection,
|
||||
)
|
||||
document.save()
|
||||
|
||||
def tearDown(self):
|
||||
for doc in ContentDocument.objects.all():
|
||||
doc.file.storage.delete(doc.file.name)
|
||||
doc.delete()
|
||||
|
||||
def test_new_document_is_created(self):
|
||||
self.assertEqual(ContentDocument.objects.all().count(), 1)
|
||||
self.assertEqual(ContentDocument.objects.filter(title=TITLE).count(), 1)
|
||||
|
||||
def test_document_exists_on_s3(self):
|
||||
document = ContentDocument.objects.get(title=TITLE)
|
||||
self.assertTrue(document.file.storage.exists(document.file.name))
|
||||
|
||||
def test_download_document_from_s3(self):
|
||||
document = ContentDocument.objects.get(title=TITLE)
|
||||
self.assertEqual(document.file.read(), b"these are the file contents!")
|
||||
|
||||
def test_delete_document_from_s3(self):
|
||||
document = ContentDocument.objects.get(title=TITLE)
|
||||
document.file.storage.delete(document.file.name)
|
||||
document.delete()
|
||||
self.assertFalse(document.file.storage.exists(document.file.name))
|
||||
|
||||
@skipIf(
|
||||
settings.AWS_S3_FILE_OVERWRITE,
|
||||
"This test only works if AWS_S3_FILE_OVERWRITE is False",
|
||||
)
|
||||
def test_duplicate_title_and_filename(self):
|
||||
collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
|
||||
document = ContentDocument.objects.create(
|
||||
title=TITLE,
|
||||
display_text="Musterlösung Fahrzeug",
|
||||
description="Musterlösung für den Auftrag Fahrzeug",
|
||||
link_display_text="Dokument laden",
|
||||
file=SimpleUploadedFile(
|
||||
"testdocument.txt", b"these are the file contents! For sure!"
|
||||
),
|
||||
collection=collection,
|
||||
)
|
||||
|
||||
document2 = ContentDocument.objects.create(
|
||||
title=TITLE,
|
||||
display_text="Musterlösung Fahrzeug",
|
||||
description="Musterlösung für den Auftrag Fahrzeug",
|
||||
link_display_text="Dokument laden",
|
||||
file=SimpleUploadedFile(
|
||||
"testdocument.txt", b"these are the file contents! But different!"
|
||||
),
|
||||
collection=collection,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
document.file.read(), b"these are the file contents! For sure!"
|
||||
)
|
||||
self.assertEqual(
|
||||
document2.file.read(), b"these are the file contents! But different!"
|
||||
)
|
||||
self.assertTrue(document.file.name != document2.file.name)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 627 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 638 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
|
|
@ -1,55 +0,0 @@
|
|||
import os
|
||||
|
||||
import factory
|
||||
from wagtail.core.models import Collection
|
||||
|
||||
from vbv_lernwelt.course.models import Course
|
||||
from vbv_lernwelt.media_library.models import LibraryDocument
|
||||
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||
LibraryDocumentFactory,
|
||||
)
|
||||
|
||||
|
||||
def create_default_collections():
|
||||
c = Collection.objects.all().delete()
|
||||
|
||||
root, created = Collection.objects.get_or_create(name="Root", depth=0)
|
||||
|
||||
for course in Course.objects.all():
|
||||
course_collection = root.add_child(name=course.title)
|
||||
for cat in course.coursecategory_set.all():
|
||||
cat_collection = course_collection.add_child(name=cat.title)
|
||||
|
||||
|
||||
def create_default_documents():
|
||||
LibraryDocument.objects.all().delete()
|
||||
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "../static/media/documents/"
|
||||
)
|
||||
|
||||
collection = Collection.objects.get(name="Fahrzeug")
|
||||
|
||||
filename = "SchweizerischesZivilgesetzbuch.pdf"
|
||||
document = LibraryDocumentFactory(
|
||||
title="V1 C25 ZGB CH",
|
||||
display_text="Schweizerisches Zivilgesetzbuch",
|
||||
description="Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.",
|
||||
link_display_text="Dokument laden",
|
||||
file=factory.django.FileField(
|
||||
from_path=os.path.join(path, filename), filename=filename
|
||||
),
|
||||
collection=collection,
|
||||
)
|
||||
|
||||
filename = "SmallPDF.pdf"
|
||||
document = LibraryDocumentFactory(
|
||||
title="V1 C25 ",
|
||||
display_text="Pdf showcase ",
|
||||
description="Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.",
|
||||
link_display_text="Dokument laden",
|
||||
file=factory.django.FileField(
|
||||
from_path=os.path.join(path, filename), filename=filename
|
||||
),
|
||||
collection=collection,
|
||||
)
|
||||
|
|
@ -106,19 +106,19 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
notification = Notification.objects.get(
|
||||
recipient__username=expected_recipient
|
||||
)
|
||||
self.assertEquals(action_object, notification.action_object)
|
||||
self.assertEquals("ASSIGNMENT_REMINDER", notification.notification_trigger)
|
||||
self.assertEquals("INFORMATION", notification.notification_category)
|
||||
self.assertEquals(EXPECTED_MEMBER_VERB, notification.verb)
|
||||
self.assertEqual(action_object, notification.action_object)
|
||||
self.assertEqual("ASSIGNMENT_REMINDER", notification.notification_trigger)
|
||||
self.assertEqual("INFORMATION", notification.notification_category)
|
||||
self.assertEqual(EXPECTED_MEMBER_VERB, notification.verb)
|
||||
|
||||
template_data = notification.data["template_data"]
|
||||
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
action_object.learning_content.get_parent_circle().title,
|
||||
template_data["circle"],
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
action_object.learning_content.get_frontend_url(),
|
||||
notification.target_url,
|
||||
)
|
||||
|
|
@ -140,17 +140,17 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
)
|
||||
|
||||
if assignment_type == AssignmentType.CASEWORK:
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER.name,
|
||||
email_template,
|
||||
)
|
||||
elif assignment_type == AssignmentType.PREP_ASSIGNMENT:
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER.name,
|
||||
email_template,
|
||||
)
|
||||
elif type(action_object) == CourseSessionEdoniqTest:
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER.name,
|
||||
email_template,
|
||||
)
|
||||
|
|
@ -176,7 +176,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
send_assignment_reminder_notifications()
|
||||
|
||||
# THEN
|
||||
self.assertEquals(3, len(Notification.objects.all()))
|
||||
self.assertEqual(3, len(Notification.objects.all()))
|
||||
self._assert_member_assignment_notifications(
|
||||
action_object=should_be_sent,
|
||||
expected_recipients=RECIPIENT_STUDENTS,
|
||||
|
|
@ -214,7 +214,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
send_assignment_reminder_notifications()
|
||||
|
||||
# THEN
|
||||
self.assertEquals(3, len(Notification.objects.all()))
|
||||
self.assertEqual(3, len(Notification.objects.all()))
|
||||
self._assert_member_assignment_notifications(
|
||||
action_object=casework,
|
||||
expected_recipients=RECIPIENT_STUDENTS,
|
||||
|
|
@ -236,16 +236,16 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
send_assignment_reminder_notifications()
|
||||
|
||||
# THEN
|
||||
self.assertEquals(1, len(Notification.objects.all()))
|
||||
self.assertEqual(1, len(Notification.objects.all()))
|
||||
|
||||
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
||||
self.assertEquals(casework, notification.action_object)
|
||||
self.assertEquals("INFORMATION", notification.notification_category)
|
||||
self.assertEquals(EXPECTED_EXPERT_VERB, notification.verb)
|
||||
self.assertEquals(
|
||||
self.assertEqual(casework, notification.action_object)
|
||||
self.assertEqual("INFORMATION", notification.notification_category)
|
||||
self.assertEqual(EXPECTED_EXPERT_VERB, notification.verb)
|
||||
self.assertEqual(
|
||||
casework.evaluation_deadline.url_expert, notification.target_url
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"CASEWORK_EXPERT_EVALUATION_REMINDER", notification.notification_trigger
|
||||
)
|
||||
|
||||
|
|
@ -276,7 +276,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
send_assignment_reminder_notifications()
|
||||
|
||||
# THEN
|
||||
self.assertEquals(3, len(Notification.objects.all()))
|
||||
self.assertEqual(3, len(Notification.objects.all()))
|
||||
self._assert_member_assignment_notifications(
|
||||
action_object=prep_assignment,
|
||||
expected_recipients=RECIPIENT_STUDENTS,
|
||||
|
|
|
|||
|
|
@ -65,52 +65,52 @@ class TestAttendanceCourseReminders(TestCase):
|
|||
|
||||
send_attendance_reminder_notifications()
|
||||
|
||||
self.assertEquals(4, len(Notification.objects.all()))
|
||||
self.assertEqual(4, len(Notification.objects.all()))
|
||||
notification = Notification.objects.get(
|
||||
recipient__username="test-student1@example.com"
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"Erinnerung: Bald findet ein Präsenzkurs statt",
|
||||
notification.verb,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"INFORMATION",
|
||||
notification.notification_category,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"ATTENDANCE_COURSE_REMINDER",
|
||||
notification.notification_trigger,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac,
|
||||
notification.action_object,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.course_session,
|
||||
notification.course_session,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
|
||||
notification.target_url,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.learning_content.title,
|
||||
notification.data["template_data"]["attendance_course"],
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.location,
|
||||
notification.data["template_data"]["location"],
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.trainer,
|
||||
notification.data["template_data"]["trainer"],
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"),
|
||||
notification.data["template_data"]["start"],
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"),
|
||||
notification.data["template_data"]["end"],
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue