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

|
||||
|
||||
- These Files are handled stored as wagtail documents. As a model and the file itself is stored in S3.
|
||||
|
||||
### Frontend access to content documents
|
||||
|
||||
For the frontend django generates a fixed url per file /media/documents/<document-id>
|
||||
|
||||
When the frontend requests this file, django checks if the user has permissions to access the file.
|
||||
If so, django generates a temporary url that is valid for a limited time. Then sends a redirect to the frontend.
|
||||
|
||||
In this waz the frontend does not need to know about the permissions. Content grapql can be cached if needed and urls
|
||||
can be shared by the users.
|
||||
|
||||
content_documents
|
||||
user_documents
|
||||
|
||||
public files
|
||||
|
||||
## User documents
|
||||
|
||||
- User uploaded files are stored in S3. but the permissions is handled by django. Same process as content files.
|
||||
|
||||
Same process as content files. But the url is /media/user-uploads/<file-id>
|
||||
And the files are not managed by Wagtail. Due to another model, they are not visible to the user in the CMS.
|
||||
|
||||
## Content images
|
||||
|
||||
Content Images are served directly from S3. The permissions are handled by dja
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -18,8 +18,7 @@ def main():
|
|||
from django.conf import settings
|
||||
|
||||
settings.DEBUG = True
|
||||
from django.db import connection
|
||||
from django.db import reset_queries
|
||||
from django.db import connection, reset_queries
|
||||
|
||||
reset_queries()
|
||||
|
||||
|
|
|
|||
|
|
@ -12,16 +12,15 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
|
|||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
||||
django.setup()
|
||||
|
||||
from vbv_lernwelt.core.schema import Query
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.core.schema import Query
|
||||
|
||||
|
||||
def main():
|
||||
from django.conf import settings
|
||||
|
||||
settings.DEBUG = True
|
||||
from django.db import connection
|
||||
from django.db import reset_queries
|
||||
from django.db import connection, reset_queries
|
||||
|
||||
reset_queries()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
|
|||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
|
||||
django.setup()
|
||||
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||
from vbv_lernwelt.notify.email.email_services import (
|
||||
create_template_data_from_course_session_attendance_course,
|
||||
EmailTemplate,
|
||||
send_email,
|
||||
create_template_data_from_course_session_attendance_course,
|
||||
)
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||
|
||||
|
||||
def main():
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ THIRD_PARTY_APPS = [
|
|||
|
||||
LOCAL_APPS = [
|
||||
"vbv_lernwelt.core",
|
||||
"vbv_lernwelt.media_files",
|
||||
"vbv_lernwelt.sso",
|
||||
"vbv_lernwelt.course",
|
||||
"vbv_lernwelt.learnpath",
|
||||
|
|
@ -212,23 +213,13 @@ STATICFILES_FINDERS = [
|
|||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
]
|
||||
|
||||
USE_AWS = env("USE_AWS", False)
|
||||
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", "")
|
||||
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", "")
|
||||
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", "")
|
||||
AWS_S3_CUSTOM_DOMAIN = "%s.s3.amazonaws.com" % AWS_STORAGE_BUCKET_NAME
|
||||
|
||||
# MEDIA
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
|
||||
MEDIA_ROOT = str(APPS_DIR / "media")
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
||||
if USE_AWS:
|
||||
# https://wagtail.org/blog/amazon-s3-for-media-files/
|
||||
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
|
||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||
else:
|
||||
MEDIA_URL = "/server/media/"
|
||||
|
||||
MEDIA_URL = "/server/media/"
|
||||
|
||||
IT_SERVE_VUE = env.bool("IT_SERVE_VUE", DEBUG)
|
||||
IT_SERVE_VUE_URL = env("IT_SERVE_VUE_URL", "http://localhost:5173")
|
||||
|
|
@ -252,7 +243,19 @@ WAGTAIL_ENABLE_UPDATE_CHECK = False
|
|||
WAGTAIL_ENABLE_WHATS_NEW_BANNER = False
|
||||
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES
|
||||
|
||||
WAGTAILDOCS_DOCUMENT_MODEL = "media_library.LibraryDocument"
|
||||
WAGTAILDOCS_DOCUMENT_MODEL = "media_files.ContentDocument"
|
||||
WAGTAILIMAGES_IMAGE_MODEL = "media_files.ContentImage"
|
||||
|
||||
# this setting makes that the document is served by django, and the url is the django url.
|
||||
# https://docs.wagtail.org/en/stable/reference/settings.html#wagtaildocs-serve-method
|
||||
# The file is served by django as streaming response. If it should be serverd by nginx, then install django sendfile
|
||||
WAGTAILDOCS_SERVE_METHOD = "serve_view"
|
||||
# WAGTAILDOCS_INLINE_CONTENT_TYPES = ['application/pdf', 'text/plain']
|
||||
|
||||
|
||||
WAGTAILIMAGES_MAX_UPLOAD_SIZE = 20 * 1024 * 1024 # 20MB
|
||||
# WAGTAILIMAGES_RENDITION_STORAGE = 'myapp.backends.MyCustomStorage'
|
||||
|
||||
|
||||
WAGTAILADMIN_RICH_TEXT_EDITORS = {
|
||||
"default": {
|
||||
|
|
@ -645,7 +648,7 @@ NOTIFICATIONS_NOTIFICATION_MODEL = "notify.Notification"
|
|||
SENDGRID_API_KEY = env("IT_SENDGRID_API_KEY", default="")
|
||||
|
||||
# S3 BUCKET CONFIGURATION
|
||||
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="local") # local | s3
|
||||
FILE_UPLOAD_STORAGE = env("FILE_UPLOAD_STORAGE", default="s3") # local | s3
|
||||
|
||||
if FILE_UPLOAD_STORAGE == "local":
|
||||
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=5242880)
|
||||
|
|
@ -654,18 +657,19 @@ if FILE_UPLOAD_STORAGE == "s3":
|
|||
# Using django-storages
|
||||
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html
|
||||
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||
|
||||
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID")
|
||||
AWS_S3_ACCESS_KEY_ID = env("AWS_S3_ACCESS_KEY_ID", default="AKIAZJLREPUVWNBTJ5VY")
|
||||
AWS_S3_SECRET_ACCESS_KEY = env("AWS_S3_SECRET_ACCESS_KEY")
|
||||
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
|
||||
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME")
|
||||
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", "eu-central-1")
|
||||
AWS_S3_SIGNATURE_VERSION = env("AWS_S3_SIGNATURE_VERSION", default="s3v4")
|
||||
AWS_STORAGE_BUCKET_NAME = env(
|
||||
"AWS_STORAGE_BUCKET_NAME", default="myvbv-dev.iterativ.ch"
|
||||
)
|
||||
AWS_S3_FILE_OVERWRITE = env("AWS_S3_FILE_OVERWRITE", False)
|
||||
FILE_MAX_SIZE = env.int("FILE_MAX_SIZE", default=20971520) # 20MB
|
||||
|
||||
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="private")
|
||||
|
||||
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=300) # seconds
|
||||
AWS_PRESIGNED_EXPIRY = env.int("AWS_PRESIGNED_EXPIRY", default=7200) # seconds
|
||||
|
||||
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = (
|
||||
"jpg",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
|
||||
import os
|
||||
|
||||
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||
|
||||
from .base import * # noqa
|
||||
|
|
@ -8,6 +9,7 @@ from .base import * # noqa
|
|||
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
|
||||
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||
|
||||
# Select faster password hasher during tests
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
|
||||
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
||||
|
||||
|
|
@ -15,16 +17,7 @@ PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
|||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
|
||||
WHITENOISE_MANIFEST_STRICT = False
|
||||
|
||||
# Dummy data
|
||||
AWS_S3_ACCESS_KEY_ID = "SOMEKEY"
|
||||
AWS_S3_SECRET_ACCESS_KEY = "SOMEACCESSKEY"
|
||||
AWS_STORAGE_BUCKET_NAME = "myvbv-dev.iterativ.ch"
|
||||
AWS_S3_REGION_NAME = "eu-central-1"
|
||||
AWS_S3_SIGNATURE_VERSION = "s3v4"
|
||||
FILE_MAX_SIZE = 20971520 # 20MB
|
||||
AWS_DEFAULT_ACL = "private"
|
||||
AWS_PRESIGNED_EXPIRY = 300
|
||||
AWS_S3_FILE_OVERWRITE = True
|
||||
|
||||
|
||||
class DisableMigrations(dict):
|
||||
|
|
@ -36,8 +29,3 @@ class DisableMigrations(dict):
|
|||
|
||||
|
||||
MIGRATION_MODULES = DisableMigrations()
|
||||
|
||||
# Select faster password hasher during tests
|
||||
PASSWORD_HASHERS = [
|
||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
import os
|
||||
|
||||
os.environ["IT_APP_ENVIRONMENT"] = "local"
|
||||
os.environ["AWS_S3_SECRET_ACCESS_KEY"] = os.environ.get(
|
||||
"AWS_S3_SECRET_ACCESS_KEY",
|
||||
"!!!default_for_quieting_cypress_within_pycharm!!!",
|
||||
)
|
||||
|
||||
from .base import * # noqa
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ from vbv_lernwelt.importer.views import (
|
|||
from vbv_lernwelt.notify.views import email_notification_settings
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
from wagtail.documents import urls as media_library_urls
|
||||
|
||||
|
||||
class SignedIntConverter(IntConverter):
|
||||
|
|
@ -88,7 +88,7 @@ urlpatterns = [
|
|||
|
||||
# wagtail urls
|
||||
path('server/cms/', include(wagtailadmin_urls)),
|
||||
path('server/documents/', include(wagtaildocs_urls)),
|
||||
path('server/documents/', include(media_library_urls)),
|
||||
path('server/pages/', include(wagtail_urls)),
|
||||
|
||||
# core
|
||||
|
|
@ -132,6 +132,7 @@ urlpatterns = [
|
|||
name="request_assignment_completion_status"),
|
||||
|
||||
# documents
|
||||
# TODO: remfactor to files app
|
||||
path(r'api/core/document/start/', document_upload_start,
|
||||
name='file_upload_start'),
|
||||
path(r'api/core/document/<str:document_id>/', document_delete,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ mypy # https://github.com/python/mypy
|
|||
django-stubs # https://github.com/typeddjango/django-stubs
|
||||
pytest # https://github.com/pytest-dev/pytest
|
||||
pytest-sugar # https://github.com/Frozenball/pytest-sugar
|
||||
pytest-xdist #
|
||||
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -38,9 +38,7 @@ azure-core==1.29.1
|
|||
azure-identity==1.14.0
|
||||
# via -r requirements.in
|
||||
azure-storage-blob==12.17.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# django-storages
|
||||
# via -r requirements.in
|
||||
backcall==0.2.0
|
||||
# via ipython
|
||||
bcrypt==4.0.1
|
||||
|
|
@ -176,7 +174,7 @@ django-ratelimit==4.1.0
|
|||
# via -r requirements.in
|
||||
django-redis==5.3.0
|
||||
# via -r requirements.in
|
||||
django-storages[azure]==1.13.2
|
||||
django-storages==1.13.2
|
||||
# via -r requirements.in
|
||||
django-stubs==4.2.3
|
||||
# via
|
||||
|
|
@ -209,6 +207,8 @@ exceptiongroup==1.1.2
|
|||
# via
|
||||
# anyio
|
||||
# pytest
|
||||
execnet==2.0.2
|
||||
# via pytest-xdist
|
||||
executing==1.2.0
|
||||
# via stack-data
|
||||
factory-boy==3.3.0
|
||||
|
|
@ -397,7 +397,9 @@ pyflakes==3.1.0
|
|||
pygments==2.16.1
|
||||
# via ipython
|
||||
pyjwt[crypto]==2.8.0
|
||||
# via msal
|
||||
# via
|
||||
# msal
|
||||
# pyjwt
|
||||
pylint==2.17.5
|
||||
# via
|
||||
# pylint-django
|
||||
|
|
@ -415,10 +417,13 @@ pytest==7.4.0
|
|||
# -r requirements-dev.in
|
||||
# pytest-django
|
||||
# pytest-sugar
|
||||
# pytest-xdist
|
||||
pytest-django==4.5.2
|
||||
# via -r requirements-dev.in
|
||||
pytest-sugar==0.9.7
|
||||
# via -r requirements-dev.in
|
||||
pytest-xdist==3.5.0
|
||||
# via -r requirements-dev.in
|
||||
python-dateutil==2.8.2
|
||||
# via
|
||||
# -r requirements.in
|
||||
|
|
@ -616,7 +621,9 @@ wheel==0.41.1
|
|||
whitenoise[brotli]==6.5.0
|
||||
# via -r requirements.in
|
||||
willow[heif]==1.6.1
|
||||
# via wagtail
|
||||
# via
|
||||
# wagtail
|
||||
# willow
|
||||
wrapt==1.15.0
|
||||
# via astroid
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
pytest --junitxml=../test-reports/coverage.xml
|
||||
|
||||
# limit test to 6 parallel processes, otherwise ratelimit of s3 could be hit
|
||||
pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
coverage run -m pytest --junitxml=../test-reports/coverage.xml $1
|
||||
coverage run -m pytest --numprocesses auto --maxprocesses=6 --junitxml=../test-reports/coverage.xml $1
|
||||
|
||||
coverage_python=`coverage report -m | tail -n1 | awk '{print $4}'`
|
||||
commit=`git rev-parse HEAD`
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from vbv_lernwelt.course.consts import (
|
|||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||
)
|
||||
from vbv_lernwelt.course.models import CoursePage
|
||||
from vbv_lernwelt.media_files.models import ContentDocument
|
||||
from wagtail.blocks import StreamValue
|
||||
from wagtail.blocks.list_block import ListBlock, ListValue
|
||||
from wagtail.rich_text import RichText
|
||||
|
|
@ -39,6 +40,7 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
|
|||
needs_expert_evaluation=True,
|
||||
competence_certificate=competence_certificate,
|
||||
effort_required="ca. 5 Stunden",
|
||||
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
|
||||
intro_text=replace_whitespace(
|
||||
"""
|
||||
<h3>Ausgangslage</h3>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
|||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
|
||||
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
|
||||
from vbv_lernwelt.media_files.graphql.types import ContentDocumentObjectType
|
||||
|
||||
|
||||
class AssignmentCompletionObjectType(DjangoObjectType):
|
||||
|
|
@ -52,6 +53,7 @@ class AssignmentObjectType(DjangoObjectType):
|
|||
learning_content_page_id=graphene.ID(required=False),
|
||||
assignment_user_id=graphene.UUID(required=False),
|
||||
)
|
||||
solution_sample = graphene.Field(ContentDocumentObjectType)
|
||||
|
||||
class Meta:
|
||||
model = Assignment
|
||||
|
|
@ -67,6 +69,9 @@ class AssignmentObjectType(DjangoObjectType):
|
|||
"competence_certificate",
|
||||
)
|
||||
|
||||
def resolve_solution_sample(self, info):
|
||||
return self.solution_sample
|
||||
|
||||
def resolve_max_points(self, info):
|
||||
return self.get_max_points()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 3.2.20 on 2023-12-05 16:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("media_files", "0001_initial"),
|
||||
("assignment", "0010_assignmentcompletion_edoniq_extended_time_flag"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="assignment",
|
||||
name="solution_sample",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Musterlösung",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="media_files.contentdocument",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -200,12 +200,22 @@ class Assignment(CourseBasePage):
|
|||
help_text="Beurteilungsschritte",
|
||||
)
|
||||
|
||||
solution_sample = models.ForeignKey(
|
||||
"media_files.ContentDocument",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
help_text="Musterlösung",
|
||||
)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("assignment_type"),
|
||||
FieldPanel("needs_expert_evaluation"),
|
||||
PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"),
|
||||
FieldPanel("intro_text"),
|
||||
FieldPanel("effort_required"),
|
||||
FieldPanel("solution_sample"),
|
||||
FieldPanel("performance_objectives"),
|
||||
FieldPanel("tasks"),
|
||||
FieldPanel("evaluation_description"),
|
||||
|
|
|
|||
|
|
@ -106,14 +106,14 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
)
|
||||
|
||||
task_data = data["task_completion_data"][task_id]
|
||||
self.assertDictEqual(
|
||||
task_data,
|
||||
{
|
||||
"user_data": {
|
||||
"fileId": file_id,
|
||||
"fileInfo": {"id": file_id, "name": "file.txt", "url": file_url},
|
||||
}
|
||||
},
|
||||
self.maxDiff = None
|
||||
self.assertEqual(task_data["user_data"]["fileId"], file_id)
|
||||
self.assertEqual(task_data["user_data"]["fileInfo"]["id"], file_id)
|
||||
self.assertEqual(task_data["user_data"]["fileInfo"]["name"], "file.txt")
|
||||
self.assertTrue(
|
||||
task_data["user_data"]["fileInfo"]["url"].startswith(
|
||||
"https://s3.eu-central-1.amazonaws.com/myvbv-dev.iterativ.ch"
|
||||
)
|
||||
)
|
||||
|
||||
# check DB data
|
||||
|
|
@ -194,31 +194,31 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
# check notification
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
notification = Notification.objects.first()
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben.",
|
||||
notification.verb,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"test-trainer1@example.com",
|
||||
notification.recipient.email,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"test-student1@example.com",
|
||||
notification.actor.email,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"USER_INTERACTION",
|
||||
notification.notification_category,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"CASEWORK_SUBMITTED",
|
||||
notification.notification_trigger,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
notification.action_object,
|
||||
db_entry,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
notification.course_session,
|
||||
self.course_session,
|
||||
)
|
||||
|
|
@ -422,35 +422,35 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
|||
# check notification
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
notification = Notification.objects.first()
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"Test Trainer1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» bewertet.",
|
||||
notification.verb,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"test-student1@example.com",
|
||||
notification.recipient.email,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"test-trainer1@example.com",
|
||||
notification.actor.email,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"USER_INTERACTION",
|
||||
notification.notification_category,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"CASEWORK_EVALUATED",
|
||||
notification.notification_trigger,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
notification.action_object,
|
||||
db_entry,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
notification.course_session,
|
||||
self.course_session,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
notification.target_url,
|
||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -82,6 +82,12 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
|||
LearningUnitFactory,
|
||||
TopicFactory,
|
||||
)
|
||||
from vbv_lernwelt.media_files.create_default_documents import (
|
||||
create_default_collections,
|
||||
create_default_content_documents,
|
||||
)
|
||||
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||
from vbv_lernwelt.media_files.models import ContentDocument, ContentImage, UserImage
|
||||
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||
MediaLibraryCategoryPageFactory,
|
||||
MediaLibraryContentPageFactory,
|
||||
|
|
@ -92,6 +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):
|
||||
# create_locales_for_wagtail()
|
||||
create_default_collections()
|
||||
create_default_content_documents()
|
||||
if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
|
||||
create_default_images()
|
||||
|
||||
course = create_test_course_with_categories()
|
||||
competence_certificate = create_test_competence_navi()
|
||||
|
||||
|
|
@ -523,6 +534,14 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
|||
slug__startswith=f"test-lehrgang-assignment-reflexion"
|
||||
),
|
||||
),
|
||||
|
||||
assignment = Assignment.objects.get(
|
||||
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
|
||||
)
|
||||
assignment.solution_sample = ContentDocument.objects.get(
|
||||
title="Musterlösung Fahrzeug"
|
||||
)
|
||||
assignment.save()
|
||||
LearningContentAssignmentFactory(
|
||||
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
|
||||
parent=circle,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
|||
LearningUnitFactory,
|
||||
TopicFactory,
|
||||
)
|
||||
from vbv_lernwelt.media_files.create_default_documents import (
|
||||
create_default_collections,
|
||||
create_default_content_documents,
|
||||
)
|
||||
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||
LearnMediaBlockFactory,
|
||||
)
|
||||
|
|
@ -40,6 +45,7 @@ def create_uk_learning_path(course_id=COURSE_UK, user=None, skip_locales=True):
|
|||
user = User.objects.get(username="info@iterativ.ch")
|
||||
|
||||
course_page = CoursePage.objects.get(course_id=course_id)
|
||||
|
||||
lp = LearningPathFactory(
|
||||
title="Lernpfad",
|
||||
parent=course_page,
|
||||
|
|
|
|||
|
|
@ -100,6 +100,12 @@ from vbv_lernwelt.learnpath.models import (
|
|||
LearningContentAssignment,
|
||||
LearningContentAttendanceCourse,
|
||||
)
|
||||
from vbv_lernwelt.media_files.create_default_documents import (
|
||||
create_default_collections,
|
||||
create_default_content_documents,
|
||||
create_default_user_documents,
|
||||
)
|
||||
from vbv_lernwelt.media_files.create_default_images import create_default_images
|
||||
from vbv_lernwelt.media_library.create_default_media_library import (
|
||||
create_default_media_library,
|
||||
)
|
||||
|
|
@ -128,6 +134,11 @@ ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
|
|||
def command(course):
|
||||
print("Creating default courses", course)
|
||||
|
||||
create_default_collections()
|
||||
create_default_content_documents()
|
||||
create_default_user_documents()
|
||||
create_default_images()
|
||||
|
||||
if COURSE_VERSICHERUNGSVERMITTLERIN_ID in course:
|
||||
create_versicherungsvermittlerin_course()
|
||||
|
||||
|
|
@ -285,6 +296,9 @@ def create_course_uk_de(course_id=COURSE_UK, lang="de"):
|
|||
create_uk_competence_profile(course_id=course_id)
|
||||
create_default_media_library(course_id=course_id)
|
||||
|
||||
create_default_collections()
|
||||
create_default_content_documents()
|
||||
|
||||
|
||||
def create_course_uk_de_course_sessions():
|
||||
course = Course.objects.get(id=COURSE_UK)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
recipient__username=expected_recipient
|
||||
)
|
||||
self.assertEquals(action_object, notification.action_object)
|
||||
self.assertEquals("ASSIGNMENT_REMINDER", notification.notification_trigger)
|
||||
self.assertEquals("INFORMATION", notification.notification_category)
|
||||
self.assertEquals(EXPECTED_MEMBER_VERB, notification.verb)
|
||||
self.assertEqual(action_object, notification.action_object)
|
||||
self.assertEqual("ASSIGNMENT_REMINDER", notification.notification_trigger)
|
||||
self.assertEqual("INFORMATION", notification.notification_category)
|
||||
self.assertEqual(EXPECTED_MEMBER_VERB, notification.verb)
|
||||
|
||||
template_data = notification.data["template_data"]
|
||||
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
action_object.learning_content.get_parent_circle().title,
|
||||
template_data["circle"],
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
action_object.learning_content.get_frontend_url(),
|
||||
notification.target_url,
|
||||
)
|
||||
|
|
@ -140,17 +140,17 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
)
|
||||
|
||||
if assignment_type == AssignmentType.CASEWORK:
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER.name,
|
||||
email_template,
|
||||
)
|
||||
elif assignment_type == AssignmentType.PREP_ASSIGNMENT:
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER.name,
|
||||
email_template,
|
||||
)
|
||||
elif type(action_object) == CourseSessionEdoniqTest:
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER.name,
|
||||
email_template,
|
||||
)
|
||||
|
|
@ -176,7 +176,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
send_assignment_reminder_notifications()
|
||||
|
||||
# THEN
|
||||
self.assertEquals(3, len(Notification.objects.all()))
|
||||
self.assertEqual(3, len(Notification.objects.all()))
|
||||
self._assert_member_assignment_notifications(
|
||||
action_object=should_be_sent,
|
||||
expected_recipients=RECIPIENT_STUDENTS,
|
||||
|
|
@ -214,7 +214,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
send_assignment_reminder_notifications()
|
||||
|
||||
# THEN
|
||||
self.assertEquals(3, len(Notification.objects.all()))
|
||||
self.assertEqual(3, len(Notification.objects.all()))
|
||||
self._assert_member_assignment_notifications(
|
||||
action_object=casework,
|
||||
expected_recipients=RECIPIENT_STUDENTS,
|
||||
|
|
@ -236,16 +236,16 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
send_assignment_reminder_notifications()
|
||||
|
||||
# THEN
|
||||
self.assertEquals(1, len(Notification.objects.all()))
|
||||
self.assertEqual(1, len(Notification.objects.all()))
|
||||
|
||||
notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER)
|
||||
self.assertEquals(casework, notification.action_object)
|
||||
self.assertEquals("INFORMATION", notification.notification_category)
|
||||
self.assertEquals(EXPECTED_EXPERT_VERB, notification.verb)
|
||||
self.assertEquals(
|
||||
self.assertEqual(casework, notification.action_object)
|
||||
self.assertEqual("INFORMATION", notification.notification_category)
|
||||
self.assertEqual(EXPECTED_EXPERT_VERB, notification.verb)
|
||||
self.assertEqual(
|
||||
casework.evaluation_deadline.url_expert, notification.target_url
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"CASEWORK_EXPERT_EVALUATION_REMINDER", notification.notification_trigger
|
||||
)
|
||||
|
||||
|
|
@ -276,7 +276,7 @@ class TestAssignmentCourseRemindersTest(TestCase):
|
|||
send_assignment_reminder_notifications()
|
||||
|
||||
# THEN
|
||||
self.assertEquals(3, len(Notification.objects.all()))
|
||||
self.assertEqual(3, len(Notification.objects.all()))
|
||||
self._assert_member_assignment_notifications(
|
||||
action_object=prep_assignment,
|
||||
expected_recipients=RECIPIENT_STUDENTS,
|
||||
|
|
|
|||
|
|
@ -65,52 +65,52 @@ class TestAttendanceCourseReminders(TestCase):
|
|||
|
||||
send_attendance_reminder_notifications()
|
||||
|
||||
self.assertEquals(4, len(Notification.objects.all()))
|
||||
self.assertEqual(4, len(Notification.objects.all()))
|
||||
notification = Notification.objects.get(
|
||||
recipient__username="test-student1@example.com"
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"Erinnerung: Bald findet ein Präsenzkurs statt",
|
||||
notification.verb,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"INFORMATION",
|
||||
notification.notification_category,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"ATTENDANCE_COURSE_REMINDER",
|
||||
notification.notification_trigger,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac,
|
||||
notification.action_object,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.course_session,
|
||||
notification.course_session,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
|
||||
notification.target_url,
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.learning_content.title,
|
||||
notification.data["template_data"]["attendance_course"],
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.location,
|
||||
notification.data["template_data"]["location"],
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.trainer,
|
||||
notification.data["template_data"]["trainer"],
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"),
|
||||
notification.data["template_data"]["start"],
|
||||
)
|
||||
self.assertEquals(
|
||||
self.assertEqual(
|
||||
self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"),
|
||||
notification.data["template_data"]["end"],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"ignore hash 6": "A035C8C19219BA821ECEA86B64E628F8D684696D",
|
||||
"ignore hash 7": "96334b4eb6a7ae5b0d86abd7febcbcc67323bb94",
|
||||
"ignore hash 8": "MTgwMTYwfEFQTXxBUFBMSUNBVElPTnwxMDQ5Njk0MDU0",
|
||||
"ignore hash 9": "82ef9a8178dcb4df0b71540fa06d7da826ecb26e1977e230bdc8c9d6f9f1af84",
|
||||
"json base64 content": "regex:\"content\": \"",
|
||||
"img base64 content": "regex:data:image/png;base64,.*",
|
||||
"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/media/
|
||||
server/vbv_lernwelt/edoniq_test/certificates/test.key
|
||||
server/vbv_lernwelt/shop/tests/test_create_signature.py
|
||||
supabase.md
|
||||
scripts/supabase/init.sql
|
||||
ramon.wenger@iterativ.ch.gpg
|
||||
|
|
|
|||
Loading…
Reference in New Issue