Merged in feature/VBV-453-graphql-learningpath-server (pull request #161)

Feature/VBV-453 graphql learningpath server

Approved-by: Elia Bieri
This commit is contained in:
Daniel Egger 2023-07-21 06:40:25 +00:00
commit 0798efc7f2
19 changed files with 1053 additions and 150 deletions

View File

@ -258,9 +258,6 @@ There are some rules when it comes to the folder structure of the frontend.
## GraphQL
When you change something on the server side run the following command to update the
graphql schema:
```bash
python manage.py graphql_schema
```
@ -270,13 +267,25 @@ generated code
```bash
npm run codegen
# `npm run dev` includes `npm run codegen` as step...
npm run dev
```
💡 If you run `npm run dev`, the codegen command will be run automatically in watch mode."
If you run `npm run dev`, the codegen command will be run automatically in watch mode.
For the `ObjectTypes` on the server, please use the postfix `ObjectType` for types,
like `LearningContentAttendanceCourseObjectType`.
This will prevent problems with the hand written types on the client side,
when `npm run codegen` will create the types automatically.
When you change something on the server side run the following command to update the
graphql schema.
### Open Questions
- The `id` field has to be a string?
- Is running `codegen` a prerequisite so that it even works?
- What about the generated types from `codegen`? Hand written types seem to be better.
- The functions is `cacheExchange` should be nearer the concrete implementation
- The functions in `cacheExchange` should be nearer the concrete implementation...

View File

@ -138,6 +138,20 @@ export type AttendanceUserType = {
user_id: Scalars['UUID']['output'];
};
export type CircleObjectType = CoursePageInterface & {
__typename?: 'CircleObjectType';
content_type?: Maybe<Scalars['String']['output']>;
description: Scalars['String']['output'];
frontend_url?: Maybe<Scalars['String']['output']>;
goals: Scalars['String']['output'];
id?: Maybe<Scalars['ID']['output']>;
learning_sequences?: Maybe<Array<Maybe<LearningSequenceObjectType>>>;
live?: Maybe<Scalars['Boolean']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
/** An enumeration. */
export type CoreUserLanguageChoices =
/** Deutsch */
@ -147,6 +161,15 @@ export type CoreUserLanguageChoices =
/** Italiano */
| 'IT';
export type CourseObjectType = {
__typename?: 'CourseObjectType';
category_name: Scalars['String']['output'];
id: Scalars['ID']['output'];
learning_path?: Maybe<LearningPathObjectType>;
slug: Scalars['String']['output'];
title: Scalars['String']['output'];
};
export type CoursePageInterface = {
content_type?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
@ -170,15 +193,6 @@ export type CourseSessionAttendanceCourseType = {
trainer: Scalars['String']['output'];
};
export type CourseType = {
__typename?: 'CourseType';
category_name: Scalars['String']['output'];
id: Scalars['ID']['output'];
learning_path?: Maybe<LearningPathType>;
slug: Scalars['String']['output'];
title: Scalars['String']['output'];
};
export type ErrorType = {
__typename?: 'ErrorType';
field: Scalars['String']['output'];
@ -187,14 +201,170 @@ export type ErrorType = {
export type FeedbackResponse = Node & {
__typename?: 'FeedbackResponse';
circle: CircleObjectType;
created_at: Scalars['DateTime']['output'];
data?: Maybe<Scalars['GenericScalar']['output']>;
/** The ID of the object */
id: Scalars['ID']['output'];
};
export type LearningPathType = CoursePageInterface & {
__typename?: 'LearningPathType';
export type LearningContentAssignmentObjectType = LearningContentInterface & {
__typename?: 'LearningContentAssignmentObjectType';
assignment_type: LearnpathLearningContentAssignmentAssignmentTypeChoices;
content?: Maybe<Scalars['String']['output']>;
content_assignment: AssignmentObjectType;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentAttendanceCourseObjectType = LearningContentInterface & {
__typename?: 'LearningContentAttendanceCourseObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentDocumentListObjectType = LearningContentInterface & {
__typename?: 'LearningContentDocumentListObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentFeedbackObjectType = LearningContentInterface & {
__typename?: 'LearningContentFeedbackObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentInterface = {
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentLearningModuleObjectType = LearningContentInterface & {
__typename?: 'LearningContentLearningModuleObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentMediaLibraryObjectType = LearningContentInterface & {
__typename?: 'LearningContentMediaLibraryObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentPlaceholderObjectType = LearningContentInterface & {
__typename?: 'LearningContentPlaceholderObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentRichTextObjectType = LearningContentInterface & {
__typename?: 'LearningContentRichTextObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentTestObjectType = LearningContentInterface & {
__typename?: 'LearningContentTestObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningContentVideoObjectType = LearningContentInterface & {
__typename?: 'LearningContentVideoObjectType';
content?: Maybe<Scalars['String']['output']>;
content_type?: Maybe<Scalars['String']['output']>;
description?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
minutes?: Maybe<Scalars['Int']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningPathObjectType = CoursePageInterface & {
__typename?: 'LearningPathObjectType';
content_type?: Maybe<Scalars['String']['output']>;
depth: Scalars['Int']['output'];
draft_title: Scalars['String']['output'];
@ -222,10 +392,45 @@ export type LearningPathType = CoursePageInterface & {
show_in_menus: Scalars['Boolean']['output'];
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
topics?: Maybe<Array<Maybe<TopicObjectType>>>;
translation_key?: Maybe<Scalars['String']['output']>;
url_path: Scalars['String']['output'];
};
export type LearningSequenceObjectType = CoursePageInterface & {
__typename?: 'LearningSequenceObjectType';
content_type?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
icon: Scalars['String']['output'];
id?: Maybe<Scalars['ID']['output']>;
learning_units?: Maybe<Array<Maybe<LearningUnitObjectType>>>;
live?: Maybe<Scalars['Boolean']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type LearningUnitObjectType = CoursePageInterface & {
__typename?: 'LearningUnitObjectType';
content_type?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
learning_contents?: Maybe<Array<Maybe<LearningContentInterface>>>;
live?: Maybe<Scalars['Boolean']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
/** An enumeration. */
export type LearnpathLearningContentAssignmentAssignmentTypeChoices =
/** CASEWORK */
| 'CASEWORK'
/** PREP_ASSIGNMENT */
| 'PREP_ASSIGNMENT'
/** REFLECTION */
| 'REFLECTION';
export type Mutation = {
__typename?: 'Mutation';
send_feedback?: Maybe<SendFeedbackPayload>;
@ -266,8 +471,20 @@ export type Query = {
__typename?: 'Query';
assignment?: Maybe<AssignmentObjectType>;
assignment_completion?: Maybe<AssignmentCompletionObjectType>;
course?: Maybe<CourseType>;
circle?: Maybe<CircleObjectType>;
course?: Maybe<CourseObjectType>;
course_session_attendance_course?: Maybe<CourseSessionAttendanceCourseType>;
learning_content_assignment?: Maybe<LearningContentAssignmentObjectType>;
learning_content_attendance_course?: Maybe<LearningContentAttendanceCourseObjectType>;
learning_content_document_list?: Maybe<LearningContentDocumentListObjectType>;
learning_content_feedback?: Maybe<LearningContentFeedbackObjectType>;
learning_content_learning_module?: Maybe<LearningContentLearningModuleObjectType>;
learning_content_media_library?: Maybe<LearningContentMediaLibraryObjectType>;
learning_content_placeholder?: Maybe<LearningContentPlaceholderObjectType>;
learning_content_rich_text?: Maybe<LearningContentRichTextObjectType>;
learning_content_test?: Maybe<LearningContentTestObjectType>;
learning_content_video?: Maybe<LearningContentVideoObjectType>;
learning_path?: Maybe<LearningPathObjectType>;
};
@ -285,6 +502,12 @@ export type QueryAssignmentCompletionArgs = {
};
export type QueryCircleArgs = {
id?: InputMaybe<Scalars['Int']['input']>;
slug?: InputMaybe<Scalars['String']['input']>;
};
export type QueryCourseArgs = {
id?: InputMaybe<Scalars['Int']['input']>;
};
@ -295,6 +518,12 @@ export type QueryCourseSessionAttendanceCourseArgs = {
id: Scalars['ID']['input'];
};
export type QueryLearningPathArgs = {
id?: InputMaybe<Scalars['Int']['input']>;
slug?: InputMaybe<Scalars['String']['input']>;
};
export type SendFeedbackInput = {
clientMutationId?: InputMaybe<Scalars['String']['input']>;
course_session: Scalars['Int']['input'];
@ -310,6 +539,19 @@ export type SendFeedbackPayload = {
feedback_response?: Maybe<FeedbackResponse>;
};
export type TopicObjectType = CoursePageInterface & {
__typename?: 'TopicObjectType';
circles?: Maybe<Array<Maybe<CircleObjectType>>>;
content_type?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
is_visible: Scalars['Boolean']['output'];
live?: Maybe<Scalars['Boolean']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type UserType = {
__typename?: 'UserType';
avatar_url: Scalars['String']['output'];
@ -373,7 +615,7 @@ export type CourseQueryQueryVariables = Exact<{
}>;
export type CourseQueryQuery = { __typename?: 'Query', course?: { __typename?: 'CourseType', id: string, slug: string, title: string, category_name: string, learning_path?: { __typename?: 'LearningPathType', id?: string | null } | null } | null };
export type CourseQueryQuery = { __typename?: 'Query', course?: { __typename?: 'CourseObjectType', id: string, slug: string, title: string, category_name: string, learning_path?: { __typename?: 'LearningPathObjectType', id?: string | null } | null } | null };
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<SendFeedbackMutationMutation, SendFeedbackMutationMutationVariables>;

View File

@ -1,58 +1,82 @@
type Query {
circle(id: Int, slug: String): CircleObjectType
learning_path(id: Int, slug: String): LearningPathObjectType
learning_content_media_library: LearningContentMediaLibraryObjectType
learning_content_assignment: LearningContentAssignmentObjectType
learning_content_attendance_course: LearningContentAttendanceCourseObjectType
learning_content_feedback: LearningContentFeedbackObjectType
learning_content_learning_module: LearningContentLearningModuleObjectType
learning_content_placeholder: LearningContentPlaceholderObjectType
learning_content_rich_text: LearningContentRichTextObjectType
learning_content_test: LearningContentTestObjectType
learning_content_video: LearningContentVideoObjectType
learning_content_document_list: LearningContentDocumentListObjectType
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType
course(id: Int): CourseType
course(id: Int): CourseObjectType
assignment(id: ID, slug: String): AssignmentObjectType
assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
}
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]
type CircleObjectType implements CoursePageInterface {
description: String!
goals: String!
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
learning_sequences: [LearningSequenceObjectType]
}
"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
scalar DateTime
type AttendanceUserType {
user_id: UUID!
status: AttendanceUserStatus!
first_name: String
last_name: String
email: String
interface CoursePageInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
}
"""
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
in fields, resolvers and input.
"""
scalar UUID
"""An enumeration."""
enum AttendanceUserStatus {
PRESENT
ABSENT
type LearningSequenceObjectType implements CoursePageInterface {
icon: String!
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
learning_units: [LearningUnitObjectType]
}
type CourseType {
id: ID!
title: String!
category_name: String!
slug: String!
learning_path: LearningPathType
type LearningUnitObjectType implements CoursePageInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
learning_contents: [LearningContentInterface]
}
type LearningPathType implements CoursePageInterface {
interface LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningPathObjectType implements CoursePageInterface {
id: ID
path: String!
depth: Int!
@ -91,17 +115,15 @@ type LearningPathType implements CoursePageInterface {
search_description: String!
latest_revision_created_at: DateTime
frontend_url: String
topics: [TopicObjectType]
}
interface CoursePageInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
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 {
"""
@ -116,6 +138,12 @@ type UserType {
language: CoreUserLanguageChoices!
}
"""
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
in fields, resolvers and input.
"""
scalar UUID
"""An enumeration."""
enum CoreUserLanguageChoices {
"""Deutsch"""
@ -128,6 +156,46 @@ enum CoreUserLanguageChoices {
IT
}
type TopicObjectType implements CoursePageInterface {
is_visible: Boolean!
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
circles: [CircleObjectType]
}
type LearningContentMediaLibraryObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningContentAssignmentObjectType implements LearningContentInterface {
content_assignment: AssignmentObjectType!
assignment_type: LearnpathLearningContentAssignmentAssignmentTypeChoices!
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type AssignmentObjectType implements CoursePageInterface {
assignment_type: AssignmentAssignmentAssignmentTypeChoices!
@ -168,6 +236,156 @@ enum AssignmentAssignmentAssignmentTypeChoices {
scalar JSONStreamField
"""An enumeration."""
enum LearnpathLearningContentAssignmentAssignmentTypeChoices {
"""CASEWORK"""
CASEWORK
"""PREP_ASSIGNMENT"""
PREP_ASSIGNMENT
"""REFLECTION"""
REFLECTION
}
type LearningContentAttendanceCourseObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningContentFeedbackObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningContentLearningModuleObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningContentPlaceholderObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningContentRichTextObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningContentTestObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningContentVideoObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
type LearningContentDocumentListObjectType implements LearningContentInterface {
id: ID
title: String
slug: String
content_type: String
live: Boolean
translation_key: String
frontend_url: String
minutes: Int
description: String
content: String
}
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]
}
type AttendanceUserType {
user_id: UUID!
status: AttendanceUserStatus!
first_name: String
last_name: String
email: String
}
"""An enumeration."""
enum AttendanceUserStatus {
PRESENT
ABSENT
}
type CourseObjectType {
id: ID!
title: String!
category_name: String!
slug: String!
learning_path: LearningPathObjectType
}
type AssignmentCompletionObjectType {
id: UUID!
created_at: DateTime!
@ -234,6 +452,7 @@ type FeedbackResponse implements Node {
id: ID!
data: GenericScalar
created_at: DateTime!
circle: CircleObjectType!
}
"""An object with an ID"""

View File

@ -9,10 +9,11 @@ export const AttendanceUserInputType = "AttendanceUserInputType";
export const AttendanceUserStatus = "AttendanceUserStatus";
export const AttendanceUserType = "AttendanceUserType";
export const Boolean = "Boolean";
export const CircleObjectType = "CircleObjectType";
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
export const CourseObjectType = "CourseObjectType";
export const CoursePageInterface = "CoursePageInterface";
export const CourseSessionAttendanceCourseType = "CourseSessionAttendanceCourseType";
export const CourseType = "CourseType";
export const DateTime = "DateTime";
export const ErrorType = "ErrorType";
export const FeedbackResponse = "FeedbackResponse";
@ -22,12 +23,27 @@ export const ID = "ID";
export const Int = "Int";
export const JSONStreamField = "JSONStreamField";
export const JSONString = "JSONString";
export const LearningPathType = "LearningPathType";
export const LearningContentAssignmentObjectType = "LearningContentAssignmentObjectType";
export const LearningContentAttendanceCourseObjectType = "LearningContentAttendanceCourseObjectType";
export const LearningContentDocumentListObjectType = "LearningContentDocumentListObjectType";
export const LearningContentFeedbackObjectType = "LearningContentFeedbackObjectType";
export const LearningContentInterface = "LearningContentInterface";
export const LearningContentLearningModuleObjectType = "LearningContentLearningModuleObjectType";
export const LearningContentMediaLibraryObjectType = "LearningContentMediaLibraryObjectType";
export const LearningContentPlaceholderObjectType = "LearningContentPlaceholderObjectType";
export const LearningContentRichTextObjectType = "LearningContentRichTextObjectType";
export const LearningContentTestObjectType = "LearningContentTestObjectType";
export const LearningContentVideoObjectType = "LearningContentVideoObjectType";
export const LearningPathObjectType = "LearningPathObjectType";
export const LearningSequenceObjectType = "LearningSequenceObjectType";
export const LearningUnitObjectType = "LearningUnitObjectType";
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
export const Mutation = "Mutation";
export const Node = "Node";
export const Query = "Query";
export const SendFeedbackInput = "SendFeedbackInput";
export const SendFeedbackPayload = "SendFeedbackPayload";
export const String = "String";
export const TopicObjectType = "TopicObjectType";
export const UUID = "UUID";
export const UserType = "UserType";

View File

@ -24,7 +24,7 @@ def main():
reset_queries()
page = Page.objects.get(
slug="versicherungsvermittlerin", locale__language_code="de-CH"
slug="überbetriebliche-kurse-lp", locale__language_code="de-CH"
)
serializer = page.specific.get_serializer_class()(page.specific)
@ -32,6 +32,13 @@ def main():
print(len(json.dumps(serializer.data)))
print(len(connection.queries))
# reference
page = Page.objects.get(
slug="überbetriebliche-kurse-lp", locale__language_code="de-CH"
)
list(page.get_descendants().specific())
print(len(connection.queries))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
import os
import sys
import django
import graphene
from graphene import Context
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.core.schema import Query
from vbv_lernwelt.core.models import User
def main():
from django.conf import settings
settings.DEBUG = True
from django.db import connection
from django.db import reset_queries
reset_queries()
schema = graphene.Schema(query=Query, auto_camelcase=False)
context = Context()
context.user = User.objects.get(username="admin")
result = schema.execute(
"""
{
learning_path(slug: "überbetriebliche-kurse-lp") {
id
title
content_type
topics {
id
title
content_type
circles {
id
title
content_type
learning_sequences {
id
title
icon
learning_units {
id
title
learning_contents {
id
title
}
}
}
}
}
}
}
""",
context=context,
)
print(result)
print(len(connection.queries))
# reference
reset_queries()
if __name__ == "__main__":
main()

View File

@ -4,7 +4,7 @@ from graphene_django import DjangoObjectType
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
from vbv_lernwelt.core.graphql.types import JSONStreamField
from vbv_lernwelt.course.schema import CoursePageInterface
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
class AssignmentObjectType(DjangoObjectType):

View File

@ -2,13 +2,16 @@ 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.graphql.queries 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
from vbv_lernwelt.learnpath.graphql.queries import CircleQuery
class Query(AssignmentQuery, CourseQuery, CourseSessionQuery, graphene.ObjectType):
class Query(
AssignmentQuery, CourseQuery, CourseSessionQuery, CircleQuery, graphene.ObjectType
):
pass

View File

@ -1,7 +1,7 @@
import wagtail.api.v2.serializers as wagtail_serializers
from rest_framework.fields import SerializerMethodField
from vbv_lernwelt.learnpath.utils import get_wagtail_type
from vbv_lernwelt.core.utils import get_django_content_type
def get_it_serializer_class(
@ -33,7 +33,7 @@ def get_it_serializer_class(
class ItWagtailTypeField(wagtail_serializers.TypeField):
def to_representation(self, obj):
name = get_wagtail_type(obj)
name = get_django_content_type(obj)
return name

View File

@ -39,19 +39,17 @@ class DayUserRateThrottle(UserRateThrottle):
scope = "day-throttle"
def find_first(iterable, default=False, pred=None):
"""Returns the first true value in the iterable.
If no true value is found, returns *default*
If *pred* is not None, returns the first item
for which pred(item) is true.
"""
# first_true([a,b,c], x) --> a or b or c or x
# first_true([a,b], x, f) --> a if f(a) else b if f(b) else x
def find_first(iterable, pred=None, default=None):
return next(filter(pred, iterable), default)
def find_first_index(iterable, pred, default=None):
return next((i for i, x in enumerate(iterable) if pred(x)), default)
def replace_whitespace(text, replacement=" "):
return re.sub(r"\s+", replacement, text).strip()
def get_django_content_type(obj):
return obj._meta.app_label + "." + type(obj).__name__

View File

@ -0,0 +1,19 @@
import graphene
from vbv_lernwelt.core.utils import get_django_content_type
class CoursePageInterface(graphene.Interface):
id = graphene.ID()
title = graphene.String()
slug = graphene.String()
content_type = graphene.String()
live = graphene.Boolean()
translation_key = graphene.String()
frontend_url = graphene.String()
def resolve_frontend_url(self, info):
return self.get_frontend_url()
def resolve_content_type(self, info):
return get_django_content_type(self)

View File

@ -1,16 +1,15 @@
import graphene
from graphql import GraphQLError
from vbv_lernwelt.course.graphql.types import CourseType
from vbv_lernwelt.course.graphql.types import CourseObjectType
from vbv_lernwelt.course.models import Course
from vbv_lernwelt.course.permissions import has_course_access
class CourseQuery:
course = graphene.Field(CourseType, id=graphene.Int())
class CourseQuery(graphene.ObjectType):
course = graphene.Field(CourseObjectType, id=graphene.Int())
def resolve_course(root, info, id):
course = Course.objects.get(pk=id)
if has_course_access(info.context.user, course):
return course
return GraphQLError("You do not have access to this course")
raise PermissionError("You do not have access to this course")

View File

@ -8,7 +8,7 @@ from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.course.models import Course, CourseBasePage
from vbv_lernwelt.course.permissions import has_course_access
from vbv_lernwelt.course.schema import CoursePageInterface
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
logger = structlog.get_logger(__name__)
@ -39,12 +39,12 @@ def resolve_course_page(
raise e
class CourseType(DjangoObjectType):
learning_path = graphene.Field(CoursePageInterface)
class CourseObjectType(DjangoObjectType):
learning_path = graphene.Field(LearningPathObjectType)
class Meta:
model = Course
fields = ("id", "title", "category_name", "slug")
fields = ("id", "title", "category_name", "slug", "learning_path")
def resolve_learning_path(self, info):
return self.get_learning_path()

View File

@ -1,46 +0,0 @@
import graphene
from graphene_django import DjangoObjectType
from vbv_lernwelt.course.models import Course
from vbv_lernwelt.course.permissions import has_course_access
from vbv_lernwelt.learnpath.models import LearningPath
class CoursePageInterface(graphene.Interface):
id = graphene.ID()
title = graphene.String()
slug = graphene.String()
content_type = graphene.String()
live = graphene.Boolean()
translation_key = graphene.String()
frontend_url = graphene.String()
def resolve_frontend_url(self, info):
return self.get_frontend_url()
class LearningPathType(DjangoObjectType):
class Meta:
model = LearningPath
interfaces = (CoursePageInterface,)
class CourseType(DjangoObjectType):
learning_path = graphene.Field(LearningPathType)
class Meta:
model = Course
fields = ("id", "title", "category_name", "slug", "learning_path")
def resolve_learning_path(self, info):
return self.get_learning_path()
class CourseQuery(graphene.ObjectType):
course = graphene.Field(CourseType, id=graphene.Int())
def resolve_course(root, info, id):
course = Course.objects.get(pk=id)
if has_course_access(info.context.user, course):
return course
raise PermissionError("You do not have access to this course")

View File

@ -1,5 +1,5 @@
from vbv_lernwelt.core.utils import get_django_content_type
from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
from vbv_lernwelt.learnpath.utils import get_wagtail_type
def mark_course_completion(
@ -15,7 +15,7 @@ def mark_course_completion(
and page.specific.has_course_completion_status
):
return ValueError(
f"Page {page.id} of type {get_wagtail_type(page)}"
f"Page {page.id} of type {get_django_content_type(page)}"
f" cannot be marked as completed"
)
@ -25,7 +25,7 @@ def mark_course_completion(
course_session_id=course_session.id,
)
cc.completion_status = completion_status
cc.page_type = get_wagtail_type(page.specific)
cc.page_type = get_django_content_type(page.specific)
cc.save()
return cc

View File

@ -5,6 +5,8 @@ from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from wagtail.models import Page
from vbv_lernwelt.core.utils import get_django_content_type
from vbv_lernwelt.course.models import (
CircleDocument,
CourseCompletion,
@ -27,7 +29,6 @@ from vbv_lernwelt.course.serializers import (
from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.files.models import UploadFile
from vbv_lernwelt.files.services import FileDirectUploadService
from vbv_lernwelt.learnpath.utils import get_wagtail_type
logger = structlog.get_logger(__name__)
@ -115,7 +116,7 @@ def mark_course_completion_view(request):
"mark_course_completion successful",
label="completion_api",
page_id=page_id,
page_type=get_wagtail_type(page.specific),
page_type=get_django_content_type(page.specific),
page_slug=page.slug,
page_title=page.title,
user_id=request.user.id,

View File

@ -0,0 +1,81 @@
import graphene
from vbv_lernwelt.course.graphql.types import resolve_course_page
from vbv_lernwelt.learnpath.graphql.types import (
CircleObjectType,
LearningContentAssignmentObjectType,
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentFeedbackObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
LearningContentPlaceholderObjectType,
LearningContentRichTextObjectType,
LearningContentTestObjectType,
LearningContentVideoObjectType,
LearningPathObjectType,
)
from vbv_lernwelt.learnpath.models import Circle, LearningPath
class CircleQuery:
circle = graphene.Field(CircleObjectType, id=graphene.Int(), slug=graphene.String())
learning_path = graphene.Field(
LearningPathObjectType, id=graphene.Int(), slug=graphene.String()
)
def resolve_circle(root, info, id=None, slug=None):
return resolve_course_page(Circle, root, info, id=id, slug=slug)
def resolve_learning_path(root, info, id=None, slug=None):
return resolve_course_page(LearningPath, root, info, id=id, slug=slug)
# dummy import, so that graphene recognizes the types
learning_content_media_library = graphene.Field(
LearningContentMediaLibraryObjectType
)
learning_content_assignment = graphene.Field(LearningContentAssignmentObjectType)
learning_content_attendance_course = graphene.Field(
LearningContentAttendanceCourseObjectType
)
learning_content_feedback = graphene.Field(LearningContentFeedbackObjectType)
learning_content_learning_module = graphene.Field(
LearningContentLearningModuleObjectType
)
learning_content_placeholder = graphene.Field(LearningContentPlaceholderObjectType)
learning_content_rich_text = graphene.Field(LearningContentRichTextObjectType)
learning_content_test = graphene.Field(LearningContentTestObjectType)
learning_content_video = graphene.Field(LearningContentVideoObjectType)
learning_content_document_list = graphene.Field(
LearningContentDocumentListObjectType
)
def resolve_learning_content_media_library(self, info):
return None
def resolve_learning_content_assignment(self, info):
return None
def resolve_learning_content_attendance_course(self, info):
return None
def resolve_learning_content_feedback(self, info):
return None
def resolve_learning_content_learning_module(self, info):
return None
def resolve_learning_content_placeholder(self, info):
return None
def resolve_learning_content_rich_text(self, info):
return None
def resolve_learning_content_test(self, info):
return None
def resolve_learning_content_video(self, info):
return None
def resolve_learning_content_document_list(self, info):
return None

View File

@ -0,0 +1,282 @@
import graphene
import structlog
from graphene_django import DjangoObjectType
from vbv_lernwelt.core.utils import find_first_index
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
from vbv_lernwelt.learnpath.models import (
Circle,
LearningContentAssignment,
LearningContentAttendanceCourse,
LearningContentDocumentList,
LearningContentFeedback,
LearningContentLearningModule,
LearningContentMediaLibrary,
LearningContentPlaceholder,
LearningContentRichText,
LearningContentTest,
LearningContentVideo,
LearningPath,
LearningSequence,
LearningUnit,
Topic,
)
logger = structlog.get_logger(__name__)
class LearningContentInterface(CoursePageInterface):
minutes = graphene.Int()
description = graphene.String()
content = graphene.String()
@classmethod
def resolve_type(cls, instance, info):
if isinstance(instance, LearningContentAssignment):
return LearningContentAssignmentObjectType
elif isinstance(instance, LearningContentAttendanceCourse):
return LearningContentAttendanceCourseObjectType
elif isinstance(instance, LearningContentFeedback):
return LearningContentFeedbackObjectType
elif isinstance(instance, LearningContentLearningModule):
return LearningContentLearningModuleObjectType
elif isinstance(instance, LearningContentMediaLibrary):
return LearningContentMediaLibraryObjectType
elif isinstance(instance, LearningContentPlaceholder):
return LearningContentPlaceholderObjectType
elif isinstance(instance, LearningContentRichText):
return LearningContentRichTextObjectType
elif isinstance(instance, LearningContentTest):
return LearningContentTestObjectType
elif isinstance(instance, LearningContentVideo):
return LearningContentVideoObjectType
elif isinstance(instance, LearningContentDocumentList):
return LearningContentDocumentListObjectType
else:
logger.error(
"Could not resolve type for LearningContentInterface",
django_instance=instance,
)
return None
class LearningContentAttendanceCourseObjectType(DjangoObjectType):
class Meta:
model = LearningContentAttendanceCourse
interfaces = (LearningContentInterface,)
fields = []
class LearningContentVideoObjectType(DjangoObjectType):
class Meta:
model = LearningContentVideo
interfaces = (LearningContentInterface,)
fields = []
class LearningContentPlaceholderObjectType(DjangoObjectType):
class Meta:
model = LearningContentPlaceholder
interfaces = (LearningContentInterface,)
fields = []
class LearningContentFeedbackObjectType(DjangoObjectType):
class Meta:
model = LearningContentFeedback
interfaces = (LearningContentInterface,)
fields = []
class LearningContentLearningModuleObjectType(DjangoObjectType):
class Meta:
model = LearningContentLearningModule
interfaces = (LearningContentInterface,)
fields = []
class LearningContentMediaLibraryObjectType(DjangoObjectType):
class Meta:
model = LearningContentMediaLibrary
interfaces = (LearningContentInterface,)
fields = []
class LearningContentTestObjectType(DjangoObjectType):
class Meta:
model = LearningContentTest
interfaces = (LearningContentInterface,)
fields = []
class LearningContentRichTextObjectType(DjangoObjectType):
class Meta:
model = LearningContentRichText
interfaces = (LearningContentInterface,)
fields = []
class LearningContentAssignmentObjectType(DjangoObjectType):
class Meta:
model = LearningContentAssignment
interfaces = (LearningContentInterface,)
fields = [
"content_assignment",
"assignment_type",
]
class LearningContentDocumentListObjectType(DjangoObjectType):
class Meta:
model = LearningContentDocumentList
interfaces = (LearningContentInterface,)
fields = []
class LearningUnitObjectType(DjangoObjectType):
learning_contents = graphene.List(LearningContentInterface)
class Meta:
model = LearningUnit
interfaces = (CoursePageInterface,)
fields = []
@staticmethod
def resolve_learning_contents(root: LearningUnit, info, **kwargs):
siblings = None
if hasattr(info.context, "circle_descendants"):
circle_descendants = info.context.circle_descendants
index = circle_descendants.index(root)
siblings = circle_descendants[index + 1 :]
if not siblings:
siblings = root.get_siblings().live().specific()
learning_contents = []
for sibling in siblings:
if (
sibling.specific_class == LearningUnit
or sibling.specific_class == LearningSequence
):
break
else:
learning_contents.append(sibling.specific)
return learning_contents
class LearningSequenceObjectType(DjangoObjectType):
learning_units = graphene.List(LearningUnitObjectType)
class Meta:
model = LearningSequence
interfaces = (CoursePageInterface,)
fields = ["icon"]
@staticmethod
def resolve_learning_units(root: LearningSequence, info, **kwargs):
siblings = None
if hasattr(info.context, "circle_descendants"):
circle_descendants = info.context.circle_descendants
index = circle_descendants.index(root)
siblings = circle_descendants[index + 1 :]
if not siblings:
siblings = root.get_siblings().live().specific()
learning_units = []
for sibling in siblings:
if sibling.specific_class == LearningSequence:
# finished with this sequence
break
if sibling.specific_class == LearningUnit:
learning_units.append(sibling.specific)
return learning_units
class CircleObjectType(DjangoObjectType):
learning_sequences = graphene.List(LearningSequenceObjectType)
class Meta:
model = Circle
interfaces = (CoursePageInterface,)
fields = [
"description",
"goals",
]
@staticmethod
def resolve_learning_sequences(root: Circle, info, **kwargs):
circle_descendants = None
if hasattr(info.context, "learning_path_descendants"):
children = info.context.learning_path_descendants
circle_start_index = children.index(root)
next_circle_index = find_first_index(
children[circle_start_index + 1 :],
pred=lambda child: child.specific_class == Circle,
)
circle_descendants = children[circle_start_index + 1 : next_circle_index]
if not circle_descendants:
circle_descendants = list(root.get_descendants().live().specific())
# store flattened descendents to improve performance (no need for db queries)
info.context.circle_descendants = list(circle_descendants)
return [
descendant.specific
for descendant in circle_descendants
if descendant.specific_class == LearningSequence
]
class TopicObjectType(DjangoObjectType):
circles = graphene.List(CircleObjectType)
class Meta:
model = Topic
interfaces = (CoursePageInterface,)
fields = [
"is_visible",
]
@staticmethod
def resolve_circles(root: LearningPath, info, **kwargs):
siblings = None
if hasattr(info.context, "learning_path_descendants"):
learning_path_descendants = info.context.learning_path_descendants
index = learning_path_descendants.index(root)
siblings = learning_path_descendants[index + 1 :]
if not siblings:
siblings = root.get_next_siblings().live().specific()
circles = []
for sibling in siblings:
if sibling.specific_class == Topic:
# finished with this topic
break
if sibling.specific_class == Circle:
circles.append(sibling.specific)
return circles
class LearningPathObjectType(DjangoObjectType):
topics = graphene.List(TopicObjectType)
class Meta:
model = LearningPath
interfaces = (CoursePageInterface,)
@staticmethod
def resolve_topics(root: LearningPath, info, **kwargs):
learning_path_descendants_qs = root.get_descendants().live().specific()
# store flattened descendents to improve performance (no need for db queries)
info.context.learning_path_descendants = list(learning_path_descendants_qs)
return [
descendant.specific
for descendant in learning_path_descendants_qs
if descendant.specific_class == Topic
]

View File

@ -1,2 +0,0 @@
def get_wagtail_type(obj):
return obj._meta.app_label + "." + type(obj).__name__