From a75bb14e4cfb0fb54b3c35684f8783c60508812b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Jun 2023 17:11:23 +0200 Subject: [PATCH 01/15] Add simple graphql types for CourseSessionAttendanceCourse --- client/src/gql/graphql.ts | 16 ++++++++ client/src/gql/schema.graphql | 23 +++++++---- server/vbv_lernwelt/core/schema.py | 3 +- .../course_session/graphql/__init__.py | 0 .../course_session/graphql/mutations.py | 7 ++++ .../course_session/graphql/queries.py | 41 +++++++++++++++++++ .../course_session/graphql/types.py | 36 ++++++++++++++++ .../course_session/serializers.py | 2 +- .../course_session/tests/test_graphql.py | 26 ++++++++++++ 9 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 server/vbv_lernwelt/course_session/graphql/__init__.py create mode 100644 server/vbv_lernwelt/course_session/graphql/mutations.py create mode 100644 server/vbv_lernwelt/course_session/graphql/queries.py create mode 100644 server/vbv_lernwelt/course_session/graphql/types.py create mode 100644 server/vbv_lernwelt/course_session/tests/test_graphql.py diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index 158e17c7..2ffabbbf 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -127,6 +127,15 @@ export type CoursePageInterface = { translation_key?: Maybe; }; +export type CourseSessionAttendanceCourseType = { + __typename?: 'CourseSessionAttendanceCourseType'; + end?: Maybe; + id: Scalars['ID']; + location: Scalars['String']; + start?: Maybe; + trainer: Scalars['String']; +}; + export type CourseType = { __typename?: 'CourseType'; category_name: Scalars['String']['output']; @@ -216,6 +225,7 @@ export type Query = { assignment?: Maybe; assignment_completion?: Maybe; course?: Maybe; + course_session_attendance_course?: Maybe; }; @@ -236,6 +246,12 @@ export type QueryCourseArgs = { id?: InputMaybe; }; + +export type QueryCourse_Session_Attendance_CourseArgs = { + assignment_user_id?: InputMaybe; + id: Scalars['ID']; +}; + export type SendFeedbackInput = { clientMutationId?: InputMaybe; course_session: Scalars['Int']['input']; diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index ab93d32e..bc2d6843 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -1,9 +1,25 @@ type Query { + course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType course(id: Int): CourseType assignment(id: ID, slug: String): AssignmentObjectType assignment_completion(assignment_id: ID!, course_session_id: ID!, assignment_user_id: ID): AssignmentCompletionObjectType } +type CourseSessionAttendanceCourseType { + id: ID! + location: String! + trainer: String! + end: DateTime + start: DateTime +} + +""" +The `DateTime` scalar type represents a DateTime +value as specified by +[iso8601](https://en.wikipedia.org/wiki/ISO_8601). +""" +scalar DateTime + type CourseType { id: ID! title: String! @@ -63,13 +79,6 @@ interface CoursePageInterface { frontend_url: String } -""" -The `DateTime` scalar type represents a DateTime -value as specified by -[iso8601](https://en.wikipedia.org/wiki/ISO_8601). -""" -scalar DateTime - type UserType { id: ID! diff --git a/server/vbv_lernwelt/core/schema.py b/server/vbv_lernwelt/core/schema.py index 69355e81..e2a2476e 100644 --- a/server/vbv_lernwelt/core/schema.py +++ b/server/vbv_lernwelt/core/schema.py @@ -3,10 +3,11 @@ import graphene from vbv_lernwelt.assignment.graphql.mutations import AssignmentMutation from vbv_lernwelt.assignment.graphql.queries import AssignmentQuery from vbv_lernwelt.course.schema import CourseQuery +from vbv_lernwelt.course_session.graphql.queries import CourseSessionQuery from vbv_lernwelt.feedback.graphql.mutations import FeedbackMutation -class Query(AssignmentQuery, CourseQuery, graphene.ObjectType): +class Query(AssignmentQuery, CourseQuery, CourseSessionQuery, graphene.ObjectType): pass diff --git a/server/vbv_lernwelt/course_session/graphql/__init__.py b/server/vbv_lernwelt/course_session/graphql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session/graphql/mutations.py b/server/vbv_lernwelt/course_session/graphql/mutations.py new file mode 100644 index 00000000..24c280cd --- /dev/null +++ b/server/vbv_lernwelt/course_session/graphql/mutations.py @@ -0,0 +1,7 @@ +import structlog + +logger = structlog.get_logger(__name__) + + +class CourseSessionMutation: + pass diff --git a/server/vbv_lernwelt/course_session/graphql/queries.py b/server/vbv_lernwelt/course_session/graphql/queries.py new file mode 100644 index 00000000..66f61994 --- /dev/null +++ b/server/vbv_lernwelt/course_session/graphql/queries.py @@ -0,0 +1,41 @@ +import graphene +from rest_framework.exceptions import PermissionDenied + +from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert +from vbv_lernwelt.course_session.graphql.types import CourseSessionAttendanceCourseType +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse + + +class CourseSessionQuery(object): + course_session_attendance_course = graphene.Field( + CourseSessionAttendanceCourseType, + id=graphene.ID(required=True), + assignment_user_id=graphene.ID(required=False), + ) + + def resolve_course_session_attendance_course( + root, + info, + id=None, + user_id=graphene.ID(required=False), + ): + if user_id is None: + user_id = info.context.user.id + + attendance_course = CourseSessionAttendanceCourse.objects.filter( + id=id, + ).first() + + if attendance_course is None: + return None + + if str(user_id) == str(info.context.user.id) or is_course_session_expert( + info.context.user, attendance_course.course_session_id + ): + course_id = CourseSession.objects.get( + id=attendance_course.course_session_id + ).course_id + if has_course_access(info.context.user, course_id): + return attendance_course + raise PermissionDenied() diff --git a/server/vbv_lernwelt/course_session/graphql/types.py b/server/vbv_lernwelt/course_session/graphql/types.py new file mode 100644 index 00000000..4125511d --- /dev/null +++ b/server/vbv_lernwelt/course_session/graphql/types.py @@ -0,0 +1,36 @@ +import graphene +from graphene_django import DjangoObjectType + +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse + + +class CourseSessionAttendanceCourseType(DjangoObjectType): + course_session_id = graphene.ID(source="course_session_id") + learning_content_id = graphene.ID(source="learning_content_id") + due_date_id = graphene.ID(source="due_date_id") + end = graphene.DateTime() + start = graphene.DateTime() + + class Meta: + model = CourseSessionAttendanceCourse + fields = ( + "id", + "course_session_id", + "learning_content_id", + "due_date_id", + "location", + "trainer", + "start", + "end", + # "attendance_user_list", + ) + + def resolve_start(self, info): + if self.due_date is None: + return None + return self.due_date.start + + def resolve_end(self, info): + if self.due_date is None: + return None + return self.due_date.end diff --git a/server/vbv_lernwelt/course_session/serializers.py b/server/vbv_lernwelt/course_session/serializers.py index defd7c6a..3e176f0c 100644 --- a/server/vbv_lernwelt/course_session/serializers.py +++ b/server/vbv_lernwelt/course_session/serializers.py @@ -14,7 +14,7 @@ class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer): model = CourseSessionAttendanceCourse fields = [ "id", - "course_session_id", + "course_session", "learning_content_id", "due_date_id", "location", diff --git a/server/vbv_lernwelt/course_session/tests/test_graphql.py b/server/vbv_lernwelt/course_session/tests/test_graphql.py new file mode 100644 index 00000000..6b63c8c7 --- /dev/null +++ b/server/vbv_lernwelt/course_session/tests/test_graphql.py @@ -0,0 +1,26 @@ +import json + +from graphene_django.utils.testing import GraphQLTestCase + + +class MyFancyTestCase(GraphQLTestCase): + def test_some_query(self): + response = self.query( + """ + query { + myModel { + id + name + } + } + """, + op_name="myModel", + ) + + content = json.loads(response.content) + + # This validates the status code and if you get errors + self.assertResponseNoErrors(response) + + # Add some more asserts if you like + ... From eb0b03f4136d3b24130280de6ca398d16db71d7d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 23 Jun 2023 18:34:08 +0200 Subject: [PATCH 02/15] Create mutation with test case --- client/src/gql/graphql.ts | 310 ++++++++++-------- client/src/gql/schema.graphql | 40 ++- server/vbv_lernwelt/core/schema.py | 5 +- .../course/creators/test_course.py | 27 +- .../course_session/graphql/mutations.py | 59 +++- .../course_session/graphql/types.py | 11 +- ...onattendancecourse_attendance_user_list.py | 18 + server/vbv_lernwelt/course_session/models.py | 5 + .../course_session/tests/test_graphql.py | 92 +++++- 9 files changed, 393 insertions(+), 174 deletions(-) create mode 100644 server/vbv_lernwelt/course_session/migrations/0002_coursesessionattendancecourse_attendance_user_list.py diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index 2ffabbbf..f87befd5 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -5,132 +5,147 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -export type MakeEmpty = { [_ in K]?: never }; -export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { - ID: { input: string; output: string; } - String: { input: string; output: string; } - Boolean: { input: boolean; output: boolean; } - Int: { input: number; output: number; } - Float: { input: number; output: number; } + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; /** * The `DateTime` scalar type represents a DateTime * value as specified by * [iso8601](https://en.wikipedia.org/wiki/ISO_8601). */ - DateTime: { input: any; output: any; } + DateTime: any; /** * The `GenericScalar` scalar type represents a generic * GraphQL scalar value that could be: * String, Boolean, Int, Float, List or Object. */ - GenericScalar: { input: any; output: any; } - JSONStreamField: { input: any; output: any; } + GenericScalar: any; + JSONStreamField: any; /** * Allows use of a JSON String for input / output from the GraphQL schema. * * Use of this type is *not recommended* as you lose the benefits of having a defined, static * schema (one of the key benefits of GraphQL). */ - JSONString: { input: any; output: any; } + JSONString: any; }; /** An enumeration. */ -export type AssignmentAssignmentAssignmentTypeChoices = +export enum AssignmentAssignmentAssignmentTypeChoices { /** CASEWORK */ - | 'CASEWORK' + Casework = 'CASEWORK', /** PREP_ASSIGNMENT */ - | 'PREP_ASSIGNMENT' + PrepAssignment = 'PREP_ASSIGNMENT', /** REFLECTION */ - | 'REFLECTION'; + Reflection = 'REFLECTION' +} /** An enumeration. */ -export type AssignmentAssignmentCompletionCompletionStatusChoices = +export enum AssignmentAssignmentCompletionCompletionStatusChoices { /** EVALUATION_IN_PROGRESS */ - | 'EVALUATION_IN_PROGRESS' + EvaluationInProgress = 'EVALUATION_IN_PROGRESS', /** EVALUATION_SUBMITTED */ - | 'EVALUATION_SUBMITTED' + EvaluationSubmitted = 'EVALUATION_SUBMITTED', /** IN_PROGRESS */ - | 'IN_PROGRESS' + InProgress = 'IN_PROGRESS', /** SUBMITTED */ - | 'SUBMITTED'; + Submitted = 'SUBMITTED' +} export type AssignmentCompletionMutation = { __typename?: 'AssignmentCompletionMutation'; - assignment_completion?: Maybe; + assignment_completion?: Maybe; }; -export type AssignmentCompletionObjectType = { - __typename?: 'AssignmentCompletionObjectType'; - additional_json_data: Scalars['JSONString']['output']; - assignment: AssignmentObjectType; +export type AssignmentCompletionType = { + __typename?: 'AssignmentCompletionType'; + additional_json_data: Scalars['JSONString']; + assignment: AssignmentType; assignment_user: UserType; - completion_data?: Maybe; + completion_data?: Maybe; completion_status: AssignmentAssignmentCompletionCompletionStatusChoices; - created_at: Scalars['DateTime']['output']; - evaluation_grade?: Maybe; - evaluation_points?: Maybe; - evaluation_submitted_at?: Maybe; + created_at: Scalars['DateTime']; + evaluation_grade?: Maybe; + evaluation_points?: Maybe; + evaluation_submitted_at?: Maybe; evaluation_user?: Maybe; - id: Scalars['ID']['output']; - submitted_at?: Maybe; - updated_at: Scalars['DateTime']['output']; + id: Scalars['ID']; + submitted_at?: Maybe; + updated_at: Scalars['DateTime']; }; -/** An enumeration. */ -export type AssignmentCompletionStatus = - | 'EVALUATION_IN_PROGRESS' - | 'EVALUATION_SUBMITTED' - | 'IN_PROGRESS' - | 'SUBMITTED'; - -export type AssignmentObjectType = CoursePageInterface & { - __typename?: 'AssignmentObjectType'; +export type AssignmentType = CoursePageInterface & { + __typename?: 'AssignmentType'; assignment_type: AssignmentAssignmentAssignmentTypeChoices; - content_type?: Maybe; + content_type?: Maybe; /** Zeitaufwand als Text */ - effort_required: Scalars['String']['output']; + effort_required: Scalars['String']; /** Beschreibung der Bewertung */ - evaluation_description: Scalars['String']['output']; + evaluation_description: Scalars['String']; /** URL zum Beurteilungsinstrument */ - evaluation_document_url: Scalars['String']['output']; - evaluation_tasks?: Maybe; - frontend_url?: Maybe; - id?: Maybe; + evaluation_document_url: Scalars['String']; + evaluation_tasks?: Maybe; + frontend_url?: Maybe; + id?: Maybe; /** Erläuterung der Ausgangslage */ - intro_text: Scalars['String']['output']; - live?: Maybe; - performance_objectives?: Maybe; - slug?: Maybe; - tasks?: Maybe; - title?: Maybe; - translation_key?: Maybe; + intro_text: Scalars['String']; + live?: Maybe; + performance_objectives?: Maybe; + slug?: Maybe; + tasks?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type AttendanceCourseUserMutation = { + __typename?: 'AttendanceCourseUserMutation'; + course_session_attendance_course?: Maybe; +}; + +export type AttendanceUserInputType = { + user_id?: InputMaybe; +}; + +export type AttendanceUserType = { + __typename?: 'AttendanceUserType'; + email?: Maybe; + first_name?: Maybe; + last_name?: Maybe; + user_id?: Maybe; }; /** An enumeration. */ -export type CoreUserLanguageChoices = +export enum CoreUserLanguageChoices { /** Deutsch */ - | 'DE' + De = 'DE', /** Français */ - | 'FR' + Fr = 'FR', /** Italiano */ - | 'IT'; + It = 'IT' +} export type CoursePageInterface = { - content_type?: Maybe; - frontend_url?: Maybe; - id?: Maybe; - live?: Maybe; - slug?: Maybe; - title?: Maybe; - translation_key?: Maybe; + content_type?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; }; export type CourseSessionAttendanceCourseType = { __typename?: 'CourseSessionAttendanceCourseType'; + attendance_user_list?: Maybe>>; + course_session_id?: Maybe; + due_date_id?: Maybe; end?: Maybe; id: Scalars['ID']; + learning_content_id?: Maybe; location: Scalars['String']; start?: Maybe; trainer: Scalars['String']; @@ -138,112 +153,119 @@ export type CourseSessionAttendanceCourseType = { export type CourseType = { __typename?: 'CourseType'; - category_name: Scalars['String']['output']; - id: Scalars['ID']['output']; + category_name: Scalars['String']; + id: Scalars['ID']; learning_path?: Maybe; - slug: Scalars['String']['output']; - title: Scalars['String']['output']; + slug: Scalars['String']; + title: Scalars['String']; }; export type ErrorType = { __typename?: 'ErrorType'; - field: Scalars['String']['output']; - messages: Array; + field: Scalars['String']; + messages: Array; }; export type FeedbackResponse = Node & { __typename?: 'FeedbackResponse'; - created_at: Scalars['DateTime']['output']; - data?: Maybe; + created_at: Scalars['DateTime']; + data?: Maybe; /** The ID of the object */ - id: Scalars['ID']['output']; + id: Scalars['ID']; }; export type LearningPathType = CoursePageInterface & { __typename?: 'LearningPathType'; - content_type?: Maybe; - depth: Scalars['Int']['output']; - draft_title: Scalars['String']['output']; - expire_at?: Maybe; - expired: Scalars['Boolean']['output']; - first_published_at?: Maybe; - frontend_url?: Maybe; - go_live_at?: Maybe; - has_unpublished_changes: Scalars['Boolean']['output']; - id?: Maybe; - last_published_at?: Maybe; - latest_revision_created_at?: Maybe; - live?: Maybe; - locked: Scalars['Boolean']['output']; - locked_at?: Maybe; + content_type?: Maybe; + depth: Scalars['Int']; + draft_title: Scalars['String']; + expire_at?: Maybe; + expired: Scalars['Boolean']; + first_published_at?: Maybe; + frontend_url?: Maybe; + go_live_at?: Maybe; + has_unpublished_changes: Scalars['Boolean']; + id?: Maybe; + last_published_at?: Maybe; + latest_revision_created_at?: Maybe; + live?: Maybe; + locked: Scalars['Boolean']; + locked_at?: Maybe; locked_by?: Maybe; - numchild: Scalars['Int']['output']; + numchild: Scalars['Int']; owner?: Maybe; - path: Scalars['String']['output']; + path: Scalars['String']; /** Die informative Beschreibung, dargestellt in Suchmaschinen-Ergebnissen unter der Überschrift. */ - search_description: Scalars['String']['output']; + search_description: Scalars['String']; /** Der Titel der Seite, dargestellt in Suchmaschinen-Ergebnissen als die verlinkte Überschrift. */ - seo_title: Scalars['String']['output']; + seo_title: Scalars['String']; /** Ob ein Link zu dieser Seite in automatisch generierten Menüs auftaucht. */ - show_in_menus: Scalars['Boolean']['output']; - slug?: Maybe; - title?: Maybe; - translation_key?: Maybe; - url_path: Scalars['String']['output']; + show_in_menus: Scalars['Boolean']; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; + url_path: Scalars['String']; }; export type Mutation = { __typename?: 'Mutation'; send_feedback?: Maybe; + update_course_session_attendance_course_users?: Maybe; upsert_assignment_completion?: Maybe; }; -export type MutationSendFeedbackArgs = { +export type MutationSend_FeedbackArgs = { input: SendFeedbackInput; }; -export type MutationUpsertAssignmentCompletionArgs = { - assignment_id: Scalars['ID']['input']; - assignment_user_id?: InputMaybe; - completion_data_string?: InputMaybe; - completion_status?: InputMaybe; - course_session_id: Scalars['ID']['input']; - evaluation_grade?: InputMaybe; - evaluation_points?: InputMaybe; +export type MutationUpdate_Course_Session_Attendance_Course_UsersArgs = { + attendance_user_list: Array>; + id: Scalars['ID']; +}; + + +export type MutationUpsert_Assignment_CompletionArgs = { + assignment_id: Scalars['ID']; + assignment_user_id?: InputMaybe; + completion_data_string?: InputMaybe; + completion_status?: InputMaybe; + course_session_id: Scalars['ID']; + evaluation_grade?: InputMaybe; + evaluation_points?: InputMaybe; }; /** An object with an ID */ export type Node = { /** The ID of the object */ - id: Scalars['ID']['output']; + id: Scalars['ID']; }; export type Query = { __typename?: 'Query'; - assignment?: Maybe; - assignment_completion?: Maybe; + assignment?: Maybe; + assignment_completion?: Maybe; course?: Maybe; course_session_attendance_course?: Maybe; }; export type QueryAssignmentArgs = { - id?: InputMaybe; - slug?: InputMaybe; + id?: InputMaybe; + slug?: InputMaybe; }; -export type QueryAssignmentCompletionArgs = { - assignment_id: Scalars['ID']['input']; - assignment_user_id?: InputMaybe; - course_session_id: Scalars['ID']['input']; +export type QueryAssignment_CompletionArgs = { + assignment_id: Scalars['ID']; + assignment_user_id?: InputMaybe; + course_session_id: Scalars['ID']; }; export type QueryCourseArgs = { - id?: InputMaybe; + id?: InputMaybe; }; @@ -253,15 +275,15 @@ export type QueryCourse_Session_Attendance_CourseArgs = { }; export type SendFeedbackInput = { - clientMutationId?: InputMaybe; - course_session: Scalars['Int']['input']; - data?: InputMaybe; - page: Scalars['String']['input']; + clientMutationId?: InputMaybe; + course_session: Scalars['Int']; + data?: InputMaybe; + page: Scalars['String']; }; export type SendFeedbackPayload = { __typename?: 'SendFeedbackPayload'; - clientMutationId?: Maybe; + clientMutationId?: Maybe; /** May contain more than one error for same field. */ errors?: Maybe>>; feedback_response?: Maybe; @@ -269,14 +291,14 @@ export type SendFeedbackPayload = { export type UserType = { __typename?: 'UserType'; - avatar_url: Scalars['String']['output']; - email: Scalars['String']['output']; - first_name: Scalars['String']['output']; - id: Scalars['ID']['output']; + avatar_url: Scalars['String']; + email: Scalars['String']; + first_name: Scalars['String']; + id: Scalars['ID']; language: CoreUserLanguageChoices; - last_name: Scalars['String']['output']; + last_name: Scalars['String']; /** Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_. */ - username: Scalars['String']['output']; + username: Scalars['String']; }; export type SendFeedbackMutationMutationVariables = Exact<{ @@ -287,29 +309,29 @@ export type SendFeedbackMutationMutationVariables = Exact<{ export type SendFeedbackMutationMutation = { __typename?: 'Mutation', send_feedback?: { __typename?: 'SendFeedbackPayload', feedback_response?: { __typename?: 'FeedbackResponse', id: string } | null, errors?: Array<{ __typename?: 'ErrorType', field: string, messages: Array } | null> | null } | null }; export type UpsertAssignmentCompletionMutationVariables = Exact<{ - assignmentId: Scalars['ID']['input']; - courseSessionId: Scalars['ID']['input']; - assignmentUserId?: InputMaybe; - completionStatus: AssignmentCompletionStatus; - completionDataString: Scalars['String']['input']; - evaluationGrade?: InputMaybe; - evaluationPoints?: InputMaybe; + assignmentId: Scalars['ID']; + courseSessionId: Scalars['ID']; + assignmentUserId?: InputMaybe; + completionStatus: Scalars['String']; + completionDataString: Scalars['String']; + evaluationGrade?: InputMaybe; + evaluationPoints?: InputMaybe; }>; -export type UpsertAssignmentCompletionMutation = { __typename?: 'Mutation', upsert_assignment_completion?: { __typename?: 'AssignmentCompletionMutation', assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null } | null } | null }; +export type UpsertAssignmentCompletionMutation = { __typename?: 'Mutation', upsert_assignment_completion?: { __typename?: 'AssignmentCompletionMutation', assignment_completion?: { __typename?: 'AssignmentCompletionType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null } | null } | null }; export type AssignmentCompletionQueryQueryVariables = Exact<{ - assignmentId: Scalars['ID']['input']; - courseSessionId: Scalars['ID']['input']; - assignmentUserId?: InputMaybe; + assignmentId: Scalars['ID']; + courseSessionId: Scalars['ID']; + assignmentUserId?: InputMaybe; }>; -export type AssignmentCompletionQueryQuery = { __typename?: 'Query', assignment?: { __typename?: 'AssignmentObjectType', assignment_type: AssignmentAssignmentAssignmentTypeChoices, content_type?: string | null, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id?: string | null, intro_text: string, performance_objectives?: any | null, slug?: string | null, tasks?: any | null, title?: string | null, translation_key?: string | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null, evaluation_user?: { __typename?: 'UserType', id: string } | null, assignment_user: { __typename?: 'UserType', id: string } } | null }; +export type AssignmentCompletionQueryQuery = { __typename?: 'Query', assignment?: { __typename?: 'AssignmentType', assignment_type: AssignmentAssignmentAssignmentTypeChoices, content_type?: string | null, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id?: string | null, intro_text: string, performance_objectives?: any | null, slug?: string | null, tasks?: any | null, title?: string | null, translation_key?: string | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null, evaluation_user?: { __typename?: 'UserType', id: string } | null, assignment_user: { __typename?: 'UserType', id: string } } | null }; export type CourseQueryQueryVariables = Exact<{ - courseId: Scalars['Int']['input']; + courseId: Scalars['Int']; }>; @@ -317,6 +339,6 @@ export type CourseQueryQuery = { __typename?: 'Query', course?: { __typename?: ' 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":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SendFeedbackInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"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":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode; -export const UpsertAssignmentCompletionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertAssignmentCompletion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"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":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AssignmentCompletionStatus"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationGrade"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsert_assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_data_string"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_grade"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationGrade"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_points"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_grade"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UpsertAssignmentCompletionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertAssignmentCompletion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"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":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationGrade"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsert_assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_status"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionStatus"}}},{"kind":"Argument","name":{"kind":"Name","value":"completion_data_string"},"value":{"kind":"Variable","name":{"kind":"Name","value":"completionDataString"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_grade"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationGrade"}}},{"kind":"Argument","name":{"kind":"Name","value":"evaluation_points"},"value":{"kind":"Variable","name":{"kind":"Name","value":"evaluationPoints"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_grade"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}}]}}]} as unknown as DocumentNode; export const AssignmentCompletionQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"assignmentCompletionQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"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":"assignmentUserId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"effort_required"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_description"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_document_url"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_tasks"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"intro_text"}},{"kind":"Field","name":{"kind":"Name","value":"performance_objectives"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tasks"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"translation_key"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"assignment_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assignment_user_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assignmentUserId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment_user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_grade"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion_data"}}]}}]}}]} as unknown as DocumentNode; export const CourseQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"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":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"category_name"}},{"kind":"Field","name":{"kind":"Name","value":"learning_path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} 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 bc2d6843..b6811b08 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -1,16 +1,20 @@ type Query { course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType course(id: Int): CourseType - assignment(id: ID, slug: String): AssignmentObjectType - assignment_completion(assignment_id: ID!, course_session_id: ID!, assignment_user_id: ID): AssignmentCompletionObjectType + assignment(id: ID, slug: String): AssignmentType + assignment_completion(assignment_id: ID!, course_session_id: ID!, assignment_user_id: ID): AssignmentCompletionType } type CourseSessionAttendanceCourseType { id: ID! location: String! trainer: String! + course_session_id: ID + learning_content_id: ID + due_date_id: ID end: DateTime start: DateTime + attendance_user_list: [AttendanceUserType] } """ @@ -20,6 +24,13 @@ value as specified by """ scalar DateTime +type AttendanceUserType { + user_id: ID + first_name: String + last_name: String + email: String +} + type CourseType { id: ID! title: String! @@ -105,7 +116,7 @@ enum CoreUserLanguageChoices { IT } -type AssignmentObjectType implements CoursePageInterface { +type AssignmentType implements CoursePageInterface { assignment_type: AssignmentAssignmentAssignmentTypeChoices! """Erläuterung der Ausgangslage""" @@ -145,7 +156,7 @@ enum AssignmentAssignmentAssignmentTypeChoices { scalar JSONStreamField -type AssignmentCompletionObjectType { +type AssignmentCompletionType { id: ID! created_at: DateTime! updated_at: DateTime! @@ -155,7 +166,7 @@ type AssignmentCompletionObjectType { evaluation_grade: Float evaluation_points: Float assignment_user: UserType! - assignment: AssignmentObjectType! + assignment: AssignmentType! completion_status: AssignmentAssignmentCompletionCompletionStatusChoices! completion_data: GenericScalar additional_json_data: JSONString! @@ -193,7 +204,8 @@ scalar JSONString type Mutation { send_feedback(input: SendFeedbackInput!): SendFeedbackPayload - upsert_assignment_completion(assignment_id: ID!, assignment_user_id: ID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_grade: Float, evaluation_points: Float): AssignmentCompletionMutation + update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation + upsert_assignment_completion(assignment_id: ID!, assignment_user_id: ID, completion_data_string: String, completion_status: String, course_session_id: ID!, evaluation_grade: Float, evaluation_points: Float): AssignmentCompletionMutation } type SendFeedbackPayload { @@ -229,14 +241,14 @@ input SendFeedbackInput { clientMutationId: String } -type AssignmentCompletionMutation { - assignment_completion: AssignmentCompletionObjectType +type AttendanceCourseUserMutation { + course_session_attendance_course: CourseSessionAttendanceCourseType } -"""An enumeration.""" -enum AssignmentCompletionStatus { - IN_PROGRESS - SUBMITTED - EVALUATION_IN_PROGRESS - EVALUATION_SUBMITTED +input AttendanceUserInputType { + user_id: ID +} + +type AssignmentCompletionMutation { + assignment_completion: AssignmentCompletionType } \ No newline at end of file diff --git a/server/vbv_lernwelt/core/schema.py b/server/vbv_lernwelt/core/schema.py index e2a2476e..ed89136d 100644 --- a/server/vbv_lernwelt/core/schema.py +++ b/server/vbv_lernwelt/core/schema.py @@ -3,6 +3,7 @@ import graphene from vbv_lernwelt.assignment.graphql.mutations import AssignmentMutation from vbv_lernwelt.assignment.graphql.queries import AssignmentQuery from vbv_lernwelt.course.schema import CourseQuery +from vbv_lernwelt.course_session.graphql.mutations import CourseSessionMutation from vbv_lernwelt.course_session.graphql.queries import CourseSessionQuery from vbv_lernwelt.feedback.graphql.mutations import FeedbackMutation @@ -11,7 +12,9 @@ class Query(AssignmentQuery, CourseQuery, CourseSessionQuery, graphene.ObjectTyp pass -class Mutation(AssignmentMutation, FeedbackMutation, graphene.ObjectType): +class Mutation( + AssignmentMutation, CourseSessionMutation, FeedbackMutation, graphene.ObjectType +): pass diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 1e3df840..d4ea4043 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -1,5 +1,4 @@ import json -import random from datetime import datetime, timedelta import wagtail_factories @@ -41,6 +40,7 @@ from vbv_lernwelt.course_session.models import ( CourseSessionAssignment, CourseSessionAttendanceCourse, ) +from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.learnpath.models import ( Circle, LearningContentAssignment, @@ -98,6 +98,31 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False): id=TEST_COURSE_SESSION_BERN_ID, start_date=now, ) + CourseSessionAttendanceCourse.objects.create( + course_session=cs_bern, + learning_content=LearningContentAttendanceCourse.objects.get( + slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" + ), + due_date=DueDate.objects.create( + course_session=cs_bern, + start=timezone.make_aware( + (datetime.now() + timedelta(days=15)).replace( + hour=10, minute=30, second=0, microsecond=0 + ) + ), + end=timezone.make_aware( + (datetime.now() + timedelta(days=15)).replace( + hour=17, minute=30, second=0, microsecond=0 + ) + ), + page=LearningContentAttendanceCourse.objects.get( + slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" + ), + ), + location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", + trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch", + ) + cs_zurich = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Zürich 2022 a", diff --git a/server/vbv_lernwelt/course_session/graphql/mutations.py b/server/vbv_lernwelt/course_session/graphql/mutations.py index 24c280cd..95d5c137 100644 --- a/server/vbv_lernwelt/course_session/graphql/mutations.py +++ b/server/vbv_lernwelt/course_session/graphql/mutations.py @@ -1,7 +1,64 @@ +import graphene import structlog +from rest_framework.exceptions import PermissionDenied + +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.permissions import has_course_access +from vbv_lernwelt.course_session.graphql.types import CourseSessionAttendanceCourseType +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse logger = structlog.get_logger(__name__) +class AttendanceUserInputType(graphene.InputObjectType): + user_id = graphene.ID(required=True) + + +class AttendanceCourseUserMutation(graphene.Mutation): + course_session_attendance_course = graphene.Field(CourseSessionAttendanceCourseType) + + class Input: + id = graphene.ID(required=True) + attendance_user_list = graphene.List(AttendanceUserInputType, required=True) + + @classmethod + def mutate( + cls, + root, + info, + id, + attendance_user_list, + ): + attendance_course = CourseSessionAttendanceCourse.objects.get(id=id) + + if not has_course_access( + info.context.user, + attendance_course.course_session.course_id, + ): + raise PermissionDenied() + + # FIXME: create completion for every user in attendance_user_list + + result_user_list = [] + for attendance_user in attendance_user_list: + u = User.objects.filter(id=attendance_user.user_id).first() + if u is not None: + result_user_list.append( + { + "user_id": u.id, + "email": u.email, + "first_name": u.first_name, + "last_name": u.last_name, + } + ) + + attendance_course.attendance_user_list = result_user_list + attendance_course.save() + + return AttendanceCourseUserMutation( + course_session_attendance_course=attendance_course + ) + + class CourseSessionMutation: - pass + update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field() diff --git a/server/vbv_lernwelt/course_session/graphql/types.py b/server/vbv_lernwelt/course_session/graphql/types.py index 4125511d..18206e26 100644 --- a/server/vbv_lernwelt/course_session/graphql/types.py +++ b/server/vbv_lernwelt/course_session/graphql/types.py @@ -4,12 +4,22 @@ from graphene_django import DjangoObjectType from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +class AttendanceUserType(graphene.ObjectType): + user_id = graphene.ID() + first_name = graphene.String() + last_name = graphene.String() + email = graphene.String() + + class CourseSessionAttendanceCourseType(DjangoObjectType): course_session_id = graphene.ID(source="course_session_id") learning_content_id = graphene.ID(source="learning_content_id") due_date_id = graphene.ID(source="due_date_id") end = graphene.DateTime() start = graphene.DateTime() + attendance_user_list = graphene.List( + AttendanceUserType, source="attendance_user_list" + ) class Meta: model = CourseSessionAttendanceCourse @@ -22,7 +32,6 @@ class CourseSessionAttendanceCourseType(DjangoObjectType): "trainer", "start", "end", - # "attendance_user_list", ) def resolve_start(self, info): diff --git a/server/vbv_lernwelt/course_session/migrations/0002_coursesessionattendancecourse_attendance_user_list.py b/server/vbv_lernwelt/course_session/migrations/0002_coursesessionattendancecourse_attendance_user_list.py new file mode 100644 index 00000000..0fe26463 --- /dev/null +++ b/server/vbv_lernwelt/course_session/migrations/0002_coursesessionattendancecourse_attendance_user_list.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2023-06-23 15:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("course_session", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="coursesessionattendancecourse", + name="attendance_user_list", + field=models.JSONField(default=list), + ), + ] diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py index 91d9f7f6..7cd6738c 100644 --- a/server/vbv_lernwelt/course_session/models.py +++ b/server/vbv_lernwelt/course_session/models.py @@ -29,6 +29,11 @@ class CourseSessionAttendanceCourse(models.Model): location = models.CharField(max_length=255, blank=True, default="") trainer = models.CharField(max_length=255, blank=True, default="") + # because the attendance list is more of a snapshot of the current state + # we will store the attendance list as a JSONField + # the important field of the list type is "user_id" + attendance_user_list = models.JSONField(default=list) + def save(self, *args, **kwargs): if not self.pk: title = "" diff --git a/server/vbv_lernwelt/course_session/tests/test_graphql.py b/server/vbv_lernwelt/course_session/tests/test_graphql.py index 6b63c8c7..d678480d 100644 --- a/server/vbv_lernwelt/course_session/tests/test_graphql.py +++ b/server/vbv_lernwelt/course_session/tests/test_graphql.py @@ -2,25 +2,93 @@ import json from graphene_django.utils.testing import GraphQLTestCase +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession -class MyFancyTestCase(GraphQLTestCase): - def test_some_query(self): + +class AttendanceCourseUserMutationTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def setUp(self): + create_default_users() + create_test_course(include_vv=False, with_sessions=True) + self.course_session = CourseSession.objects.get(title="Test Bern 2022 a") + self.attendance_course = ( + self.course_session.coursesessionattendancecourse_set.first() + ) + self.trainer = User.objects.get(username="test-trainer1@example.com") + self.client.force_login(self.trainer) + + def test_simple_query(self): response = self.query( """ - query { - myModel { - id - name - } + { + course_session_attendance_course(id:1) { + id + trainer + } } - """, - op_name="myModel", + """ ) content = json.loads(response.content) - # This validates the status code and if you get errors + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["course_session_attendance_course"]["trainer"], + "Roland Grossenbacher, roland.grossenbacher@helvetia.ch", + ) + + def test_mutation_add_user_when_present(self): + student = User.objects.get(username="test-student1@example.com") + + query = f""" + mutation {{ + update_course_session_attendance_course_users( + id:1, + attendance_user_list:[ + {{user_id: {student.id}}}, + {{user_id: "123123123"}} + ] + ) {{ + course_session_attendance_course {{ + id + attendance_user_list {{ + user_id + first_name + last_name + email + }} + }} + }} + }} + """ + print(query) + response = self.query(query) self.assertResponseNoErrors(response) - # Add some more asserts if you like - ... + data = json.loads(response.content) + + self.maxDiff = None + self.assertDictEqual( + { + "data": { + "update_course_session_attendance_course_users": { + "course_session_attendance_course": { + "id": str(self.attendance_course.id), + "attendance_user_list": [ + { + "user_id": str(student.id), + "first_name": student.first_name, + "last_name": student.last_name, + "email": student.email, + } + ], + } + } + } + }, + data, + ) From 46b14bd4e76aa01e4b0eda3f39ac9d6a2b6335fa Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Jun 2023 14:24:30 +0200 Subject: [PATCH 03/15] Refactor attendance_course code into its own module --- .../course_session/services/attendance.py | 50 ++++++++++++ .../course_session/tests/test_attendance.py | 80 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 server/vbv_lernwelt/course_session/services/attendance.py create mode 100644 server/vbv_lernwelt/course_session/tests/test_attendance.py diff --git a/server/vbv_lernwelt/course_session/services/attendance.py b/server/vbv_lernwelt/course_session/services/attendance.py new file mode 100644 index 00000000..d506b1a0 --- /dev/null +++ b/server/vbv_lernwelt/course_session/services/attendance.py @@ -0,0 +1,50 @@ +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.services import mark_course_completion +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse + + +def update_attendance_list( + attendance_course: CourseSessionAttendanceCourse, attendance_user_list: list +): + user_id_list_before = attendance_course.attendance_user_list or [] + + result_user_list = [] + for attendance_user in attendance_user_list: + u = User.objects.filter(id=attendance_user.get("user_id")).first() + if u is not None: + result_user_list.append( + { + "user_id": u.id, + "email": u.email, + "first_name": u.first_name, + "last_name": u.last_name, + } + ) + mark_course_completion( + page_key=attendance_course.learning_content.translation_key, + user=u, + course_session=attendance_course.course_session, + completion_status="success", + ) + + attendance_course.attendance_user_list = result_user_list + attendance_course.save() + + user_id_list_after = attendance_course.attendance_user_list or [] + + user_id_list_removed = [ + user_id for user_id in user_id_list_before if user_id not in user_id_list_after + ] + + for user_id in user_id_list_removed: + # FIXME: create completion for every user in attendance_user_list + u = User.objects.filter(id=user_id).first() + if u is not None: + mark_course_completion( + page_key=attendance_course.learning_content.translation_key, + user=u, + course_session=attendance_course.course_session, + completion_status="fail", + ) + + return attendance_course diff --git a/server/vbv_lernwelt/course_session/tests/test_attendance.py b/server/vbv_lernwelt/course_session/tests/test_attendance.py new file mode 100644 index 00000000..393bd431 --- /dev/null +++ b/server/vbv_lernwelt/course_session/tests/test_attendance.py @@ -0,0 +1,80 @@ +from django.test import TestCase + +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession, CourseCompletion +from vbv_lernwelt.course.services import mark_course_completion +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.services.attendance import update_attendance_list + + +class AttendanceServicesTestCase(TestCase): + def setUp(self): + create_default_users() + create_test_course(include_vv=False, with_sessions=True) + self.course_session = CourseSession.objects.get(title="Test Bern 2022 a") + self.attendance_course = ( + self.course_session.coursesessionattendancecourse_set.first() + ) + self.trainer = User.objects.get(username="test-trainer1@example.com") + self.client.force_login(self.trainer) + + def test_updateAttendanceList_withSingleUserId_findsDetailsAndStoresResult(self): + student = User.objects.get(username="test-student1@example.com") + + update_attendance_list(self.attendance_course, [{"user_id": student.id}]) + + attendance_course = CourseSessionAttendanceCourse.objects.get( + id=self.attendance_course.id + ) + self.assertEqual( + attendance_course.attendance_user_list[0]["user_id"], student.id + ) + self.assertEqual( + attendance_course.attendance_user_list, + [ + { + "email": "test-student1@example.com", + "user_id": -21, + "last_name": "Student1", + "first_name": "Test", + } + ], + ) + + def test_updateAttendanceList_willUpdateUserCourseCompletion(self): + student = User.objects.get(username="test-student1@example.com") + update_attendance_list(self.attendance_course, [{"user_id": student.id}]) + + self.assertEqual(CourseCompletion.objects.count(), 1) + cc = CourseCompletion.objects.first() + self.assertEqual(cc.user, student) + self.assertEqual(cc.completion_status, "success") + self.assertEqual( + cc.page_key, self.attendance_course.learning_content.translation_key + ) + self.assertEqual( + cc.page_slug, "test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" + ) + + def test_updateAttendanceList_withRemovedUser_willUpdateUserCourseCompletion(self): + student = User.objects.get(username="test-student1@example.com") + mark_course_completion( + page_key=self.attendance_course.learning_content.translation_key, + user=student, + course_session=self.course_session, + completion_status="success", + ) + update_attendance_list(self.attendance_course, []) + + self.assertEqual(CourseCompletion.objects.count(), 1) + cc = CourseCompletion.objects.first() + self.assertEqual(cc.user, student) + self.assertEqual(cc.completion_status, "success") + self.assertEqual( + cc.page_key, self.attendance_course.learning_content.translation_key + ) + self.assertEqual( + cc.page_slug, "test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" + ) From b9c9b009ff1fe1a53e424a431576ebc7f678d5ce Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Jun 2023 14:44:46 +0200 Subject: [PATCH 04/15] Add more typing --- .../course_session/graphql/mutations.py | 8 +++++- .../course_session/graphql/types.py | 6 ++++- .../course_session/services/attendance.py | 26 ++++++++++++++++--- .../course_session/tests/test_attendance.py | 22 +++++++++++++--- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/server/vbv_lernwelt/course_session/graphql/mutations.py b/server/vbv_lernwelt/course_session/graphql/mutations.py index 95d5c137..5b9b7a67 100644 --- a/server/vbv_lernwelt/course_session/graphql/mutations.py +++ b/server/vbv_lernwelt/course_session/graphql/mutations.py @@ -4,14 +4,20 @@ from rest_framework.exceptions import PermissionDenied from vbv_lernwelt.core.models import User from vbv_lernwelt.course.permissions import has_course_access -from vbv_lernwelt.course_session.graphql.types import CourseSessionAttendanceCourseType +from vbv_lernwelt.course_session.graphql.types import ( + CourseSessionAttendanceCourseType, +) from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus logger = structlog.get_logger(__name__) class AttendanceUserInputType(graphene.InputObjectType): user_id = graphene.ID(required=True) + status = graphene.Field( + graphene.Enum.from_enum(AttendanceUserStatus), required=True + ) class AttendanceCourseUserMutation(graphene.Mutation): diff --git a/server/vbv_lernwelt/course_session/graphql/types.py b/server/vbv_lernwelt/course_session/graphql/types.py index 18206e26..36dd82f1 100644 --- a/server/vbv_lernwelt/course_session/graphql/types.py +++ b/server/vbv_lernwelt/course_session/graphql/types.py @@ -2,10 +2,14 @@ import graphene from graphene_django import DjangoObjectType from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus class AttendanceUserType(graphene.ObjectType): - user_id = graphene.ID() + user_id = graphene.ID(required=True) + status = graphene.Field( + graphene.Enum.from_enum(AttendanceUserStatus), required=True + ) first_name = graphene.String() last_name = graphene.String() email = graphene.String() diff --git a/server/vbv_lernwelt/course_session/services/attendance.py b/server/vbv_lernwelt/course_session/services/attendance.py index d506b1a0..2eca1ea9 100644 --- a/server/vbv_lernwelt/course_session/services/attendance.py +++ b/server/vbv_lernwelt/course_session/services/attendance.py @@ -1,12 +1,26 @@ +import enum +from typing import List, TypedDict + from vbv_lernwelt.core.models import User from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +class AttendanceUserStatus(enum.Enum): + PRESENT = "PRESENT" + ABSENT = "ABSENT" + + +class AttendanceUser(TypedDict): + user_id: str + status: AttendanceUserStatus + + def update_attendance_list( - attendance_course: CourseSessionAttendanceCourse, attendance_user_list: list + attendance_course: CourseSessionAttendanceCourse, + attendance_user_list: List[AttendanceUser], ): - user_id_list_before = attendance_course.attendance_user_list or [] + user_id_list_before = [u["user_id"] for u in attendance_course.attendance_user_list] result_user_list = [] for attendance_user in attendance_user_list: @@ -15,22 +29,26 @@ def update_attendance_list( result_user_list.append( { "user_id": u.id, + "status": attendance_user.get("status"), "email": u.email, "first_name": u.first_name, "last_name": u.last_name, } ) + completion_status = ( + "success" if attendance_user.get("status") == "PRESENT" else "fail" + ) mark_course_completion( page_key=attendance_course.learning_content.translation_key, user=u, course_session=attendance_course.course_session, - completion_status="success", + completion_status=completion_status, ) attendance_course.attendance_user_list = result_user_list attendance_course.save() - user_id_list_after = attendance_course.attendance_user_list or [] + user_id_list_after = [u["user_id"] for u in attendance_course.attendance_user_list] user_id_list_removed = [ user_id for user_id in user_id_list_before if user_id not in user_id_list_after diff --git a/server/vbv_lernwelt/course_session/tests/test_attendance.py b/server/vbv_lernwelt/course_session/tests/test_attendance.py index 393bd431..878e84d6 100644 --- a/server/vbv_lernwelt/course_session/tests/test_attendance.py +++ b/server/vbv_lernwelt/course_session/tests/test_attendance.py @@ -23,7 +23,9 @@ class AttendanceServicesTestCase(TestCase): def test_updateAttendanceList_withSingleUserId_findsDetailsAndStoresResult(self): student = User.objects.get(username="test-student1@example.com") - update_attendance_list(self.attendance_course, [{"user_id": student.id}]) + update_attendance_list( + self.attendance_course, [{"user_id": student.id, "status": "PRESENT"}] + ) attendance_course = CourseSessionAttendanceCourse.objects.get( id=self.attendance_course.id @@ -36,6 +38,7 @@ class AttendanceServicesTestCase(TestCase): [ { "email": "test-student1@example.com", + "status": "PRESENT", "user_id": -21, "last_name": "Student1", "first_name": "Test", @@ -45,8 +48,9 @@ class AttendanceServicesTestCase(TestCase): def test_updateAttendanceList_willUpdateUserCourseCompletion(self): student = User.objects.get(username="test-student1@example.com") - update_attendance_list(self.attendance_course, [{"user_id": student.id}]) - + update_attendance_list( + self.attendance_course, [{"user_id": student.id, "status": "PRESENT"}] + ) self.assertEqual(CourseCompletion.objects.count(), 1) cc = CourseCompletion.objects.first() self.assertEqual(cc.user, student) @@ -60,6 +64,16 @@ class AttendanceServicesTestCase(TestCase): def test_updateAttendanceList_withRemovedUser_willUpdateUserCourseCompletion(self): student = User.objects.get(username="test-student1@example.com") + self.attendance_course.attendance_user_list = [ + { + "email": "test-student1@example.com", + "status": "PRESENT", + "user_id": "-21", + "last_name": "Student1", + "first_name": "Test", + } + ] + self.attendance_course.save() mark_course_completion( page_key=self.attendance_course.learning_content.translation_key, user=student, @@ -71,7 +85,7 @@ class AttendanceServicesTestCase(TestCase): self.assertEqual(CourseCompletion.objects.count(), 1) cc = CourseCompletion.objects.first() self.assertEqual(cc.user, student) - self.assertEqual(cc.completion_status, "success") + self.assertEqual(cc.completion_status, "fail") self.assertEqual( cc.page_key, self.attendance_course.learning_content.translation_key ) From ab8dbd09ef9cf4f301dac3653e3a1cbbe2a4ffe6 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Jun 2023 15:16:08 +0200 Subject: [PATCH 05/15] Working with enums in graphql... --- client/src/gql/graphql.ts | 12 +++++-- client/src/gql/schema.graphql | 12 +++++-- .../course_session/graphql/mutations.py | 33 ++++++------------- .../course_session/services/attendance.py | 8 +++-- .../course_session/tests/test_attendance.py | 13 +++++--- .../course_session/tests/test_graphql.py | 18 +++++----- 6 files changed, 55 insertions(+), 41 deletions(-) diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index f87befd5..5fdafc40 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -107,15 +107,23 @@ export type AttendanceCourseUserMutation = { }; export type AttendanceUserInputType = { - user_id?: InputMaybe; + status: AttendanceUserStatus; + user_id: Scalars['ID']; }; +/** An enumeration. */ +export enum AttendanceUserStatus { + Absent = 'ABSENT', + Present = 'PRESENT' +} + export type AttendanceUserType = { __typename?: 'AttendanceUserType'; email?: Maybe; first_name?: Maybe; last_name?: Maybe; - user_id?: Maybe; + status: AttendanceUserStatus; + user_id: Scalars['ID']; }; /** An enumeration. */ diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index b6811b08..af82b73b 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -25,12 +25,19 @@ value as specified by scalar DateTime type AttendanceUserType { - user_id: ID + user_id: ID! + status: AttendanceUserStatus! first_name: String last_name: String email: String } +"""An enumeration.""" +enum AttendanceUserStatus { + PRESENT + ABSENT +} + type CourseType { id: ID! title: String! @@ -246,7 +253,8 @@ type AttendanceCourseUserMutation { } input AttendanceUserInputType { - user_id: ID + user_id: ID! + status: AttendanceUserStatus! } type AssignmentCompletionMutation { diff --git a/server/vbv_lernwelt/course_session/graphql/mutations.py b/server/vbv_lernwelt/course_session/graphql/mutations.py index 5b9b7a67..64e6c15f 100644 --- a/server/vbv_lernwelt/course_session/graphql/mutations.py +++ b/server/vbv_lernwelt/course_session/graphql/mutations.py @@ -2,13 +2,13 @@ import graphene import structlog from rest_framework.exceptions import PermissionDenied -from vbv_lernwelt.core.models import User from vbv_lernwelt.course.permissions import has_course_access -from vbv_lernwelt.course_session.graphql.types import ( - CourseSessionAttendanceCourseType, -) +from vbv_lernwelt.course_session.graphql.types import CourseSessionAttendanceCourseType from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse -from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus +from vbv_lernwelt.course_session.services.attendance import ( + AttendanceUserStatus, + update_attendance_list, +) logger = structlog.get_logger(__name__) @@ -37,29 +37,16 @@ class AttendanceCourseUserMutation(graphene.Mutation): ): attendance_course = CourseSessionAttendanceCourse.objects.get(id=id) - if not has_course_access( + if not attendance_course or not has_course_access( info.context.user, attendance_course.course_session.course_id, ): raise PermissionDenied() - # FIXME: create completion for every user in attendance_user_list - - result_user_list = [] - for attendance_user in attendance_user_list: - u = User.objects.filter(id=attendance_user.user_id).first() - if u is not None: - result_user_list.append( - { - "user_id": u.id, - "email": u.email, - "first_name": u.first_name, - "last_name": u.last_name, - } - ) - - attendance_course.attendance_user_list = result_user_list - attendance_course.save() + attendance_course = update_attendance_list( + attendance_course=attendance_course, + attendance_user_list=attendance_user_list, + ) return AttendanceCourseUserMutation( course_session_attendance_course=attendance_course diff --git a/server/vbv_lernwelt/course_session/services/attendance.py b/server/vbv_lernwelt/course_session/services/attendance.py index 2eca1ea9..c52306ed 100644 --- a/server/vbv_lernwelt/course_session/services/attendance.py +++ b/server/vbv_lernwelt/course_session/services/attendance.py @@ -29,14 +29,18 @@ def update_attendance_list( result_user_list.append( { "user_id": u.id, - "status": attendance_user.get("status"), + "status": attendance_user.get( + "status", AttendanceUserStatus.PRESENT + ).value, "email": u.email, "first_name": u.first_name, "last_name": u.last_name, } ) completion_status = ( - "success" if attendance_user.get("status") == "PRESENT" else "fail" + "success" + if attendance_user.get("status").value == "PRESENT" + else "fail" ) mark_course_completion( page_key=attendance_course.learning_content.translation_key, diff --git a/server/vbv_lernwelt/course_session/tests/test_attendance.py b/server/vbv_lernwelt/course_session/tests/test_attendance.py index 878e84d6..43b4c9b6 100644 --- a/server/vbv_lernwelt/course_session/tests/test_attendance.py +++ b/server/vbv_lernwelt/course_session/tests/test_attendance.py @@ -3,10 +3,13 @@ from django.test import TestCase from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.models import User from vbv_lernwelt.course.creators.test_course import create_test_course -from vbv_lernwelt.course.models import CourseSession, CourseCompletion +from vbv_lernwelt.course.models import CourseCompletion, CourseSession from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse -from vbv_lernwelt.course_session.services.attendance import update_attendance_list +from vbv_lernwelt.course_session.services.attendance import ( + AttendanceUserStatus, + update_attendance_list, +) class AttendanceServicesTestCase(TestCase): @@ -24,7 +27,8 @@ class AttendanceServicesTestCase(TestCase): student = User.objects.get(username="test-student1@example.com") update_attendance_list( - self.attendance_course, [{"user_id": student.id, "status": "PRESENT"}] + self.attendance_course, + [{"user_id": student.id, "status": AttendanceUserStatus.PRESENT}], ) attendance_course = CourseSessionAttendanceCourse.objects.get( @@ -49,7 +53,8 @@ class AttendanceServicesTestCase(TestCase): def test_updateAttendanceList_willUpdateUserCourseCompletion(self): student = User.objects.get(username="test-student1@example.com") update_attendance_list( - self.attendance_course, [{"user_id": student.id, "status": "PRESENT"}] + self.attendance_course, + [{"user_id": student.id, "status": AttendanceUserStatus.PRESENT}], ) self.assertEqual(CourseCompletion.objects.count(), 1) cc = CourseCompletion.objects.first() diff --git a/server/vbv_lernwelt/course_session/tests/test_graphql.py b/server/vbv_lernwelt/course_session/tests/test_graphql.py index d678480d..fcb4aab0 100644 --- a/server/vbv_lernwelt/course_session/tests/test_graphql.py +++ b/server/vbv_lernwelt/course_session/tests/test_graphql.py @@ -23,13 +23,13 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase): def test_simple_query(self): response = self.query( - """ - { - course_session_attendance_course(id:1) { + f""" + {{ + course_session_attendance_course(id:{self.attendance_course.id}) {{ id trainer - } - } + }} + }} """ ) @@ -47,10 +47,10 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase): query = f""" mutation {{ update_course_session_attendance_course_users( - id:1, + id:{self.attendance_course.id}, attendance_user_list:[ - {{user_id: {student.id}}}, - {{user_id: "123123123"}} + {{user_id: {student.id}, status: PRESENT}}, + {{user_id: "123123123", status: PRESENT}}, ] ) {{ course_session_attendance_course {{ @@ -60,6 +60,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase): first_name last_name email + status }} }} }} @@ -84,6 +85,7 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase): "first_name": student.first_name, "last_name": student.last_name, "email": student.email, + "status": "PRESENT", } ], } From 3bd489d2aec1626786d66a51d13fe475a189e25d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 26 Jun 2023 17:05:45 +0200 Subject: [PATCH 06/15] Refactor `CourseCompletion` model --- .../competences/PerformanceCriteriaRow.vue | 4 +- client/src/components/ui/ItProgress.vue | 12 +++--- client/src/constants.ts | 6 +-- .../assignmentsPage/AssignmentDetails.vue | 2 +- .../AssignmentSubmissionProgress.vue | 6 +-- .../pages/cockpit/cockpitPage/CockpitPage.vue | 6 +-- .../pages/competence/CompetenceIndexPage.vue | 8 ++-- .../competence/PerformanceCriteriaPage.vue | 8 ++-- .../SinglePerformanceCriteriaPage.vue | 12 +++--- .../circlePage/LearningSequence.vue | 16 +++---- .../selfEvaluationPage/SelfEvaluation.vue | 6 +-- client/src/services/assignmentService.ts | 4 +- client/src/services/circle.ts | 10 ++--- client/src/services/learningPath.ts | 6 +-- client/src/stores/circle.ts | 14 +++--- client/src/stores/competence.ts | 16 +++---- client/src/stores/completion.ts | 2 +- client/src/types.ts | 13 +++--- .../commands/create_default_courses.py | 30 ++++++------- .../migrations/0006_auto_20230626_1724.py | 43 +++++++++++++++++++ server/vbv_lernwelt/course/models.py | 23 ++++++---- server/vbv_lernwelt/course/serializers.py | 5 +-- server/vbv_lernwelt/course/services.py | 25 ++++++----- .../course/tests/test_completion_api.py | 42 ++++++++++-------- server/vbv_lernwelt/course/views.py | 17 +++----- .../course_session/services/attendance.py | 10 ++--- .../course_session/tests/test_attendance.py | 22 +++------- 27 files changed, 206 insertions(+), 162 deletions(-) create mode 100644 server/vbv_lernwelt/course/migrations/0006_auto_20230626_1724.py diff --git a/client/src/components/competences/PerformanceCriteriaRow.vue b/client/src/components/competences/PerformanceCriteriaRow.vue index a0ecd306..7bdc099a 100644 --- a/client/src/components/competences/PerformanceCriteriaRow.vue +++ b/client/src/components/competences/PerformanceCriteriaRow.vue @@ -19,10 +19,10 @@ const props = withDefaults(defineProps(), {
diff --git a/client/src/components/ui/ItProgress.vue b/client/src/components/ui/ItProgress.vue index 37403218..fba9ec6b 100644 --- a/client/src/components/ui/ItProgress.vue +++ b/client/src/components/ui/ItProgress.vue @@ -1,7 +1,7 @@ diff --git a/client/src/constants.ts b/client/src/constants.ts index 4c7d1916..0b981057 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -1,5 +1,5 @@ import type { CourseCompletionStatus } from "@/types"; -export const COMPLETION_SUCCESS: CourseCompletionStatus = "success"; -export const COMPLETION_FAILURE: CourseCompletionStatus = "fail"; -export const COMPLETION_UNKNOWN: CourseCompletionStatus = "unknown"; +export const COMPLETION_SUCCESS: CourseCompletionStatus = "SUCCESS"; +export const COMPLETION_FAILURE: CourseCompletionStatus = "FAIL"; +export const COMPLETION_UNKNOWN: CourseCompletionStatus = "UNKNOWN"; diff --git a/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue b/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue index 515ad009..b36b0fdb 100644 --- a/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue +++ b/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue @@ -129,7 +129,7 @@ const assignmentDetail = computed(() =>