Merged develop into feature/VBV-684-export-trainer-rl

This commit is contained in:
Christian Cueni 2024-05-29 09:41:49 +00:00
commit 0cad9666c5
44 changed files with 929 additions and 212 deletions

View File

@ -106,7 +106,12 @@ function evaluationForTask(task: AssignmentEvaluationTask) {
return expertData;
}
const maxPoints = computed(() => maxAssignmentPoints(props.assignment));
const maxPoints = computed(() => {
if (props.assignmentCompletion.completion_status === "EVALUATION_SUBMITTED") {
return props.assignmentCompletion.evaluation_max_points;
}
return maxAssignmentPoints(props.assignment);
});
const userPoints = computed(() =>
userAssignmentPoints(props.assignment, props.assignmentCompletion)
);
@ -128,23 +133,50 @@ const userPoints = computed(() =>
</h3>
<h3 v-else class="mb-6" data-cy="sub-title">{{ $t(text.evaluationSubmission) }}</h3>
<section class="mb-6 border p-6" data-cy="result-section">
<section
v-if="props.assignment.assignment_type === 'CASEWORK'"
class="flex items-center"
>
<section v-if="props.assignment.assignment_type === 'CASEWORK'">
<div class="flex items-center">
<div class="heading-1 py-4" data-cy="user-points">
<template
v-if="
props.assignmentCompletion.completion_status == 'EVALUATION_SUBMITTED'
"
>
{{ props.assignmentCompletion.evaluation_points_final }}
</template>
<template v-else>
{{ userPoints }}
</template>
</div>
<div class="pl-2" data-cy="total-points">
{{ $t("assignment.von x Punkten", { x: maxPoints }) }}
({{
(
((props.assignmentCompletion?.evaluation_points ?? 0) /
((props.assignmentCompletion?.evaluation_points_final ?? 0) /
(props.assignmentCompletion?.evaluation_max_points ?? 1)) *
100
).toFixed(0)
}}%)
</div>
</div>
<div
v-if="assignmentCompletion.evaluation_points_deducted > 0"
data-cy="points-deducted"
class="mb-4 text-gray-900"
>
<div>
{{ $t("a.Punkte aus Bewertung") }}:
{{ assignmentCompletion.evaluation_points }}
</div>
<div>
{{ $t("a.Abgezogene Punkte") }}:
{{ assignmentCompletion.evaluation_points_deducted }}
</div>
<div>
{{ $t("a.Grund") }}:
{{ assignmentCompletion.evaluation_points_deducted_reason }}
</div>
</div>
</section>
<div

View File

@ -164,6 +164,7 @@ const getIconName = (lc: LearningContent) => {
v-for="submittable in submittables"
:key="submittable.id"
class="flex flex-col justify-between gap-2 py-4 lg:flex-row lg:gap-4"
:data-cy="`submittable-${submittable.content.slug}`"
>
<div class="flex flex-row items-center gap-2 lg:w-1/3">
<div class="min-h-9 min-w-9">

View File

@ -17,9 +17,9 @@ 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 $evaluationUserId: ID\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 evaluation_user_id: $evaluationUserId\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 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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\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 competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\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.CompetenceCertificateForUserQueryDocument,
"\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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\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 competence_certificate_weight\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_deducted\n evaluation_points_final\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 competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\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.CompetenceCertificateForUserQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\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 configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n }\n }\n": types.DashboardConfigDocument,
@ -63,15 +63,15 @@ 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 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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\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 first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\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.
*/
export function graphql(source: "\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"): (typeof documents)["\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"];
export function graphql(source: "\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 competence_certificate_weight\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_deducted\n evaluation_points_final\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"): (typeof documents)["\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 competence_certificate_weight\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_deducted\n evaluation_points_final\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"];
/**
* 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 competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\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"): (typeof documents)["\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\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"];
export function graphql(source: "\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\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"): (typeof documents)["\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\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"];
/**
* 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

@ -512,6 +512,7 @@ type AssignmentObjectType implements CoursePageInterface {
evaluation_tasks: JSONStreamField
performance_objectives: JSONStreamField
max_points: Int
competence_certificate_weight: Float
learning_content: LearningContentInterface
completion(course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
solution_sample: ContentDocumentObjectType
@ -559,6 +560,9 @@ type AssignmentCompletionObjectType {
submitted_at: DateTime
evaluation_submitted_at: DateTime
evaluation_user: UserObjectType
evaluation_points_deducted: Float!
evaluation_points_deducted_reason: String!
evaluation_points_deducted_user: UserObjectType
evaluation_passed: Boolean
edoniq_extended_time_flag: Boolean!
assignment_user: UserObjectType!
@ -570,6 +574,7 @@ type AssignmentCompletionObjectType {
task_completion_data: GenericScalar
learning_content_page_id: ID
evaluation_points: Float
evaluation_points_final: Float
evaluation_max_points: Float
}

View File

@ -77,6 +77,10 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
}
evaluation_points
evaluation_max_points
evaluation_points_deducted
evaluation_points_deducted_reason
evaluation_points_final
evaluation_passed
edoniq_extended_time_flag
completion_data
@ -95,11 +99,14 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
...CoursePageFields
assignment_type
max_points
competence_certificate_weight
completion(course_session_id: $courseSessionId) {
id
completion_status
submitted_at
evaluation_points
evaluation_points_deducted
evaluation_points_final
evaluation_max_points
evaluation_passed
}
@ -131,11 +138,14 @@ export const COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY = graphql(`
...CoursePageFields
assignment_type
max_points
competence_certificate_weight
completion(course_session_id: $courseSessionId) {
id
completion_status
submitted_at
evaluation_points
evaluation_points_final
evaluation_points_deducted
evaluation_max_points
evaluation_passed
}

View File

@ -81,18 +81,24 @@ const getIconName = () => {
>
<div class="flex flex-col lg:items-center">
<div class="heading-2">
{{ assignment.completion?.evaluation_points }}
{{ assignment.completion?.evaluation_points_final }}
</div>
<div>
{{ $t("assignment.von x Punkten", { x: assignment.max_points }) }}
({{
(
((assignment.completion?.evaluation_points ?? 0) /
((assignment.completion?.evaluation_points_final ?? 0) /
(assignment.completion?.evaluation_max_points ?? 1)) *
100
).toFixed(0)
}}%)
</div>
<div
v-if="(assignment.completion?.evaluation_points_deducted ?? 0) > 0"
class="text-gray-900"
>
{{ $t("a.mit Abzug") }}
</div>
<div
v-if="assignment.completion && !assignment.completion.evaluation_passed"
class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5"

View File

@ -5,8 +5,8 @@ import CompetenceAssignmentRow from "@/pages/competence/CompetenceAssignmentRow.
import { computed } from "vue";
import ItProgress from "@/components/ui/ItProgress.vue";
import {
assignmentsMaxEvaluationPoints,
assignmentsUserPoints,
calcCompetenceCertificateGrade,
competenceCertificateProgressStatusCount,
} from "@/pages/competence/utils";
@ -18,14 +18,18 @@ const props = defineProps<{
frontendUrl?: string;
}>();
const totalPointsEvaluatedAssignments = computed(() => {
return assignmentsMaxEvaluationPoints(props.competenceCertificate.assignments);
});
const userPointsEvaluatedAssignments = computed(() => {
return assignmentsUserPoints(props.competenceCertificate.assignments);
});
const userGrade = computed(() => {
return calcCompetenceCertificateGrade(props.competenceCertificate.assignments, true);
});
const userGradeRounded2Places = computed(() => {
return calcCompetenceCertificateGrade(props.competenceCertificate.assignments, false);
});
const numAssignmentsEvaluated = computed(() => {
return props.competenceCertificate.assignments.filter((a) => {
return a.completion?.completion_status === "EVALUATION_SUBMITTED";
@ -70,15 +74,29 @@ const frontendUrl = computed(() => {
</h3>
</div>
<section v-if="userPointsEvaluatedAssignments > 0" class="flex items-center">
<section v-if="userPointsEvaluatedAssignments > 0">
<div class="flex items-center">
<div
class="py-4"
:class="{ 'heading-1': props.detailView, 'heading-2': !props.detailView }"
:data-cy="`certificate-${competenceCertificate.slug}-grade`"
>
{{ userPointsEvaluatedAssignments }}
{{ $t("a.Note") }}: {{ userGrade }}
</div>
<div class="ml-1">
{{ $t("assignment.von x Punkten", { x: totalPointsEvaluatedAssignments }) }}
</div>
<div
class="text-gray-900"
:data-cy="`certificate-${competenceCertificate.slug}-grade-percent`"
>
{{ $t("a.Ungerundete Note") }}: {{ userGradeRounded2Places }}.
<a
:href="$t('a.wegleitungUkUrl')"
target="_blank"
rel="noopener noreferrer"
class="underline"
>
{{ $t("a.Wegleitung üK") }}
</a>
</div>
</section>
<section v-else class="py-2">

View File

@ -5,8 +5,8 @@ import type { CompetenceCertificate } from "@/types";
import { useCertificateQuery } from "@/composables";
import CompetenceCertificateComponent from "@/pages/competence/CompetenceCertificateComponent.vue";
import {
assignmentsMaxEvaluationPoints,
assignmentsUserPoints,
calcCompetencesTotalGrade,
} from "@/pages/competence/utils";
import { useRoute } from "vue-router";
import { getCertificates } from "@/services/competence";
@ -44,8 +44,8 @@ const assignments = computed(() => {
return competenceCertificates?.value?.flatMap((cc) => cc.assignments);
});
const totalPointsEvaluatedAssignments = computed(() => {
return assignmentsMaxEvaluationPoints(assignments.value ?? []);
const totalGrade = computed(() => {
return calcCompetencesTotalGrade(competenceCertificates.value ?? []);
});
const userPointsEvaluatedAssignments = computed(() => {
@ -86,16 +86,15 @@ onMounted(async () => {
>
{{ $t("a.Zwischenstand") }}
</div>
<h3 class="mt-2 lg:order-first lg:mt-0">{{ $t("a.Gesamtpunktzahl") }}</h3>
<h3 class="mt-2 lg:order-first lg:mt-0">{{ $t("a.Erfahrungsnote üK") }}</h3>
</div>
<section v-if="userPointsEvaluatedAssignments > 0" class="flex items-center">
<div class="heading-1 py-4">
{{ userPointsEvaluatedAssignments }}
</div>
<div class="pl-2">
{{ $t("assignment.von x Punkten", { x: totalPointsEvaluatedAssignments }) }}
</div>
<section
v-if="userPointsEvaluatedAssignments > 0"
class="flex items-center"
data-cy="certificate-total-grade"
>
<div class="heading-1 py-4">{{ $t("a.Note") }}: {{ totalGrade }}</div>
</section>
<section v-else class="my-4">
{{ $t("a.competenceCertificateNoUserPoints") }}

View File

@ -6,8 +6,9 @@ import { computed } from "vue";
import type { CompetenceCertificate } from "@/types";
import { useCurrentCourseSession } from "@/composables";
import {
assignmentsMaxEvaluationPoints,
assignmentsUserPoints,
calcCompetenceCertificateGrade,
calcCompetencesTotalGrade,
competenceCertificateProgressStatusCount,
} from "@/pages/competence/utils";
import ItProgress from "@/components/ui/ItProgress.vue";
@ -42,10 +43,6 @@ const allAssignments = computed(() => {
return competenceCertificates.value.flatMap((cc) => cc.assignments);
});
const totalPointsEvaluatedAssignments = computed(() => {
return assignmentsMaxEvaluationPoints(allAssignments.value);
});
const userPointsEvaluatedAssignments = computed(() => {
return assignmentsUserPoints(allAssignments.value);
});
@ -69,11 +66,14 @@ const router = useRouter();
</div>
<div class="mt-4" data-cy="certificate-total-points-text">
<div v-if="userPointsEvaluatedAssignments > 0">
{{ $t("a.Zwischenstand") }} {{ $t("a.Gesamtpunktzahl") }}:
{{ $t("a.Erfahrungsnote üK") }}:
<span class="font-bold">
{{ userPointsEvaluatedAssignments }}
{{ calcCompetencesTotalGrade(competenceCertificates ?? []) }}
</span>
<span class="rounded-full bg-gray-200 px-2.5 py-0.5 text-sm lg:ml-2">
{{ $t("a.Zwischenstand") }}
</span>
{{ $t("assignment.von x Punkten", { x: totalPointsEvaluatedAssignments }) }}
</div>
<div v-else>
{{ $t("a.competenceCertificateNoUserPoints") }}
@ -92,14 +92,13 @@ const router = useRouter();
{{ certificate.title }}
</div>
<div class="mt-4 lg:mt-0">
<span class="text-bold">
{{ assignmentsUserPoints(certificate.assignments) }}
<span
v-if="calcCompetenceCertificateGrade(certificate.assignments)"
class="text-bold"
>
{{ $t("a.Note") }}:
{{ calcCompetenceCertificateGrade(certificate.assignments) }}
</span>
{{
$t("assignment.von x Punkten", {
x: assignmentsMaxEvaluationPoints(certificate.assignments),
})
}}
</div>
<div class="flex">
<div>

View File

@ -1,5 +1,6 @@
import type { StatusCount } from "@/components/ui/ItProgress.vue";
import type { CompetenceCertificateAssignment } from "@/types";
import { percentToRoundedGrade } from "@/services/assignmentService";
import type { CompetenceCertificate, CompetenceCertificateAssignment } from "@/types";
import _ from "lodash";
export function assignmentsMaxEvaluationPoints(
@ -16,10 +17,61 @@ export function assignmentsUserPoints(assignments: CompetenceCertificateAssignme
return +_.sum(
assignments
.filter((a) => a.completion?.completion_status === "EVALUATION_SUBMITTED")
.map((a) => a.completion?.evaluation_points ?? 0)
.map((a) => a.completion?.evaluation_points_final ?? 0)
).toFixed(1);
}
export function calcCompetenceCertificateGrade(
assignments: CompetenceCertificateAssignment[],
roundedToHalfGrade = true
) {
const evaluatedAssignments = assignments.filter(
(a) => a.completion?.completion_status === "EVALUATION_SUBMITTED"
);
const adjustedResults = evaluatedAssignments.map((a) => {
return (
((a.completion?.evaluation_points_final ?? 0) / a.max_points) *
a.competence_certificate_weight
);
});
const adjustedAssignmentCount = _.sum(
evaluatedAssignments.map((a) => a.competence_certificate_weight)
);
if (adjustedAssignmentCount === 0) {
return undefined;
}
return percentToRoundedGrade(
_.sum(adjustedResults) / adjustedAssignmentCount,
roundedToHalfGrade
);
}
export function calcCompetencesTotalGrade(
competenceCertificates: CompetenceCertificate[]
) {
// für das Total der Kompetenznote werden jeweils die gerundenten Noten der
// einzelnen Kompetenznachweise verwendet und dann noch einmal gerundet.
const competenceCertificateGrades = competenceCertificates
.map((cc) => {
return calcCompetenceCertificateGrade(cc.assignments);
})
.filter((g) => {
// filter out "empty" grades
return !!g;
});
const percentGraded =
// @ts-ignore `g` cannot be undefined here
_.sum(competenceCertificateGrades.map((g) => g - 1)) /
(competenceCertificateGrades.length * 5);
return percentToRoundedGrade(percentGraded);
}
export function competenceCertificateProgressStatusCount(
assignments: CompetenceCertificateAssignment[]
) {

View File

@ -49,7 +49,10 @@ const total = (metrics: AssignmentCompletionMetricsType) => {
:items="courseStatistics.assignments.records"
>
<template #default="{ item }">
<div class="flex justify-between">
<div
class="flex justify-between"
:data-cy=" (item as AssignmentStatisticsRecordType).assignment_title"
>
<div>
<h4 class="font-bold">
{{ (item as AssignmentStatisticsRecordType).assignment_title }}

View File

@ -36,11 +36,20 @@ const { data, error } = useCSRFFetch(
"a.Die Einladung konnte nicht akzeptiert werden. Bitte melde dich beim Support."
)
}}
<div>
<ul>
<li>
{{ $t("a.Versicherungsvermittler/-in") }}
<a class="underline" href="mailto:vermittler@vbv-afa.ch">
vermittler@vbv-afa.ch
</a>
</div>
</li>
<li>
{{ $t("a.Überbetriebliche Kurse") }}
<a class="underline" href="mailto:uek-support@vbv-afa.ch">
uek-support@vbv-afa.ch
</a>
</li>
</ul>
<div v-if="error.message" class="my-4">
{{ $t("a.Fehlermeldung") }}: {{ error.message }}
</div>

View File

@ -169,7 +169,7 @@ async function startTest() {
<div class="my-4">
{{ $t("a.Resultat") }}:
<span class="font-bold">
{{ assignmentCompletion.evaluation_points }}
{{ assignmentCompletion.evaluation_points_final }}
</span>
{{
$t("assignment.von x Punkten", {
@ -178,7 +178,7 @@ async function startTest() {
}}
({{
(
((assignmentCompletion.evaluation_points ?? 0) /
((assignmentCompletion.evaluation_points_final ?? 0) /
(assignmentCompletion.evaluation_max_points ?? 1)) *
100
).toFixed(0)
@ -191,6 +191,24 @@ async function startTest() {
</span>
</div>
<div
v-if="assignmentCompletion.evaluation_points_deducted > 0"
class="my-4 text-gray-900"
>
<div>
{{ $t("a.Punkte aus Bewertung") }}:
{{ assignmentCompletion.evaluation_points }}
</div>
<div>
{{ $t("a.Abgezogene Punkte") }}:
{{ assignmentCompletion.evaluation_points_deducted }}
</div>
<div>
{{ $t("a.Grund") }}:
{{ assignmentCompletion.evaluation_points_deducted_reason }}
</div>
</div>
<div class="mt-4">
<button class="btn-primary inline-flex items-center" @click="startTest()">
{{ $t("edoniqTest.viewResults") }}

View File

@ -34,7 +34,7 @@ const { t } = useTranslation();
<div>
<div class="mx-auto mb-8 mt-0 p-0 lg:mt-8 lg:p-4">
<h2 class="mb-4 text-2xl font-semibold">
So startest du mit diesem Lehrgang:
{{ $t("a.So startest du mit diesem Lehrgang") }}:
</h2>
<ol class="circle-numbered-list">
<li>

View File

@ -34,7 +34,7 @@ const { t } = useTranslation();
<div>
<div class="mx-auto mb-8 mt-0 p-0 lg:mt-8 lg:p-4">
<h2 class="mb-4 text-2xl font-semibold">
So startest du mit diesem Lehrgang:
{{ $t("a.So startest du mit diesem Lehrgang") }}:
</h2>
<ol class="circle-numbered-list">
<li>

View File

@ -1,5 +1,5 @@
import { describe, it } from "vitest";
import { pointsToGrade } from "../assignmentService";
import { percentToRoundedGrade, pointsToGrade } from "../assignmentService";
describe("assignmentService", () => {
it("pointsToGrade", () => {
@ -29,4 +29,60 @@ describe("assignmentService", () => {
expect(pointsToGrade(1, 24)).toBe(1);
expect(pointsToGrade(0, 24)).toBe(1);
});
it("percentToRoundedGrade with half grades", () => {
expect(percentToRoundedGrade(24 / 24)).toBe(6);
expect(percentToRoundedGrade(23 / 24)).toBe(6);
expect(percentToRoundedGrade(22 / 24)).toBe(5.5);
expect(percentToRoundedGrade(21 / 24)).toBe(5.5);
expect(percentToRoundedGrade(20 / 24)).toBe(5);
expect(percentToRoundedGrade(19 / 24)).toBe(5);
expect(percentToRoundedGrade(18 / 24)).toBe(5);
expect(percentToRoundedGrade(17 / 24)).toBe(4.5);
expect(percentToRoundedGrade(16 / 24)).toBe(4.5);
expect(percentToRoundedGrade(15 / 24)).toBe(4);
expect(percentToRoundedGrade(14 / 24)).toBe(4);
expect(percentToRoundedGrade(13 / 24)).toBe(3.5);
expect(percentToRoundedGrade(12 / 24)).toBe(3.5);
expect(percentToRoundedGrade(11 / 24)).toBe(3.5);
expect(percentToRoundedGrade(10 / 24)).toBe(3);
expect(percentToRoundedGrade(9 / 24)).toBe(3);
expect(percentToRoundedGrade(8 / 24)).toBe(2.5);
expect(percentToRoundedGrade(7 / 24)).toBe(2.5);
expect(percentToRoundedGrade(6 / 24)).toBe(2.5);
expect(percentToRoundedGrade(5 / 24)).toBe(2);
expect(percentToRoundedGrade(4 / 24)).toBe(2);
expect(percentToRoundedGrade(3 / 24)).toBe(1.5);
expect(percentToRoundedGrade(2 / 24)).toBe(1.5);
expect(percentToRoundedGrade(1 / 24)).toBe(1);
expect(percentToRoundedGrade(0 / 24)).toBe(1);
});
it("percentToRoundedGrade with 2 decimal places", () => {
expect(percentToRoundedGrade(24 / 24, false)).toBeCloseTo(6);
expect(percentToRoundedGrade(23 / 24, false)).toBeCloseTo(5.79);
expect(percentToRoundedGrade(22 / 24, false)).toBeCloseTo(5.58);
expect(percentToRoundedGrade(21 / 24, false)).toBeCloseTo(5.38);
expect(percentToRoundedGrade(20 / 24, false)).toBeCloseTo(5.17);
expect(percentToRoundedGrade(19 / 24, false)).toBeCloseTo(4.96);
expect(percentToRoundedGrade(18 / 24, false)).toBeCloseTo(4.75);
expect(percentToRoundedGrade(17 / 24, false)).toBeCloseTo(4.54);
expect(percentToRoundedGrade(16 / 24, false)).toBeCloseTo(4.33);
expect(percentToRoundedGrade(15 / 24, false)).toBeCloseTo(4.13);
expect(percentToRoundedGrade(14 / 24, false)).toBeCloseTo(3.92);
expect(percentToRoundedGrade(13 / 24, false)).toBeCloseTo(3.71);
expect(percentToRoundedGrade(12 / 24, false)).toBeCloseTo(3.5);
expect(percentToRoundedGrade(11 / 24, false)).toBeCloseTo(3.29);
expect(percentToRoundedGrade(10 / 24, false)).toBeCloseTo(3.08);
expect(percentToRoundedGrade(9 / 24, false)).toBeCloseTo(2.88);
expect(percentToRoundedGrade(8 / 24, false)).toBeCloseTo(2.67);
expect(percentToRoundedGrade(7 / 24, false)).toBeCloseTo(2.46);
expect(percentToRoundedGrade(6 / 24, false)).toBeCloseTo(2.25);
expect(percentToRoundedGrade(5 / 24, false)).toBeCloseTo(2.04);
expect(percentToRoundedGrade(4 / 24, false)).toBeCloseTo(1.83);
expect(percentToRoundedGrade(3 / 24, false)).toBeCloseTo(1.63);
expect(percentToRoundedGrade(2 / 24, false)).toBeCloseTo(1.42);
expect(percentToRoundedGrade(1 / 24, false)).toBeCloseTo(1.21);
expect(percentToRoundedGrade(0 / 24, false)).toBeCloseTo(1);
});
});

View File

@ -47,7 +47,7 @@ export async function loadAssignmentCompletionStatusData(
if (userAssignmentStatus?.completion_status === "EVALUATION_SUBMITTED") {
gradedUsers.push({
user: csu,
points: userAssignmentStatus.evaluation_points ?? 0,
points: userAssignmentStatus.evaluation_points_final ?? 0,
maxPoints: userAssignmentStatus.evaluation_max_points ?? 0,
passed: userAssignmentStatus.evaluation_passed ?? false,
});
@ -90,3 +90,15 @@ export function pointsToGrade(points: number, maxPoints: number) {
const halfGrade = grade / 2;
return Math.min(halfGrade, 5) + 1;
}
export function percentToRoundedGrade(percent: number, roundedToHalfGrade = true) {
if (roundedToHalfGrade) {
// Round to half-grades
const grade = Math.round(percent * 10);
const halfGrade = grade / 2;
return Math.min(halfGrade, 5) + 1;
} else {
// Round to 2 decimal places
return Math.round((percent * 5 + 1) * 100) / 100;
}
}

View File

@ -392,6 +392,7 @@ export type ActionCompetence = Omit<
export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
assignment_type: "CASEWORK" | "EDONIQ_TEST";
max_points: number;
competence_certificate_weight: number;
learning_content:
| (BaseCourseWagtailPage & {
circle: CircleLight;
@ -402,6 +403,9 @@ export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
completion_status: AssignmentCompletionStatus;
evaluation_submitted_at: string | null;
evaluation_points: number | null;
evaluation_points_final: number | null;
evaluation_points_deducted: number | null;
evaluation_points_reason: string;
evaluation_max_points: number | null;
evaluation_passed: boolean | null;
} | null;
@ -566,6 +570,8 @@ export interface UserAssignmentCompletionStatus {
assignment_user_id: string;
completion_status: AssignmentCompletionStatus;
evaluation_points: number | null;
evaluation_points_final: number | null;
evaluation_points_deducted: number | null;
evaluation_max_points: number | null;
evaluation_passed: boolean;
learning_content_page_id: string;

View File

@ -0,0 +1,69 @@
import { login } from "../helpers";
describe("cockpitPointsDeducted.cy.js", () => {
it("will show results with points", () => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 6,4,6,3,3 --create-edoniq-test-results 19 24 0"
);
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
// check edoniq test with deducted points
cy.get(
'[data-cy="submittable-test-lehrgang-lp-circle-fahrzeug-lc-wissens-und-verständnisfragen"]'
).should("contain", "1 von 3 Bewertungen freigegeben");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-wissens-und-verständnisfragen"]'
).click();
cy.get('[data-cy="Student1"]')
.should("contain", "19 von 24 Punkten")
.and("contain", "79%")
.and("not.contain", "Nicht bestanden");
// check casework with deducted points
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="submittable-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).should("contain", "1 von 3 Bewertungen freigegeben");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="Student1"]')
.should("contain", "22 von 24 Punkten")
.and("contain", "92%")
.and("not.contain", "Nicht bestanden");
});
it("will show results with deducted points", () => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 6,4,6,3,3 --assignment-points-deducted 14 --create-edoniq-test-results 19 24 8"
);
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit");
// check edoniq test with deducted points
cy.get(
'[data-cy="submittable-test-lehrgang-lp-circle-fahrzeug-lc-wissens-und-verständnisfragen"]'
).should("contain", "1 von 3 Bewertungen freigegeben");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-wissens-und-verständnisfragen"]'
).click();
cy.get('[data-cy="Student1"]')
.should("contain", "11 von 24 Punkten")
.and("contain", "46%")
.and("contain", "Nicht bestanden");
// check casework with deducted points
cy.visit("/course/test-lehrgang/cockpit");
cy.get(
'[data-cy="submittable-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).should("contain", "1 von 3 Bewertungen freigegeben");
cy.get(
'[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]'
).click();
cy.get('[data-cy="Student1"]')
.should("contain", "8 von 24 Punkten")
.and("contain", "33%")
.and("contain", "Nicht bestanden");
});
});

View File

@ -14,9 +14,7 @@ describe("competenceCertificate.cy.js", () => {
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.should("contain", "0 von 0 Punkten")
.and("contain", "0 von 2 Kompetenznachweis-Elementen");
).and("contain", "0 von 2 Kompetenznachweis-Elementen");
// check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click();
@ -48,31 +46,37 @@ describe("competenceCertificate.cy.js", () => {
it("check with finished passed edoniq test", () => {
cy.manageCommand(
"cypress_reset --create-assignment-completion --create-edoniq-test-results 19 24"
"cypress_reset --create-assignment-completion --create-edoniq-test-results 19 24 0"
);
login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/competence");
cy.get('[data-cy="certificate-total-points-text"]').contains(
"Zwischenstand Gesamtpunktzahl: 19 von 24 Punkten"
"Erfahrungsnote üK: 5"
);
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.should("contain", "19 von 24 Punkten")
.should("contain", "Note: 5")
.and("contain", "1 von 2 Kompetenznachweis-Elementen");
// check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click();
cy.get('[data-cy="certificate-total-points-text"]')
.should("contain", "19")
.should("contain", "Erfahrungsnote üK")
.and("contain", "Zwischenstand");
cy.get('[data-cy="certificate-total-grade"]').should("contain", "Note: 5");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 5");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 4.96");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.should("contain", "19")
.and("contain", "Zwischenstand")
.and("contain", "1 von 2 Kompetenznachweis-Elementen");
@ -104,7 +108,7 @@ describe("competenceCertificate.cy.js", () => {
it("check with finished failed edoniq test", () => {
cy.manageCommand(
"cypress_reset --create-assignment-completion --create-edoniq-test-results 10 24"
"cypress_reset --create-assignment-completion --create-edoniq-test-results 10 24 0"
);
login("test-student1@example.com", "test");
@ -113,6 +117,13 @@ describe("competenceCertificate.cy.js", () => {
"/course/test-lehrgang/competence/certificates/kompetenznachweis-1"
);
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 3");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 3.08");
cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
)
@ -133,33 +144,39 @@ describe("competenceCertificate.cy.js", () => {
it("check with finished edoniq test and finished casework", () => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --create-edoniq-test-results 19 24"
"cypress_reset --create-assignment-evaluation --create-edoniq-test-results 19 24 0"
);
login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/competence");
cy.get('[data-cy="certificate-total-points-text"]').contains(
"Zwischenstand Gesamtpunktzahl: 43 von 48 Punkten"
"Erfahrungsnote üK: 5.5"
);
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.should("contain", "43 von 48 Punkten")
.should("contain", "Note: 5.5")
.and("contain", "2 von 2 Kompetenznachweis-Elementen");
// check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click();
cy.get('[data-cy="certificate-total-points-text"]')
.should("contain", "43")
.should("contain", "Erfahrungsnote üK")
.and("contain", "Note: 5.5")
.and("not.contain", "Zwischenstand");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.should("contain", "43")
.and("not.contain", "Zwischenstand")
.and("contain", "2 von 2 Kompetenznachweis-Elementen");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 5.5");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 5.48");
// check certificate detail page
cy.get(
@ -180,6 +197,110 @@ describe("competenceCertificate.cy.js", () => {
.and("contain", "Bewertung freigegeben");
});
it("check with finished edoniq test with deducted points", () => {
cy.manageCommand(
"cypress_reset --create-assignment-completion --create-edoniq-test-results 19 24 8"
);
login("test-student1@example.com", "test");
// go to certificate detail page
cy.visit(
"/course/test-lehrgang/competence/certificates/kompetenznachweis-1"
);
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 3.5");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 3.29");
cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"]'
)
.should("contain", "11")
.and("contain", "Bewertung freigegeben")
.and("contain", "46%")
.and("contain", "mit Abzug")
.and("contain", "Nicht bestanden");
// it can open learning content page directly
cy.get(
'[data-cy="assignment-test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo"] [data-cy="open-learning-content"]'
).click();
cy.get('[data-cy="test-result"]')
.should("contain", "11 von 24 Punkten")
.and("contain", "46%")
.and("contain", "Punkte aus Bewertung: 19")
.and("contain", "Abgezogene Punkte: 8")
.and("contain", "Grund: Edoniq Punkteabzug Test")
.and("contain", "Nicht bestanden");
});
it("check with finished casework and points deducted", () => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 4,6,4,3,2 --assignment-points-deducted 5"
);
login("test-student1@example.com", "test");
cy.visit("/course/test-lehrgang/competence");
cy.get('[data-cy="certificate-total-points-text"]').contains(
"Erfahrungsnote üK: 4"
);
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.should("contain", "Note: 4")
.and("contain", "1 von 2 Kompetenznachweis-Elementen");
// check on certificates page
cy.get('[data-cy="certificates-show-all-button"]').click();
cy.get('[data-cy="certificate-total-points-text"]')
.should("contain", "Erfahrungsnote üK")
.and("contain", "Note: 4")
.and("contain", "Zwischenstand");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1"]'
)
.and("contain", "Zwischenstand")
.and("contain", "1 von 2 Kompetenznachweis-Elementen");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade"]'
).should("contain", "Note: 4");
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-grade-percent"]'
).should("contain", "Ungerundete Note: 3.92");
// check certificate detail page
cy.get(
'[data-cy="certificate-test-lehrgang-competencenavi-certificates-kompetenznachweis-1-detail-link"]'
).click();
cy.get(
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"]'
)
.should("contain", "14")
.and("contain", "von 24 Punkten")
.and("contain", "58%")
.and("contain", "mit Abzug")
.and("contain", "Bewertung freigegeben");
cy.get(
'[data-cy="assignment-test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"] [data-cy="open-learning-content"]'
).click();
cy.get('[data-cy="user-points"]').should("contain", "14");
cy.get('[data-cy="total-points"]').should(
"contain",
"von 24 Punkten (58%)"
);
cy.get('[data-cy="points-deducted"]')
.should("contain", "Punkte aus Bewertung: 19")
.and("contain", "Abgezogene Punkte: 5")
.and("contain", "Grund: Assignment Punkteabzug Test");
});
it("should display link to details", () => {
cy.manageCommand("cypress_reset");
login("test-student1@example.com", "test");

View File

@ -13,6 +13,7 @@ const clickOnDetailsLink = (within) => {
};
describe("dashboardSupervisor.cy.js", () => {
describe("with data", () => {
beforeEach(() => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days"
@ -25,7 +26,10 @@ describe("dashboardSupervisor.cy.js", () => {
it("contains correct numbers", () => {
// we have no completed assignments, but some are in progress
// -> makes sure that the numbers are correct
getDashboardStatistics("assignments.completed").should("have.text", "1");
getDashboardStatistics("assignments.completed").should(
"have.text",
"1"
);
getDashboardStatistics("assignments.passed").should("have.text", "34%");
});
@ -98,3 +102,33 @@ describe("dashboardSupervisor.cy.js", () => {
});
});
});
describe("with deducted points", () => {
beforeEach(() => {
cy.manageCommand(
"cypress_reset --create-assignment-evaluation --assignment-evaluation-scores 6,6,6,3,3 --assignment-points-deducted 14 --create-edoniq-test-results 19 24 8"
);
login("test-supervisor1@example.com", "test");
cy.visit("/");
});
it("contains numbers with deduction", () => {
// we have no completed assignments, but some are in progress
// -> makes sure that the numbers are correct
getDashboardStatistics("assignments.completed").should("have.text", "2");
getDashboardStatistics("assignments.passed").should("have.text", "0%");
// check data on the details page
cy.get(
'[data-cy="dashboard.stats.assignments"] [data-cy="basebox.detailsLink"]'
).click();
cy.get(
'[data-cy="Edoniq Wissens- und Verständisfragen - Circle Fahrzeug (Demo)"]'
).should("contain", "0 von 3 bestanden");
cy.get(
'[data-cy="Überprüfen einer Motorfahrzeugs-Versicherungspolice"]'
).should("contain", "0 von 3 bestanden");
});
});
});

View File

@ -7,6 +7,7 @@ For more information on this file, see
https://docs.djangoproject.com/en/dev/howto/deployment/asgi/
"""
import os
import sys
from pathlib import Path

View File

@ -1,6 +1,7 @@
"""
Base settings to build other settings files upon.
"""
import logging
from pathlib import Path
@ -398,9 +399,9 @@ if IT_DJANGO_LOGGING_CONF == "IT_DJANGO_LOGGING_CONF_CONSOLE_COLOR":
"propagate": False,
},
"django.server": {
"handlers": ["null"]
if IT_LOCAL_HIDE_DJANGO_SERVER_LOGS
else ["default"],
"handlers": (
["null"] if IT_LOCAL_HIDE_DJANGO_SERVER_LOGS else ["default"]
),
"level": "INFO",
"propagate": False,
},

View File

@ -13,6 +13,7 @@ middleware here, or combine a Django application with an application of another
framework.
"""
import os
import sys
from pathlib import Path

View File

@ -13,8 +13,43 @@ class AssignmentCompletionAdmin(admin.ModelAdmin):
date_hierarchy = "created_at"
list_display = [
"id",
"completion_status",
"assignment",
"get_circle",
"assignment_user",
"course_session",
"completion_status",
"evaluation_points",
"evaluation_points_deducted",
]
list_filter = [
"completion_status",
"assignment__assignment_type",
"course_session__course",
"course_session",
]
search_fields = ["assignment_user__email"]
readonly_fields = [
"assignment_user",
"assignment",
"completion_data",
"course_session",
"learning_content_page",
"evaluation_points",
"submitted_at",
"evaluation_user",
"evaluation_submitted_at",
"evaluation_points_deducted_user",
]
def get_circle(self, obj):
try:
return obj.learning_content_page.specific.get_circle().title
except Exception:
return ""
get_circle.short_description = "Circle"
def save_model(self, request, obj, form, change):
if change and "evaluation_points_deducted" in form.changed_data:
obj.evaluation_points_deducted_user = request.user
super().save_model(request, obj, form, change)

View File

@ -19,6 +19,7 @@ class AssignmentCompletionObjectType(DjangoObjectType):
# rounded to sensible representation
evaluation_points = graphene.Float()
evaluation_points_final = graphene.Float()
evaluation_max_points = graphene.Float()
class Meta:
@ -38,6 +39,9 @@ class AssignmentCompletionObjectType(DjangoObjectType):
"evaluation_user",
"additional_json_data",
"edoniq_extended_time_flag",
"evaluation_points_deducted",
"evaluation_points_deducted_reason",
"evaluation_points_deducted_user",
"evaluation_passed",
"task_completion_data",
)
@ -47,6 +51,11 @@ class AssignmentCompletionObjectType(DjangoObjectType):
return round(self.evaluation_points, 1) # noqa
return None
def resolve_evaluation_points_final(self, info):
if self.evaluation_points:
return round(self.evaluation_points_final, 1) # noqa
return None
def resolve_evaluation_max_points(self, info):
if self.evaluation_max_points:
return round(self.evaluation_max_points, 1) # noqa
@ -58,6 +67,7 @@ class AssignmentObjectType(DjangoObjectType):
evaluation_tasks = JSONStreamField()
performance_objectives = JSONStreamField()
max_points = graphene.Int()
competence_certificate_weight = graphene.Float()
learning_content = graphene.Field(LearningContentInterface)
completion = graphene.Field(
AssignmentCompletionObjectType,
@ -87,6 +97,9 @@ class AssignmentObjectType(DjangoObjectType):
def resolve_max_points(self, info):
return self.get_max_points()
def resolve_competence_certificate_weight(self, info):
return self.competence_certificate_weight
def resolve_learning_content(self, info):
return self.find_attached_learning_content()

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.20 on 2024-05-03 14:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assignment", "0012_auto_20240124_1004"),
]
operations = [
migrations.AddField(
model_name="assignment",
name="competence_certificate_weight",
field=models.FloatField(
default=1.0, help_text="Gewichtung für den Kompetenznachweis"
),
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 3.2.20 on 2024-05-21 14:52
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("assignment", "0013_assignment_competence_certificate_weight"),
]
operations = [
migrations.AddField(
model_name="assignmentcompletion",
name="evaluation_points_deducted",
field=models.FloatField(default=0.0, verbose_name="Punkteabzug"),
),
migrations.AddField(
model_name="assignmentcompletion",
name="evaluation_points_deducted_reason",
field=models.TextField(
blank=True, default="", verbose_name="Punkteabzug Begründung"
),
),
migrations.AddField(
model_name="assignmentcompletion",
name="evaluation_points_deducted_user",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="assignmentcompletion",
name="completion_data",
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -153,6 +153,10 @@ class Assignment(CourseBasePage):
blank=True,
on_delete=models.SET_NULL,
)
competence_certificate_weight = models.FloatField(
default=1.0,
help_text="Gewichtung für den Kompetenznachweis",
)
intro_text = RichTextField(
help_text="Erläuterung der Ausgangslage",
@ -336,10 +340,34 @@ class AssignmentCompletion(models.Model):
related_name="+",
)
evaluation_points = models.FloatField(null=True, blank=True)
evaluation_points_deducted = models.FloatField(
default=0.0, verbose_name="Punkteabzug"
)
evaluation_points_deducted_reason = models.TextField(
default="", blank=True, verbose_name="Punkteabzug Begründung"
)
evaluation_points_deducted_user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="+",
)
evaluation_max_points = models.FloatField(null=True, blank=True)
evaluation_passed = models.BooleanField(null=True, blank=True)
edoniq_extended_time_flag = models.BooleanField(default=False)
@property
def evaluation_points_final(self):
"""
Das ist das relevante Feld für die Punkteberechnung, es berücksichtigt
den Punkteabzug aus `evaluation_points_deducted`
"""
if self.evaluation_points is None:
return None
return self.evaluation_points - self.evaluation_points_deducted
assignment_user = models.ForeignKey(User, on_delete=models.CASCADE)
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
@ -360,7 +388,7 @@ class AssignmentCompletion(models.Model):
default=AssignmentCompletionStatus.IN_PROGRESS.value,
)
completion_data = models.JSONField(default=dict)
completion_data = models.JSONField(default=dict, blank=True)
additional_json_data = models.JSONField(default=dict, blank=True)
class Meta:
@ -387,6 +415,14 @@ class AssignmentCompletion(models.Model):
data[task.id] = get_task_data(task, self.completion_data)
return data
def save(
self,
**kwargs,
):
if self.evaluation_points_deducted > 0:
recalculate_assignment_passed(self)
super().save(**kwargs)
def get_file_info(file_id):
file_info = UploadFile.objects.filter(id=file_id).first()
@ -444,3 +480,13 @@ class AssignmentCompletionAuditLog(models.Model):
evaluation_points = models.FloatField(null=True, blank=True)
evaluation_max_points = models.FloatField(null=True, blank=True)
evaluation_passed = models.BooleanField(null=True, blank=True)
def recalculate_assignment_passed(ac: AssignmentCompletion):
if ac.evaluation_points_final is not None and ac.evaluation_max_points is not None:
# if more or equal than 55% of the points are reached, the assignment is passed
ac.evaluation_passed = (
ac.evaluation_points_final / ac.evaluation_max_points
) >= 0.55
return ac

View File

@ -21,6 +21,8 @@ class AssignmentCompletionSerializer(serializers.ModelSerializer):
"evaluation_user",
"additional_json_data",
"evaluation_points",
"evaluation_points_deducted",
"evaluation_points_deducted_reason",
"evaluation_max_points",
"evaluation_passed",
]

View File

@ -15,6 +15,7 @@ from vbv_lernwelt.assignment.models import (
AssignmentCompletionStatus,
AssignmentType,
is_valid_assignment_completion_status,
recalculate_assignment_passed,
)
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.utils import find_first
@ -178,11 +179,7 @@ def update_assignment_completion(
# if no evaluation_passed is provided, we calculate it from the points
if evaluation_passed is None and ac.evaluation_max_points > 0:
if evaluation_points is not None and ac.evaluation_max_points is not None:
# if more or equal than 60% of the points are reached, the assignment is passed
ac.evaluation_passed = (
evaluation_points / ac.evaluation_max_points
) >= 0.55
recalculate_assignment_passed(ac)
else:
ac.evaluation_passed = evaluation_passed

View File

@ -21,6 +21,7 @@ def request_assignment_completion_status(request, assignment_id, course_session_
"assignment_user_id",
"completion_status",
"evaluation_points",
"evaluation_points_deducted",
"evaluation_max_points",
"evaluation_passed",
"learning_content_page_id",
@ -29,6 +30,13 @@ def request_assignment_completion_status(request, assignment_id, course_session_
# Convert the learning_content_page_id to a string
data = list(qs) # Evaluate the queryset
for item in data:
if item["evaluation_points"] is not None:
# only `evaluation_points_final` is relevant for the frontend
item["evaluation_points_final"] = (
item["evaluation_points"] - item["evaluation_points_deducted"]
)
else:
item["evaluation_points_final"] = None
item["learning_content_page_id"] = str(item["learning_content_page_id"])
return Response(status=200, data=data)

View File

@ -66,11 +66,16 @@ from vbv_lernwelt.self_evaluation_feedback.models import (
default=None,
help="Provide assignment evaluation scores in the format: 6,6,6,3,3",
)
@click.option(
"--assignment-points-deducted",
default=0,
help="Provide assignment points deducted",
)
@click.option(
"--create-edoniq-test-results",
type=(int, int),
default=(None, None),
metavar="USER_POINTS MAX_POINTS",
type=(int, int, float),
default=(None, None, 0.0),
metavar="USER_POINTS MAX_POINTS POINTS_DEDUCTED",
help="Create edoniq result data for test-student1@example.com with user points and max points",
)
@click.option(
@ -112,6 +117,7 @@ def command(
create_assignment_completion,
create_assignment_evaluation,
assignment_evaluation_scores,
assignment_points_deducted,
create_edoniq_test_results,
create_feedback_responses,
create_course_completion_performance_criteria,
@ -171,9 +177,10 @@ def command(
assignment_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
evaluation_user=User.objects.get(id=TEST_TRAINER1_USER_ID),
input_scores=assignment_evaluation_scores,
points_deducted=assignment_points_deducted,
)
user_points, max_points = create_edoniq_test_results
user_points, max_points, points_deducted = create_edoniq_test_results
if user_points is not None and max_points is not None:
print(
f"Create edoniq test results: User Points: {user_points}, Max Points: {max_points}"
@ -186,6 +193,7 @@ def command(
assignment_user=User.objects.get(id=TEST_STUDENT1_USER_ID),
user_points=user_points,
max_points=max_points,
evaluation_points_deducted=points_deducted,
)
if create_feedback_responses:

View File

@ -58,3 +58,10 @@ def pretty_print_json(json_string):
# pylint: disable=broad-except
except Exception:
return json_string
def safe_deque_popleft(deq, default=None):
try:
return deq.popleft()
except IndexError:
return default

View File

@ -151,6 +151,9 @@ def cypress_reset_view(request):
assignment_evaluation_scores = request.data.get("assignment_evaluation_scores")
if assignment_evaluation_scores:
options["assignment_evaluation_scores"] = assignment_evaluation_scores
options["assignment_points_deducted"] = float(
request.data.get("assignment_points_deducted") or 0
)
options["create_feedback_responses"] = (
request.data.get("create_feedback_responses") == "true"
@ -159,10 +162,12 @@ def cypress_reset_view(request):
# edoniq test results
edoniq_test_user_points = request.data.get("edoniq_test_user_points")
edoniq_test_max_points = request.data.get("edoniq_test_max_points")
edoniq_points_deducted = request.data.get("edoniq_test_points_deducted") or 0
if bool(edoniq_test_user_points and edoniq_test_max_points):
options["create_edoniq_test_results"] = (
int(edoniq_test_user_points),
int(edoniq_test_max_points),
float(edoniq_points_deducted),
)
options["create_course_completion_performance_criteria"] = (

View File

@ -1,3 +1,4 @@
from collections import deque
from datetime import datetime
from dateutil.relativedelta import MO, relativedelta, TH, TU, WE
@ -42,6 +43,7 @@ from vbv_lernwelt.core.constants import (
TEST_TRAINER1_USER_ID,
)
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.utils import safe_deque_popleft
from vbv_lernwelt.course.consts import COURSE_TEST_ID
from vbv_lernwelt.course.factories import CoursePageFactory
from vbv_lernwelt.course.models import (
@ -350,20 +352,27 @@ def create_test_assignment_submitted_data(assignment, course_session, user):
def create_test_assignment_evaluation_data(
assignment, course_session, assignment_user, evaluation_user, input_scores=None
assignment,
course_session,
assignment_user,
evaluation_user,
input_scores=None,
points_deducted=0,
):
if assignment and course_session and assignment_user and evaluation_user:
subtasks = assignment.get_evaluation_tasks()
evaluation_points = 0
input_scores_deque = deque(input_scores) if input_scores else deque()
for index, evaluation_task in enumerate(subtasks):
task_score = evaluation_task["value"]["max_points"]
if input_scores[index] < len(input_scores):
input_score = safe_deque_popleft(input_scores_deque)
if input_score is not None:
task_score = input_scores[index]
evaluation_points += task_score
update_assignment_completion(
ac, _ = update_assignment_completion(
assignment_user=assignment_user,
assignment=assignment,
course_session=course_session,
@ -380,7 +389,7 @@ def create_test_assignment_evaluation_data(
evaluation_user=evaluation_user,
)
update_assignment_completion(
ac, _ = update_assignment_completion(
assignment_user=assignment_user,
assignment=assignment,
course_session=course_session,
@ -391,12 +400,27 @@ def create_test_assignment_evaluation_data(
evaluation_points=evaluation_points,
)
# take the last input score as deduction if there is one left...
if points_deducted > 0:
ac.evaluation_points_deducted = points_deducted
ac.evaluation_points_deducted_reason = "Assignment Punkteabzug Test"
ac.save()
def create_edoniq_test_result_data(
assignment, course_session, assignment_user, user_points=19, max_points=24
assignment,
course_session,
assignment_user,
user_points=19,
max_points=24,
evaluation_points_deducted=0,
):
assignment.assignment.evaluation_tasks.raw_data[0]["value"][
"max_points"
] = max_points
assignment.assignment.save()
if assignment and course_session and assignment_user:
update_assignment_completion(
ac, _ = update_assignment_completion(
assignment_user=assignment_user,
assignment=assignment,
course_session=course_session,
@ -408,6 +432,11 @@ def create_edoniq_test_result_data(
evaluation_max_points=max_points,
)
if evaluation_points_deducted > 0:
ac.evaluation_points_deducted = evaluation_points_deducted
ac.evaluation_points_deducted_reason = "Edoniq Punkteabzug Test"
ac.save()
def create_feedback_response_data(
course_session,

View File

@ -103,7 +103,7 @@ class DashboardQuery(graphene.ObjectType):
course_session_ids.add(course_session_id)
return CourseStatisticsType(
_id=course.id, # noqa
_id=f"mentor:{course.id}", # noqa
course_id=course.id, # noqa
course_title=course.title, # noqa
course_slug=course.slug, # noqa
@ -188,14 +188,22 @@ class DashboardQuery(graphene.ObjectType):
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
assignment_user=user,
course_session__course=course,
).values("evaluation_max_points", "evaluation_points")
).values(
"evaluation_max_points", "evaluation_points", "evaluation_points_deducted"
)
evaluation_results = list(evaluation_results)
points_max_count = sum(
[result.get("evaluation_max_points", 0) for result in evaluation_results]
)
points_achieved_count = sum(
[result.get("evaluation_points", 0) for result in evaluation_results]
[
(
result.get("evaluation_points", 0)
- result.get("evaluation_points_deducted", 0)
)
for result in evaluation_results
]
)
return CourseProgressType(

View File

@ -58,12 +58,17 @@ class AssignmentsStatisticsType(graphene.ObjectType):
summary = graphene.Field(AssignmentStatisticsSummaryType, required=True)
def create_assignment_summary(course_id, metrics) -> AssignmentStatisticsSummaryType:
def create_assignment_summary(
course_id, metrics, urql_id: str = None
) -> AssignmentStatisticsSummaryType:
if urql_id is None:
urql_id = str(course_id)
completed_metrics = [m for m in metrics if m.ranking_completed]
if not completed_metrics:
return AssignmentStatisticsSummaryType(
_id=course_id,
_id=urql_id,
completed_count=0,
average_passed=0,
total_passed=0,
@ -80,7 +85,7 @@ def create_assignment_summary(course_id, metrics) -> AssignmentStatisticsSummary
total_failed = sum([m.failed_count for m in completed_metrics])
return AssignmentStatisticsSummaryType(
_id=course_id, # noqa
_id=urql_id, # noqa
completed_count=completed_count, # noqa
average_passed=average_passed_completed, # noqa
total_passed=total_passed, # noqa
@ -92,6 +97,7 @@ def get_assignment_completion_metrics(
course_session: CourseSession,
assignment: vbv_lernwelt.assignment.models.Assignment,
user_selection_ids: List[str] | None,
urql_id_postfix: str = "",
) -> AssignmentCompletionMetricsType:
if user_selection_ids:
course_session_users = user_selection_ids
@ -120,7 +126,7 @@ def get_assignment_completion_metrics(
average_passed = math.ceil(passed_count / participants_count * 100)
return AssignmentCompletionMetricsType(
_id=f"{course_session.id}-{assignment.id}", # noqa
_id=f"{course_session.id}-{assignment.id}@{urql_id_postfix}", # noqa
passed_count=passed_count, # noqa
failed_count=failed_count, # noqa
unranked_count=unranked_count, # noqa
@ -132,6 +138,7 @@ def get_assignment_completion_metrics(
def create_record(
course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest,
user_selection_ids: List[str] | None,
urql_id_postfix: str = "",
) -> AssignmentStatisticsRecordType:
if isinstance(course_session_assignment, CourseSessionAssignment):
due_date = course_session_assignment.submission_deadline
@ -142,7 +149,7 @@ def create_record(
return AssignmentStatisticsRecordType(
# make sure it's unique, across all types of assignments!
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}",
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}",
# noqa
course_session_id=str(course_session_assignment.course_session.id), # noqa
circle_id=learning_content.get_circle().id, # noqa
@ -155,6 +162,7 @@ def create_record(
course_session=course_session_assignment.course_session, # noqa
assignment=learning_content.content_assignment, # noqa
user_selection_ids=user_selection_ids, # noqa
urql_id_postfix=urql_id_postfix, # noqa
),
details_url=due_date.url_expert, # noqa
deadline=due_date.start, # noqa
@ -166,7 +174,11 @@ def assignments(
course_session_selection_ids: graphene.List(graphene.ID),
user_selection_ids: List[str] | None = None,
circle_ids: List[graphene.ID] | None = None,
urql_id: str = None,
) -> AssignmentsStatisticsType:
if urql_id is None:
urql_id = str(course_id)
course_sessions = CourseSession.objects.filter(
id__in=course_session_selection_ids,
)
@ -176,21 +188,25 @@ def assignments(
for csa in query_competence_course_session_assignments(
[course_session.id], circle_ids
):
record = create_record(csa, user_selection_ids)
record = create_record(csa, user_selection_ids, urql_id_postfix=urql_id)
records.append(record)
for cset in query_competence_course_session_edoniq_tests(
[course_session.id], circle_ids
):
record = create_record(
course_session_assignment=cset, user_selection_ids=user_selection_ids
course_session_assignment=cset,
user_selection_ids=user_selection_ids,
urql_id_postfix=urql_id,
)
records.append(record)
return AssignmentsStatisticsType(
_id=course_id, # noqa
_id=urql_id, # noqa
records=sorted(records, key=lambda r: r.deadline), # noqa
summary=create_assignment_summary( # noqa
course_id=course_id, metrics=[r.metrics for r in records] # noqa
course_id=course_id,
metrics=[r.metrics for r in records], # noqa
urql_id=urql_id, # noqa
),
)

View File

@ -38,7 +38,11 @@ def attendance_day_presences(
course_id: graphene.ID,
course_session_selection_ids: graphene.List(graphene.ID),
circle_ids: List[graphene.ID] = None,
urql_id: str = None,
) -> AttendanceDayPresencesStatisticsType:
if urql_id is None:
urql_id = str(course_id)
completed = CourseSessionAttendanceCourse.objects.filter(
course_session_id__in=course_session_selection_ids,
due_date__end__lt=timezone.now(),
@ -76,7 +80,7 @@ def attendance_day_presences(
records.append(
PresenceRecordStatisticsType(
_id=f"attendance_day:{attendance_day.id}", # noqa
_id=f"{urql_id}:attendance_day:{attendance_day.id}", # noqa
course_session_id=course_session.id, # noqa
generation=course_session.generation, # noqa
circle_id=circle.id, # noqa
@ -88,7 +92,7 @@ def attendance_day_presences(
)
summary = AttendanceSummaryStatisticsType(
_id=course_id, # noqa
_id=urql_id, # noqa
days_completed=len(records), # noqa
participants_present=calculate_avg_participation(records), # noqa
)

View File

@ -36,6 +36,7 @@ def competences(
course_slug: str,
user_selection_ids: List[str] | None = None,
circle_ids: List[str] | None = None,
urql_id_postfix: str = "",
) -> Tuple[List[CompetenceRecordStatisticsType], int, int]:
completions = CourseCompletion.objects.filter(
course_session_id__in=course_session_selection_ids,
@ -70,7 +71,7 @@ def competences(
if not circle:
continue
combined_id = f"{circle.id}-{completion.course_session.id}"
combined_id = f"{circle.id}-{completion.course_session.id}@{urql_id_postfix}"
competence_records.setdefault(combined_id, {}).setdefault(
learning_unit,

View File

@ -107,6 +107,7 @@ class BaseStatisticsType(graphene.ObjectType):
course_session_selection_ids=root.course_session_selection_ids,
user_selection_ids=user_selection_ids,
circle_ids=root.get_circle_ids(_info),
urql_id=str(root._id),
)
def get_circle_ids(self, info):
@ -133,6 +134,7 @@ class CourseStatisticsType(BaseStatisticsType):
course_id=root.course_id,
course_session_selection_ids=root.course_session_selection_ids,
circle_ids=root.get_circle_ids(info),
urql_id=str(root._id),
)
def resolve_feedback_responses(root, info) -> FeedbackStatisticsResponsesType:
@ -141,6 +143,7 @@ class CourseStatisticsType(BaseStatisticsType):
course_id=root.course_id,
course_slug=root.course_slug,
circle_ids=root.get_circle_ids(info),
urql_id=str(root._id),
)
def resolve_competences(root, info) -> CompetencesStatisticsType:
@ -156,6 +159,7 @@ class CourseStatisticsType(BaseStatisticsType):
],
user_selection_ids=user_selection_ids, # noqa
circle_ids=root.get_circle_ids(info), # noqa
urql_id_postfix=str(root._id), # noqa
)
return CompetencesStatisticsType(
_id=root._id, # noqa

View File

@ -38,7 +38,11 @@ def feedback_responses(
course_id: graphene.ID,
course_slug: graphene.String,
circle_ids: List[graphene.ID] = None,
urql_id: str = None,
) -> FeedbackStatisticsResponsesType:
if urql_id is None:
urql_id = str(course_id)
# Get all course sessions for this user in the given course
course_sessions = CourseSession.objects.filter(
id__in=course_session_selection_ids,
@ -65,6 +69,7 @@ def feedback_responses(
course_session_id=course_session.id,
generation=course_session.generation,
course_slug=str(course_slug),
urql_id_postfix=urql_id,
)
)
@ -76,10 +81,10 @@ def feedback_responses(
avg = 0
return FeedbackStatisticsResponsesType(
_id=course_id, # noqa
_id=urql_id, # noqa
records=circle_feedbacks, # noqa
summary=FeedbackStatisticsSummaryType( # noqa
_id=course_id, # noqa
_id=urql_id, # noqa
satisfaction_average=avg, # noqa
satisfaction_max=4, # noqa
total_responses=total_responses, # noqa
@ -92,6 +97,7 @@ def circle_feedback_average(
course_session_id,
generation: str,
course_slug: str,
urql_id_postfix: str = "",
):
circle_data = {}
records = []
@ -119,7 +125,7 @@ def circle_feedback_average(
records.append(
FeedbackStatisticsRecordType(
_id=f"circle:{circle_id}-course_session:{course_session_id}", # noqa
_id=f"circle:{circle_id}-course_session:{course_session_id}@{urql_id_postfix}", # noqa
course_session_id=course_session_id, # noqa
generation=generation, # noqa
circle_id=circle_id, # noqa

View File

@ -73,6 +73,10 @@
<label>
evaluation score:
<input type="text" name="assignment_evaluation_scores" placeholder="6,6,6,3,3">
</label><br>
<label>
points deducted:
<input type="number" name="assignment_points_deducted" min="0">
</label>
<div style="margin-bottom: 8px; padding: 4px; border-bottom: 1px lightblue solid"></div>
@ -84,6 +88,10 @@
<label>
max points:
<input type="number" name="edoniq_test_max_points" min="0">
</label><br>
<label>
points deducted:
<input type="number" name="edoniq_test_points_deducted" min="0">
</label>
<div style="margin-bottom: 8px; padding: 4px; border-bottom: 1px lightblue solid"></div>