VBV-440: Musterlösung und Refactoring S3

This commit is contained in:
Lorenz Padberg 2023-12-05 17:26:05 +01:00 committed by Daniel Egger
parent 421a10524b
commit e8ae8bdc14
68 changed files with 1483 additions and 177 deletions

View File

@ -87,10 +87,14 @@ def main(app_name, image_name, environment_file):
"IT_DJANGO_SECRET_KEY": env.str(
"IT_DJANGO_SECRET_KEY", generate_random_string(63)
),
"AWS_S3_ACCESS_KEY_ID": env.str("AWS_S3_ACCESS_KEY_ID", ""),
"AWS_S3_ACCESS_KEY_ID": env.str(
"AWS_S3_ACCESS_KEY_ID", "AKIAZJLREPUVWNBTJ5VY"
),
"AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""),
"AWS_S3_REGION_NAME": "eu-central-1",
"AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch",
"AWS_S3_REGION_NAME": env.str("AWS_S3_REGION_NAME", "eu-central-1"),
"AWS_STORAGE_BUCKET_NAME": env.str(
"AWS_STORAGE_BUCKET_NAME", "myvbv-dev.iterativ.ch"
),
"FILE_UPLOAD_STORAGE": "s3",
"IT_DJANGO_DEBUG": "false",
"IT_SERVE_VUE": "false",

View File

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

View File

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

View File

@ -23,6 +23,7 @@ export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"
export const CompetencePerformanceStatisticsSummaryType = "CompetencePerformanceStatisticsSummaryType";
export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType";
export const CompetencesStatisticsType = "CompetencesStatisticsType";
export const ContentDocumentObjectType = "ContentDocumentObjectType";
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
export const CourseObjectType = "CourseObjectType";
export const CoursePageInterface = "CoursePageInterface";

View File

