From bd95776ec7478229a26b11c934e8eab2302caa2c Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Tue, 10 Sep 2024 17:00:21 +0200 Subject: [PATCH 01/14] Initial implementation --- README.md | 12 + .../src/components/dashboard/CoursePanel.vue | 14 ++ .../TrainingResponsibleStatistics.vue | 72 ++++++ client/src/gql/gql.ts | 5 + client/src/gql/graphql.ts | 36 +++ client/src/gql/schema.graphql | 212 ++++++++++-------- client/src/gql/typenames.ts | 3 + client/src/graphql/queries.ts | 21 ++ .../src/pages/dashboard/DashboardCostPage.vue | 65 ++++++ .../pages/dashboard/DashboardPersonsPage.vue | 40 +++- client/src/router/index.ts | 5 + client/src/services/dashboard.ts | 35 ++- client/src/utils/format_currency_chf.ts | 17 ++ ...create_ausbildungsverantwortlicher_mobi.py | 48 ++++ scripts/create_berufsbildner_mobi.py | 8 +- server/vbv_lernwelt/core/constants.py | 1 + .../vbv_lernwelt/core/create_default_users.py | 10 + .../vbv_lernwelt/dashboard/graphql/queries.py | 14 ++ .../dashboard/graphql/types/dashboard.py | 86 ++++++- server/vbv_lernwelt/dashboard/utils.py | 10 +- server/vbv_lernwelt/dashboard/views.py | 6 + server/vbv_lernwelt/shop/views.py | 4 + 22 files changed, 615 insertions(+), 109 deletions(-) create mode 100644 client/src/components/dashboard/TrainingResponsibleStatistics.vue create mode 100644 client/src/pages/dashboard/DashboardCostPage.vue create mode 100644 client/src/utils/format_currency_chf.ts create mode 100644 scripts/create_ausbildungsverantwortlicher_mobi.py diff --git a/README.md b/README.md index 68c667d1..5ba494da 100644 --- a/README.md +++ b/README.md @@ -309,3 +309,15 @@ graphql schema. - The `id` field has to be a string? - What about the generated types from `codegen`? Hand written types seem to be better. - The functions in `cacheExchange` should be nearer the concrete implementation... + +## Load prod data for testing + +1. Checkout the [vbv-devops](https://bitbucket.org/iterativ/iterativ-devops/src/master/) repository +2. Change into the `backups` directory +3. Run `python3 check_vbv_backup.py`. This downloads the latest backup from S3 and restores it to the `vbv-lernwelt` database. +4. Reset all user passwords. Open `shell_plus` in the `server` directory of the `vbv_lernwelt` repository and run + + ```python + for csu in CourseSessionUser.objects.all(): + csu.user.set_password("test") + ``` diff --git a/client/src/components/dashboard/CoursePanel.vue b/client/src/components/dashboard/CoursePanel.vue index b73210f3..89a3a335 100644 --- a/client/src/components/dashboard/CoursePanel.vue +++ b/client/src/components/dashboard/CoursePanel.vue @@ -10,6 +10,7 @@ import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.v import type { DashboardCourseConfigType, WidgetType } from "@/services/dashboard"; import { getCockpitUrl, getLearningMentorUrl, getLearningPathUrl } from "@/utils/utils"; import { computed } from "vue"; +import TrainingResponsibleStatistics from "./TrainingResponsibleStatistics.vue"; const mentorWidgets = [ "MentorTasksWidget", @@ -71,6 +72,13 @@ const actionButtonProps = computed<{ href: string; text: string; cyKey: string } cyKey: "progress-dashboard-continue-course-link", }; } + if (props.courseConfig?.role_key === "Ausbildungsverantwortlicher") { + return { + href: getLearningPathUrl(props.courseConfig?.course_slug), + text: "a.Vorschau Teilnehmer", + cyKey: "tr-dashboard-link", + }; + } return { href: getLearningPathUrl(props.courseConfig?.course_slug), text: "Weiter lernen", @@ -193,6 +201,12 @@ function hasActionButton(): boolean { :agent-role="courseConfig.role_key" /> + + diff --git a/client/src/components/dashboard/TrainingResponsibleStatistics.vue b/client/src/components/dashboard/TrainingResponsibleStatistics.vue new file mode 100644 index 00000000..d6a36054 --- /dev/null +++ b/client/src/components/dashboard/TrainingResponsibleStatistics.vue @@ -0,0 +1,72 @@ + + + diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index 065feb95..b9c76014 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -28,6 +28,7 @@ const documents = { "\n query dashboardCourseData($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n }\n }\n": types.DashboardCourseDataDocument, "\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n region\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n region\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n region\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n region\n assignment_title\n assignment_type_translation_key\n competence_certificate_title\n competence_certificate_id\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_evaluation_percent\n average_passed\n competence_certificate_weight\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n region\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument, "\n query mentorCourseStatistics($courseId: ID!, $agentRole: String!) {\n mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n course_session_properties {\n _id\n sessions {\n id\n name\n region\n }\n generations\n circles {\n id\n name\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n average_evaluation_percent\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n course_session_title\n circle_id\n generation\n region\n assignment_title\n assignment_type_translation_key\n competence_certificate_id\n competence_certificate_title\n details_url\n learning_content_id\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n competence_certificate_weight\n average_evaluation_percent\n average_passed\n }\n }\n }\n }\n }\n": types.MentorCourseStatisticsDocument, + "\n query trainingResponsibleStatistics($courseSessionId: ID!) {\n training_responsible_statistics(course_session_id: $courseSessionId) {\n _id\n cost_per_year {\n _id\n year\n total_cost\n }\n participants_per_year {\n _id\n year\n participants {\n id\n chosen_profile\n }\n }\n }\n }\n": types.TrainingResponsibleStatisticsDocument, "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, }; @@ -105,6 +106,10 @@ export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query mentorCourseStatistics($courseId: ID!, $agentRole: String!) {\n mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n course_session_properties {\n _id\n sessions {\n id\n name\n region\n }\n generations\n circles {\n id\n name\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n average_evaluation_percent\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n course_session_title\n circle_id\n generation\n region\n assignment_title\n assignment_type_translation_key\n competence_certificate_id\n competence_certificate_title\n details_url\n learning_content_id\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n competence_certificate_weight\n average_evaluation_percent\n average_passed\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query mentorCourseStatistics($courseId: ID!, $agentRole: String!) {\n mentor_course_statistics(course_id: $courseId, agent_role: $agentRole) {\n _id\n course_id\n course_title\n course_slug\n course_session_selection_ids\n user_selection_ids\n course_session_properties {\n _id\n sessions {\n id\n name\n region\n }\n generations\n circles {\n id\n name\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n total_passed\n total_failed\n average_evaluation_percent\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n course_session_title\n circle_id\n generation\n region\n assignment_title\n assignment_type_translation_key\n competence_certificate_id\n competence_certificate_title\n details_url\n learning_content_id\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n competence_certificate_weight\n average_evaluation_percent\n average_passed\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 trainingResponsibleStatistics($courseSessionId: ID!) {\n training_responsible_statistics(course_session_id: $courseSessionId) {\n _id\n cost_per_year {\n _id\n year\n total_cost\n }\n participants_per_year {\n _id\n year\n participants {\n id\n chosen_profile\n }\n }\n }\n }\n"): (typeof documents)["\n query trainingResponsibleStatistics($courseSessionId: ID!) {\n training_responsible_statistics(course_session_id: $courseSessionId) {\n _id\n cost_per_year {\n _id\n year\n total_cost\n }\n participants_per_year {\n _id\n year\n participants {\n id\n chosen_profile\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index 5f3d2297..0783825d 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -359,6 +359,13 @@ export type CoreUserLanguageChoices = /** Italiano */ | 'IT'; +export type CostForYear = { + __typename?: 'CostForYear'; + _id: Scalars['ID']['output']; + total_cost: Scalars['Int']['output']; + year: Scalars['Int']['output']; +}; + export type CourseConfigurationObjectType = { __typename?: 'CourseConfigurationObjectType'; enable_circle_documents: Scalars['Boolean']['output']; @@ -922,6 +929,13 @@ export type MutationUpsertAssignmentCompletionArgs = { learning_content_page_id?: InputMaybe; }; +export type ParticipantsForYear = { + __typename?: 'ParticipantsForYear'; + _id: Scalars['ID']['output']; + participants: Array>; + year: Scalars['Int']['output']; +}; + export type PerformanceCriteriaObjectType = CoursePageInterface & { __typename?: 'PerformanceCriteriaObjectType'; competence_id: Scalars['String']['output']; @@ -991,6 +1005,7 @@ export type Query = { learning_content_video?: Maybe; learning_path?: Maybe; mentor_course_statistics?: Maybe; + training_responsible_statistics?: Maybe; }; @@ -1063,6 +1078,11 @@ export type QueryMentorCourseStatisticsArgs = { course_id: Scalars['ID']['input']; }; + +export type QueryTrainingResponsibleStatisticsArgs = { + course_session_id: Scalars['ID']['input']; +}; + export type SendFeedbackMutation = { __typename?: 'SendFeedbackMutation'; /** May contain more than one error for same field. */ @@ -1113,6 +1133,14 @@ export type TopicObjectType = CoursePageInterface & { translation_key: Scalars['String']['output']; }; +export type TrainingResponsibleStatisticsType = { + __typename?: 'TrainingResponsibleStatisticsType'; + _id: Scalars['ID']['output']; + cost_per_year: Array>; + course_session_id: Scalars['ID']['output']; + participants_per_year: Array>; +}; + export type UpdateCourseProfileError = { __typename?: 'UpdateCourseProfileError'; message?: Maybe; @@ -1456,6 +1484,13 @@ export type MentorCourseStatisticsQueryVariables = Exact<{ export type MentorCourseStatisticsQuery = { __typename?: 'Query', mentor_course_statistics?: { __typename?: 'BaseStatisticsType', _id: string, course_id: string, course_title: string, course_slug: string, course_session_selection_ids: Array, user_selection_ids?: Array | null, course_session_properties: { __typename?: 'StatisticsCourseSessionPropertiesType', _id: string, generations: Array, sessions: Array<{ __typename?: 'StatisticsCourseSessionDataType', id: string, name: string, region: string }>, circles: Array<{ __typename?: 'StatisticsCircleDataType', id: string, name: string }> }, assignments: { __typename?: 'AssignmentsStatisticsType', _id: string, summary: { __typename?: 'AssignmentStatisticsSummaryType', _id: string, completed_count: number, average_passed: number, total_passed: number, total_failed: number, average_evaluation_percent?: number | null }, records: Array<{ __typename?: 'AssignmentStatisticsRecordType', _id: string, course_session_id: string, course_session_assignment_id: string, course_session_title?: string | null, circle_id: string, generation: string, region: string, assignment_title: string, assignment_type_translation_key: string, competence_certificate_id?: string | null, competence_certificate_title?: string | null, details_url: string, learning_content_id: string, deadline: string, metrics: { __typename?: 'AssignmentCompletionMetricsType', _id: string, passed_count: number, failed_count: number, unranked_count: number, ranking_completed: boolean, competence_certificate_weight?: number | null, average_evaluation_percent?: number | null, average_passed: number } }> } } | null }; +export type TrainingResponsibleStatisticsQueryVariables = Exact<{ + courseSessionId: Scalars['ID']['input']; +}>; + + +export type TrainingResponsibleStatisticsQuery = { __typename?: 'Query', training_responsible_statistics?: { __typename?: 'TrainingResponsibleStatisticsType', _id: string, cost_per_year: Array<{ __typename?: 'CostForYear', _id: string, year: number, total_cost: number } | null>, participants_per_year: Array<{ __typename?: 'ParticipantsForYear', _id: string, year: number, participants: Array<{ __typename?: 'CourseSessionUserType', id: string, chosen_profile: string } | null> } | null> } | null }; + export type SendFeedbackMutationMutationVariables = Exact<{ courseSessionId: Scalars['ID']['input']; learningContentId: Scalars['ID']['input']; @@ -1482,4 +1517,5 @@ export const DashboardProgressDocument = {"kind":"Document","definitions":[{"kin export const DashboardCourseDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"dashboardCourseData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_progress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_id"}},{"kind":"Field","name":{"kind":"Name","value":"session_to_continue_id"}}]}}]}}]} as unknown as DocumentNode; export const CourseStatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseStatistics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_statistics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_title"}},{"kind":"Field","name":{"kind":"Name","value":"course_slug"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"sessions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"region"}}]}},{"kind":"Field","name":{"kind":"Name","value":"generations"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"course_session_selection_ids"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_selection_metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"session_count"}},{"kind":"Field","name":{"kind":"Name","value":"participant_count"}},{"kind":"Field","name":{"kind":"Name","value":"expert_count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"attendance_day_presences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"due_date"}},{"kind":"Field","name":{"kind":"Name","value":"participants_present"}},{"kind":"Field","name":{"kind":"Name","value":"participants_total"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"days_completed"}},{"kind":"Field","name":{"kind":"Name","value":"participants_present"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"feedback_responses"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"experts"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_average"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_max"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_average"}},{"kind":"Field","name":{"kind":"Name","value":"satisfaction_max"}},{"kind":"Field","name":{"kind":"Name","value":"total_responses"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"completed_count"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}},{"kind":"Field","name":{"kind":"Name","value":"total_passed"}},{"kind":"Field","name":{"kind":"Name","value":"total_failed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_assignment_id"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type_translation_key"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_title"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_id"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}},{"kind":"Field","name":{"kind":"Name","value":"deadline"}},{"kind":"Field","name":{"kind":"Name","value":"metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"passed_count"}},{"kind":"Field","name":{"kind":"Name","value":"failed_count"}},{"kind":"Field","name":{"kind":"Name","value":"unranked_count"}},{"kind":"Field","name":{"kind":"Name","value":"ranking_completed"}},{"kind":"Field","name":{"kind":"Name","value":"average_evaluation_percent"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_weight"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"competences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"success_total"}},{"kind":"Field","name":{"kind":"Name","value":"fail_total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"success_count"}},{"kind":"Field","name":{"kind":"Name","value":"fail_count"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const MentorCourseStatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"mentorCourseStatistics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentRole"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mentor_course_statistics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}},{"kind":"Argument","name":{"kind":"Name","value":"agent_role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentRole"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_title"}},{"kind":"Field","name":{"kind":"Name","value":"course_slug"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_selection_ids"}},{"kind":"Field","name":{"kind":"Name","value":"user_selection_ids"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"sessions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"region"}}]}},{"kind":"Field","name":{"kind":"Name","value":"generations"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"completed_count"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}},{"kind":"Field","name":{"kind":"Name","value":"total_passed"}},{"kind":"Field","name":{"kind":"Name","value":"total_failed"}},{"kind":"Field","name":{"kind":"Name","value":"average_evaluation_percent"}}]}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_assignment_id"}},{"kind":"Field","name":{"kind":"Name","value":"course_session_title"}},{"kind":"Field","name":{"kind":"Name","value":"circle_id"}},{"kind":"Field","name":{"kind":"Name","value":"generation"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type_translation_key"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_id"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_title"}},{"kind":"Field","name":{"kind":"Name","value":"details_url"}},{"kind":"Field","name":{"kind":"Name","value":"learning_content_id"}},{"kind":"Field","name":{"kind":"Name","value":"deadline"}},{"kind":"Field","name":{"kind":"Name","value":"metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"passed_count"}},{"kind":"Field","name":{"kind":"Name","value":"failed_count"}},{"kind":"Field","name":{"kind":"Name","value":"unranked_count"}},{"kind":"Field","name":{"kind":"Name","value":"ranking_completed"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_weight"}},{"kind":"Field","name":{"kind":"Name","value":"average_evaluation_percent"}},{"kind":"Field","name":{"kind":"Name","value":"average_passed"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const TrainingResponsibleStatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"trainingResponsibleStatistics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"training_responsible_statistics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"cost_per_year"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"year"}},{"kind":"Field","name":{"kind":"Name","value":"total_cost"}}]}},{"kind":"Field","name":{"kind":"Name","value":"participants_per_year"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_id"}},{"kind":"Field","name":{"kind":"Name","value":"year"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chosen_profile"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const SendFeedbackMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendFeedbackMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GenericScalar"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentType"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"submitted"},"value":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedback_response"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"submitted"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index 1c644eb4..a68ce255 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -1,6 +1,7 @@ type Query { course_statistics(course_id: ID!): CourseStatisticsType mentor_course_statistics(course_id: ID!, agent_role: String!): BaseStatisticsType + training_responsible_statistics(course_session_id: ID!): TrainingResponsibleStatisticsType course_progress(course_id: ID!): CourseProgressType dashboard_config: [DashboardConfigType!]! learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType @@ -200,42 +201,61 @@ type BaseStatisticsType { course_session_properties: StatisticsCourseSessionPropertiesType! } -type CourseProgressType { +type TrainingResponsibleStatisticsType { _id: ID! - course_id: ID! - session_to_continue_id: ID - competence: ProgressDashboardCompetenceType - assignment: ProgressDashboardAssignmentType + course_session_id: ID! + cost_per_year: [CostForYear]! + participants_per_year: [ParticipantsForYear]! } -type ProgressDashboardCompetenceType { +type CostForYear { _id: ID! - total_count: Int! - success_count: Int! - fail_count: Int! + year: Int! + total_cost: Int! } -type ProgressDashboardAssignmentType { +type ParticipantsForYear { _id: ID! - total_count: Int! - points_max_count: Int! - points_achieved_count: Int! + year: Int! + participants: [CourseSessionUserType]! } -type DashboardConfigType { +type CourseSessionUserType { + id: UUID! + chosen_profile: String! + course_session: CourseSessionObjectType! +} + +""" +Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects +in fields, resolvers and input. +""" +scalar UUID + +type CourseSessionObjectType { id: ID! - name: String! - slug: String! - dashboard_type: DashboardType! - course_configuration: CourseConfigurationObjectType! + created_at: DateTime! + updated_at: DateTime! + course: CourseObjectType! + title: String! + start_date: Date + end_date: Date + attendance_courses: [CourseSessionAttendanceCourseObjectType!]! + assignments: [CourseSessionAssignmentObjectType!]! + edoniq_tests: [CourseSessionEdoniqTestObjectType!]! + users: [CourseSessionUserObjectsType!]! } -enum DashboardType { - STATISTICS_DASHBOARD - PROGRESS_DASHBOARD - SIMPLE_DASHBOARD - MENTOR_DASHBOARD - PRAXISBILDNER_DASHBOARD +type CourseObjectType { + id: ID! + title: String! + category_name: String! + slug: String! + configuration: CourseConfigurationObjectType! + learning_path: LearningPathObjectType! + action_competences: [ActionCompetenceObjectType!]! + profiles: [String] + course_session_users(id: String): [CourseSessionUserType]! } type CourseConfigurationObjectType { @@ -270,20 +290,8 @@ interface CoursePageInterface { course: CourseObjectType } -type CourseObjectType { - id: ID! - title: String! - category_name: String! - slug: String! - configuration: CourseConfigurationObjectType! - learning_path: LearningPathObjectType! - action_competences: [ActionCompetenceObjectType!]! - profiles: [String] - course_session_users(id: String): [CourseSessionUserType]! -} - -type ActionCompetenceObjectType implements CoursePageInterface { - competence_id: String! +type TopicObjectType implements CoursePageInterface { + is_visible: Boolean! id: ID! title: String! slug: String! @@ -292,11 +300,13 @@ type ActionCompetenceObjectType implements CoursePageInterface { translation_key: String! frontend_url: String! course: CourseObjectType - performance_criteria: [PerformanceCriteriaObjectType!]! + circles: [CircleObjectType!]! } -type PerformanceCriteriaObjectType implements CoursePageInterface { - competence_id: String! +type CircleObjectType implements CoursePageInterface { + description: String! + goals: String! + is_base_circle: Boolean! id: ID! title: String! slug: String! @@ -305,7 +315,21 @@ type PerformanceCriteriaObjectType implements CoursePageInterface { translation_key: String! frontend_url: String! course: CourseObjectType - learning_unit: LearningUnitObjectType + learning_sequences: [LearningSequenceObjectType!]! + profiles: [String]! +} + +type LearningSequenceObjectType implements CoursePageInterface { + icon: String! + id: ID! + title: String! + slug: String! + content_type: String! + live: Boolean! + translation_key: String! + frontend_url: String! + course: CourseObjectType + learning_units: [LearningUnitObjectType!]! } type LearningUnitObjectType implements CoursePageInterface { @@ -345,30 +369,30 @@ type CircleLightObjectType { slug: String! } -type CourseSessionUserType { - id: UUID! - chosen_profile: String! - course_session: CourseSessionObjectType! +type PerformanceCriteriaObjectType implements CoursePageInterface { + competence_id: String! + id: ID! + title: String! + slug: String! + content_type: String! + live: Boolean! + translation_key: String! + frontend_url: String! + course: CourseObjectType + learning_unit: LearningUnitObjectType } -""" -Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects -in fields, resolvers and input. -""" -scalar UUID - -type CourseSessionObjectType { +type ActionCompetenceObjectType implements CoursePageInterface { + competence_id: String! id: ID! - created_at: DateTime! - updated_at: DateTime! - course: CourseObjectType! title: String! - start_date: Date - end_date: Date - attendance_courses: [CourseSessionAttendanceCourseObjectType!]! - assignments: [CourseSessionAssignmentObjectType!]! - edoniq_tests: [CourseSessionEdoniqTestObjectType!]! - users: [CourseSessionUserObjectsType!]! + slug: String! + content_type: String! + live: Boolean! + translation_key: String! + frontend_url: String! + course: CourseObjectType + performance_criteria: [PerformanceCriteriaObjectType!]! } """ @@ -717,46 +741,42 @@ type CourseSessionUserExpertCircleType { slug: String! } -type TopicObjectType implements CoursePageInterface { - is_visible: Boolean! - id: ID! - title: String! - slug: String! - content_type: String! - live: Boolean! - translation_key: String! - frontend_url: String! - course: CourseObjectType - circles: [CircleObjectType!]! +type CourseProgressType { + _id: ID! + course_id: ID! + session_to_continue_id: ID + competence: ProgressDashboardCompetenceType + assignment: ProgressDashboardAssignmentType } -type CircleObjectType implements CoursePageInterface { - description: String! - goals: String! - is_base_circle: Boolean! - id: ID! - title: String! - slug: String! - content_type: String! - live: Boolean! - translation_key: String! - frontend_url: String! - course: CourseObjectType - learning_sequences: [LearningSequenceObjectType!]! - profiles: [String]! +type ProgressDashboardCompetenceType { + _id: ID! + total_count: Int! + success_count: Int! + fail_count: Int! } -type LearningSequenceObjectType implements CoursePageInterface { - icon: String! +type ProgressDashboardAssignmentType { + _id: ID! + total_count: Int! + points_max_count: Int! + points_achieved_count: Int! +} + +type DashboardConfigType { id: ID! - title: String! + name: String! slug: String! - content_type: String! - live: Boolean! - translation_key: String! - frontend_url: String! - course: CourseObjectType - learning_units: [LearningUnitObjectType!]! + dashboard_type: DashboardType! + course_configuration: CourseConfigurationObjectType! +} + +enum DashboardType { + STATISTICS_DASHBOARD + PROGRESS_DASHBOARD + SIMPLE_DASHBOARD + MENTOR_DASHBOARD + PRAXISBILDNER_DASHBOARD } type LearningContentMediaLibraryObjectType implements CoursePageInterface & LearningContentInterface { diff --git a/client/src/gql/typenames.ts b/client/src/gql/typenames.ts index c6712dfd..b601f647 100644 --- a/client/src/gql/typenames.ts +++ b/client/src/gql/typenames.ts @@ -26,6 +26,7 @@ export const CompetenceRecordStatisticsType = "CompetenceRecordStatisticsType"; export const CompetencesStatisticsType = "CompetencesStatisticsType"; export const ContentDocumentObjectType = "ContentDocumentObjectType"; export const CoreUserLanguageChoices = "CoreUserLanguageChoices"; +export const CostForYear = "CostForYear"; export const CourseConfigurationObjectType = "CourseConfigurationObjectType"; export const CourseObjectType = "CourseObjectType"; export const CoursePageInterface = "CoursePageInterface"; @@ -74,6 +75,7 @@ export const LearningSequenceObjectType = "LearningSequenceObjectType"; export const LearningUnitObjectType = "LearningUnitObjectType"; export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices"; export const Mutation = "Mutation"; +export const ParticipantsForYear = "ParticipantsForYear"; export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType"; export const PresenceRecordStatisticsType = "PresenceRecordStatisticsType"; export const ProgressDashboardAssignmentType = "ProgressDashboardAssignmentType"; @@ -86,6 +88,7 @@ export const StatisticsCourseSessionPropertiesType = "StatisticsCourseSessionPro export const StatisticsCourseSessionsSelectionMetricType = "StatisticsCourseSessionsSelectionMetricType"; export const String = "String"; export const TopicObjectType = "TopicObjectType"; +export const TrainingResponsibleStatisticsType = "TrainingResponsibleStatisticsType"; export const UUID = "UUID"; export const UpdateCourseProfileError = "UpdateCourseProfileError"; export const UpdateCourseProfileResult = "UpdateCourseProfileResult"; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index 8beea4f4..246e060a 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -607,3 +607,24 @@ export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(` } } `); + +export const TRAINING_RESPONSIBLE_STATISTICS = graphql(` + query trainingResponsibleStatistics($courseSessionId: ID!) { + training_responsible_statistics(course_session_id: $courseSessionId) { + _id + cost_per_year { + _id + year + total_cost + } + participants_per_year { + _id + year + participants { + id + chosen_profile + } + } + } + } +`); diff --git a/client/src/pages/dashboard/DashboardCostPage.vue b/client/src/pages/dashboard/DashboardCostPage.vue new file mode 100644 index 00000000..bfc8428d --- /dev/null +++ b/client/src/pages/dashboard/DashboardCostPage.vue @@ -0,0 +1,65 @@ + + + diff --git a/client/src/pages/dashboard/DashboardPersonsPage.vue b/client/src/pages/dashboard/DashboardPersonsPage.vue index 6e89792f..706f9018 100644 --- a/client/src/pages/dashboard/DashboardPersonsPage.vue +++ b/client/src/pages/dashboard/DashboardPersonsPage.vue @@ -168,6 +168,29 @@ const roles = computed(() => { }); const selectedRole = ref(roles.value[0]); +const chosenProfiles = computed(() => { + const values = _(dashboardPersons.value) + .map((cs) => { + return Object.assign({}, cs, { + name: cs.chosen_profile, + id: cs.chosen_profile, + }); + }) + .filter((cs) => cs.chosen_profile !== "all") + .uniqBy("id") + .orderBy("name") + .value(); + + return [ + { + id: "all", + name: `${t("Zulassungsprofil")}: ${t("a.Alle")}`, + }, + ...values, + ]; +}); +const selectedChosenProfile = ref(chosenProfiles.value[0]); + const filteredPersons = computed(() => { return _.orderBy( dashboardPersons.value @@ -208,6 +231,12 @@ const filteredPersons = computed(() => { return person.course_sessions.some( (cs) => cs.user_role_display === selectedRole.value.id ); + }) + .filter((person) => { + if (selectedChosenProfile.value.id === "") { + return true; + } + return person.chosen_profile === selectedChosenProfile.value.id; }), ["last_name", "first_name"] ); @@ -219,7 +248,8 @@ const filtersVisible = computed(() => { courseSessions.value.length > 2 || regions.value.length > 2 || generations.value.length > 2 || - roles.value.length > 2 + roles.value.length > 2 || + chosenProfiles.value.length > 2 ); }); @@ -342,6 +372,14 @@ watch(selectedRegion, () => { :items="roles" borderless > + +
import("@/pages/dashboard/DashboardDueDatesPage.vue"), }, + { + path: "/dashboard/cost/:courseSessionId", + component: () => import("@/pages/dashboard/DashboardCostPage.vue"), + props: true, + }, { path: "/course/:courseSlug/media", props: true, diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index 3c701010..5045e37e 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -4,6 +4,7 @@ import { DASHBOARD_COURSE_SESSION_PROGRESS, DASHBOARD_COURSE_STATISTICS, DASHBOARD_MENTOR_COMPETENCE_SUMMARY, + TRAINING_RESPONSIBLE_STATISTICS, } from "@/graphql/queries"; import { itGetCached, itPost } from "@/fetchHelpers"; @@ -12,6 +13,8 @@ import type { CourseProgressType, CourseStatisticsType, DashboardConfigType, + TrainingResponsibleStatisticsQuery, + TrainingResponsibleStatisticsType, } from "@/gql/graphql"; import type { DashboardPersonsPageMode, @@ -34,7 +37,8 @@ export type DashboardRoleKeyType = | "Member" | "MentorUK" | "MentorVV" - | "Berufsbildner"; + | "Berufsbildner" + | "Ausbildungsverantwortlicher"; export type WidgetType = | "ProgressWidget" @@ -44,7 +48,8 @@ export type WidgetType = | "MentorCompetenceWidget" | "CompetenceCertificateWidget" | "UKStatisticsWidget" - | "UKBerufsbildnerStatisticsWidget"; + | "UKBerufsbildnerStatisticsWidget" + | "TrainingResponsibleStatisticsWidget"; export type DashboardPersonCourseSessionType = { id: string; @@ -78,6 +83,7 @@ export type DashboardPersonType = { passed_count: number; failed_count: number; }; + chosen_profile: string; }; export type DashboardCourseConfigType = { @@ -118,6 +124,31 @@ export const fetchStatisticData = async ( } }; +export const fetchTrainingResponsibleStatistics = async ( + courseSessionId: string +): Promise => { + try { + const res = await graphqlClient.query(TRAINING_RESPONSIBLE_STATISTICS, { + courseSessionId, + }); + + if (res.error) { + console.error( + "Error fetching training responsible statistics for course session ID:", + courseSessionId, + res.error + ); + } + return res.data || null; + } catch (error) { + console.error( + `Error fetching training responsible statistics for course session ID: ${courseSessionId}`, + error + ); + return null; + } +}; + export const fetchProgressData = async ( courseId: string ): Promise => { diff --git a/client/src/utils/format_currency_chf.ts b/client/src/utils/format_currency_chf.ts new file mode 100644 index 00000000..6c5b4365 --- /dev/null +++ b/client/src/utils/format_currency_chf.ts @@ -0,0 +1,17 @@ +export function formatCurrencyChfCentimes(amount: number | undefined) { + if (!amount) { + return formatCurrencyChf(undefined); + } + return formatCurrencyChf(amount / 100); +} + +// 10378.2 => 10'378 +export function formatCurrencyChf(amount: number | undefined) { + if (!amount) { + return "?"; + } + return new Intl.NumberFormat("de-CH", { + style: "decimal", + maximumFractionDigits: 0, + }).format(amount); +} diff --git a/scripts/create_ausbildungsverantwortlicher_mobi.py b/scripts/create_ausbildungsverantwortlicher_mobi.py new file mode 100644 index 00000000..c0435458 --- /dev/null +++ b/scripts/create_ausbildungsverantwortlicher_mobi.py @@ -0,0 +1,48 @@ +import os +import sys + +import django +from django.contrib.auth.hashers import make_password + +sys.path.append("../server") + +os.environ.setdefault("IT_APP_ENVIRONMENT", "local") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base") +django.setup() + +from vbv_lernwelt.course.consts import UK_COURSE_IDS +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.core.admin import User +from vbv_lernwelt.learning_mentor.models import ( + AgentParticipantRelation, + AgentParticipantRoleType, +) + + +def main(): + berufsbildner, _ = User.objects.get_or_create( + id="5f984be9-3024-4169-9c7b-c9e827c18fd8" + ) + berufsbildner.username = "training-responsible-mobi@example.com" + berufsbildner.email = "training-responsible-mobi@example.com" + berufsbildner.language = "de" + berufsbildner.first_name = "Ausbildungsverantwortlicher" + berufsbildner.last_name = "Mobi" + berufsbildner.password = make_password("test") + berufsbildner.save() + + for csu in ( + CourseSessionUser.objects.filter(user__username__contains="@mobi") + .filter(course_session__course__configuration__is_uk=False) + .filter(role=CourseSessionUser.Role.MEMBER.value) + .exclude(course_session_id__in=UK_COURSE_IDS) + ): + AgentParticipantRelation.objects.get_or_create( + agent=berufsbildner, + participant=csu, + role=AgentParticipantRoleType.BERUFSBILDNER.value, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/create_berufsbildner_mobi.py b/scripts/create_berufsbildner_mobi.py index 85eaeccd..f92d193d 100644 --- a/scripts/create_berufsbildner_mobi.py +++ b/scripts/create_berufsbildner_mobi.py @@ -22,17 +22,17 @@ def main(): berufsbildner, _ = User.objects.get_or_create( id="5f984be9-3024-4169-9c7b-c9e827c18fd8" ) - berufsbildner.username = "berufsbildner-mobi@example.com" - berufsbildner.email = "berufsbildner-mobi@example.com" + berufsbildner.username = "training-responsible-mobi@example.com" + berufsbildner.email = "training-responsible-mobi@example.com" berufsbildner.language = "de" - berufsbildner.first_name = "Berufsbildner" + berufsbildner.first_name = "Ausbildungsverantwortlicher" berufsbildner.last_name = "Mobi" berufsbildner.password = make_password("test") berufsbildner.save() for csu in ( CourseSessionUser.objects.filter(user__username__contains="@mobi") - .filter(course_session__course__configuration__is_uk=True) + .filter(course_session__course__configuration__is_uk=False) .filter(role=CourseSessionUser.Role.MEMBER.value) .exclude(course_session_id__in=[4, 5, 6]) ): diff --git a/server/vbv_lernwelt/core/constants.py b/server/vbv_lernwelt/core/constants.py index dd19d88a..2f0b9722 100644 --- a/server/vbv_lernwelt/core/constants.py +++ b/server/vbv_lernwelt/core/constants.py @@ -26,6 +26,7 @@ TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900" TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b" TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b" TEST_BERUFSBILDNER1_USER_ID = "bb83dde0-27e7-4859-8acb-a323025d712c" +TEST_LERNBEGLEITER1_USER_ID = "ffeedde0-27e7-ff59-8aff-a3230ffd712c" TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a" TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db" TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02" diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index be12dad0..58d547a4 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -7,6 +7,7 @@ from environs import Env from vbv_lernwelt.core.constants import ( ADMIN_USER_ID, TEST_BERUFSBILDNER1_USER_ID, + TEST_LERNBEGLEITER1_USER_ID, TEST_MENTOR1_USER_ID, TEST_STUDENT1_USER_ID, TEST_STUDENT1_VV_USER_ID, @@ -419,6 +420,15 @@ def create_default_users(default_password="test", set_avatar=False): language="de", avatar_image="uk1.patrizia.huggel.jpg", ) + _create_user( + _id=TEST_LERNBEGLEITER1_USER_ID, + email="test-lernbegleiter1@example.com", + first_name="Bruno", + last_name="Banani-Lernbegleiter", + password=default_password, + language="de", + avatar_image="uk1.patrizia.huggel.jpg", + ) _create_student_user( id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID, email="test-student-and-mentor2@example.com", diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index b0a92e93..6bce8418 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,3 +1,4 @@ +from hashlib import md5 from typing import Dict, List, Set, Tuple import graphene @@ -19,6 +20,7 @@ from vbv_lernwelt.dashboard.graphql.types.dashboard import ( DashboardType, ProgressDashboardAssignmentType, ProgressDashboardCompetenceType, + TrainingResponsibleStatisticsType, ) from vbv_lernwelt.iam.permissions import ( can_view_course_session, @@ -69,6 +71,11 @@ class DashboardQuery(graphene.ObjectType): agent_role=graphene.String(required=True), ) + training_responsible_statistics = graphene.Field( + TrainingResponsibleStatisticsType, + course_session_id=graphene.ID(required=True), + ) + course_progress = graphene.Field( CourseProgressType, course_id=graphene.ID(required=True) ) @@ -124,6 +131,13 @@ class DashboardQuery(graphene.ObjectType): return _agent_course_statistics(user, course_id, role=agent_role) + @staticmethod + def resolve_training_responsible_statistics(root, info, course_session_id: str): # noqa + return TrainingResponsibleStatisticsType( + _id=course_session_id, # noqa + course_session_id=course_session_id, # noqa + ) + def resolve_dashboard_config(root, info): # noqa user = info.context.user diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 69a1917a..4f9196a6 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,7 +1,13 @@ +from datetime import datetime +from itertools import groupby import graphene from graphene import Enum -from vbv_lernwelt.course.graphql.types import CourseConfigurationObjectType +from vbv_lernwelt.core.admin import User +from vbv_lernwelt.course.graphql.types import ( + CourseConfigurationObjectType, + CourseSessionUserType, +) from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.dashboard.graphql.types.assignment import ( AssignmentsStatisticsType, @@ -20,7 +26,13 @@ from vbv_lernwelt.dashboard.graphql.types.feedback import ( FeedbackStatisticsResponsesType, feedback_responses, ) +from vbv_lernwelt.learning_mentor.models import AgentParticipantRelation from vbv_lernwelt.learnpath.models import Circle +from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState +from vbv_lernwelt.shop.views import ( + COURSE_SESSION_ID_TO_PRODUCT_SKU, + PRODUCT_SKU_TO_COURSE_SESSION_ID, +) class StatisticsCourseSessionDataType(graphene.ObjectType): @@ -242,3 +254,75 @@ class CourseStatisticsType(BaseStatisticsType): participant_count=participant_count, # noqa expert_count=expert_count, # noqa ) + + +class CostForYear(graphene.ObjectType): + _id = graphene.ID(required=True) + year = graphene.Int(required=True) + # In centimes CHF + total_cost = graphene.Int(required=True) + + +class ParticipantsForYear(graphene.ObjectType): + _id = graphene.ID(required=True) + year = graphene.Int(required=True) + participants = graphene.List(CourseSessionUserType, required=True) + + +class TrainingResponsibleStatisticsType(graphene.ObjectType): + _id = graphene.ID(required=True) + course_session_id = graphene.ID(required=True) + + cost_per_year = graphene.List(CostForYear, required=True) + participants_per_year = graphene.List(ParticipantsForYear, required=True) + + @staticmethod + def resolve_cost_per_year(root, info): # noqa + user = info.context.user + relations_qs = AgentParticipantRelation.objects.filter( + agent=user, + participant__course_session=root.course_session_id, + ) + users = relations_qs.values_list("participant__user", flat=True) + + sku = COURSE_SESSION_ID_TO_PRODUCT_SKU.get(int(root.course_session_id)) + checkout_information = CheckoutInformation.objects.filter( + state=CheckoutState.PAID, + product_sku=sku, + user__in=users, + ) + + grouped_checkouts = groupby( + sorted(checkout_information, key=lambda x: x.created_at.year), + key=lambda x: x.created_at.year, + ) + return [ + CostForYear( + _id=f"{root.course_session_id} {year}", + year=year, + total_cost=sum(c.product_price for c in checkouts), + ) + for year, checkouts in grouped_checkouts + ] + + @staticmethod + def resolve_participants_per_year(root, info): + user = info.context.user + course_session_users = CourseSessionUser.objects.filter( + agentparticipantrelation__agent=user, + agentparticipantrelation__participant__course_session=root.course_session_id, + ) + + grouped_course_session_users = groupby( + sorted(course_session_users, key=lambda x: x.created_at.year), + key=lambda x: x.created_at.year, + ) + + return [ + ParticipantsForYear( + _id=f"{root.course_session_id} {year}", + year=year, + participants=list(c), + ) + for year, c in grouped_course_session_users + ] diff --git a/server/vbv_lernwelt/dashboard/utils.py b/server/vbv_lernwelt/dashboard/utils.py index 57250135..a9247d73 100644 --- a/server/vbv_lernwelt/dashboard/utils.py +++ b/server/vbv_lernwelt/dashboard/utils.py @@ -98,7 +98,8 @@ def create_course_session_dict(course_session_object, my_role, user_role): def create_person_list_with_roles( user, course_session_ids=None, include_private_data=False ): - def create_user_dict(user_object): + def create_user_dict(csu: CourseSessionUser): + user_object = csu.user user_data = { "user_id": user_object.id, "first_name": user_object.first_name, @@ -107,6 +108,7 @@ def create_person_list_with_roles( "avatar_url_small": user_object.avatar_url_small, "avatar_url": user_object.avatar_url, "course_sessions": [], + "chosen_profile": csu.chosen_profile.code if csu.chosen_profile else "all", } if include_private_data: user_data["phone_number"] = user_object.phone_number @@ -130,9 +132,7 @@ def create_person_list_with_roles( ).select_related("user") my_role = user_role(cs.roles) for csu in course_session_users: - person_data = result_persons.get( - csu.user.id, create_user_dict(csu.user) - ) + person_data = result_persons.get(csu.user.id, create_user_dict(csu)) person_data["course_sessions"].append( create_course_session_dict(cs, my_role, csu.role) ) @@ -146,7 +146,7 @@ def create_person_list_with_roles( participant_user = relation.participant.user if participant_user.id not in result_persons: - person_data = create_user_dict(participant_user) + person_data = create_user_dict(relation.participant) person_data["course_sessions"] = [course_session_entry] result_persons[participant_user.id] = person_data else: diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 3c605fd6..db5d5b80 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -62,6 +62,7 @@ class WidgetType(Enum): COMPETENCE_CERTIFICATE_WIDGET = "CompetenceCertificateWidget" UK_STATISTICS_WIDGET = "UKStatisticsWidget" UK_BERUFSBILDNER_STATISTICS_WIDGET = "UKBerufsbildnerStatisticsWidget" + TRAINING_RESPONSIBLE_STATISTICS_WIDGET = "TrainingResponsibleStatisticsWidget" class RoleKeyType(Enum): @@ -71,6 +72,7 @@ class RoleKeyType(Enum): SUPERVISOR = "Supervisor" TRAINER = "Trainer" BERUFSBILDNER = "Berufsbildner" + TRAINING_RESPONSIBLE = "Ausbildungsverantwortlicher" UNKNOWN_ROLE_KEY = "UnknownRoleKey" @@ -210,6 +212,8 @@ def get_widgets_for_course( if "BERUFSBILDNER" in relation_roles: if is_uk: widgets.append(WidgetType.UK_BERUFSBILDNER_STATISTICS_WIDGET.value) + if is_vv: + widgets.append(WidgetType.TRAINING_RESPONSIBLE_STATISTICS_WIDGET.value) return widgets @@ -237,6 +241,8 @@ def get_relevant_role_key( elif "BERUFSBILDNER" in relation_roles: if is_uk: return RoleKeyType.BERUFSBILDNER + elif is_vv: + return RoleKeyType.TRAINING_RESPONSIBLE return RoleKeyType.UNKNOWN_ROLE_KEY diff --git a/server/vbv_lernwelt/shop/views.py b/server/vbv_lernwelt/shop/views.py index eac0bcd9..8305c5e8 100644 --- a/server/vbv_lernwelt/shop/views.py +++ b/server/vbv_lernwelt/shop/views.py @@ -37,6 +37,10 @@ PRODUCT_SKU_TO_COURSE_SESSION_ID = { VV_IT_PRODUCT_SKU: 3, # vv-it } +COURSE_SESSION_ID_TO_PRODUCT_SKU = { + id: sku for sku, id in PRODUCT_SKU_TO_COURSE_SESSION_ID.items() +} + @api_view(["POST"]) def transaction_webhook(request): From c65c1be0a86a1027fbad8e4b8774f1ca927936b2 Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Thu, 12 Sep 2024 13:10:53 +0200 Subject: [PATCH 02/14] Add pie chart to dashboard --- client/package-lock.json | 45 +++++++++ client/package.json | 2 + .../AttendancePerChosenProfileChart.vue | 71 ++++++++++++++ .../TrainingResponsibleStatistics.vue | 98 ++++++++++++------- client/src/services/dashboard.ts | 1 - .../vbv_lernwelt/dashboard/graphql/queries.py | 1 - .../dashboard/graphql/types/dashboard.py | 4 +- 7 files changed, 183 insertions(+), 39 deletions(-) create mode 100644 client/src/components/dashboard/AttendancePerChosenProfileChart.vue diff --git a/client/package-lock.json b/client/package-lock.json index 78e6e22a..f4917956 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -19,6 +19,7 @@ "@vuepic/vue-datepicker": "^8.8.1", "@vueuse/core": "^10.11.0", "@vueuse/router": "^10.11.0", + "chart.js": "^4.4.4", "cypress": "^12.14.0", "d3": "^7.9.0", "dayjs": "^1.11.11", @@ -32,6 +33,7 @@ "mitt": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.31", + "vue-chartjs": "^5.3.1", "vue-router": "^4.4.0" }, "devDependencies": { @@ -2969,6 +2971,11 @@ "integrity": "sha512-4M/Mb2CxzuI1CtQhVFs6OC9ceuGPAP6SOWnpLcrdB1TcUHroXbsYDVJNOm32koRMfuCoRACbojcm4dPPcQxu0w==", "dev": true }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6001,6 +6008,17 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", + "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -14249,6 +14267,15 @@ } } }, + "node_modules/vue-chartjs": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz", + "integrity": "sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-component-type-helpers": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.26.tgz", @@ -16854,6 +16881,11 @@ "integrity": "sha512-4M/Mb2CxzuI1CtQhVFs6OC9ceuGPAP6SOWnpLcrdB1TcUHroXbsYDVJNOm32koRMfuCoRACbojcm4dPPcQxu0w==", "dev": true }, + "@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -18998,6 +19030,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", + "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -24648,6 +24688,11 @@ "@vue/shared": "3.4.31" } }, + "vue-chartjs": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz", + "integrity": "sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==" + }, "vue-component-type-helpers": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.26.tgz", diff --git a/client/package.json b/client/package.json index f5296e99..9c2813cf 100644 --- a/client/package.json +++ b/client/package.json @@ -30,6 +30,7 @@ "@vuepic/vue-datepicker": "^8.8.1", "@vueuse/core": "^10.11.0", "@vueuse/router": "^10.11.0", + "chart.js": "^4.4.4", "cypress": "^12.14.0", "d3": "^7.9.0", "dayjs": "^1.11.11", @@ -43,6 +44,7 @@ "mitt": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.31", + "vue-chartjs": "^5.3.1", "vue-router": "^4.4.0" }, "devDependencies": { diff --git a/client/src/components/dashboard/AttendancePerChosenProfileChart.vue b/client/src/components/dashboard/AttendancePerChosenProfileChart.vue new file mode 100644 index 00000000..07584563 --- /dev/null +++ b/client/src/components/dashboard/AttendancePerChosenProfileChart.vue @@ -0,0 +1,71 @@ + +w + diff --git a/client/src/components/dashboard/TrainingResponsibleStatistics.vue b/client/src/components/dashboard/TrainingResponsibleStatistics.vue index d6a36054..5bef737c 100644 --- a/client/src/components/dashboard/TrainingResponsibleStatistics.vue +++ b/client/src/components/dashboard/TrainingResponsibleStatistics.vue @@ -3,6 +3,7 @@ import type { TrainingResponsibleStatisticsQuery } from "@/gql/graphql"; import { fetchTrainingResponsibleStatistics } from "@/services/dashboard"; import { formatCurrencyChfCentimes } from "@/utils/format_currency_chf"; import { computed, onMounted, ref } from "vue"; +import AttendancePerChosenProfileChart from "./AttendancePerChosenProfileChart.vue"; import BaseBox from "./BaseBox.vue"; const props = defineProps<{ @@ -29,44 +30,73 @@ const attendanceCountInCurrentYear = computed(() => { )?.participants?.length ?? 0 ); }); + +const attendanceCountPerChosenProfile = computed(() => { + const allAttendances = + statistics.value?.training_responsible_statistics?.participants_per_year?.flatMap( + (entry) => entry?.participants ?? [] + ); + return allAttendances?.reduce( + (acc, attendance) => { + const chosenProfile = attendance?.chosen_profile || "all"; + if (!acc[chosenProfile]) { + acc[chosenProfile] = 0; + } + acc[chosenProfile] += 1; + return acc; + }, + {} as Record + ); +}); diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index 5045e37e..dccff8ed 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -14,7 +14,6 @@ import type { CourseStatisticsType, DashboardConfigType, TrainingResponsibleStatisticsQuery, - TrainingResponsibleStatisticsType, } from "@/gql/graphql"; import type { DashboardPersonsPageMode, diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index 6bce8418..4eaf5589 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,4 +1,3 @@ -from hashlib import md5 from typing import Dict, List, Set, Tuple import graphene diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 4f9196a6..bdca5301 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,9 +1,8 @@ -from datetime import datetime from itertools import groupby + import graphene from graphene import Enum -from vbv_lernwelt.core.admin import User from vbv_lernwelt.course.graphql.types import ( CourseConfigurationObjectType, CourseSessionUserType, @@ -31,7 +30,6 @@ from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState from vbv_lernwelt.shop.views import ( COURSE_SESSION_ID_TO_PRODUCT_SKU, - PRODUCT_SKU_TO_COURSE_SESSION_ID, ) From 0a4bbb0df74a086406c2ebf2110b8583e67d8c8b Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Thu, 12 Sep 2024 17:07:52 +0200 Subject: [PATCH 03/14] Sort person by paid year --- .../pages/dashboard/DashboardPersonsPage.vue | 49 +++++++++++++++++-- client/src/services/dashboard.ts | 4 +- server/vbv_lernwelt/course/models.py | 12 +++++ server/vbv_lernwelt/dashboard/utils.py | 1 + .../0020_alter_checkoutinformation_user.py | 24 +++++++++ server/vbv_lernwelt/shop/models.py | 4 +- 6 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 server/vbv_lernwelt/shop/migrations/0020_alter_checkoutinformation_user.py diff --git a/client/src/pages/dashboard/DashboardPersonsPage.vue b/client/src/pages/dashboard/DashboardPersonsPage.vue index 706f9018..457b75f5 100644 --- a/client/src/pages/dashboard/DashboardPersonsPage.vue +++ b/client/src/pages/dashboard/DashboardPersonsPage.vue @@ -10,6 +10,7 @@ import { useUserStore } from "@/stores/user"; import type { DashboardPersonsPageMode, StatisticsFilterItem } from "@/types"; import { exportDataAsXls } from "@/utils/export"; import { useRouteQuery } from "@vueuse/router"; +import dayjs from "dayjs"; import { useTranslation } from "i18next-vue"; import _ from "lodash"; import log from "loglevel"; @@ -160,7 +161,7 @@ const roles = computed(() => { return [ { - id: "", + id: UNFILTERED, name: `${t("Rolle")}: ${t("a.Alle")}`, }, ...values, @@ -191,6 +192,30 @@ const chosenProfiles = computed(() => { }); const selectedChosenProfile = ref(chosenProfiles.value[0]); +const paidYears = computed(() => { + const values = _(dashboardPersons.value) + .filter((cs) => dayjs(cs.paid_date).isValid()) + .map((cs) => { + const paidYear = dayjs(cs.paid_date).format("YYYY"); + return Object.assign({}, cs, { + name: paidYear, + id: paidYear, + }); + }) + .uniqBy("id") + .orderBy("name") + .value(); + + return [ + { + id: UNFILTERED, + name: `${t("Jahr")}: ${t("a.Alle")}`, + }, + ...values, + ]; +}); +const selectedPaidYear = ref(paidYears.value[0]); + const filteredPersons = computed(() => { return _.orderBy( dashboardPersons.value @@ -225,7 +250,7 @@ const filteredPersons = computed(() => { ); }) .filter((person) => { - if (selectedRole.value.id === "") { + if (selectedRole.value.id === UNFILTERED) { return true; } return person.course_sessions.some( @@ -233,10 +258,17 @@ const filteredPersons = computed(() => { ); }) .filter((person) => { - if (selectedChosenProfile.value.id === "") { + if (selectedChosenProfile.value.id === UNFILTERED) { return true; } return person.chosen_profile === selectedChosenProfile.value.id; + }) + .filter((person) => { + if (selectedPaidYear.value.id === UNFILTERED) { + return true; + } + const paidYear = dayjs(person.paid_date).format("YYYY"); + return paidYear == selectedPaidYear.value.id; }), ["last_name", "first_name"] ); @@ -249,7 +281,8 @@ const filtersVisible = computed(() => { regions.value.length > 2 || generations.value.length > 2 || roles.value.length > 2 || - chosenProfiles.value.length > 2 + chosenProfiles.value.length > 2 || + paidYears.value.length > 2 ); }); @@ -380,6 +413,14 @@ watch(selectedRegion, () => { :items="chosenProfiles" borderless > + +
datetime | None: + """ + Returns the date when the user paid for the course session + """ + checkout = self.user.checkout_information.filter(state=CheckoutState.PAID) + if checkout: + return checkout.first().created_at + return None + def __str__(self): return f"{self.user} ({self.course_session.title})" diff --git a/server/vbv_lernwelt/dashboard/utils.py b/server/vbv_lernwelt/dashboard/utils.py index a9247d73..8c5e2ffd 100644 --- a/server/vbv_lernwelt/dashboard/utils.py +++ b/server/vbv_lernwelt/dashboard/utils.py @@ -109,6 +109,7 @@ def create_person_list_with_roles( "avatar_url": user_object.avatar_url, "course_sessions": [], "chosen_profile": csu.chosen_profile.code if csu.chosen_profile else "all", + "paid_date": csu.paid_date.isoformat() if csu.paid_date else None, } if include_private_data: user_data["phone_number"] = user_object.phone_number diff --git a/server/vbv_lernwelt/shop/migrations/0020_alter_checkoutinformation_user.py b/server/vbv_lernwelt/shop/migrations/0020_alter_checkoutinformation_user.py new file mode 100644 index 00000000..008bf864 --- /dev/null +++ b/server/vbv_lernwelt/shop/migrations/0020_alter_checkoutinformation_user.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.13 on 2024-09-12 13:40 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("shop", "0019_alter_checkoutinformation_refno2"), + ] + + operations = [ + migrations.AlterField( + model_name="checkoutinformation", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="checkout_information", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/server/vbv_lernwelt/shop/models.py b/server/vbv_lernwelt/shop/models.py index 34be44bb..e6db05ff 100644 --- a/server/vbv_lernwelt/shop/models.py +++ b/server/vbv_lernwelt/shop/models.py @@ -43,7 +43,9 @@ class CheckoutInformation(models.Model): (INVOICE_ADDRESS_ORGANISATION, "Organisation"), ) - user = models.ForeignKey("core.User", on_delete=models.PROTECT) + user = models.ForeignKey( + "core.User", on_delete=models.PROTECT, related_name="checkout_information" + ) product_sku = models.CharField(max_length=255) product_name = models.CharField(max_length=255) From f24fd040f982a10eb3ce2ed0458f2c1de3aac38e Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Tue, 17 Sep 2024 11:49:07 +0200 Subject: [PATCH 04/14] Add test data --- client/src/services/dashboard.ts | 1 - server/vbv_lernwelt/core/constants.py | 3 +- .../vbv_lernwelt/core/create_default_users.py | 8 ++-- .../commands/reset_iterativ_test_sessions.py | 2 +- .../commands/create_default_courses.py | 45 ++++++++++++++++++- .../0020_alter_checkoutinformation_user.py | 2 +- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index 7a0c7d25..91b9de35 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -21,7 +21,6 @@ import type { XlsExportRequestData, XlsExportResponseData, } from "@/types"; -import type dayjs from "dayjs"; export type DashboardPersonRoleType = | "SUPERVISOR" diff --git a/server/vbv_lernwelt/core/constants.py b/server/vbv_lernwelt/core/constants.py index 2f0b9722..bfc79f4d 100644 --- a/server/vbv_lernwelt/core/constants.py +++ b/server/vbv_lernwelt/core/constants.py @@ -26,9 +26,10 @@ TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900" TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b" TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b" TEST_BERUFSBILDNER1_USER_ID = "bb83dde0-27e7-4859-8acb-a323025d712c" -TEST_LERNBEGLEITER1_USER_ID = "ffeedde0-27e7-ff59-8aff-a3230ffd712c" +TEST_AUSBILDUNGSVERANTWORTLICHER1_USER_ID = "ffeedde0-27e7-ff59-8aff-a3230ffd712c" TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a" TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db" +TEST_STUDENT3_VV_USER_ID = "ba8ebf0b-9de5-215e-a387-4449f9aa337b" TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02" TEST_USER_DATATRANS_HANNA_ID = "6bec1a0d-f852-47aa-a4de-072df6e07ad1" diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index 58d547a4..f7f60a2a 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -6,14 +6,15 @@ from environs import Env from vbv_lernwelt.core.constants import ( ADMIN_USER_ID, + TEST_AUSBILDUNGSVERANTWORTLICHER1_USER_ID, TEST_BERUFSBILDNER1_USER_ID, - TEST_LERNBEGLEITER1_USER_ID, TEST_MENTOR1_USER_ID, TEST_STUDENT1_USER_ID, TEST_STUDENT1_VV_USER_ID, TEST_STUDENT2_USER_ID, TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID, TEST_STUDENT3_USER_ID, + TEST_STUDENT3_VV_USER_ID, TEST_SUPERVISOR1_USER_ID, TEST_TRAINER1_USER_ID, TEST_TRAINER2_USER_ID, @@ -256,6 +257,7 @@ def create_default_users(default_password="test", set_avatar=False): last_name="Vollgas", ) _create_student_user( + id=TEST_STUDENT3_VV_USER_ID, email="patrizia.huggel@eiger-versicherungen.ch", first_name="Patrizia", last_name="Huggel", @@ -421,10 +423,10 @@ def create_default_users(default_password="test", set_avatar=False): avatar_image="uk1.patrizia.huggel.jpg", ) _create_user( - _id=TEST_LERNBEGLEITER1_USER_ID, + _id=TEST_AUSBILDUNGSVERANTWORTLICHER1_USER_ID, email="test-lernbegleiter1@example.com", first_name="Bruno", - last_name="Banani-Lernbegleiter", + last_name="Banani-Ausbildungsverantwortlicher", password=default_password, language="de", avatar_image="uk1.patrizia.huggel.jpg", diff --git a/server/vbv_lernwelt/core/management/commands/reset_iterativ_test_sessions.py b/server/vbv_lernwelt/core/management/commands/reset_iterativ_test_sessions.py index 6dff700a..33c2fe31 100644 --- a/server/vbv_lernwelt/core/management/commands/reset_iterativ_test_sessions.py +++ b/server/vbv_lernwelt/core/management/commands/reset_iterativ_test_sessions.py @@ -108,7 +108,7 @@ def delete_cs_data(cs: CourseSession): CourseSessionEdoniqTest.objects.filter(course_session=cs).delete() CourseSessionUser.objects.filter(course_session=cs).delete() - AgentParticipantRelation.objects.filter(course_session=cs).delete() + AgentParticipantRelation.objects.filter(participant__course_session=cs).delete() else: logger.info("no_course_session_found", import_id=cs.import_id) diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py index e9b4d51f..f82f2234 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -45,7 +45,12 @@ from vbv_lernwelt.competence.create_vv_new_competence_profile import ( create_vv_new_competence_profile, ) from vbv_lernwelt.competence.models import PerformanceCriteria -from vbv_lernwelt.core.constants import TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID +from vbv_lernwelt.core.constants import ( + TEST_AUSBILDUNGSVERANTWORTLICHER1_USER_ID, + TEST_STUDENT1_VV_USER_ID, + TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID, + TEST_STUDENT3_VV_USER_ID, +) from vbv_lernwelt.core.create_default_users import default_users from vbv_lernwelt.core.models import User from vbv_lernwelt.course.consts import ( @@ -92,6 +97,14 @@ from vbv_lernwelt.importer.services import ( import_students_from_excel, import_trainers_from_excel_for_training, ) +from vbv_lernwelt.learning_mentor.models import ( + AgentParticipantRelation, + AgentParticipantRoleType, +) +from vbv_lernwelt.learnpath.consts import ( + COURSE_PROFILE_NICHTLEBEN_CODE, + COURSE_PROFILE_NICHTLEBEN_ID, +) from vbv_lernwelt.learnpath.create_vv_new_learning_path import ( create_vv_motorfahrzeug_pruefung_learning_path, create_vv_new_learning_path, @@ -100,6 +113,7 @@ from vbv_lernwelt.learnpath.create_vv_new_learning_path import ( from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles from vbv_lernwelt.learnpath.models import ( Circle, + CourseProfile, LearningContent, LearningContentAssignment, LearningContentAttendanceCourse, @@ -113,6 +127,7 @@ 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, ) +from vbv_lernwelt.shop.tests.factories import CheckoutInformationFactory ADMIN_EMAILS = ["info@iterativ.ch", "admin"] @@ -239,13 +254,22 @@ def create_versicherungsvermittlerin_course( student_1_csu = CourseSessionUser.objects.create( course_session=cs, - user=User.objects.get(username="student-vv@eiger-versicherungen.ch"), + user=User.objects.get(id=TEST_STUDENT1_VV_USER_ID), ) mentor_and_student_2_learning_csu = CourseSessionUser.objects.create( course_session=cs, user=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID), role=CourseSessionUser.Role.MEMBER, ) + student_3_csu = CourseSessionUser.objects.create( + course_session=cs, + user=User.objects.get(id=TEST_STUDENT3_VV_USER_ID), + chosen_profile=CourseProfile.objects.get_or_create( + id=COURSE_PROFILE_NICHTLEBEN_ID, + code=COURSE_PROFILE_NICHTLEBEN_CODE, + order=3, + )[0], + ) CourseSessionUser.objects.create( course_session=cs, @@ -270,6 +294,23 @@ def create_versicherungsvermittlerin_course( course_session=cs, user=User.objects.get(email=admin_email), ) + # Ausbildungsverantwortlicher + AgentParticipantRelation.objects.create( + agent=User.objects.get(id=TEST_AUSBILDUNGSVERANTWORTLICHER1_USER_ID), + participant=student_1_csu, + role=AgentParticipantRoleType.BERUFSBILDNER.value, + ) + AgentParticipantRelation.objects.create( + agent=User.objects.get(id=TEST_AUSBILDUNGSVERANTWORTLICHER1_USER_ID), + participant=student_3_csu, + role=AgentParticipantRoleType.BERUFSBILDNER.value, + ) + CheckoutInformationFactory( + user=User.objects.get(id=TEST_STUDENT1_VV_USER_ID), + ) + CheckoutInformationFactory( + user=User.objects.get(id=TEST_STUDENT3_VV_USER_ID), + ) def create_versicherungsvermittlerin_pruefung_course( diff --git a/server/vbv_lernwelt/shop/migrations/0020_alter_checkoutinformation_user.py b/server/vbv_lernwelt/shop/migrations/0020_alter_checkoutinformation_user.py index 008bf864..ff270db8 100644 --- a/server/vbv_lernwelt/shop/migrations/0020_alter_checkoutinformation_user.py +++ b/server/vbv_lernwelt/shop/migrations/0020_alter_checkoutinformation_user.py @@ -1,8 +1,8 @@ # Generated by Django 4.2.13 on 2024-09-12 13:40 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): From 294c33ddc83da6555c6af8ad984ef391258f6e1e Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Tue, 17 Sep 2024 12:05:24 +0200 Subject: [PATCH 05/14] Merge Django base settings --- server/config/settings/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 48c68bf1..0abe0c4e 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -59,8 +59,14 @@ DATABASES = { default="postgres://postgres@localhost:5432/vbv_lernwelt", ) } -DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405 -DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 +DATABASES["default"]["ATOMIC_REQUESTS"] = env.bool( + "DATABASE_ATOMIC_REQUESTS", default=True +) +DATABASES["default"]["CONN_MAX_AGE"] = env.int("DATABASE_CONN_MAX_AGE", default=0) # noqa F405 +# set on 17.09.2024 https://docs.djangoproject.com/en/4.2/ref/databases/#transaction-pooling-server-side-cursors +DATABASES["default"]["DISABLE_SERVER_SIDE_CURSORS"] = env.bool( + "DATABASE_DISABLE_SERVER_SIDE_CURSORS", default=True +) # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" From a29171f32fefa7f21bf101c3b15c0d4b09a7a45b Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Tue, 17 Sep 2024 14:51:13 +0200 Subject: [PATCH 06/14] Group users by checkout information created date --- .../vbv_lernwelt/core/create_default_users.py | 7 ++++- .../dashboard/graphql/types/dashboard.py | 29 ++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index f7f60a2a..eab3f30f 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -258,6 +258,11 @@ def create_default_users(default_password="test", set_avatar=False): ) _create_student_user( id=TEST_STUDENT3_VV_USER_ID, + email="student-vv3@eiger-versicherungen.ch", + first_name="Vladi", + last_name="Volodemir", + ) + _create_student_user( email="patrizia.huggel@eiger-versicherungen.ch", first_name="Patrizia", last_name="Huggel", @@ -424,7 +429,7 @@ def create_default_users(default_password="test", set_avatar=False): ) _create_user( _id=TEST_AUSBILDUNGSVERANTWORTLICHER1_USER_ID, - email="test-lernbegleiter1@example.com", + email="test-ausbildungsverantwortlicher1@example.com", first_name="Bruno", last_name="Banani-Ausbildungsverantwortlicher", password=default_password, diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index bdca5301..645773df 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,6 +1,8 @@ +from collections import defaultdict from itertools import groupby import graphene +from django.db.models.functions import ExtractYear from graphene import Enum from vbv_lernwelt.course.graphql.types import ( @@ -306,21 +308,26 @@ class TrainingResponsibleStatisticsType(graphene.ObjectType): @staticmethod def resolve_participants_per_year(root, info): user = info.context.user - course_session_users = CourseSessionUser.objects.filter( - agentparticipantrelation__agent=user, - agentparticipantrelation__participant__course_session=root.course_session_id, + course_session_users = ( + CourseSessionUser.objects.filter( + agentparticipantrelation__agent=user, + agentparticipantrelation__participant__course_session=root.course_session_id, + ) + .annotate( + checkout_year=ExtractYear("user__checkout_information__created_at") + ) + .select_related("user") ) - - grouped_course_session_users = groupby( - sorted(course_session_users, key=lambda x: x.created_at.year), - key=lambda x: x.created_at.year, - ) - + grouped_course_session_users = defaultdict(list) + for csu in course_session_users: + checkout_year = csu.checkout_year + if checkout_year: + grouped_course_session_users[checkout_year].append(csu) return [ ParticipantsForYear( _id=f"{root.course_session_id} {year}", year=year, - participants=list(c), + participants=participants, ) - for year, c in grouped_course_session_users + for year, participants in grouped_course_session_users.items() ] From 044bba759f824fc70e539eb4e8ed79b9bc817199 Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Tue, 17 Sep 2024 14:51:20 +0200 Subject: [PATCH 07/14] Add E2E tests --- .../AttendancePerChosenProfileChart.vue | 19 +---- .../TrainingResponsibleStatistics.vue | 10 ++- .../src/components/dashboard/composables.ts | 24 ++++++ .../src/pages/dashboard/DashboardCostPage.vue | 5 +- .../pages/dashboard/DashboardPersonsPage.vue | 7 +- .../ausbildungsverantwortlicher.cy.js | 79 +++++++++++++++++++ .../commands/create_default_courses.py | 9 ++- 7 files changed, 125 insertions(+), 28 deletions(-) create mode 100644 client/src/components/dashboard/composables.ts create mode 100644 cypress/e2e/ausbildungsverantwortlicher/ausbildungsverantwortlicher.cy.js diff --git a/client/src/components/dashboard/AttendancePerChosenProfileChart.vue b/client/src/components/dashboard/AttendancePerChosenProfileChart.vue index 07584563..a5a687af 100644 --- a/client/src/components/dashboard/AttendancePerChosenProfileChart.vue +++ b/client/src/components/dashboard/AttendancePerChosenProfileChart.vue @@ -1,14 +1,14 @@ w