VBV-440: Musterlösung und Refactoring S3
This commit is contained in:
parent
421a10524b
commit
e8ae8bdc14
|
|
@ -87,10 +87,14 @@ def main(app_name, image_name, environment_file):
|
||||||
"IT_DJANGO_SECRET_KEY": env.str(
|
"IT_DJANGO_SECRET_KEY": env.str(
|
||||||
"IT_DJANGO_SECRET_KEY", generate_random_string(63)
|
"IT_DJANGO_SECRET_KEY", generate_random_string(63)
|
||||||
),
|
),
|
||||||
"AWS_S3_ACCESS_KEY_ID": env.str("AWS_S3_ACCESS_KEY_ID", ""),
|
"AWS_S3_ACCESS_KEY_ID": env.str(
|
||||||
|
"AWS_S3_ACCESS_KEY_ID", "AKIAZJLREPUVWNBTJ5VY"
|
||||||
|
),
|
||||||
"AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""),
|
"AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""),
|
||||||
"AWS_S3_REGION_NAME": "eu-central-1",
|
"AWS_S3_REGION_NAME": env.str("AWS_S3_REGION_NAME", "eu-central-1"),
|
||||||
"AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch",
|
"AWS_STORAGE_BUCKET_NAME": env.str(
|
||||||
|
"AWS_STORAGE_BUCKET_NAME", "myvbv-dev.iterativ.ch"
|
||||||
|
),
|
||||||
"FILE_UPLOAD_STORAGE": "s3",
|
"FILE_UPLOAD_STORAGE": "s3",
|
||||||
"IT_DJANGO_DEBUG": "false",
|
"IT_DJANGO_DEBUG": "false",
|
||||||
"IT_SERVE_VUE": "false",
|
"IT_SERVE_VUE": "false",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const documents = {
|
||||||
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
|
"\n mutation UpsertAssignmentCompletion(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n $completionStatus: AssignmentCompletionStatus!\n $completionDataString: String!\n $evaluationPoints: Float\n $initializeCompletion: Boolean\n ) {\n upsert_assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n assignment_user_id: $assignmentUserId\n completion_status: $completionStatus\n completion_data_string: $completionDataString\n evaluation_points: $evaluationPoints\n initialize_completion: $initializeCompletion\n ) {\n assignment_completion {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_points\n completion_data\n task_completion_data\n }\n }\n }\n": types.UpsertAssignmentCompletionDocument,
|
||||||
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
|
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
|
||||||
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
|
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
|
||||||
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||||
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n enable_circle_documents\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
||||||
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n enable_circle_documents\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
||||||
|
|
@ -60,7 +60,7 @@ export function graphql(source: "\n query attendanceCheckQuery($courseSessionId
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"): (typeof documents)["\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"];
|
export function graphql(source: "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"): (typeof documents)["\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -486,6 +486,7 @@ type AssignmentObjectType implements CoursePageInterface {
|
||||||
max_points: Int
|
max_points: Int
|
||||||
learning_content: LearningContentInterface
|
learning_content: LearningContentInterface
|
||||||
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
|
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
|
||||||
|
solution_sample: ContentDocumentObjectType
|
||||||
}
|
}
|
||||||
|
|
||||||
"""An enumeration."""
|
"""An enumeration."""
|
||||||
|
|
@ -605,6 +606,15 @@ schema (one of the key benefits of GraphQL).
|
||||||
"""
|
"""
|
||||||
scalar JSONString
|
scalar JSONString
|
||||||
|
|
||||||
|
type ContentDocumentObjectType {
|
||||||
|
id: ID!
|
||||||
|
display_text: String!
|
||||||
|
description: String!
|
||||||
|
link_display_text: String!
|
||||||
|
thumbnail: String!
|
||||||
|
url: String
|
||||||
|
}
|
||||||
|
|
||||||
"""An enumeration."""
|
"""An enumeration."""
|
||||||
enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
|
enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
|
||||||
"""PRAXIS_ASSIGNMENT"""
|
"""PRAXIS_ASSIGNMENT"""
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"
|
||||||
export const CompetencePerformanceStatisticsSummaryType = "CompetencePerformanceStatisticsSummaryType";
|
export const CompetencePerformanceStatisticsSummaryType = "CompetencePerformanceStatisticsSummaryType";
|
||||||
export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
|
export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
|
||||||
export const CompetencesStatisticsType = "CompetencesStatisticsType";
|
export const CompetencesStatisticsType = "CompetencesStatisticsType";
|
||||||
|
export const ContentDocumentObjectType = "ContentDocumentObjectType";
|
||||||
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
||||||
export const CourseObjectType = "CourseObjectType";
|
export const CourseObjectType = "CourseObjectType";
|
||||||
export const CoursePageInterface = "CoursePageInterface";
|
export const CoursePageInterface = "CoursePageInterface";
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
|
||||||
tasks
|
tasks
|
||||||
title
|
title
|
||||||
translation_key
|
translation_key
|
||||||
|
solution_sample {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
competence_certificate {
|
competence_certificate {
|
||||||
...CoursePageFields
|
...CoursePageFields
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@
|
||||||
"performanceObjectivesTitle": "Leistungsziele",
|
"performanceObjectivesTitle": "Leistungsziele",
|
||||||
"showAssessmentDocument": "Bewertungsinstrument anzeigen",
|
"showAssessmentDocument": "Bewertungsinstrument anzeigen",
|
||||||
"submissionNotificationDisclaimer": "{{name}} wird deine Ergebnisse bewerten. Du wirst per Benachrichtigung informiert, sobald die Bewertung für dich freigegeben wurde.",
|
"submissionNotificationDisclaimer": "{{name}} wird deine Ergebnisse bewerten. Du wirst per Benachrichtigung informiert, sobald die Bewertung für dich freigegeben wurde.",
|
||||||
|
"submissionShowSampleSolution": "Musterlösung anzeigen",
|
||||||
|
"submissionShowSampleSolutionText": "Hier findest du eine mögliche Lösung zu deinen Aufgaben. Vorgehen und Prozesse in deiner Organisation können von dieser Lösung abweichen.",
|
||||||
"submitAssignment": "Ergebnisse abgeben",
|
"submitAssignment": "Ergebnisse abgeben",
|
||||||
"taskDefinition": "Bearbeite die Teilaufgaben und dokumentiere deine Ergebnisse.",
|
"taskDefinition": "Bearbeite die Teilaufgaben und dokumentiere deine Ergebnisse.",
|
||||||
"taskDefinitionTitle": "Aufgabenstellung",
|
"taskDefinitionTitle": "Aufgabenstellung",
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,14 @@ const onEditTask = (task: AssignmentTask) => {
|
||||||
emit("editTask", task);
|
emit("editTask", task);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openSolutionSample = () => {
|
||||||
|
const url = props.assignment.solution_sample?.url ?? "";
|
||||||
|
|
||||||
|
if (props.assignment.solution_sample) {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await upsertAssignmentCompletionMutation.executeMutation({
|
await upsertAssignmentCompletionMutation.executeMutation({
|
||||||
|
|
@ -119,14 +127,17 @@ const onSubmit = async () => {
|
||||||
bustItGetCache(
|
bustItGetCache(
|
||||||
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
|
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
|
||||||
);
|
);
|
||||||
eventBus.emit("finishedLearningContent", true);
|
// if solution sample is available, do not close the assigment automatically
|
||||||
|
if (!props.assignment.solution_sample) {
|
||||||
|
eventBus.emit("finishedLearningContent", true);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Could not submit assignment", error);
|
log.error("Could not submit assignment", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full border border-gray-400 p-8">
|
<div class="w-full border border-gray-400 p-8" data-cy="confirm-container">
|
||||||
<h3 class="heading-3 border-b border-gray-400 pb-6">
|
<h3 class="heading-3 border-b border-gray-400 pb-6">
|
||||||
{{ $t("assignment.submitAssignment") }}
|
{{ $t("assignment.submitAssignment") }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -202,6 +213,26 @@ const onSubmit = async () => {
|
||||||
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
|
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="props.assignment.solution_sample"
|
||||||
|
class="pt-2"
|
||||||
|
data-cy="show-sample-solution"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{ $t("assignment.submissionShowSampleSolutionText") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ItButton
|
||||||
|
class="mt-6"
|
||||||
|
variant="primary"
|
||||||
|
size="normal"
|
||||||
|
:disabled="false"
|
||||||
|
data-cy="show-sample-solution-button"
|
||||||
|
@click="openSolutionSample"
|
||||||
|
>
|
||||||
|
<p>{{ $t("assignment.submissionShowSampleSolution") }}</p>
|
||||||
|
</ItButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AssignmentSubmissionResponses
|
<AssignmentSubmissionResponses
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
import { TEST_STUDENT1_USER_ID } from "../../consts";
|
||||||
import { login } from "../helpers";
|
import { login } from "../helpers";
|
||||||
|
|
||||||
// Daniel: without this comment, my tool will reformat the login import out...
|
|
||||||
|
|
||||||
function completePraxisAssignment(selectExpert = false) {
|
function completePraxisAssignment(selectExpert = false) {
|
||||||
cy.visit("/course/test-lehrgang/learn/reisen/mein-kundenstamm");
|
cy.visit("/course/test-lehrgang/learn/reisen/mein-kundenstamm");
|
||||||
cy.learningContentMultiLayoutNextStep();
|
cy.learningContentMultiLayoutNextStep();
|
||||||
|
|
@ -326,16 +324,23 @@ describe("assignmentStudent.cy.js", () => {
|
||||||
cy.get('[data-cy="submit-assignment"]').click();
|
cy.get('[data-cy="submit-assignment"]').click();
|
||||||
cy.get('[data-cy="success-text"]').should("exist");
|
cy.get('[data-cy="success-text"]').should("exist");
|
||||||
|
|
||||||
// app goes back to circle view -> check if assignment is marked as completed
|
cy.get('[data-cy="confirm-container"]')
|
||||||
cy.url().should((url) => {
|
.find('[data-cy="show-sample-solution"]')
|
||||||
expect(url).to.match(/\/fahrzeug#lu-transfer?$/);
|
.then(($elements) => {
|
||||||
});
|
if ($elements.length > 0) {
|
||||||
cy.reload();
|
// Ist die Musterlösung da?
|
||||||
|
cy.get('[data-cy="show-sample-solution"]').should("exist");
|
||||||
|
cy.get('[data-cy="show-sample-solution-button"]').should("exist");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit("/course/test-lehrgang/learn/fahrzeug/");
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
|
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice-checkbox"]'
|
||||||
).should("have.class", "cy-checked");
|
).should("have.class", "cy-checked");
|
||||||
|
|
||||||
// reopening page should get directly to last step
|
//reopening page should get directly to last step
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
|
||||||
"/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
"/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
||||||
"/Users/eliabieri/iterativ/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
"/Users/eliabieri/iterativ/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
||||||
"/Users/christiancueni/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
"/Users/christiancueni/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
||||||
|
"/Users/renzo/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin",
|
||||||
];
|
];
|
||||||
let bashCommand = `PATH=${pythonPaths.join(":")}:$PATH && ${execCommand}`;
|
let bashCommand = `PATH=${pythonPaths.join(":")}:$PATH && ${execCommand}`;
|
||||||
return cy
|
return cy
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Files handling
|
||||||
|
|
||||||
|
This document describes how files are handled in this appication.
|
||||||
|
|
||||||
|
# Types of files
|
||||||
|
|
||||||
|
static files: files that are not changed by the application, e.g. images, fonts, etc.¨
|
||||||
|
|
||||||
|
### content documents:
|
||||||
|
|
||||||
|
Files that belong to the content and are managed by the content editors in the CMS (pdf, excel, word, etc.)
|
||||||
|
|
||||||
|
### user documents:
|
||||||
|
|
||||||
|
Files that are uploaded by the users (pdf, etc.). Therefore not visible in the CMS.
|
||||||
|
Images are handled seprately from documents since images require additional processing (resizing, cropping, etc.).
|
||||||
|
Visible in the django admin.
|
||||||
|
|
||||||
|
### content images:
|
||||||
|
|
||||||
|
Images that belong to the content and are managed by the content editors in the CMS.
|
||||||
|
|
||||||
|
### user images:
|
||||||
|
|
||||||
|
Images that are uploaded by the users. Therefore not visible in the CMS. Visible in the django admin.
|
||||||
|
|
||||||
|
## Static files
|
||||||
|
|
||||||
|
These files are publicly served on S3.
|
||||||
|
|
||||||
|
## Content documents
|
||||||
|
|
||||||
|
These files are part of the content. Such as a pdf thas cointains additional information to a course.
|
||||||
|
These files are not publicly available. The content files are uploaded by the editors in the wagtail cms.
|
||||||
|
|
||||||
|
https://www.hacksoft.io/blog/direct-to-s3-file-upload-with-django
|
||||||
|
|
||||||
|
Django handles the permissions to these files. Via a view django checks if the user has permissions to access the file,
|
||||||
|
and gerates a temporary url that is valid for a limited time. Still the documents are served by django. This done for
|
||||||
|
usability reasons. The user sees the url mydomain.com/media/documents/<document-id> and not a url to S3. Therefore the
|
||||||
|
user can share the url with other users. (still they need to login and have the permissions to access the file)
|
||||||
|
|
||||||
|
The downside of this is that the django server processes these files. (could be circumvented by django-sendfile).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- These Files are handled stored as wagtail documents. As a model and the file itself is stored in S3.
|
||||||
|
|
||||||
|
### Frontend access to content documents
|
||||||
|
|
||||||
|
For the frontend django generates a fixed url per file /media/documents/<document-id>
|
||||||
|
|
||||||
|
When the frontend requests this file, django checks if the user has permissions to access the file.
|
||||||
|
If so, django generates a temporary url that is valid for a limited time. Then sends a redirect to the frontend.
|
||||||
|
|
||||||
|
In this waz the frontend does not need to know about the permissions. Content grapql can be cached if needed and urls
|
||||||
|
can be shared by the users.
|
||||||
|
|
||||||
|
content_documents
|
||||||
|
user_documents
|
||||||
|
|
||||||
|
public files
|
||||||
|
|
||||||
|
## User documents
|
||||||
|
|
||||||
|
- User uploaded files are stored in S3. but the permissions is handled by django. Same process as content files.
|
||||||
|
|
||||||
|
Same process as content files. But the url is /media/user-uploads/<file-id>
|
||||||
|
And the files are not managed by Wagtail. Due to another model, they are not visible to the user in the CMS.
|
||||||
|
|
||||||
|
## Content images
|
||||||
|
|
||||||
|
Content Images are served directly from S3. The permissions are handled by dja
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -18,8 +18,7 @@ def main():
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
settings.DEBUG = True
|
settings.DEBUG = True
|
||||||
from django.db import connection
|
from django.db import connection, reset_queries
|
||||||
from django.db import reset_queries
|
|
||||||
|
|
||||||
reset_queries()
|
reset_queries()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,15 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
from vbv_lernwelt.core.schema import Query
|
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.core.schema import Query
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
settings.DEBUG = True
|
settings.DEBUG = True
|
||||||
from django.db import connection
|
from django.db import connection, reset_queries
|
||||||
from django.db import reset_queries
|
|
||||||
|
|
||||||
reset_queries()
|
reset_queries()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
from vbv_lernwelt.notify.email.email_services import (
|
from vbv_lernwelt.notify.email.email_services import (
|
||||||
|
create_template_data_from_course_session_attendance_course,
|
||||||
EmailTemplate,
|
EmailTemplate,
|
||||||
send_email,
|
send_email,
|
||||||
create_template_data_from_course_session_attendance_course,
|
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ THIRD_PARTY_APPS = [
|
||||||
|
|
||||||
LOCAL_APPS = [
|
LOCAL_APPS = [
|
||||||
"vbv_lernwelt.core",
|
"vbv_lernwelt.core",
|
||||||
|
"vbv_lernwelt.media_files",
|
||||||
"vbv_lernwelt.sso",
|
"vbv_lernwelt.sso",
|
||||||
"vbv_lernwelt.course",
|
"vbv_lernwelt.course",
|
||||||
"vbv_lernwelt.learnpath",
|
"vbv_lernwelt.learnpath",
|
||||||
|
|
@ -212,23 +213,13 @@ STATICFILES_FINDERS = [
|
||||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
]
|
]
|
||||||
|
|
||||||
USE_AWS = env("USE_AWS", False)
|
|
||||||
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", "")
|
|
||||||
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", "")
|
|
||||||
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", "")
|
|
||||||
AWS_S3_CUSTOM_DOMAIN = "%s.s3.amazonaws.com" % AWS_STORAGE_BUCKET_NAME
|
|
||||||
|
|
||||||
# MEDIA
|
# MEDIA
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
||||||
MEDIA_ROOT = str(APPS_DIR / "media")
|
MEDIA_ROOT = str(APPS_DIR / "media")
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
||||||
if USE_AWS:
|
|
||||||
# https://wagtail.org/blog/amazon-s3-for-media-files/
|
MEDIA_URL = "/server/media/"
|
||||||
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
|
|
||||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
|
||||||
else:
|
|
||||||
MEDIA_URL = "/server/media/"
|
|
||||||
|
|
||||||
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
|
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
|
||||||
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
|
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
|
||||||
|
|
@ -252,7 +243,19 @@ WAGTAIL_ENABLE_UPDATE_CHECK = False
|
||||||
WAGTAIL_ENABLE_WHATS_NEW_BANNER = False
|
WAGTAIL_ENABLE_WHATS_NEW_BANNER = False
|
||||||
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES
|
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES
|
||||||
|
|
||||||
WAGTAILDOCS_DOCUMENT_MODEL = "media_library.LibraryDocument"
|
WAGTAILDOCS_DOCUMENT_MODEL = "media_files.ContentDocument"
|
||||||
|
WAGTAILIMAGES_IMAGE_MODEL = "media_files.ContentImage"
|
||||||
|
|
||||||
|
# this setting makes that the document is served by django, and the url is the django url.
|
||||||
|
# https://docs.wagtail.org/en/stable/reference/settings.html#wagtaildocs-serve-method
|
||||||
|
# The file is served by django as streaming response. If it should be serverd by nginx, then install django sendfile
|
||||||
|
WAGTAILDOCS_SERVE_METHOD = "serve_view"
|
||||||
|
# WAGTAILDOCS_INLINE_CONTENT_TYPES = ['application/pdf', 'text/plain']
|
||||||
|
|
||||||
|
|
||||||
|
WAGTAILIMAGES_MAX_UPLOAD_SIZE = 20 * 1024 * 1024 # 20MB
|
||||||
|
# WAGTAILIMAGES_RENDITION_STORAGE = 'myapp.backends.MyCustomStorage'
|
||||||
|
|
||||||
|
|
||||||
WAGTAILADMIN_RICH_TEXT_EDITORS = {
|
WAGTAILADMIN_RICH_TEXT_EDITORS = {
|
||||||
"default": {
|
"default": {
|
||||||
|
|
@ -645,7 +648,7 @@ NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
|
||||||
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
||||||
|
|
||||||
# S3 BUCKET CONFIGURATION
|
# S3 BUCKET CONFIGURATION
|
||||||
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="local") # local | s3
|
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="s3") # local | s3
|
||||||
|
|
||||||
if FILE_UPLOAD_STORAGE == "local":
|
if FILE_UPLOAD_STORAGE == "local":
|
||||||
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=5242880)
|
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=5242880)
|
||||||
|
|
@ -654,18 +657,19 @@ if FILE_UPLOAD_STORAGE == "s3":
|
||||||
# Using django-storages
|
# Using django-storages
|
||||||
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html
|
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html
|
||||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||||
|
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID", default="AKIAZJLREPUVWNBTJ5VY")
|
||||||
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID")
|
|
||||||
AWS_S3_SECRET_ACCESS_KEY = env("AWS_S3_SECRET_ACCESS_KEY")
|
AWS_S3_SECRET_ACCESS_KEY = env("AWS_S3_SECRET_ACCESS_KEY")
|
||||||
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
|
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", "eu-central-1")
|
||||||
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME")
|
|
||||||
AWS_S3_SIGNATURE_VERSION = env("AWS_S3_SIGNATURE_VERSION", default="s3v4")
|
AWS_S3_SIGNATURE_VERSION = env("AWS_S3_SIGNATURE_VERSION", default="s3v4")
|
||||||
|
AWS_STORAGE_BUCKET_NAME = env(
|
||||||
|
"AWS_STORAGE_BUCKET_NAME", default="myvbv-dev.iterativ.ch"
|
||||||
|
)
|
||||||
|
AWS_S3_FILE_OVERWRITE = env("AWS_S3_FILE_OVERWRITE", False)
|
||||||
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=20971520) # 20MB
|
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=20971520) # 20MB
|
||||||
|
|
||||||
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||||
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="private")
|
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="private")
|
||||||
|
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=7200) # seconds
|
||||||
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=300) # seconds
|
|
||||||
|
|
||||||
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = (
|
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = (
|
||||||
"jpg",
|
"jpg",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||||
|
|
||||||
from .base import * # noqa
|
from .base import * # noqa
|
||||||
|
|
@ -8,6 +9,7 @@ from .base import * # noqa
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
|
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
|
||||||
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||||
|
|
||||||
|
# Select faster password hasher during tests
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
|
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
|
||||||
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
||||||
|
|
||||||
|
|
@ -15,16 +17,7 @@ PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||||
|
|
||||||
WHITENOISE_MANIFEST_STRICT = False
|
WHITENOISE_MANIFEST_STRICT = False
|
||||||
|
AWS_S3_FILE_OVERWRITE = True
|
||||||
# Dummy data
|
|
||||||
AWS_S3_ACCESS_KEY_ID = "SOMEKEY"
|
|
||||||
AWS_S3_SECRET_ACCESS_KEY = "SOMEACCESSKEY"
|
|
||||||
AWS_STORAGE_BUCKET_NAME = "myvbv-dev.iterativ.ch"
|
|
||||||
AWS_S3_REGION_NAME = "eu-central-1"
|
|
||||||
AWS_S3_SIGNATURE_VERSION = "s3v4"
|
|
||||||
FILE_MAX_SIZE = 20971520 # 20MB
|
|
||||||
AWS_DEFAULT_ACL = "private"
|
|
||||||
AWS_PRESIGNED_EXPIRY = 300
|
|
||||||
|
|
||||||
|
|
||||||
class DisableMigrations(dict):
|
class DisableMigrations(dict):
|
||||||
|
|
@ -36,8 +29,3 @@ class DisableMigrations(dict):
|
||||||
|
|
||||||
|
|
||||||
MIGRATION_MODULES = DisableMigrations()
|
MIGRATION_MODULES = DisableMigrations()
|
||||||
|
|
||||||
# Select faster password hasher during tests
|
|
||||||
PASSWORD_HASHERS = [
|
|
||||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||||
|
os.environ["AWS_S3_SECRET_ACCESS_KEY"] = os.environ.get(
|
||||||
|
"AWS_S3_SECRET_ACCESS_KEY",
|
||||||
|
"!!!default_for_quieting_cypress_within_pycharm!!!",
|
||||||
|
)
|
||||||
|
|
||||||
from .base import * # noqa
|
from .base import * # noqa
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ from vbv_lernwelt.importer.views import (
|
||||||
from vbv_lernwelt.notify.views import email_notification_settings
|
from vbv_lernwelt.notify.views import email_notification_settings
|
||||||
from wagtail import urls as wagtail_urls
|
from wagtail import urls as wagtail_urls
|
||||||
from wagtail.admin import urls as wagtailadmin_urls
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
from wagtail.documents import urls as wagtaildocs_urls
|
from wagtail.documents import urls as media_library_urls
|
||||||
|
|
||||||
|
|
||||||
class SignedIntConverter(IntConverter):
|
class SignedIntConverter(IntConverter):
|
||||||
|
|
@ -88,7 +88,7 @@ urlpatterns = [
|
||||||
|
|
||||||
# wagtail urls
|
# wagtail urls
|
||||||
path('server/cms/', include(wagtailadmin_urls)),
|
path('server/cms/', include(wagtailadmin_urls)),
|
||||||
path('server/documents/', include(wagtaildocs_urls)),
|
path('server/documents/', include(media_library_urls)),
|
||||||
path('server/pages/', include(wagtail_urls)),
|
path('server/pages/', include(wagtail_urls)),
|
||||||
|
|
||||||
# core
|
# core
|
||||||
|
|
@ -132,6 +132,7 @@ urlpatterns = [
|
||||||
name="request_assignment_completion_status"),
|
name="request_assignment_completion_status"),
|
||||||
|
|
||||||
# documents
|
# documents
|
||||||
|
# TODO: remfactor to files app
|
||||||
path(r'api/core/document/start/', document_upload_start,
|
path(r'api/core/document/start/', document_upload_start,
|
||||||
name='file_upload_start'),
|
name='file_upload_start'),
|
||||||
path(r'api/core/document/<str:document_id>/', document_delete,
|
path(r'api/core/document/<str:document_id>/', document_delete,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ mypy # https://github.com/python/mypy
|
||||||
django-stubs # https://github.com/typeddjango/django-stubs
|
django-stubs # https://github.com/typeddjango/django-stubs
|
||||||
pytest # https://github.com/pytest-dev/pytest
|
pytest # https://github.com/pytest-dev/pytest
|
||||||
pytest-sugar # https://github.com/Frozenball/pytest-sugar
|
pytest-sugar # https://github.com/Frozenball/pytest-sugar
|
||||||
|
pytest-xdist #
|
||||||
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs
|
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,7 @@ azure-core==1.29.1
|
||||||
azure-identity==1.14.0
|
azure-identity==1.14.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
azure-storage-blob==12.17.0
|
azure-storage-blob==12.17.0
|
||||||
# via
|
# via -r requirements.in
|
||||||
# -r requirements.in
|
|
||||||
# django-storages
|
|
||||||
backcall==0.2.0
|
backcall==0.2.0
|
||||||
# via ipython
|
# via ipython
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
|
|
@ -176,7 +174,7 @@ django-ratelimit==4.1.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-redis==5.3.0
|
django-redis==5.3.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-storages[azure]==1.13.2
|
django-storages==1.13.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-stubs==4.2.3
|
django-stubs==4.2.3
|
||||||
# via
|
# via
|
||||||
|
|
@ -209,6 +207,8 @@ exceptiongroup==1.1.2
|
||||||
# via
|
# via
|
||||||
# anyio
|
# anyio
|
||||||
# pytest
|
# pytest
|
||||||
|
execnet==2.0.2
|
||||||
|
# via pytest-xdist
|
||||||
executing==1.2.0
|
executing==1.2.0
|
||||||
# via stack-data
|
# via stack-data
|
||||||
factory-boy==3.3.0
|
factory-boy==3.3.0
|
||||||
|
|
@ -397,7 +397,9 @@ pyflakes==3.1.0
|
||||||
pygments==2.16.1
|
pygments==2.16.1
|
||||||
# via ipython
|
# via ipython
|
||||||
pyjwt[crypto]==2.8.0
|
pyjwt[crypto]==2.8.0
|
||||||
# via msal
|
# via
|
||||||
|
# msal
|
||||||
|
# pyjwt
|
||||||
pylint==2.17.5
|
pylint==2.17.5
|
||||||
# via
|
# via
|
||||||
# pylint-django
|
# pylint-django
|
||||||
|
|
@ -415,10 +417,13 @@ pytest==7.4.0
|
||||||
# -r requirements-dev.in
|
# -r requirements-dev.in
|
||||||
# pytest-django
|
# pytest-django
|
||||||
# pytest-sugar
|
# pytest-sugar
|
||||||
|
# pytest-xdist
|
||||||
pytest-django==4.5.2
|
pytest-django==4.5.2
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
pytest-sugar==0.9.7
|
pytest-sugar==0.9.7
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
|
pytest-xdist==3.5.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
|
|
@ -616,7 +621,9 @@ wheel==0.41.1
|
||||||
whitenoise[brotli]==6.5.0
|
whitenoise[brotli]==6.5.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
willow[heif]==1.6.1
|
willow[heif]==1.6.1
|
||||||
# via wagtail
|
# via
|
||||||
|
# wagtail
|
||||||
|
# willow
|
||||||
wrapt==1.15.0
|
wrapt==1.15.0
|
||||||
# via astroid
|
# via astroid
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
pytest --junitxml=../test-reports/coverage.xml
|
|
||||||
|
# limit test to 6 parallel processes, otherwise ratelimit of s3 could be hit
|
||||||
|
pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
coverage run -m pytest --junitxml=../test-reports/coverage.xml $1
|
coverage run -m pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml $1
|
||||||
|
|
||||||
coverage_python=`coverage report -m | tail -n1 | awk '{print $4}'`
|
coverage_python=`coverage report -m | tail -n1 | awk '{print $4}'`
|
||||||
commit=`git rev-parse HEAD`
|
commit=`git rev-parse HEAD`
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from vbv_lernwelt.course.consts import (
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.models import CoursePage
|
from vbv_lernwelt.course.models import CoursePage
|
||||||
|
from vbv_lernwelt.media_files.models import ContentDocument
|
||||||
from wagtail.blocks import StreamValue
|
from wagtail.blocks import StreamValue
|
||||||
from wagtail.blocks.list_block import ListBlock, ListValue
|
from wagtail.blocks.list_block import ListBlock, ListValue
|
||||||
from wagtail.rich_text import RichText
|
from wagtail.rich_text import RichText
|
||||||
|
|
@ -39,6 +40,7 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
|
||||||
needs_expert_evaluation=True,
|
needs_expert_evaluation=True,
|
||||||
competence_certificate=competence_certificate,
|
competence_certificate=competence_certificate,
|
||||||
effort_required="ca. 5 Stunden",
|
effort_required="ca. 5 Stunden",
|
||||||
|
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
|
||||||
intro_text=replace_whitespace(
|
intro_text=replace_whitespace(
|
||||||
"""
|
"""
|
||||||
<h3>Ausgangslage</h3>
|
<h3>Ausgangslage</h3>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
||||||
from vbv_lernwelt.course.models import CourseSession
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
|
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
|
||||||
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
||||||
|
from vbv_lernwelt.media_files.graphql.types import ContentDocumentObjectType
|
||||||
|
|
||||||
|
|
||||||
class AssignmentCompletionObjectType(DjangoObjectType):
|
class AssignmentCompletionObjectType(DjangoObjectType):
|
||||||
|
|
@ -52,6 +53,7 @@ class AssignmentObjectType(DjangoObjectType):
|
||||||
learning_content_page_id=graphene.ID(required=False),
|
learning_content_page_id=graphene.ID(required=False),
|
||||||
assignment_user_id=graphene.UUID(required=False),
|
assignment_user_id=graphene.UUID(required=False),
|
||||||
)
|
)
|
||||||
|
solution_sample = graphene.Field(ContentDocumentObjectType)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Assignment
|
model = Assignment
|
||||||
|
|
@ -67,6 +69,9 @@ class AssignmentObjectType(DjangoObjectType):
|
||||||
"competence_certificate",
|
"competence_certificate",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def resolve_solution_sample(self, info):
|
||||||
|
return self.solution_sample
|
||||||
|
|
||||||
def resolve_max_points(self, info):
|
def resolve_max_points(self, info):
|
||||||
return self.get_max_points()
|
return self.get_max_points()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-12-05 16:15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("media_files", "0001_initial"),
|
||||||
|
("assignment", "0010_assignmentcompletion_edoniq_extended_time_flag"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignment",
|
||||||
|
name="solution_sample",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Musterlösung",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="media_files.contentdocument",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -200,12 +200,22 @@ class Assignment(CourseBasePage):
|
||||||
help_text="Beurteilungsschritte",
|
help_text="Beurteilungsschritte",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
solution_sample = models.ForeignKey(
|
||||||
|
"media_files.ContentDocument",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
help_text="Musterlösung",
|
||||||
|
)
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("assignment_type"),
|
FieldPanel("assignment_type"),
|
||||||
FieldPanel("needs_expert_evaluation"),
|
FieldPanel("needs_expert_evaluation"),
|
||||||
PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"),
|
PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"),
|
||||||
FieldPanel("intro_text"),
|
FieldPanel("intro_text"),
|
||||||
FieldPanel("effort_required"),
|
FieldPanel("effort_required"),
|
||||||
|
FieldPanel("solution_sample"),
|
||||||
FieldPanel("performance_objectives"),
|
FieldPanel("performance_objectives"),
|
||||||
FieldPanel("tasks"),
|
FieldPanel("tasks"),
|
||||||
FieldPanel("evaluation_description"),
|
FieldPanel("evaluation_description"),
|
||||||
|
|
|
||||||
|
|
@ -106,14 +106,14 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
task_data = data["task_completion_data"][task_id]
|
task_data = data["task_completion_data"][task_id]
|
||||||
self.assertDictEqual(
|
self.maxDiff = None
|
||||||
task_data,
|
self.assertEqual(task_data["user_data"]["fileId"], file_id)
|
||||||
{
|
self.assertEqual(task_data["user_data"]["fileInfo"]["id"], file_id)
|
||||||
"user_data": {
|
self.assertEqual(task_data["user_data"]["fileInfo"]["name"], "file.txt")
|
||||||
"fileId": file_id,
|
self.assertTrue(
|
||||||
"fileInfo": {"id": file_id, "name": "file.txt", "url": file_url},
|
task_data["user_data"]["fileInfo"]["url"].startswith(
|
||||||
}
|
"https://s3.eu-central-1.amazonaws.com/myvbv-dev.iterativ.ch"
|
||||||
},
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# check DB data
|
# check DB data
|
||||||
|
|
@ -194,31 +194,31 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
||||||
# check notification
|
# check notification
|
||||||
self.assertEqual(Notification.objects.count(), 1)
|
self.assertEqual(Notification.objects.count(), 1)
|
||||||
notification = Notification.objects.first()
|
notification = Notification.objects.first()
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben.",
|
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben.",
|
||||||
notification.verb,
|
notification.verb,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"test-trainer1@example.com",
|
"test-trainer1@example.com",
|
||||||
notification.recipient.email,
|
notification.recipient.email,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"test-student1@example.com",
|
"test-student1@example.com",
|
||||||
notification.actor.email,
|
notification.actor.email,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"USER_INTERACTION",
|
"USER_INTERACTION",
|
||||||
notification.notification_category,
|
notification.notification_category,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"CASEWORK_SUBMITTED",
|
"CASEWORK_SUBMITTED",
|
||||||
notification.notification_trigger,
|
notification.notification_trigger,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.action_object,
|
notification.action_object,
|
||||||
db_entry,
|
db_entry,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.course_session,
|
notification.course_session,
|
||||||
self.course_session,
|
self.course_session,
|
||||||
)
|
)
|
||||||
|
|
@ -422,35 +422,35 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
||||||
# check notification
|
# check notification
|
||||||
self.assertEqual(Notification.objects.count(), 1)
|
self.assertEqual(Notification.objects.count(), 1)
|
||||||
notification = Notification.objects.first()
|
notification = Notification.objects.first()
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"Test Trainer1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» bewertet.",
|
"Test Trainer1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» bewertet.",
|
||||||
notification.verb,
|
notification.verb,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"test-student1@example.com",
|
"test-student1@example.com",
|
||||||
notification.recipient.email,
|
notification.recipient.email,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"test-trainer1@example.com",
|
"test-trainer1@example.com",
|
||||||
notification.actor.email,
|
notification.actor.email,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"USER_INTERACTION",
|
"USER_INTERACTION",
|
||||||
notification.notification_category,
|
notification.notification_category,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"CASEWORK_EVALUATED",
|
"CASEWORK_EVALUATED",
|
||||||
notification.notification_trigger,
|
notification.notification_trigger,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.action_object,
|
notification.action_object,
|
||||||
db_entry,
|
db_entry,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.course_session,
|
notification.course_session,
|
||||||
self.course_session,
|
self.course_session,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
notification.target_url,
|
notification.target_url,
|
||||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice",
|
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,12 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
LearningUnitFactory,
|
LearningUnitFactory,
|
||||||
TopicFactory,
|
TopicFactory,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.media_files.create_default_documents import (
|
||||||
|
create_default_collections,
|
||||||
|
create_default_content_documents,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||||
|
from vbv_lernwelt.media_files.models import ContentDocument, ContentImage, UserImage
|
||||||
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||||
MediaLibraryCategoryPageFactory,
|
MediaLibraryCategoryPageFactory,
|
||||||
MediaLibraryContentPageFactory,
|
MediaLibraryContentPageFactory,
|
||||||
|
|
@ -92,6 +98,11 @@ from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||||
|
|
||||||
def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
# create_locales_for_wagtail()
|
# create_locales_for_wagtail()
|
||||||
|
create_default_collections()
|
||||||
|
create_default_content_documents()
|
||||||
|
if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
|
||||||
|
create_default_images()
|
||||||
|
|
||||||
course = create_test_course_with_categories()
|
course = create_test_course_with_categories()
|
||||||
competence_certificate = create_test_competence_navi()
|
competence_certificate = create_test_competence_navi()
|
||||||
|
|
||||||
|
|
@ -523,6 +534,14 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
||||||
slug__startswith=f"test-lehrgang-assignment-reflexion"
|
slug__startswith=f"test-lehrgang-assignment-reflexion"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
assignment = Assignment.objects.get(
|
||||||
|
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
|
||||||
|
)
|
||||||
|
assignment.solution_sample = ContentDocument.objects.get(
|
||||||
|
title="Musterlösung Fahrzeug"
|
||||||
|
)
|
||||||
|
assignment.save()
|
||||||
LearningContentAssignmentFactory(
|
LearningContentAssignmentFactory(
|
||||||
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
|
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
|
||||||
parent=circle,
|
parent=circle,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
LearningUnitFactory,
|
LearningUnitFactory,
|
||||||
TopicFactory,
|
TopicFactory,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.media_files.create_default_documents import (
|
||||||
|
create_default_collections,
|
||||||
|
create_default_content_documents,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||||
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||||
LearnMediaBlockFactory,
|
LearnMediaBlockFactory,
|
||||||
)
|
)
|
||||||
|
|
@ -40,6 +45,7 @@ def create_uk_learning_path(course_id=COURSE_UK, user=None, skip_locales=True):
|
||||||
user = User.objects.get(username="info@iterativ.ch")
|
user = User.objects.get(username="info@iterativ.ch")
|
||||||
|
|
||||||
course_page = CoursePage.objects.get(course_id=course_id)
|
course_page = CoursePage.objects.get(course_id=course_id)
|
||||||
|
|
||||||
lp = LearningPathFactory(
|
lp = LearningPathFactory(
|
||||||
title="Lernpfad",
|
title="Lernpfad",
|
||||||
parent=course_page,
|
parent=course_page,
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,12 @@ from vbv_lernwelt.learnpath.models import (
|
||||||
LearningContentAssignment,
|
LearningContentAssignment,
|
||||||
LearningContentAttendanceCourse,
|
LearningContentAttendanceCourse,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.media_files.create_default_documents import (
|
||||||
|
create_default_collections,
|
||||||
|
create_default_content_documents,
|
||||||
|
create_default_user_documents,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||||
from vbv_lernwelt.media_library.create_default_media_library import (
|
from vbv_lernwelt.media_library.create_default_media_library import (
|
||||||
create_default_media_library,
|
create_default_media_library,
|
||||||
)
|
)
|
||||||
|
|
@ -128,6 +134,11 @@ ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
|
||||||
def command(course):
|
def command(course):
|
||||||
print("Creating default courses", course)
|
print("Creating default courses", course)
|
||||||
|
|
||||||
|
create_default_collections()
|
||||||
|
create_default_content_documents()
|
||||||
|
create_default_user_documents()
|
||||||
|
create_default_images()
|
||||||
|
|
||||||
if COURSE_VERSICHERUNGSVERMITTLERIN_ID in course:
|
if COURSE_VERSICHERUNGSVERMITTLERIN_ID in course:
|
||||||
create_versicherungsvermittlerin_course()
|
create_versicherungsvermittlerin_course()
|
||||||
|
|
||||||
|
|
@ -285,6 +296,9 @@ def create_course_uk_de(course_id=COURSE_UK, lang="de"):
|
||||||
create_uk_competence_profile(course_id=course_id)
|
create_uk_competence_profile(course_id=course_id)
|
||||||
create_default_media_library(course_id=course_id)
|
create_default_media_library(course_id=course_id)
|
||||||
|
|
||||||
|
create_default_collections()
|
||||||
|
create_default_content_documents()
|
||||||
|
|
||||||
|
|
||||||
def create_course_uk_de_course_sessions():
|
def create_course_uk_de_course_sessions():
|
||||||
course = Course.objects.get(id=COURSE_UK)
|
course = Course.objects.get(id=COURSE_UK)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.models import UserDocument, UserImage
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserDocument)
|
||||||
|
class UserDocumentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"title",
|
||||||
|
"file",
|
||||||
|
"created_at",
|
||||||
|
"uploaded_by_user",
|
||||||
|
"file_size",
|
||||||
|
"file_hash",
|
||||||
|
)
|
||||||
|
search_fields = ("title", "uploaded_by_user__username", "tags__name")
|
||||||
|
list_filter = ("created_at", "uploaded_by_user")
|
||||||
|
autocomplete_fields = ["uploaded_by_user"]
|
||||||
|
date_hierarchy = "created_at"
|
||||||
|
readonly_fields = (
|
||||||
|
"file_size",
|
||||||
|
"file_hash",
|
||||||
|
"created_at",
|
||||||
|
"uploaded_by_user",
|
||||||
|
"file",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserImage)
|
||||||
|
class UserImageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"title",
|
||||||
|
"file",
|
||||||
|
"created_at",
|
||||||
|
"uploaded_by_user",
|
||||||
|
"file_size",
|
||||||
|
)
|
||||||
|
search_fields = ("title", "uploaded_by_user__username")
|
||||||
|
list_filter = ("created_at", "uploaded_by_user")
|
||||||
|
autocomplete_fields = ["uploaded_by_user"]
|
||||||
|
date_hierarchy = "created_at"
|
||||||
|
readonly_fields = (
|
||||||
|
"file_size",
|
||||||
|
"file_hash",
|
||||||
|
"created_at",
|
||||||
|
"uploaded_by_user",
|
||||||
|
"file",
|
||||||
|
"tags",
|
||||||
|
"title",
|
||||||
|
"focal_point_x",
|
||||||
|
"focal_point_y",
|
||||||
|
"focal_point_width",
|
||||||
|
"focal_point_height",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MediaLibraryConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "vbv_lernwelt.media_files"
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import factory
|
||||||
|
from django.conf import settings
|
||||||
|
from wagtail.models import Collection
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.models import ContentDocument, UserDocument
|
||||||
|
from vbv_lernwelt.media_files.tests.media_library_factories import (
|
||||||
|
ContentDocumentFactory,
|
||||||
|
UserDocumentFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_default_documents():
|
||||||
|
"""deletes all documents"""
|
||||||
|
if "prod" in settings.APP_ENVIRONMENT:
|
||||||
|
raise Exception("This command must not be used in production environment")
|
||||||
|
|
||||||
|
ContentDocument.objects.all().delete()
|
||||||
|
UserDocument.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_collections():
|
||||||
|
root, created = Collection.objects.get_or_create(name="Root", depth=0)
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_content_documents():
|
||||||
|
"""creates a default document for testing purposes"""
|
||||||
|
|
||||||
|
path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "./tests/test_documents/"
|
||||||
|
)
|
||||||
|
|
||||||
|
filename = "Vermittler_Motorfahrzeug_Versicherung_Musterlösung.pdf"
|
||||||
|
document = ContentDocumentFactory(
|
||||||
|
title="Musterlösung Fahrzeug",
|
||||||
|
display_text="Musterlösung Fahrzeug",
|
||||||
|
description="Musterlösung für den Auftrag Fahrzeug",
|
||||||
|
link_display_text="Dokument laden",
|
||||||
|
file=factory.django.FileField(
|
||||||
|
from_path=os.path.join(path, filename), filename=filename
|
||||||
|
),
|
||||||
|
)
|
||||||
|
document.tags.set(("Fahrzeug", "Musterlösung", "Vermittler"))
|
||||||
|
document.save()
|
||||||
|
|
||||||
|
filename = "TestExcelSheet.xlsx"
|
||||||
|
document = ContentDocumentFactory(
|
||||||
|
title="Mustertabelle",
|
||||||
|
display_text="Mustertabelle",
|
||||||
|
link_display_text="Dokument laden",
|
||||||
|
file=factory.django.FileField(
|
||||||
|
from_path=os.path.join(path, filename), filename=filename
|
||||||
|
),
|
||||||
|
)
|
||||||
|
document.tags.set(("Vermittler"))
|
||||||
|
document.save()
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_user_documents():
|
||||||
|
"""creates a default document for testing purposes"""
|
||||||
|
|
||||||
|
path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "./tests/test_documents/"
|
||||||
|
)
|
||||||
|
|
||||||
|
filename = "FallanalyseTeststudent.pdf"
|
||||||
|
document = UserDocumentFactory(
|
||||||
|
title="Lösung Fallanalyse",
|
||||||
|
file=factory.django.FileField(
|
||||||
|
from_path=os.path.join(path, filename), filename=filename
|
||||||
|
),
|
||||||
|
)
|
||||||
|
document.save()
|
||||||
|
|
||||||
|
filename = "FallanalyseTeststudent.docx"
|
||||||
|
document = UserDocumentFactory(
|
||||||
|
title="Lösung Fallanalyse",
|
||||||
|
file=factory.django.FileField(
|
||||||
|
from_path=os.path.join(path, filename), filename=filename
|
||||||
|
),
|
||||||
|
)
|
||||||
|
document.save()
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files import File
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.models import ContentImage, UserImage
|
||||||
|
|
||||||
|
|
||||||
|
def delete_default_images():
|
||||||
|
"""deletes all images"""
|
||||||
|
if "prod" in settings.APP_ENVIRONMENT:
|
||||||
|
raise Exception("This command must not be used in production environment")
|
||||||
|
ContentImage.objects.all().delete()
|
||||||
|
UserImage.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_images():
|
||||||
|
create_default_content_images()
|
||||||
|
create_default_user_images()
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_content_images():
|
||||||
|
path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "./tests/test_images/"
|
||||||
|
)
|
||||||
|
|
||||||
|
images = [
|
||||||
|
("bike_accident.jpg", "Bike Accident"),
|
||||||
|
("car_accident.jpg", "Car Accident"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for filename, title in images:
|
||||||
|
file_path = os.path.join(path, filename)
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
image, _ = ContentImage.objects.get_or_create(
|
||||||
|
title=title,
|
||||||
|
file=File(f, name=filename),
|
||||||
|
focal_point_x=600,
|
||||||
|
focal_point_y=600,
|
||||||
|
focal_point_width=300,
|
||||||
|
focal_point_height=300,
|
||||||
|
)
|
||||||
|
image.tags.set(("Fahrzeug", "Unfall", "Vermittler"))
|
||||||
|
image.save()
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_user_images():
|
||||||
|
path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "./tests/test_images/"
|
||||||
|
)
|
||||||
|
filename, title = ("user1_profile.jpg", "User1 Profile")
|
||||||
|
file_path = os.path.join(path, filename)
|
||||||
|
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
image, _ = UserImage.objects.get_or_create(
|
||||||
|
title=title,
|
||||||
|
file=File(f, name=filename),
|
||||||
|
focal_point_x=600,
|
||||||
|
focal_point_y=600,
|
||||||
|
focal_point_width=300,
|
||||||
|
focal_point_height=300,
|
||||||
|
)
|
||||||
|
image.save()
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import graphene
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.models import ContentDocument
|
||||||
|
|
||||||
|
|
||||||
|
class ContentDocumentObjectType(DjangoObjectType):
|
||||||
|
url = graphene.String(source="url")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ContentDocument
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"display_text",
|
||||||
|
"description",
|
||||||
|
"link_display_text",
|
||||||
|
"thumbnail",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import djclick as click
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.create_default_documents import (
|
||||||
|
create_default_collections,
|
||||||
|
create_default_content_documents,
|
||||||
|
create_default_user_documents,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def command():
|
||||||
|
print("Creating default documents...")
|
||||||
|
create_default_collections()
|
||||||
|
create_default_content_documents()
|
||||||
|
create_default_user_documents()
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import djclick as click
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def command():
|
||||||
|
print("Creating default images...")
|
||||||
|
create_default_images()
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import djclick as click
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.create_default_documents import delete_default_documents
|
||||||
|
from vbv_lernwelt.media_files.create_default_images import delete_default_images
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def command():
|
||||||
|
print("Deleting all images...")
|
||||||
|
delete_default_images()
|
||||||
|
print("Deleting all documents...")
|
||||||
|
delete_default_documents()
|
||||||
|
|
@ -0,0 +1,436 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-12-05 16:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import taggit.managers
|
||||||
|
import wagtail.images.models
|
||||||
|
import wagtail.models.collections
|
||||||
|
import wagtail.search.index
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import vbv_lernwelt.media_files.storage_backends
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("taggit", "0005_auto_20220424_2025"),
|
||||||
|
("wagtailcore", "0089_log_entry_data_json_null_to_object"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ContentImage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=255, verbose_name="title")),
|
||||||
|
("width", models.IntegerField(editable=False, verbose_name="width")),
|
||||||
|
("height", models.IntegerField(editable=False, verbose_name="height")),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True, db_index=True, verbose_name="created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("focal_point_x", models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
("focal_point_y", models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"focal_point_width",
|
||||||
|
models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"focal_point_height",
|
||||||
|
models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
("file_size", models.PositiveIntegerField(editable=False, null=True)),
|
||||||
|
(
|
||||||
|
"file_hash",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, db_index=True, editable=False, max_length=40
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
wagtail.images.models.WagtailImageField(
|
||||||
|
height_field="height",
|
||||||
|
storage=vbv_lernwelt.media_files.storage_backends.ContentImagesStorage,
|
||||||
|
upload_to=wagtail.images.models.get_upload_to,
|
||||||
|
verbose_name="file",
|
||||||
|
width_field="width",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"collection",
|
||||||
|
models.ForeignKey(
|
||||||
|
default=wagtail.models.collections.get_root_collection_id,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="+",
|
||||||
|
to="wagtailcore.collection",
|
||||||
|
verbose_name="collection",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags",
|
||||||
|
taggit.managers.TaggableManager(
|
||||||
|
blank=True,
|
||||||
|
help_text=None,
|
||||||
|
through="taggit.TaggedItem",
|
||||||
|
to="taggit.Tag",
|
||||||
|
verbose_name="tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uploaded_by_user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="uploaded by user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=(
|
||||||
|
wagtail.images.models.ImageFileMixin,
|
||||||
|
wagtail.search.index.Indexed,
|
||||||
|
models.Model,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserImage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=255, verbose_name="title")),
|
||||||
|
("width", models.IntegerField(editable=False, verbose_name="width")),
|
||||||
|
("height", models.IntegerField(editable=False, verbose_name="height")),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True, db_index=True, verbose_name="created at"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("focal_point_x", models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
("focal_point_y", models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"focal_point_width",
|
||||||
|
models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"focal_point_height",
|
||||||
|
models.PositiveIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
("file_size", models.PositiveIntegerField(editable=False, null=True)),
|
||||||
|
(
|
||||||
|
"file_hash",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, db_index=True, editable=False, max_length=40
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
wagtail.images.models.WagtailImageField(
|
||||||
|
height_field="height",
|
||||||
|
storage=vbv_lernwelt.media_files.storage_backends.UserImagesStorage,
|
||||||
|
upload_to=wagtail.images.models.get_upload_to,
|
||||||
|
verbose_name="file",
|
||||||
|
width_field="width",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"collection",
|
||||||
|
models.ForeignKey(
|
||||||
|
default=wagtail.models.collections.get_root_collection_id,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="+",
|
||||||
|
to="wagtailcore.collection",
|
||||||
|
verbose_name="collection",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags",
|
||||||
|
taggit.managers.TaggableManager(
|
||||||
|
blank=True,
|
||||||
|
help_text=None,
|
||||||
|
through="taggit.TaggedItem",
|
||||||
|
to="taggit.Tag",
|
||||||
|
verbose_name="tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uploaded_by_user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="uploaded by user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=(
|
||||||
|
wagtail.images.models.ImageFileMixin,
|
||||||
|
wagtail.search.index.Indexed,
|
||||||
|
models.Model,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserDocument",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=255, verbose_name="title")),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
|
||||||
|
),
|
||||||
|
("file_size", models.PositiveIntegerField(editable=False, null=True)),
|
||||||
|
(
|
||||||
|
"file_hash",
|
||||||
|
models.CharField(blank=True, editable=False, max_length=40),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.FileField(
|
||||||
|
storage=vbv_lernwelt.media_files.storage_backends.UserDocumentsStorage,
|
||||||
|
upload_to="documents",
|
||||||
|
verbose_name="file",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"collection",
|
||||||
|
models.ForeignKey(
|
||||||
|
default=wagtail.models.collections.get_root_collection_id,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="+",
|
||||||
|
to="wagtailcore.collection",
|
||||||
|
verbose_name="collection",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags",
|
||||||
|
taggit.managers.TaggableManager(
|
||||||
|
blank=True,
|
||||||
|
help_text=None,
|
||||||
|
through="taggit.TaggedItem",
|
||||||
|
to="taggit.Tag",
|
||||||
|
verbose_name="tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uploaded_by_user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="uploaded by user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "document",
|
||||||
|
"verbose_name_plural": "documents",
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=(wagtail.search.index.Indexed, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ContentDocument",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=255, verbose_name="title")),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
|
||||||
|
),
|
||||||
|
("file_size", models.PositiveIntegerField(editable=False, null=True)),
|
||||||
|
(
|
||||||
|
"file_hash",
|
||||||
|
models.CharField(blank=True, editable=False, max_length=40),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.FileField(
|
||||||
|
storage=vbv_lernwelt.media_files.storage_backends.ContentDocumentsStorage,
|
||||||
|
upload_to="documents",
|
||||||
|
verbose_name="file",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("display_text", models.CharField(default="", max_length=1024)),
|
||||||
|
("description", models.TextField(blank=True, default="")),
|
||||||
|
(
|
||||||
|
"link_display_text",
|
||||||
|
models.CharField(blank=True, default="", max_length=1024),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"thumbnail",
|
||||||
|
models.CharField(blank=True, default="", max_length=1024),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"collection",
|
||||||
|
models.ForeignKey(
|
||||||
|
default=wagtail.models.collections.get_root_collection_id,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="+",
|
||||||
|
to="wagtailcore.collection",
|
||||||
|
verbose_name="collection",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags",
|
||||||
|
taggit.managers.TaggableManager(
|
||||||
|
blank=True,
|
||||||
|
help_text=None,
|
||||||
|
through="taggit.TaggedItem",
|
||||||
|
to="taggit.Tag",
|
||||||
|
verbose_name="tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uploaded_by_user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="uploaded by user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "document",
|
||||||
|
"verbose_name_plural": "documents",
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=(wagtail.search.index.Indexed, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserImageRendition",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("filter_spec", models.CharField(db_index=True, max_length=255)),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
wagtail.images.models.WagtailImageField(
|
||||||
|
height_field="height",
|
||||||
|
upload_to=wagtail.images.models.get_rendition_upload_to,
|
||||||
|
width_field="width",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("width", models.IntegerField(editable=False)),
|
||||||
|
("height", models.IntegerField(editable=False)),
|
||||||
|
(
|
||||||
|
"focal_point_key",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, default="", editable=False, max_length=16
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"image",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="renditions",
|
||||||
|
to="media_files.userimage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"unique_together": {("image", "filter_spec", "focal_point_key")},
|
||||||
|
},
|
||||||
|
bases=(wagtail.images.models.ImageFileMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ContentImageRendition",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("filter_spec", models.CharField(db_index=True, max_length=255)),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
wagtail.images.models.WagtailImageField(
|
||||||
|
height_field="height",
|
||||||
|
upload_to=wagtail.images.models.get_rendition_upload_to,
|
||||||
|
width_field="width",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("width", models.IntegerField(editable=False)),
|
||||||
|
("height", models.IntegerField(editable=False)),
|
||||||
|
(
|
||||||
|
"focal_point_key",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, default="", editable=False, max_length=16
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"image",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="renditions",
|
||||||
|
to="media_files.contentimage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"unique_together": {("image", "filter_spec", "focal_point_key")},
|
||||||
|
},
|
||||||
|
bases=(wagtail.images.models.ImageFileMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from wagtail.documents.models import AbstractDocument, Document
|
||||||
|
from wagtail.images.models import (
|
||||||
|
AbstractImage,
|
||||||
|
AbstractRendition,
|
||||||
|
get_upload_to,
|
||||||
|
Image,
|
||||||
|
WagtailImageField,
|
||||||
|
)
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.media_files.storage_backends import (
|
||||||
|
ContentDocumentsStorage,
|
||||||
|
ContentImagesStorage,
|
||||||
|
UserDocumentsStorage,
|
||||||
|
UserImagesStorage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentDocument(AbstractDocument):
|
||||||
|
"""
|
||||||
|
Content documents are documents that are handled by the CMS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file = models.FileField(
|
||||||
|
upload_to="documents", verbose_name=_("file"), storage=ContentDocumentsStorage
|
||||||
|
)
|
||||||
|
display_text = models.CharField(max_length=1024, default="")
|
||||||
|
description = models.TextField(default="", blank=True)
|
||||||
|
link_display_text = models.CharField(max_length=1024, default="", blank=True)
|
||||||
|
thumbnail = models.CharField(default="", max_length=1024, blank=True)
|
||||||
|
|
||||||
|
admin_form_fields = Document.admin_form_fields + (
|
||||||
|
"display_text",
|
||||||
|
"description",
|
||||||
|
"link_display_text",
|
||||||
|
"thumbnail",
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_permission(self, user: User):
|
||||||
|
# TODO: 20-11-2023 Renzo: add more advanced permission handling
|
||||||
|
if user.is_authenticated:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class UserDocument(AbstractDocument):
|
||||||
|
"""
|
||||||
|
Documents that are uploaded by the user and not visible in the CMS.
|
||||||
|
Still they are inherited from the Wagtail Document model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file = models.FileField(
|
||||||
|
upload_to="documents", verbose_name=_("file"), storage=UserDocumentsStorage
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentImage(AbstractImage):
|
||||||
|
"""
|
||||||
|
Content images are images that are handled by the CMS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file = WagtailImageField(
|
||||||
|
verbose_name=_("file"),
|
||||||
|
upload_to=get_upload_to,
|
||||||
|
width_field="width",
|
||||||
|
height_field="height",
|
||||||
|
storage=ContentImagesStorage,
|
||||||
|
)
|
||||||
|
admin_form_fields = Image.admin_form_fields + (
|
||||||
|
# Then add the field names here to make them appear in the form:
|
||||||
|
# 'caption',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserImage(AbstractImage):
|
||||||
|
"""
|
||||||
|
User images are images that are uploaded by the user and not visible in the CMS.
|
||||||
|
Still they are inherited from the Wagtail Image model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file = WagtailImageField(
|
||||||
|
verbose_name=_("file"),
|
||||||
|
upload_to=get_upload_to,
|
||||||
|
width_field="width",
|
||||||
|
height_field="height",
|
||||||
|
storage=UserImagesStorage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentImageRendition(AbstractRendition):
|
||||||
|
image = models.ForeignKey(
|
||||||
|
ContentImage, on_delete=models.CASCADE, related_name="renditions"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (("image", "filter_spec", "focal_point_key"),)
|
||||||
|
|
||||||
|
|
||||||
|
class UserImageRendition(AbstractRendition):
|
||||||
|
image = models.ForeignKey(
|
||||||
|
UserImage, on_delete=models.CASCADE, related_name="renditions"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (("image", "filter_spec", "focal_point_key"),)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from storages.backends.s3boto3 import S3Boto3Storage
|
||||||
|
|
||||||
|
|
||||||
|
# inspired by https://theyashshahs.medium.com/aws-s3-signed-urls-in-django-d9e66853a42f
|
||||||
|
|
||||||
|
|
||||||
|
class ContentDocumentsStorage(S3Boto3Storage):
|
||||||
|
location = "media/content_documents"
|
||||||
|
default_acl = "private"
|
||||||
|
|
||||||
|
|
||||||
|
class ContentImagesStorage(S3Boto3Storage):
|
||||||
|
location = "media/content_images"
|
||||||
|
default_acl = "private"
|
||||||
|
|
||||||
|
|
||||||
|
class UserDocumentsStorage(S3Boto3Storage):
|
||||||
|
location = "media/user_documents"
|
||||||
|
default_acl = "private"
|
||||||
|
|
||||||
|
|
||||||
|
class UserImagesStorage(S3Boto3Storage):
|
||||||
|
location = "media/user_images"
|
||||||
|
default_acl = "private"
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Iterativ GmbH
|
||||||
|
# http://www.iterativ.ch/
|
||||||
|
#
|
||||||
|
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
|
||||||
|
#
|
||||||
|
# Created on 2022-08-16
|
||||||
|
# @author: lorenz.padberg@iterativ.ch
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import wagtail_factories
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.models import ContentDocument, UserDocument
|
||||||
|
|
||||||
|
|
||||||
|
class ContentDocumentFactory(wagtail_factories.DocumentFactory):
|
||||||
|
link_display_text = "Dokument herunter laden"
|
||||||
|
description = ""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ContentDocument
|
||||||
|
django_get_or_create = ("title", "description")
|
||||||
|
|
||||||
|
|
||||||
|
class UserDocumentFactory(wagtail_factories.DocumentFactory):
|
||||||
|
class Meta:
|
||||||
|
model = UserDocument
|
||||||
|
django_get_or_create = ("title",)
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import TestCase
|
||||||
|
from wagtail.models import Collection
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.media_files.models import ContentDocument
|
||||||
|
|
||||||
|
TITLE = "Musterlösung Fahrzeug"
|
||||||
|
|
||||||
|
|
||||||
|
class TestContentDocumentServing(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
create_default_users()
|
||||||
|
now_str = str(datetime.datetime.now().strftime("%d-%m-%Y-%H-%M"))
|
||||||
|
collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
|
||||||
|
document = ContentDocument.objects.create(
|
||||||
|
title=TITLE,
|
||||||
|
display_text="Musterlösung Fahrzeug",
|
||||||
|
description="Musterlösung für den Auftrag Fahrzeug",
|
||||||
|
link_display_text="Dokument laden",
|
||||||
|
file=SimpleUploadedFile(
|
||||||
|
f"testdocument_{now_str}.txt", b"these are the file contents!"
|
||||||
|
),
|
||||||
|
collection=collection,
|
||||||
|
)
|
||||||
|
document.save()
|
||||||
|
|
||||||
|
def test_download_document_from_wagtail_logged_in_user_200(self):
|
||||||
|
self.user = User.objects.get(username="admin")
|
||||||
|
self.client.login(username="admin", password="test")
|
||||||
|
document = ContentDocument.objects.get(title=TITLE)
|
||||||
|
client = self.client
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
document.url, f"/server/documents/{document.id}/{document.filename}"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(document.url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_download_document_from_wagtail_anonymous_user_redirect_to_login(self):
|
||||||
|
document = ContentDocument.objects.get(title=TITLE)
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.get(document.url)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertTrue("login" in response.url)
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import datetime
|
||||||
|
from unittest import skipIf
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import override_settings, TestCase
|
||||||
|
from wagtail.models import Collection
|
||||||
|
|
||||||
|
from vbv_lernwelt.media_files.models import ContentDocument
|
||||||
|
|
||||||
|
TITLE = "Musterlösung Fahrzeug"
|
||||||
|
|
||||||
|
|
||||||
|
class TestContentDocumentStorage(TestCase):
|
||||||
|
@override_settings(FILE_UPLOAD_STORAGE="s3")
|
||||||
|
def setUp(self):
|
||||||
|
now_str = str(datetime.datetime.now().strftime("%d-%m-%Y-%H"))
|
||||||
|
collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
|
||||||
|
document = ContentDocument.objects.create(
|
||||||
|
title=TITLE,
|
||||||
|
display_text="Musterlösung Fahrzeug",
|
||||||
|
description="Musterlösung für den Auftrag Fahrzeug",
|
||||||
|
link_display_text="Dokument laden",
|
||||||
|
file=SimpleUploadedFile(
|
||||||
|
f"testdocument_{now_str}.txt", b"these are the file contents!"
|
||||||
|
),
|
||||||
|
collection=collection,
|
||||||
|
)
|
||||||
|
document.save()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for doc in ContentDocument.objects.all():
|
||||||
|
doc.file.storage.delete(doc.file.name)
|
||||||
|
doc.delete()
|
||||||
|
|
||||||
|
def test_new_document_is_created(self):
|
||||||
|
self.assertEqual(ContentDocument.objects.all().count(), 1)
|
||||||
|
self.assertEqual(ContentDocument.objects.filter(title=TITLE).count(), 1)
|
||||||
|
|
||||||
|
def test_document_exists_on_s3(self):
|
||||||
|
document = ContentDocument.objects.get(title=TITLE)
|
||||||
|
self.assertTrue(document.file.storage.exists(document.file.name))
|
||||||
|
|
||||||
|
def test_download_document_from_s3(self):
|
||||||
|
document = ContentDocument.objects.get(title=TITLE)
|
||||||
|
self.assertEqual(document.file.read(), b"these are the file contents!")
|
||||||
|
|
||||||
|
def test_delete_document_from_s3(self):
|
||||||
|
document = ContentDocument.objects.get(title=TITLE)
|
||||||
|
document.file.storage.delete(document.file.name)
|
||||||
|
document.delete()
|
||||||
|
self.assertFalse(document.file.storage.exists(document.file.name))
|
||||||
|
|
||||||
|
@skipIf(
|
||||||
|
settings.AWS_S3_FILE_OVERWRITE,
|
||||||
|
"This test only works if AWS_S3_FILE_OVERWRITE is False",
|
||||||
|
)
|
||||||
|
def test_duplicate_title_and_filename(self):
|
||||||
|
collection, _ = Collection.objects.get_or_create(name="Root", depth=0)
|
||||||
|
document = ContentDocument.objects.create(
|
||||||
|
title=TITLE,
|
||||||
|
display_text="Musterlösung Fahrzeug",
|
||||||
|
description="Musterlösung für den Auftrag Fahrzeug",
|
||||||
|
link_display_text="Dokument laden",
|
||||||
|
file=SimpleUploadedFile(
|
||||||
|
"testdocument.txt", b"these are the file contents! For sure!"
|
||||||
|
),
|
||||||
|
collection=collection,
|
||||||
|
)
|
||||||
|
|
||||||
|
document2 = ContentDocument.objects.create(
|
||||||
|
title=TITLE,
|
||||||
|
display_text="Musterlösung Fahrzeug",
|
||||||
|
description="Musterlösung für den Auftrag Fahrzeug",
|
||||||
|
link_display_text="Dokument laden",
|
||||||
|
file=SimpleUploadedFile(
|
||||||
|
"testdocument.txt", b"these are the file contents! But different!"
|
||||||
|
),
|
||||||
|
collection=collection,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
document.file.read(), b"these are the file contents! For sure!"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
document2.file.read(), b"these are the file contents! But different!"
|
||||||
|
)
|
||||||
|
self.assertTrue(document.file.name != document2.file.name)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 627 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 638 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
|
|
@ -1,55 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import factory
|
|
||||||
from wagtail.core.models import Collection
|
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import Course
|
|
||||||
from vbv_lernwelt.media_library.models import LibraryDocument
|
|
||||||
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
|
||||||
LibraryDocumentFactory,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_default_collections():
|
|
||||||
c = Collection.objects.all().delete()
|
|
||||||
|
|
||||||
root, created = Collection.objects.get_or_create(name="Root", depth=0)
|
|
||||||
|
|
||||||
for course in Course.objects.all():
|
|
||||||
course_collection = root.add_child(name=course.title)
|
|
||||||
for cat in course.coursecategory_set.all():
|
|
||||||
cat_collection = course_collection.add_child(name=cat.title)
|
|
||||||
|
|
||||||
|
|
||||||
def create_default_documents():
|
|
||||||
LibraryDocument.objects.all().delete()
|
|
||||||
|
|
||||||
path = os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)), "../static/media/documents/"
|
|
||||||
)
|
|
||||||
|
|
||||||
collection = Collection.objects.get(name="Fahrzeug")
|
|
||||||
|
|
||||||
filename = "SchweizerischesZivilgesetzbuch.pdf"
|
|
||||||
document = LibraryDocumentFactory(
|
|
||||||
title="V1 C25 ZGB CH",
|
|
||||||
display_text="Schweizerisches Zivilgesetzbuch",
|
|
||||||
description="Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.",
|
|
||||||
link_display_text="Dokument laden",
|
|
||||||
file=factory.django.FileField(
|
|
||||||
from_path=os.path.join(path, filename), filename=filename
|
|
||||||
),
|
|
||||||
collection=collection,
|
|
||||||
)
|
|
||||||
|
|
||||||
filename = "SmallPDF.pdf"
|
|
||||||
document = LibraryDocumentFactory(
|
|
||||||
title="V1 C25 ",
|
|
||||||
display_text="Pdf showcase ",
|
|
||||||
description="Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.",
|
|
||||||
link_display_text="Dokument laden",
|
|
||||||
file=factory.django.FileField(
|
|
||||||
from_path=os.path.join(path, filename), filename=filename
|
|
||||||
),
|
|
||||||
collection=collection,
|
|
||||||
)
|
|
||||||
|
|
@ -106,19 +106,19 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
recipient__username=expected_recipient
|
recipient__username=expected_recipient
|
||||||
)
|
)
|
||||||
self.assertEquals(action_object, notification.action_object)
|
self.assertEqual(action_object, notification.action_object)
|
||||||
self.assertEquals("ASSIGNMENT_REMINDER", notification.notification_trigger)
|
self.assertEqual("ASSIGNMENT_REMINDER", notification.notification_trigger)
|
||||||
self.assertEquals("INFORMATION", notification.notification_category)
|
self.assertEqual("INFORMATION", notification.notification_category)
|
||||||
self.assertEquals(EXPECTED_MEMBER_VERB, notification.verb)
|
self.assertEqual(EXPECTED_MEMBER_VERB, notification.verb)
|
||||||
|
|
||||||
template_data = notification.data["template_data"]
|
template_data = notification.data["template_data"]
|
||||||
|
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
action_object.learning_content.get_parent_circle().title,
|
action_object.learning_content.get_parent_circle().title,
|
||||||
template_data["circle"],
|
template_data["circle"],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
action_object.learning_content.get_frontend_url(),
|
action_object.learning_content.get_frontend_url(),
|
||||||
notification.target_url,
|
notification.target_url,
|
||||||
)
|
)
|
||||||
|
|
@ -140,17 +140,17 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
if assignment_type == AssignmentType.CASEWORK:
|
if assignment_type == AssignmentType.CASEWORK:
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER.name,
|
EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER.name,
|
||||||
email_template,
|
email_template,
|
||||||
)
|
)
|
||||||
elif assignment_type == AssignmentType.PREP_ASSIGNMENT:
|
elif assignment_type == AssignmentType.PREP_ASSIGNMENT:
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER.name,
|
EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER.name,
|
||||||
email_template,
|
email_template,
|
||||||
)
|
)
|
||||||
elif type(action_object) == CourseSessionEdoniqTest:
|
elif type(action_object) == CourseSessionEdoniqTest:
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER.name,
|
EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER.name,
|
||||||
email_template,
|
email_template,
|
||||||
)
|
)
|
||||||
|
|
@ -176,7 +176,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
send_assignment_reminder_notifications()
|
send_assignment_reminder_notifications()
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEquals(3, len(Notification.objects.all()))
|
self.assertEqual(3, len(Notification.objects.all()))
|
||||||
self._assert_member_assignment_notifications(
|
self._assert_member_assignment_notifications(
|
||||||
action_object=should_be_sent,
|
action_object=should_be_sent,
|
||||||
expected_recipients=RECIPIENT_STUDENTS,
|
expected_recipients=RECIPIENT_STUDENTS,
|
||||||
|
|
@ -214,7 +214,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
send_assignment_reminder_notifications()
|
send_assignment_reminder_notifications()
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEquals(3, len(Notification.objects.all()))
|
self.assertEqual(3, len(Notification.objects.all()))
|
||||||
self._assert_member_assignment_notifications(
|
self._assert_member_assignment_notifications(
|
||||||
action_object=casework,
|
action_object=casework,
|
||||||
expected_recipients=RECIPIENT_STUDENTS,
|
expected_recipients=RECIPIENT_STUDENTS,
|
||||||
|
|
@ -236,16 +236,16 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
send_assignment_reminder_notifications()
|
send_assignment_reminder_notifications()
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEquals(1, len(Notification.objects.all()))
|
self.assertEqual(1, len(Notification.objects.all()))
|
||||||
|
|
||||||
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
||||||
self.assertEquals(casework, notification.action_object)
|
self.assertEqual(casework, notification.action_object)
|
||||||
self.assertEquals("INFORMATION", notification.notification_category)
|
self.assertEqual("INFORMATION", notification.notification_category)
|
||||||
self.assertEquals(EXPECTED_EXPERT_VERB, notification.verb)
|
self.assertEqual(EXPECTED_EXPERT_VERB, notification.verb)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
casework.evaluation_deadline.url_expert, notification.target_url
|
casework.evaluation_deadline.url_expert, notification.target_url
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"CASEWORK_EXPERT_EVALUATION_REMINDER", notification.notification_trigger
|
"CASEWORK_EXPERT_EVALUATION_REMINDER", notification.notification_trigger
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -276,7 +276,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
||||||
send_assignment_reminder_notifications()
|
send_assignment_reminder_notifications()
|
||||||
|
|
||||||
# THEN
|
# THEN
|
||||||
self.assertEquals(3, len(Notification.objects.all()))
|
self.assertEqual(3, len(Notification.objects.all()))
|
||||||
self._assert_member_assignment_notifications(
|
self._assert_member_assignment_notifications(
|
||||||
action_object=prep_assignment,
|
action_object=prep_assignment,
|
||||||
expected_recipients=RECIPIENT_STUDENTS,
|
expected_recipients=RECIPIENT_STUDENTS,
|
||||||
|
|
|
||||||
|
|
@ -65,52 +65,52 @@ class TestAttendanceCourseReminders(TestCase):
|
||||||
|
|
||||||
send_attendance_reminder_notifications()
|
send_attendance_reminder_notifications()
|
||||||
|
|
||||||
self.assertEquals(4, len(Notification.objects.all()))
|
self.assertEqual(4, len(Notification.objects.all()))
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
recipient__username="test-student1@example.com"
|
recipient__username="test-student1@example.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"Erinnerung: Bald findet ein Präsenzkurs statt",
|
"Erinnerung: Bald findet ein Präsenzkurs statt",
|
||||||
notification.verb,
|
notification.verb,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"INFORMATION",
|
"INFORMATION",
|
||||||
notification.notification_category,
|
notification.notification_category,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"ATTENDANCE_COURSE_REMINDER",
|
"ATTENDANCE_COURSE_REMINDER",
|
||||||
notification.notification_trigger,
|
notification.notification_trigger,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac,
|
self.csac,
|
||||||
notification.action_object,
|
notification.action_object,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.course_session,
|
self.csac.course_session,
|
||||||
notification.course_session,
|
notification.course_session,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
|
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
|
||||||
notification.target_url,
|
notification.target_url,
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.learning_content.title,
|
self.csac.learning_content.title,
|
||||||
notification.data["template_data"]["attendance_course"],
|
notification.data["template_data"]["attendance_course"],
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.location,
|
self.csac.location,
|
||||||
notification.data["template_data"]["location"],
|
notification.data["template_data"]["location"],
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.trainer,
|
self.csac.trainer,
|
||||||
notification.data["template_data"]["trainer"],
|
notification.data["template_data"]["trainer"],
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"),
|
self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"),
|
||||||
notification.data["template_data"]["start"],
|
notification.data["template_data"]["start"],
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"),
|
self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"),
|
||||||
notification.data["template_data"]["end"],
|
notification.data["template_data"]["end"],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
"ignore hash 6": "A035C8C19219BA821ECEA86B64E628F8D684696D",
|
"ignore hash 6": "A035C8C19219BA821ECEA86B64E628F8D684696D",
|
||||||
"ignore hash 7": "96334b4eb6a7ae5b0d86abd7febcbcc67323bb94",
|
"ignore hash 7": "96334b4eb6a7ae5b0d86abd7febcbcc67323bb94",
|
||||||
"ignore hash 8": "MTgwMTYwfEFQTXxBUFBMSUNBVElPTnwxMDQ5Njk0MDU0",
|
"ignore hash 8": "MTgwMTYwfEFQTXxBUFBMSUNBVElPTnwxMDQ5Njk0MDU0",
|
||||||
|
"ignore hash 9": "82ef9a8178dcb4df0b71540fa06d7da826ecb26e1977e230bdc8c9d6f9f1af84",
|
||||||
"json base64 content": "regex:\"content\": \"",
|
"json base64 content": "regex:\"content\": \"",
|
||||||
"img base64 content": "regex:data:image/png;base64,.*",
|
"img base64 content": "regex:data:image/png;base64,.*",
|
||||||
"sentry url": "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352",
|
"sentry url": "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ server/vbv_lernwelt/notify/email/email_services.py
|
||||||
server/vbv_lernwelt/static/
|
server/vbv_lernwelt/static/
|
||||||
server/vbv_lernwelt/media/
|
server/vbv_lernwelt/media/
|
||||||
server/vbv_lernwelt/edoniq_test/certificates/test.key
|
server/vbv_lernwelt/edoniq_test/certificates/test.key
|
||||||
|
server/vbv_lernwelt/shop/tests/test_create_signature.py
|
||||||
supabase.md
|
supabase.md
|
||||||
scripts/supabase/init.sql
|
scripts/supabase/init.sql
|
||||||
ramon.wenger@iterativ.ch.gpg
|
ramon.wenger@iterativ.ch.gpg
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue