Add GraphQL server code for learning path

This commit is contained in:
Daniel Egger 2023-07-17 17:51:49 +02:00
parent 8e16daa525
commit da03b407bb
15 changed files with 496 additions and 79 deletions

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__