From a95974c54f59cb3b4ca3a1b54913744cb7767f2a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 1 Sep 2023 11:51:44 +0200 Subject: [PATCH] Extend graphql scheme for KompetenzNavi --- client/src/gql/graphql.ts | 52 ++++- client/src/gql/schema.graphql | 205 +++++++++++------- client/src/gql/typenames.ts | 1 + scripts/competenceCertificate.graphql | 36 ++- .../competence/graphql/queries.py | 39 +++- .../vbv_lernwelt/competence/graphql/types.py | 17 +- server/vbv_lernwelt/competence/models.py | 20 ++ server/vbv_lernwelt/core/schema.py | 4 +- server/vbv_lernwelt/course/graphql/types.py | 54 +++-- .../vbv_lernwelt/learnpath/graphql/queries.py | 24 +- 10 files changed, 330 insertions(+), 122 deletions(-) diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index a23ed2ab..4f84135c 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -162,6 +162,42 @@ export type CircleObjectType = CoursePageInterface & { translation_key?: Maybe; }; +export type CompetenceCertificateListObjectType = CoursePageInterface & { + __typename?: 'CompetenceCertificateListObjectType'; + circle?: Maybe; + competence_certificates?: Maybe>>; + content_type?: Maybe; + course?: 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; + locked_by?: Maybe; + numchild: Scalars['Int']['output']; + owner?: Maybe; + path: Scalars['String']['output']; + /** Die informative Beschreibung, dargestellt in Suchmaschinen-Ergebnissen unter der Überschrift. */ + search_description: Scalars['String']['output']; + /** Der Titel der Seite, dargestellt in Suchmaschinen-Ergebnissen als die verlinkte Überschrift. */ + seo_title: Scalars['String']['output']; + /** 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']; +}; + export type CompetenceCertificateObjectType = CoursePageInterface & { __typename?: 'CompetenceCertificateObjectType'; assignments?: Maybe>>; @@ -532,6 +568,7 @@ export type Query = { assignment_completion?: Maybe; circle?: Maybe; competence_certificate?: Maybe; + competence_certificate_list?: Maybe; course?: Maybe; course_session_attendance_course?: Maybe; learning_content_assignment?: Maybe; @@ -563,13 +600,22 @@ export type QueryAssignmentCompletionArgs = { export type QueryCircleArgs = { - id?: InputMaybe; + id?: InputMaybe; slug?: InputMaybe; }; export type QueryCompetenceCertificateArgs = { id?: InputMaybe; + slug?: InputMaybe; +}; + + +export type QueryCompetenceCertificateListArgs = { + course_id?: InputMaybe; + course_slug?: InputMaybe; + id?: InputMaybe; + slug?: InputMaybe; }; @@ -585,7 +631,9 @@ export type QueryCourseSessionAttendanceCourseArgs = { export type QueryLearningPathArgs = { - id?: InputMaybe; + course_id?: InputMaybe; + course_slug?: InputMaybe; + id?: InputMaybe; slug?: InputMaybe; }; diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index cc80187a..837a5eab 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -1,6 +1,6 @@ type Query { - circle(id: Int, slug: String): CircleObjectType - learning_path(id: Int, slug: String): LearningPathObjectType + learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType + circle(id: ID, slug: String): CircleObjectType learning_content_media_library: LearningContentMediaLibraryObjectType learning_content_assignment: LearningContentAssignmentObjectType learning_content_attendance_course: LearningContentAttendanceCourseObjectType @@ -13,46 +13,12 @@ type Query { learning_content_document_list: LearningContentDocumentListObjectType course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType course(id: Int): CourseObjectType - competence_certificate(id: ID): CompetenceCertificateObjectType + competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType + competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType 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 CircleObjectType implements CoursePageInterface { - description: String! - goals: String! - id: ID - title: String - slug: String - content_type: String - live: Boolean - translation_key: String - frontend_url: String - circle: CircleObjectType - course: CourseObjectType - learning_sequences: [LearningSequenceObjectType] -} - -interface CoursePageInterface { - id: ID - title: String - slug: String - content_type: String - live: Boolean - translation_key: String - frontend_url: String - circle: CircleObjectType - course: CourseObjectType -} - -type CourseObjectType { - id: ID! - title: String! - category_name: String! - slug: String! - learning_path: LearningPathObjectType -} - type LearningPathObjectType implements CoursePageInterface { id: ID path: String! @@ -97,6 +63,83 @@ type LearningPathObjectType implements CoursePageInterface { topics: [TopicObjectType] } +interface CoursePageInterface { + id: ID + title: String + slug: String + content_type: String + live: Boolean + translation_key: String + frontend_url: String + circle: CircleObjectType + course: CourseObjectType +} + +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 + circle: CircleObjectType + course: CourseObjectType + learning_sequences: [LearningSequenceObjectType] +} + +type CourseObjectType { + id: ID! + title: String! + category_name: String! + slug: String! + learning_path: LearningPathObjectType +} + +type LearningSequenceObjectType implements CoursePageInterface { + icon: String! + id: ID + title: String + slug: String + content_type: String + live: Boolean + translation_key: String + frontend_url: String + circle: CircleObjectType + course: CourseObjectType + learning_units: [LearningUnitObjectType] +} + +type LearningUnitObjectType implements CoursePageInterface { + id: ID + title: String + slug: String + content_type: String + live: Boolean + translation_key: String + frontend_url: String + circle: CircleObjectType + course: CourseObjectType + learning_contents: [LearningContentInterface] +} + +interface LearningContentInterface { + id: ID + title: String + slug: String + content_type: String + live: Boolean + translation_key: String + frontend_url: String + circle: CircleObjectType + course: CourseObjectType + minutes: Int + description: String + content: String +} + """ The `DateTime` scalar type represents a DateTime value as specified by @@ -149,48 +192,6 @@ type TopicObjectType implements CoursePageInterface { circles: [CircleObjectType] } -type LearningSequenceObjectType implements CoursePageInterface { - icon: String! - id: ID - title: String - slug: String - content_type: String - live: Boolean - translation_key: String - frontend_url: String - circle: CircleObjectType - course: CourseObjectType - learning_units: [LearningUnitObjectType] -} - -type LearningUnitObjectType implements CoursePageInterface { - id: ID - title: String - slug: String - content_type: String - live: Boolean - translation_key: String - frontend_url: String - circle: CircleObjectType - course: CourseObjectType - learning_contents: [LearningContentInterface] -} - -interface LearningContentInterface { - id: ID - title: String - slug: String - content_type: String - live: Boolean - translation_key: String - frontend_url: String - circle: CircleObjectType - course: CourseObjectType - minutes: Int - description: String - content: String -} - type LearningContentMediaLibraryObjectType implements LearningContentInterface { id: ID title: String @@ -451,6 +452,50 @@ type CompetenceCertificateObjectType implements CoursePageInterface { assignments: [AssignmentObjectType] } +type CompetenceCertificateListObjectType implements CoursePageInterface { + id: ID + path: String! + depth: Int! + numchild: Int! + translation_key: String + live: Boolean + has_unpublished_changes: Boolean! + first_published_at: DateTime + last_published_at: DateTime + go_live_at: DateTime + expire_at: DateTime + expired: Boolean! + locked: Boolean! + locked_at: DateTime + locked_by: UserType + title: String + draft_title: String! + slug: String + content_type: String + url_path: String! + owner: UserType + + """ + Der Titel der Seite, dargestellt in Suchmaschinen-Ergebnissen als die verlinkte Überschrift. + """ + seo_title: String! + + """ + Ob ein Link zu dieser Seite in automatisch generierten Menüs auftaucht. + """ + show_in_menus: Boolean! + + """ + Die informative Beschreibung, dargestellt in Suchmaschinen-Ergebnissen unter der Überschrift. + """ + search_description: String! + latest_revision_created_at: DateTime + frontend_url: String + circle: CircleObjectType + course: CourseObjectType + competence_certificates: [CompetenceCertificateObjectType] +} + type AssignmentCompletionObjectType { id: UUID! created_at: DateTime! diff --git a/client/src/gql/typenames.ts b/client/src/gql/typenames.ts index 51e70750..b6f895a1 100644 --- a/client/src/gql/typenames.ts +++ b/client/src/gql/typenames.ts @@ -10,6 +10,7 @@ export const AttendanceUserStatus = "AttendanceUserStatus"; export const AttendanceUserType = "AttendanceUserType"; export const Boolean = "Boolean"; export const CircleObjectType = "CircleObjectType"; +export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType"; export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"; export const CoreUserLanguageChoices = "CoreUserLanguageChoices"; export const CourseObjectType = "CourseObjectType"; diff --git a/scripts/competenceCertificate.graphql b/scripts/competenceCertificate.graphql index 40e842b2..dba5d385 100644 --- a/scripts/competenceCertificate.graphql +++ b/scripts/competenceCertificate.graphql @@ -1,15 +1,29 @@ +fragment CoursePageFields on CoursePageInterface { + title + id + slug + content_type +} + { - competence_certificate(id:1) { - title, - assignments { - id - title - max_points - learning_content { - title - frontend_url - content_type + competence_certificate_list(course_slug:"test-lehrgang") { + ...CoursePageFields + competence_certificates { + ...CoursePageFields + assignments { + ...CoursePageFields + assignment_type + max_points + learning_content { + title + id + slug + content_type + circle { + ...CoursePageFields + } + } } } } -} \ No newline at end of file +} diff --git a/server/vbv_lernwelt/competence/graphql/queries.py b/server/vbv_lernwelt/competence/graphql/queries.py index 182cf6f1..86c49189 100644 --- a/server/vbv_lernwelt/competence/graphql/queries.py +++ b/server/vbv_lernwelt/competence/graphql/queries.py @@ -1,14 +1,41 @@ import graphene -from vbv_lernwelt.competence.graphql.types import CompetenceCertificateObjectType -from vbv_lernwelt.competence.models import CompetenceCertificate +from vbv_lernwelt.competence.graphql.types import ( + CompetenceCertificateObjectType, + CompetenceCertificateListObjectType, +) +from vbv_lernwelt.competence.models import ( + CompetenceCertificate, + CompetenceCertificateList, +) +from vbv_lernwelt.course.graphql.types import resolve_course_page class CompetenceCertificateQuery(object): competence_certificate = graphene.Field( - CompetenceCertificateObjectType, - id=graphene.ID(), + CompetenceCertificateObjectType, id=graphene.ID(), slug=graphene.String() ) - def resolve_competence_certificate(root, info, id): - return CompetenceCertificate.objects.get(id=id) + competence_certificate_list = graphene.Field( + CompetenceCertificateListObjectType, + id=graphene.ID(), + slug=graphene.String(), + course_id=graphene.ID(), + course_slug=graphene.String(), + ) + + def resolve_competence_certificate(root, info, id=None, slug=None): + return resolve_course_page(CompetenceCertificate, root, info, id=id, slug=slug) + + def resolve_competence_certificate_list( + root, info, id=None, slug=None, course_id=None, course_slug=None + ): + return resolve_course_page( + CompetenceCertificateList, + root, + info, + id=id, + slug=slug, + course_id=course_id, + course_slug=course_slug, + ) diff --git a/server/vbv_lernwelt/competence/graphql/types.py b/server/vbv_lernwelt/competence/graphql/types.py index 1b46c9aa..f12a12d2 100644 --- a/server/vbv_lernwelt/competence/graphql/types.py +++ b/server/vbv_lernwelt/competence/graphql/types.py @@ -2,7 +2,10 @@ from graphene import List from graphene_django import DjangoObjectType from vbv_lernwelt.assignment.graphql.types import AssignmentObjectType -from vbv_lernwelt.competence.models import CompetenceCertificate +from vbv_lernwelt.competence.models import ( + CompetenceCertificate, + CompetenceCertificateList, +) from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface @@ -14,6 +17,16 @@ class CompetenceCertificateObjectType(DjangoObjectType): interfaces = (CoursePageInterface,) fields = ["assignments"] - # resolver for the assignments def resolve_assignments(self, info): return self.assignment_set.all() + + +class CompetenceCertificateListObjectType(DjangoObjectType): + competence_certificates = List(CompetenceCertificateObjectType) + + class Meta: + model = CompetenceCertificateList + interfaces = (CoursePageInterface,) + + def resolve_competence_certificates(self, info): + return CompetenceCertificate.objects.child_of(self) diff --git a/server/vbv_lernwelt/competence/models.py b/server/vbv_lernwelt/competence/models.py index de2328f0..b823ec0b 100644 --- a/server/vbv_lernwelt/competence/models.py +++ b/server/vbv_lernwelt/competence/models.py @@ -30,6 +30,16 @@ class CompetenceCertificateList(CourseBasePage): parent_page_types = ["competence.CompetenceNaviPage"] subpage_types = ["competence.CompetenceCertificate"] + def get_frontend_url(self): + return f"/course/{self.slug.replace('-competencenavi-certificates', '')}/competence/certificates" + + def save(self, clean=True, user=None, log_action=False, **kwargs): + self.slug = find_available_slug( + slugify(f"{self.get_parent().slug}-certificates", allow_unicode=True), + ignore_page_id=self.id, + ) + super(CompetenceCertificateList, self).save(clean, user, log_action, **kwargs) + class CompetenceCertificate(CourseBasePage): """einzelner Kompetenznachweis""" @@ -40,6 +50,16 @@ class CompetenceCertificate(CourseBasePage): course = self.get_course() return f"{course.title} - {self.title}" + def get_frontend_url(self): + return f"/course/{self.slug.replace('-competencenavi-certificates-', '/competence/certificates/')}" + + def save(self, clean=True, user=None, log_action=False, **kwargs): + self.slug = find_available_slug( + slugify(f"{self.get_parent().slug}-{self.title}", allow_unicode=True), + ignore_page_id=self.id, + ) + super(CompetenceCertificate, self).save(clean, user, log_action, **kwargs) + class CompetenceProfilePage(CourseBasePage): serialize_field_names = [ diff --git a/server/vbv_lernwelt/core/schema.py b/server/vbv_lernwelt/core/schema.py index 11d9a045..c336e533 100644 --- a/server/vbv_lernwelt/core/schema.py +++ b/server/vbv_lernwelt/core/schema.py @@ -7,7 +7,7 @@ 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 +from vbv_lernwelt.learnpath.graphql.queries import LearningPathQuery class Query( @@ -15,7 +15,7 @@ class Query( CompetenceCertificateQuery, CourseQuery, CourseSessionQuery, - CircleQuery, + LearningPathQuery, graphene.ObjectType, ): pass diff --git a/server/vbv_lernwelt/course/graphql/types.py b/server/vbv_lernwelt/course/graphql/types.py index 33f268d9..39985bae 100644 --- a/server/vbv_lernwelt/course/graphql/types.py +++ b/server/vbv_lernwelt/course/graphql/types.py @@ -6,7 +6,7 @@ from graphene_django import DjangoObjectType from graphql import GraphQLError from rest_framework.exceptions import PermissionDenied -from vbv_lernwelt.course.models import Course, CourseBasePage +from vbv_lernwelt.course.models import Course, CourseBasePage, CoursePage from vbv_lernwelt.course.permissions import has_course_access from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType @@ -14,23 +14,49 @@ logger = structlog.get_logger(__name__) def resolve_course_page( - page_model_class: Type[CourseBasePage], root, info, id=None, slug=None + page_model_class: Type[CourseBasePage], + root, + info, + id=None, + slug=None, + course_id=None, + course_slug=None, ): try: - if id is None and slug is None: - raise GraphQLError("Either 'id' or 'slug' must be provided.") + if id is None and slug is None and course_id is None and course_slug is None: + raise GraphQLError( + "Either 'id', 'slug', 'course_id' or 'course_slug' must be provided." + ) - page = None - if id is not None: - page = page_model_class.objects.get(pk=id) - elif slug is not None: - page = page_model_class.objects.get(slug=slug) + if id or slug: + # try to fetch page directly + page = None + if id is not None: + page = page_model_class.objects.get(pk=id) + elif slug is not None: + page = page_model_class.objects.get(slug=slug) - if page and not has_course_access( - info.context.user, page.specific.get_course().id - ): - raise PermissionDenied("You do not have access to this course.") - return page + if page and not has_course_access( + info.context.user, page.specific.get_course().id + ): + raise PermissionDenied("You do not have access to this course.") + return page + + if course_id or course_slug: + # fetch first page of type `page_model_class` via course + # makes sense for "Index" pages like "lernpfad" or "kompetenznachweise" etc + course_page = None + if course_id is not None: + course_page = CoursePage.objects.get(pk=course_id) + elif course_slug is not None: + course_page = CoursePage.objects.get(slug=course_slug) + + page = course_page.get_descendants().type(page_model_class).first() + if page and not has_course_access( + info.context.user, page.specific.get_course().id + ): + raise PermissionDenied("You do not have access to this course.") + return page.specific except PermissionDenied as e: raise e diff --git a/server/vbv_lernwelt/learnpath/graphql/queries.py b/server/vbv_lernwelt/learnpath/graphql/queries.py index 188300de..00e9faf8 100644 --- a/server/vbv_lernwelt/learnpath/graphql/queries.py +++ b/server/vbv_lernwelt/learnpath/graphql/queries.py @@ -18,17 +18,31 @@ from vbv_lernwelt.learnpath.graphql.types import ( from vbv_lernwelt.learnpath.models import Circle, LearningPath -class CircleQuery: - circle = graphene.Field(CircleObjectType, id=graphene.Int(), slug=graphene.String()) +class LearningPathQuery: learning_path = graphene.Field( - LearningPathObjectType, id=graphene.Int(), slug=graphene.String() + LearningPathObjectType, + id=graphene.ID(), + slug=graphene.String(), + course_id=graphene.ID(), + course_slug=graphene.String(), ) + circle = graphene.Field(CircleObjectType, id=graphene.ID(), 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) + def resolve_learning_path( + root, info, id=None, slug=None, course_id=None, course_slug=None + ): + return resolve_course_page( + LearningPath, + root, + info, + id=id, + slug=slug, + course_id=course_id, + course_slug=course_slug, + ) # dummy import, so that graphene recognizes the types learning_content_media_library = graphene.Field(