@ -46,6 +46,10 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
tasks
title
translation_key
solution_sample {
id
url
}
competence_certificate {
...CoursePageFields
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,73 @@
# Files handling
This document describes how files are handled in this appication.
# Types of files
static files: files that are not changed by the application, e.g. images, fonts, etc.¨
### content documents:
Files that belong to the content and are managed by the content editors in the CMS (pdf, excel, word, etc.)
### user documents:
Files that are uploaded by the users (pdf, etc.). Therefore not visible in the CMS.
Images are handled seprately from documents since images require additional processing (resizing, cropping, etc.).
Visible in the django admin.
### content images:
Images that belong to the content and are managed by the content editors in the CMS.
### user images:
Images that are uploaded by the users. Therefore not visible in the CMS. Visible in the django admin.
## Static files
These files are publicly served on S3.
## Content documents
These files are part of the content. Such as a pdf thas cointains additional information to a course.
These files are not publicly available. The content files are uploaded by the editors in the wagtail cms.
https://www.hacksoft.io/blog/direct-to-s3-file-upload-with-django
Django handles the permissions to these files. Via a view django checks if the user has permissions to access the file,
and gerates a temporary url that is valid for a limited time. Still the documents are served by django. This done for
usability reasons. The user sees the url mydomain.com/media/documents/<document-id> and not a url to S3. Therefore the
user can share the url with other users. (still they need to login and have the permissions to access the file)
The downside of this is that the django server processes these files. (could be circumvented by django-sendfile).
![](./assets/files-presign.png)
- These Files are handled stored as wagtail documents. As a model and the file itself is stored in S3.
### Frontend access to content documents
For the frontend django generates a fixed url per file /media/documents/<document-id>
When the frontend requests this file, django checks if the user has permissions to access the file.
If so, django generates a temporary url that is valid for a limited time. Then sends a redirect to the frontend.
In this waz the frontend does not need to know about the permissions. Content grapql can be cached if needed and urls
can be shared by the users.
content_documents
user_documents
public files
## User documents
- User uploaded files are stored in S3. but the permissions is handled by django. Same process as content files.
Same process as content files. But the url is /media/user-uploads/<file-id>
And the files are not managed by Wagtail. Due to another model, they are not visible to the user in the CMS.
## Content images
Content Images are served directly from S3. The permissions are handled by dja

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -18,8 +18,7 @@ def main():
from django.conf import settings
settings.DEBUG = True
from django.db import connection
from django.db import reset_queries
from django.db import connection, reset_queries
reset_queries()

View File

@ -12,16 +12,15 @@ os.environ.setdefault("IT_APP_ENVIRONMENT", "local")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
django.setup()
from vbv_lernwelt.core.schema import Query
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.schema import Query
def main():
from django.conf import settings
settings.DEBUG = True
from django.db import connection
from django.db import reset_queries
from django.db import connection, reset_queries
reset_queries()

View File

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

View File

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

View File

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

View File

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

View File

@ -56,7 +56,7 @@ from vbv_lernwelt.importer.views import (
from vbv_lernwelt.notify.views import email_notification_settings
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
from wagtail.documents import urls as media_library_urls
class SignedIntConverter(IntConverter):
@ -88,7 +88,7 @@ urlpatterns = [
# wagtail urls
path('server/cms/', include(wagtailadmin_urls)),
path('server/documents/', include(wagtaildocs_urls)),
path('server/documents/', include(media_library_urls)),
path('server/pages/', include(wagtail_urls)),
# core
@ -132,6 +132,7 @@ urlpatterns = [
name="request_assignment_completion_status"),
# documents
# TODO: remfactor to files app
path(r'api/core/document/start/', document_upload_start,
name='file_upload_start'),
path(r'api/core/document/<str:document_id>/', document_delete,

View File

@ -9,6 +9,7 @@ mypy # https://github.com/python/mypy
django-stubs # https://github.com/typeddjango/django-stubs
pytest # https://github.com/pytest-dev/pytest
pytest-sugar # https://github.com/Frozenball/pytest-sugar
pytest-xdist #
djangorestframework-stubs # https://github.com/typeddjango/djangorestframework-stubs

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ from vbv_lernwelt.course.consts import (
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
)
from vbv_lernwelt.course.models import CoursePage
from vbv_lernwelt.media_files.models import ContentDocument
from wagtail.blocks import StreamValue
from wagtail.blocks.list_block import ListBlock, ListValue
from wagtail.rich_text import RichText
@ -39,6 +40,7 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
needs_expert_evaluation=True,
competence_certificate=competence_certificate,
effort_required="ca. 5 Stunden",
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
intro_text=replace_whitespace(
"""
<h3>Ausgangslage</h3>

View File

@ -9,6 +9,7 @@ from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert
from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface
from vbv_lernwelt.media_files.graphql.types import ContentDocumentObjectType
class AssignmentCompletionObjectType(DjangoObjectType):
@ -52,6 +53,7 @@ class AssignmentObjectType(DjangoObjectType):
learning_content_page_id=graphene.ID(required=False),
assignment_user_id=graphene.UUID(required=False),
)
solution_sample = graphene.Field(ContentDocumentObjectType)
class Meta:
model = Assignment
@ -67,6 +69,9 @@ class AssignmentObjectType(DjangoObjectType):
"competence_certificate",
)
def resolve_solution_sample(self, info):
return self.solution_sample
def resolve_max_points(self, info):
return self.get_max_points()

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.20 on 2023-12-05 16:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("media_files", "0001_initial"),
("assignment", "0010_assignmentcompletion_edoniq_extended_time_flag"),
]
operations = [
migrations.AddField(
model_name="assignment",
name="solution_sample",
field=models.ForeignKey(
blank=True,
help_text="Musterlösung",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="media_files.contentdocument",
),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,59 @@
import datetime
import requests
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from vbv_lernwelt.core.models import User
from vbv_lernwelt.files.integrations import s3_get_client
from vbv_lernwelt.files.models import UploadFile
class UploadFileIntegrationTest(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.s3_client = s3_get_client()
def setUp(self):
self.user = User.objects.create(username="testuser")
# Creating a dummy file for upload
self.dummy_file = SimpleUploadedFile(
"testfile.txt", b"these are the file contents!"
)
self.upload_file = UploadFile.objects.create(
original_file_name="testfile.txt",
file_name="testfile123.txt",
file_type="text/plain",
uploaded_by=self.user,
file=self.dummy_file,
)
def tearDown(self):
self.upload_file.delete_file()
def test_upload_to_s3(self):
# Verify if file is uploaded to S3
response = self.s3_client.get_object(
Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=str(self.upload_file.file)
)
self.assertEqual(response["Body"].read(), b"these are the file contents!")
def test_url_property(self):
self.upload_file.upload_finished_at = datetime.datetime.now()
self.upload_file.save()
url = self.upload_file.url
response = requests.get(url)
# Assert that the URL is a valid presigned S3 URL and accessible
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"these are the file contents!")
def test_delete_file_method(self):
file_path = str(self.upload_file.file)
self.upload_file.delete_file()
# Assert that the file is deleted from S3
with self.assertRaises(self.s3_client.exceptions.NoSuchKey):
self.s3_client.get_object(
Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_path
)

View File

@ -0,0 +1,81 @@
import os
import boto3
import requests
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from vbv_lernwelt.core.models import User
from vbv_lernwelt.files.integrations import (
s3_delete_file,
s3_generate_presigned_post,
s3_generate_presigned_url,
s3_get_client,
)
class TestIntegrationsIntegrationTest(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.s3_client = s3_get_client()
def setUp(self):
self.user = User.objects.create(username="testuser")
# Creating a dummy file for upload
self.dummy_file = SimpleUploadedFile(
"testfile.txt", b"these are the file contents!"
)
def test_s3_generate_presigned_post(self):
# Test generating a presigned POST for file upload
presigned_post_data = s3_generate_presigned_post(
file_path=f"{self.user.id}/testfile.txt",
file_type="text/plain",
file_name="testfile.txt",
)
self.assertIn("url", presigned_post_data)
self.assertIn("fields", presigned_post_data)
# Upload file using the presigned URL
files = {"file": self.dummy_file}
response = requests.post(
presigned_post_data["url"], data=presigned_post_data["fields"], files=files
)
self.assertEqual(response.status_code, 204)
def test_s3_generate_presigned_url(self):
# First, manually upload a file to S3 for testing
self.s3_client.upload_fileobj
self.s3_client.upload_fileobj(
self.dummy_file, settings.AWS_STORAGE_BUCKET_NAME, "testfile.txt"
)
# Test generating a presigned URL for the uploaded file
presigned_url = s3_generate_presigned_url(file_path="testfile.txt")
response = requests.get(presigned_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"these are the file contents!")
def test_s3_delete_file(self):
# Upload a file to S3 for testing
self.s3_client.upload_fileobj(
self.dummy_file, settings.AWS_STORAGE_BUCKET_NAME, "testfile.txt"
)
# Test deleting the file
s3_delete_file(file_path="testfile.txt")
# Assert that the file no longer exists
with self.assertRaises(boto3.exceptions.botocore.client.ClientError):
self.s3_client.head_object(
Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key="testfile.txt"
)
@classmethod
def tearDownClass(cls):
# Clean up any remaining files in the S3 bucket
s3_delete_file(file_path="testfile.txt")
super().tearDownClass()

View File

@ -0,0 +1,54 @@
from django.contrib import admin
from vbv_lernwelt.media_files.models import UserDocument, UserImage
@admin.register(UserDocument)
class UserDocumentAdmin(admin.ModelAdmin):
list_display = (
"title",
"file",
"created_at",
"uploaded_by_user",
"file_size",
"file_hash",
)
search_fields = ("title", "uploaded_by_user__username", "tags__name")
list_filter = ("created_at", "uploaded_by_user")
autocomplete_fields = ["uploaded_by_user"]
date_hierarchy = "created_at"
readonly_fields = (
"file_size",
"file_hash",
"created_at",
"uploaded_by_user",
"file",
)
@admin.register(UserImage)
class UserImageAdmin(admin.ModelAdmin):
list_display = (
"title",
"file",
"created_at",
"uploaded_by_user",
"file_size",
)
search_fields = ("title", "uploaded_by_user__username")
list_filter = ("created_at", "uploaded_by_user")
autocomplete_fields = ["uploaded_by_user"]
date_hierarchy = "created_at"
readonly_fields = (
"file_size",
"file_hash",
"created_at",
"uploaded_by_user",
"file",
"tags",
"title",
"focal_point_x",
"focal_point_y",
"focal_point_width",
"focal_point_height",
)

View File

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

View File

@ -0,0 +1,83 @@
import os
import factory
from django.conf import settings
from wagtail.models import Collection
from vbv_lernwelt.media_files.models import ContentDocument, UserDocument
from vbv_lernwelt.media_files.tests.media_library_factories import (
ContentDocumentFactory,
UserDocumentFactory,
)
def delete_default_documents():
"""deletes all documents"""
if "prod" in settings.APP_ENVIRONMENT:
raise Exception("This command must not be used in production environment")
ContentDocument.objects.all().delete()
UserDocument.objects.all().delete()
def create_default_collections():
root, created = Collection.objects.get_or_create(name="Root", depth=0)
def create_default_content_documents():
"""creates a default document for testing purposes"""
path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "./tests/test_documents/"
)
filename = "Vermittler_Motorfahrzeug_Versicherung_Musterlösung.pdf"
document = ContentDocumentFactory(
title="Musterlösung Fahrzeug",
display_text="Musterlösung Fahrzeug",
description="Musterlösung für den Auftrag Fahrzeug",
link_display_text="Dokument laden",
file=factory.django.FileField(
from_path=os.path.join(path, filename), filename=filename
),
)
document.tags.set(("Fahrzeug", "Musterlösung", "Vermittler"))
document.save()
filename = "TestExcelSheet.xlsx"
document = ContentDocumentFactory(
title="Mustertabelle",
display_text="Mustertabelle",
link_display_text="Dokument laden",
file=factory.django.FileField(
from_path=os.path.join(path, filename), filename=filename
),
)
document.tags.set(("Vermittler"))
document.save()
def create_default_user_documents():
"""creates a default document for testing purposes"""
path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "./tests/test_documents/"
)
filename = "FallanalyseTeststudent.pdf"
document = UserDocumentFactory(
title="Lösung Fallanalyse",
file=factory.django.FileField(
from_path=os.path.join(path, filename), filename=filename
),
)
document.save()
filename = "FallanalyseTeststudent.docx"
document = UserDocumentFactory(
title="Lösung Fallanalyse",
file=factory.django.FileField(
from_path=os.path.join(path, filename), filename=filename
),
)
document.save()

View File

@ -0,0 +1,63 @@
import os
from django.conf import settings
from django.core.files import File
from vbv_lernwelt.media_files.models import ContentImage, UserImage
def delete_default_images():
"""deletes all images"""
if "prod" in settings.APP_ENVIRONMENT:
raise Exception("This command must not be used in production environment")
ContentImage.objects.all().delete()
UserImage.objects.all().delete()
def create_default_images():
create_default_content_images()
create_default_user_images()
def create_default_content_images():
path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "./tests/test_images/"
)
images = [
("bike_accident.jpg", "Bike Accident"),
("car_accident.jpg", "Car Accident"),
]
for filename, title in images:
file_path = os.path.join(path, filename)
with open(file_path, "rb") as f:
image, _ = ContentImage.objects.get_or_create(
title=title,
file=File(f, name=filename),
focal_point_x=600,
focal_point_y=600,
focal_point_width=300,
focal_point_height=300,
)
image.tags.set(("Fahrzeug", "Unfall", "Vermittler"))
image.save()
def create_default_user_images():
path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "./tests/test_images/"
)
filename, title = ("user1_profile.jpg", "User1 Profile")
file_path = os.path.join(path, filename)
with open(file_path, "rb") as f:
image, _ = UserImage.objects.get_or_create(
title=title,
file=File(f, name=filename),
focal_point_x=600,
focal_point_y=600,
focal_point_width=300,
focal_point_height=300,
)
image.save()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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