From da03b407bba828db80ebbd768e5f8dc2cedeef30 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 17 Jul 2023 17:51:49 +0200 Subject: [PATCH 1/2] Add GraphQL server code for learning path --- scripts/count_queries.py | 9 +- scripts/graphene_count_queries.py | 75 +++++ .../vbv_lernwelt/assignment/graphql/types.py | 2 +- server/vbv_lernwelt/core/schema.py | 7 +- .../vbv_lernwelt/core/serializer_helpers.py | 4 +- server/vbv_lernwelt/core/utils.py | 20 +- .../vbv_lernwelt/course/graphql/interfaces.py | 19 ++ server/vbv_lernwelt/course/graphql/queries.py | 9 +- server/vbv_lernwelt/course/graphql/types.py | 8 +- server/vbv_lernwelt/course/schema.py | 46 --- server/vbv_lernwelt/course/services.py | 6 +- server/vbv_lernwelt/course/views.py | 5 +- .../vbv_lernwelt/learnpath/graphql/queries.py | 81 +++++ .../vbv_lernwelt/learnpath/graphql/types.py | 282 ++++++++++++++++++ server/vbv_lernwelt/learnpath/utils.py | 2 - 15 files changed, 496 insertions(+), 79 deletions(-) create mode 100644 scripts/graphene_count_queries.py create mode 100644 server/vbv_lernwelt/course/graphql/interfaces.py delete mode 100644 server/vbv_lernwelt/course/schema.py create mode 100644 server/vbv_lernwelt/learnpath/graphql/queries.py create mode 100644 server/vbv_lernwelt/learnpath/graphql/types.py delete mode 100644 server/vbv_lernwelt/learnpath/utils.py diff --git a/scripts/count_queries.py b/scripts/count_queries.py index 905f001e..484787f1 100644 --- a/scripts/count_queries.py +++ b/scripts/count_queries.py @@ -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() diff --git a/scripts/graphene_count_queries.py b/scripts/graphene_count_queries.py new file mode 100644 index 00000000..94b85982 --- /dev/null +++ b/scripts/graphene_count_queries.py @@ -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() diff --git a/server/vbv_lernwelt/assignment/graphql/types.py b/server/vbv_lernwelt/assignment/graphql/types.py index d0f4aa6f..3ba46191 100644 --- a/server/vbv_lernwelt/assignment/graphql/types.py +++ b/server/vbv_lernwelt/assignment/graphql/types.py @@ -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): diff --git a/server/vbv_lernwelt/core/schema.py b/server/vbv_lernwelt/core/schema.py index ed89136d..d389be78 100644 --- a/server/vbv_lernwelt/core/schema.py +++ b/server/vbv_lernwelt/core/schema.py @@ -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 diff --git a/server/vbv_lernwelt/core/serializer_helpers.py b/server/vbv_lernwelt/core/serializer_helpers.py index cc82eef4..781a1dda 100644 --- a/server/vbv_lernwelt/core/serializer_helpers.py +++ b/server/vbv_lernwelt/core/serializer_helpers.py @@ -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 diff --git a/server/vbv_lernwelt/core/utils.py b/server/vbv_lernwelt/core/utils.py index 67b85f3e..8291ac53 100644 --- a/server/vbv_lernwelt/core/utils.py +++ b/server/vbv_lernwelt/core/utils.py @@ -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__ diff --git a/server/vbv_lernwelt/course/graphql/interfaces.py b/server/vbv_lernwelt/course/graphql/interfaces.py new file mode 100644 index 00000000..10dfc250 --- /dev/null +++ b/server/vbv_lernwelt/course/graphql/interfaces.py @@ -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) diff --git a/server/vbv_lernwelt/course/graphql/queries.py b/server/vbv_lernwelt/course/graphql/queries.py index be7b075b..c0473cf1 100644 --- a/server/vbv_lernwelt/course/graphql/queries.py +++ b/server/vbv_lernwelt/course/graphql/queries.py @@ -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") diff --git a/server/vbv_lernwelt/course/graphql/types.py b/server/vbv_lernwelt/course/graphql/types.py index d9a94ad3..33f268d9 100644 --- a/server/vbv_lernwelt/course/graphql/types.py +++ b/server/vbv_lernwelt/course/graphql/types.py @@ -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() diff --git a/server/vbv_lernwelt/course/schema.py b/server/vbv_lernwelt/course/schema.py deleted file mode 100644 index 5393f005..00000000 --- a/server/vbv_lernwelt/course/schema.py +++ /dev/null @@ -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") diff --git a/server/vbv_lernwelt/course/services.py b/server/vbv_lernwelt/course/services.py index b26dd882..34c71574 100644 --- a/server/vbv_lernwelt/course/services.py +++ b/server/vbv_lernwelt/course/services.py @@ -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 diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index 1310b731..fb4dcadc 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -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, diff --git a/server/vbv_lernwelt/learnpath/graphql/queries.py b/server/vbv_lernwelt/learnpath/graphql/queries.py new file mode 100644 index 00000000..188300de --- /dev/null +++ b/server/vbv_lernwelt/learnpath/graphql/queries.py @@ -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 diff --git a/server/vbv_lernwelt/learnpath/graphql/types.py b/server/vbv_lernwelt/learnpath/graphql/types.py new file mode 100644 index 00000000..4ce4b41e --- /dev/null +++ b/server/vbv_lernwelt/learnpath/graphql/types.py @@ -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 + ] diff --git a/server/vbv_lernwelt/learnpath/utils.py b/server/vbv_lernwelt/learnpath/utils.py deleted file mode 100644 index f5cbf0ad..00000000 --- a/server/vbv_lernwelt/learnpath/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def get_wagtail_type(obj): - return obj._meta.app_label + "." + type(obj).__name__ From eb50fb0b59af2218f6ee6c0e96663beb6dab4492 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 18 Jul 2023 09:37:42 +0200 Subject: [PATCH 2/2] Update generated types --- README.md | 21 ++- client/src/gql/graphql.ts | 268 ++++++++++++++++++++++++++-- client/src/gql/schema.graphql | 319 ++++++++++++++++++++++++++++------ client/src/gql/typenames.ts | 20 ++- 4 files changed, 557 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 49646a5c..820739b1 100644 --- a/README.md +++ b/README.md @@ -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... diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index f0e7a382..696d5bfa 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -138,6 +138,20 @@ export type AttendanceUserType = { user_id: Scalars['UUID']['output']; }; +export type CircleObjectType = CoursePageInterface & { + __typename?: 'CircleObjectType'; + content_type?: Maybe; + description: Scalars['String']['output']; + frontend_url?: Maybe; + goals: Scalars['String']['output']; + id?: Maybe; + learning_sequences?: Maybe>>; + live?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + /** 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; + slug: Scalars['String']['output']; + title: Scalars['String']['output']; +}; + export type CoursePageInterface = { content_type?: Maybe; frontend_url?: Maybe; @@ -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; - 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; /** 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; + content_assignment: AssignmentObjectType; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentAttendanceCourseObjectType = LearningContentInterface & { + __typename?: 'LearningContentAttendanceCourseObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentDocumentListObjectType = LearningContentInterface & { + __typename?: 'LearningContentDocumentListObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentFeedbackObjectType = LearningContentInterface & { + __typename?: 'LearningContentFeedbackObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentInterface = { + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentLearningModuleObjectType = LearningContentInterface & { + __typename?: 'LearningContentLearningModuleObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentMediaLibraryObjectType = LearningContentInterface & { + __typename?: 'LearningContentMediaLibraryObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentPlaceholderObjectType = LearningContentInterface & { + __typename?: 'LearningContentPlaceholderObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentRichTextObjectType = LearningContentInterface & { + __typename?: 'LearningContentRichTextObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentTestObjectType = LearningContentInterface & { + __typename?: 'LearningContentTestObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningContentVideoObjectType = LearningContentInterface & { + __typename?: 'LearningContentVideoObjectType'; + content?: Maybe; + content_type?: Maybe; + description?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + minutes?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningPathObjectType = CoursePageInterface & { + __typename?: 'LearningPathObjectType'; content_type?: Maybe; 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; title?: Maybe; + topics?: Maybe>>; translation_key?: Maybe; url_path: Scalars['String']['output']; }; +export type LearningSequenceObjectType = CoursePageInterface & { + __typename?: 'LearningSequenceObjectType'; + content_type?: Maybe; + frontend_url?: Maybe; + icon: Scalars['String']['output']; + id?: Maybe; + learning_units?: Maybe>>; + live?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +export type LearningUnitObjectType = CoursePageInterface & { + __typename?: 'LearningUnitObjectType'; + content_type?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + learning_contents?: Maybe>>; + live?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + +/** An enumeration. */ +export type LearnpathLearningContentAssignmentAssignmentTypeChoices = + /** CASEWORK */ + | 'CASEWORK' + /** PREP_ASSIGNMENT */ + | 'PREP_ASSIGNMENT' + /** REFLECTION */ + | 'REFLECTION'; + export type Mutation = { __typename?: 'Mutation'; send_feedback?: Maybe; @@ -266,8 +471,20 @@ export type Query = { __typename?: 'Query'; assignment?: Maybe; assignment_completion?: Maybe; - course?: Maybe; + circle?: Maybe; + course?: Maybe; course_session_attendance_course?: Maybe; + learning_content_assignment?: Maybe; + learning_content_attendance_course?: Maybe; + learning_content_document_list?: Maybe; + learning_content_feedback?: Maybe; + learning_content_learning_module?: Maybe; + learning_content_media_library?: Maybe; + learning_content_placeholder?: Maybe; + learning_content_rich_text?: Maybe; + learning_content_test?: Maybe; + learning_content_video?: Maybe; + learning_path?: Maybe; }; @@ -285,6 +502,12 @@ export type QueryAssignmentCompletionArgs = { }; +export type QueryCircleArgs = { + id?: InputMaybe; + slug?: InputMaybe; +}; + + export type QueryCourseArgs = { id?: InputMaybe; }; @@ -295,6 +518,12 @@ export type QueryCourseSessionAttendanceCourseArgs = { id: Scalars['ID']['input']; }; + +export type QueryLearningPathArgs = { + id?: InputMaybe; + slug?: InputMaybe; +}; + export type SendFeedbackInput = { clientMutationId?: InputMaybe; course_session: Scalars['Int']['input']; @@ -310,6 +539,19 @@ export type SendFeedbackPayload = { feedback_response?: Maybe; }; +export type TopicObjectType = CoursePageInterface & { + __typename?: 'TopicObjectType'; + circles?: Maybe>>; + content_type?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + is_visible: Scalars['Boolean']['output']; + live?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + export type UserType = { __typename?: 'UserType'; avatar_url: Scalars['String']['output']; @@ -358,7 +600,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; diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index 15cea857..d96790bf 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -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""" diff --git a/client/src/gql/typenames.ts b/client/src/gql/typenames.ts index e4fd7627..fb87f08b 100644 --- a/client/src/gql/typenames.ts +++ b/client/src/gql/typenames.ts @@ -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";