Refactor module server code

This commit is contained in:
Ramon Wenger 2021-04-12 17:18:12 +02:00
parent da2253a73d
commit 85706d73d1
8 changed files with 317 additions and 250 deletions

View File

@ -428,7 +428,7 @@ type CustomQuery {
chapter(id: ID!): ChapterNode
contentBlock(id: ID!): ContentBlockNode
books(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, slug_Icontains: String, slug_In: [String], title: String, title_Icontains: String, title_In: [String]): BookNodeConnection
topics(before: String, after: String, first: Int, last: Int): TopicConnection
topics(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, slug_Icontains: String, slug_In: [String], title: String, title_Icontains: String, title_In: [String]): TopicNodeConnection
modules(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, slug_Icontains: String, slug_In: [String], title: String, title_Icontains: String, title_In: [String]): ModuleNodeConnection
chapters(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ChapterNodeConnection
objectiveGroup(id: ID!): ObjectiveGroupNode
@ -955,16 +955,6 @@ type TeamNode implements Node {
pk: Int
}
type TopicConnection {
pageInfo: PageInfo!
edges: [TopicEdge]!
}
type TopicEdge {
node: TopicNode
cursor: String!
}
type TopicNode implements Node {
title: String!
slug: String!

View File

@ -0,0 +1,44 @@
import graphene
from graphene import relay
from books.schema.nodes import ModuleNode, TopicNode
class NodeConnection(relay.Connection):
"""
Custom connection type, so we don't need to deal with the edges and nodes if we don't want to, while still
adhering to the relay specification
Idea from: https://javascript.plainenglish.io/graphql-pagination-using-edges-vs-nodes-in-connections-f2ddb8edffa0
Always has property called `nodes` which is filled with the Meta node class.
Example:
class TopicConnection(NodeConnection):
class Meta:
node = TopicNode
# will have
# nodes = graphene.List(TopicNode)
"""
class Meta:
abstract = True
def resolve_nodes(self, *args, **kwargs):
return [edge.node for edge in self.edges]
@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
cls.nodes = graphene.List(node)
return super().__init_subclass_with_meta__(
node, name, **options
)
class TopicConnection(NodeConnection):
class Meta:
node = TopicNode
class ModuleConnection(NodeConnection):
class Meta:
node = ModuleNode

View File

@ -4,7 +4,7 @@ from graphene import relay
from api.utils import get_object
from books.models import Chapter
from books.schema.inputs import UserGroupBlockVisibility
from books.schema.queries import ChapterNode
from books.schema.nodes import ChapterNode
from users.models import SchoolClass

View File

@ -8,7 +8,7 @@ from graphql_relay import from_global_id
from api.utils import get_object, get_errors
from books.models import ContentBlock, Chapter, SchoolClass
from books.schema.inputs import ContentBlockInput
from books.schema.queries import ContentBlockNode
from ..nodes import ContentBlockNode
from core.utils import set_hidden_for, set_visible_for
from .utils import handle_content_block, set_user_defined_block_type

View File

@ -5,7 +5,7 @@ from graphene import relay
from api.utils import get_object
from books.models import Module, RecentModule
from books.schema.queries import ModuleNode
from books.schema.nodes import ModuleNode
from users.models import SchoolClass

View File

@ -3,7 +3,7 @@ from graphene import relay
from api.utils import get_object
from books.models import Topic
from books.schema.queries import TopicNode
from books.schema.nodes import TopicNode
class UpdateLastTopic(relay.ClientIDMutation):

View File

@ -0,0 +1,260 @@
import graphene
from django.db.models import Q
from graphene import relay
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from assignments.models import StudentSubmission
from assignments.schema.types import StudentSubmissionNode
from books.models import ContentBlock, Chapter, Module, RecentModule, Topic, Book
from books.utils import are_solutions_enabled_for
from core.logger import get_logger
from notes.models import ContentBlockBookmark, ChapterBookmark, ModuleBookmark
from notes.schema import ContentBlockBookmarkNode, ChapterBookmarkNode, ModuleBookmarkNode
from rooms.models import ModuleRoomSlug
from surveys.models import Answer
from surveys.schema import AnswerNode
logger = get_logger(__name__)
class TextBlockNode(graphene.ObjectType):
text = graphene.String()
def resolve_text(root, info, **kwargs):
return root['value']['text']
class ContentNode(graphene.Union):
class Meta:
types = (TextBlockNode,)
@classmethod
def resolve_type(cls, instance, info):
logger.info(instance)
if instance['type'] == 'text_block':
return TextBlockNode
class ContentBlockNode(DjangoObjectType):
mine = graphene.Boolean()
bookmarks = graphene.List(ContentBlockBookmarkNode)
# contents = graphene.List(ContentNode)
class Meta:
model = ContentBlock
only_fields = [
'slug', 'title', 'type', 'contents', 'hidden_for', 'visible_for', 'user_created'
]
filter_fields = [
'slug', 'title',
]
interfaces = (relay.Node,)
def resolve_mine(self, info, **kwargs):
return self.owner is not None and self.owner.pk == info.context.user.pk
def resolve_contents(self, info, **kwargs):
updated_stream_data = []
for content in self.contents.stream_data:
# only show solutions to teachers and students for whom their teachers have them enabled
if content['type'] == 'solution' \
and not (are_solutions_enabled_for(info.context.user, self.module) or info.context.user.is_teacher()):
logger.debug('Solution is hidden for this user')
continue
if content['type'] == 'content_list_item':
for index, list_block in enumerate(content['value']):
content['value'][index] = process_module_room_slug_block(list_block)
content = process_module_room_slug_block(content)
updated_stream_data.append(content)
self.contents.stream_data = updated_stream_data
return self.contents
def resolve_bookmarks(self, info, **kwargs):
return ContentBlockBookmark.objects.filter(
user=info.context.user,
content_block=self
)
class ChapterNode(DjangoObjectType):
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
bookmark = graphene.Field(ChapterBookmarkNode)
class Meta:
model = Chapter
only_fields = [
'slug', 'title', 'description', 'title_hidden_for', 'description_hidden_for'
]
filter_fields = [
'slug', 'title',
]
interfaces = (relay.Node,)
def resolve_content_blocks(self, info, **kwargs):
user = info.context.user
school_classes = user.school_classes.values_list('pk')
by_parent = ContentBlock.get_by_parent(self) \
.prefetch_related('visible_for') \
.prefetch_related('hidden_for')
# don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret
default_blocks = Q(user_created=False)
owned_by_user = Q(user_created=True, owner=user)
teacher_created_and_visible = Q(Q(user_created=True) & Q(visible_for__in=school_classes))
if user.has_perm('users.can_manage_school_class_content'): # teacher
return by_parent.filter(default_blocks | owned_by_user | teacher_created_and_visible).distinct()
else: # student
return by_parent.filter(default_blocks | teacher_created_and_visible).distinct()
def resolve_bookmark(self, info, **kwargs):
return ChapterBookmark.objects.filter(
user=info.context.user,
chapter=self
).first()
class ModuleNode(DjangoObjectType):
pk = graphene.Int()
chapters = DjangoFilterConnectionField(ChapterNode)
topic = graphene.Field('books.schema.queries.TopicNode')
hero_image = graphene.String()
solutions_enabled = graphene.Boolean()
bookmark = graphene.Field(ModuleBookmarkNode)
my_submissions = DjangoFilterConnectionField(StudentSubmissionNode)
my_answers = DjangoFilterConnectionField(AnswerNode)
my_content_bookmarks = DjangoFilterConnectionField(ContentBlockBookmarkNode)
my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode)
class Meta:
model = Module
only_fields = [
'slug', 'title', 'meta_title', 'teaser', 'intro', 'objective_groups', 'assignments', 'hero_image', 'topic'
]
filter_fields = {
'slug': ['exact', 'icontains', 'in'],
'title': ['exact', 'icontains', 'in'],
}
interfaces = (relay.Node,)
def resolve_pk(self, info, **kwargs):
return self.id
def resolve_hero_image(self, info, **kwargs):
if self.hero_image:
return self.hero_image.file.url
def resolve_chapters(self, info, **kwargs):
return Chapter.get_by_parent(self)
def resolve_topic(self, info, **kwargs):
return self.get_parent().specific
def resolve_solutions_enabled(self, info, **kwargs):
school_class = info.context.user.selected_class()
return self.solutions_enabled_for.filter(pk=school_class.pk).exists() if school_class is not None else False
def resolve_bookmark(self, info, **kwags):
return ModuleBookmark.objects.filter(
user=info.context.user,
module=self
).first()
def resolve_my_submissions(self, info, **kwargs):
user = info.context.user
return StudentSubmission.objects.filter(student=user, assignment__module=self)
# we want:
# StudentSubmission
def resolve_my_answers(self, info, **kwargs):
user = info.context.user
return Answer.objects.filter(owner=user, survey__module=self)
# Survey
def resolve_my_content_bookmarks(self, info, **kwargs):
user = info.context.user
content_blocks = ContentBlock.objects.live().descendant_of(self)
return ContentBlockBookmark.objects.filter(content_block__in=content_blocks, user=user)
# Bookmark Text
# Bookmark Image etc
# Bookmark Other
# Note
#
def resolve_my_chapter_bookmarks(self, info, **kwargs):
user = info.context.user
chapters = Chapter.objects.live().descendant_of(self)
return ChapterBookmark.objects.filter(chapter__in=chapters, user=user)
def resolve_objective_groups(self, root, **kwargs):
return self.objective_groups.all() \
.prefetch_related('hidden_for')
class RecentModuleNode(DjangoObjectType):
class Meta:
model = RecentModule
interfaces = (relay.Node,)
class TopicNode(DjangoObjectType):
pk = graphene.Int()
modules = DjangoFilterConnectionField(ModuleNode)
class Meta:
model = Topic
only_fields = [
'slug', 'title', 'teaser', 'description', 'vimeo_id', 'order', 'instructions'
]
filter_fields = {
'slug': ['exact', 'icontains', 'in'],
'title': ['exact', 'icontains', 'in'],
}
interfaces = (relay.Node,)
def resolve_pk(self, *args, **kwargs):
return self.id
def resolve_modules(self, *args, **kwargs):
return Module.get_by_parent(self)
class BookNode(DjangoObjectType):
pk = graphene.Int()
topics = DjangoFilterConnectionField(TopicNode)
class Meta:
model = Book
only_fields = [
'slug', 'title',
]
filter_fields = {
'slug': ['exact', 'icontains', 'in'],
'title': ['exact', 'icontains', 'in'],
}
interfaces = (relay.Node,)
def resolve_pk(self, *args, **kwargs):
return self.id
def resolve_topics(self, *args, **kwargs):
return Topic.get_by_parent(self)
def process_module_room_slug_block(content):
if content['type'] == 'module_room_slug':
try:
module_room_slug = ModuleRoomSlug.objects.get(title=content['value']['title'])
content['value'] = {
'title': content['value']['title'],
'slug': module_room_slug.slug
}
except ModuleRoomSlug.DoesNotExist:
pass
return content

View File

@ -1,244 +1,15 @@
import graphene
from django.db.models import Q
from graphene import relay
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from api.utils import get_object
from assignments.models import StudentSubmission
from assignments.schema.types import StudentSubmissionNode
from books.utils import are_solutions_enabled_for
from notes.models import ContentBlockBookmark, ChapterBookmark, ModuleBookmark
from notes.schema import ContentBlockBookmarkNode, ChapterBookmarkNode, ModuleBookmarkNode
from rooms.models import ModuleRoomSlug
from surveys.models import Answer
from surveys.schema import AnswerNode
from ..models import Book, Topic, Module, Chapter, ContentBlock, RecentModule
from core.logger import get_logger
from ..models import Book, Topic, Module, Chapter
from .nodes import ContentBlockNode, ChapterNode, ModuleNode, TopicNode, BookNode
from .connections import TopicConnection, ModuleConnection
def process_module_room_slug_block(content):
if content['type'] == 'module_room_slug':
try:
module_room_slug = ModuleRoomSlug.objects.get(title=content['value']['title'])
content['value'] = {
'title': content['value']['title'],
'slug': module_room_slug.slug
}
except ModuleRoomSlug.DoesNotExist:
pass
return content
class ContentBlockNode(DjangoObjectType):
mine = graphene.Boolean()
bookmarks = graphene.List(ContentBlockBookmarkNode)
class Meta:
model = ContentBlock
only_fields = [
'slug', 'title', 'type', 'contents', 'hidden_for', 'visible_for', 'user_created'
]
filter_fields = [
'slug', 'title',
]
interfaces = (relay.Node,)
def resolve_mine(self, info, **kwargs):
return self.owner is not None and self.owner.pk == info.context.user.pk
def resolve_contents(self, info, **kwargs):
updated_stream_data = []
for content in self.contents.stream_data:
# only show solutions to teachers and students for whom their teachers have them enabled
if content['type'] == 'solution' and not (are_solutions_enabled_for(info.context.user, self.module) or info.context.user.is_teacher()):
continue
if content['type'] == 'content_list_item':
for index, list_block in enumerate(content['value']):
content['value'][index] = process_module_room_slug_block(list_block)
content = process_module_room_slug_block(content)
updated_stream_data.append(content)
self.contents.stream_data = updated_stream_data
return self.contents
def resolve_bookmarks(self, info, **kwargs):
return ContentBlockBookmark.objects.filter(
user=info.context.user,
content_block=self
)
class ChapterNode(DjangoObjectType):
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
bookmark = graphene.Field(ChapterBookmarkNode)
class Meta:
model = Chapter
only_fields = [
'slug', 'title', 'description', 'title_hidden_for', 'description_hidden_for'
]
filter_fields = [
'slug', 'title',
]
interfaces = (relay.Node,)
def resolve_content_blocks(self, info, **kwargs):
user = info.context.user
school_classes = user.school_classes.values_list('pk')
by_parent = ContentBlock.get_by_parent(self) \
.prefetch_related('visible_for') \
.prefetch_related('hidden_for')
# don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret
default_blocks = Q(user_created=False)
owned_by_user = Q(user_created=True, owner=user)
teacher_created_and_visible = Q(Q(user_created=True) & Q(visible_for__in=school_classes))
if user.has_perm('users.can_manage_school_class_content'): # teacher
return by_parent.filter(default_blocks | owned_by_user | teacher_created_and_visible).distinct()
else: # student
return by_parent.filter(default_blocks | teacher_created_and_visible).distinct()
def resolve_bookmark(self, info, **kwags):
return ChapterBookmark.objects.filter(
user=info.context.user,
chapter=self
).first()
class ModuleNode(DjangoObjectType):
pk = graphene.Int()
chapters = DjangoFilterConnectionField(ChapterNode)
topic = graphene.Field('books.schema.queries.TopicNode')
hero_image = graphene.String()
solutions_enabled = graphene.Boolean()
bookmark = graphene.Field(ModuleBookmarkNode)
my_submissions = DjangoFilterConnectionField(StudentSubmissionNode)
my_answers = DjangoFilterConnectionField(AnswerNode)
my_content_bookmarks = DjangoFilterConnectionField(ContentBlockBookmarkNode)
my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode)
class Meta:
model = Module
only_fields = [
'slug', 'title', 'meta_title', 'teaser', 'intro', 'objective_groups', 'assignments', 'hero_image', 'topic'
]
filter_fields = {
'slug': ['exact', 'icontains', 'in'],
'title': ['exact', 'icontains', 'in'],
}
interfaces = (relay.Node,)
def resolve_pk(self, info, **kwargs):
return self.id
def resolve_hero_image(self, info, **kwargs):
if self.hero_image:
return self.hero_image.file.url
def resolve_chapters(self, info, **kwargs):
return Chapter.get_by_parent(self)
def resolve_topic(self, info, **kwargs):
return self.get_parent().specific
def resolve_solutions_enabled(self, info, **kwargs):
school_class = info.context.user.selected_class()
return self.solutions_enabled_for.filter(pk=school_class.pk).exists() if school_class is not None else False
def resolve_bookmark(self, info, **kwags):
return ModuleBookmark.objects.filter(
user=info.context.user,
module=self
).first()
def resolve_my_submissions(self, info, **kwargs):
user = info.context.user
return StudentSubmission.objects.filter(student=user, assignment__module=self)
# we want:
# StudentSubmission
def resolve_my_answers(self, info, **kwargs):
user = info.context.user
return Answer.objects.filter(owner=user, survey__module=self)
# Survey
def resolve_my_content_bookmarks(self, info, **kwargs):
user = info.context.user
content_blocks = ContentBlock.objects.live().descendant_of(self)
return ContentBlockBookmark.objects.filter(content_block__in=content_blocks, user=user)
# Bookmark Text
# Bookmark Image etc
# Bookmark Other
# Note
#
def resolve_my_chapter_bookmarks(self, info, **kwargs):
user = info.context.user
chapters = Chapter.objects.live().descendant_of(self)
return ChapterBookmark.objects.filter(chapter__in=chapters, user=user)
def resolve_objective_groups(self, root, **kwargs):
return self.objective_groups.all() \
.prefetch_related('hidden_for')
class RecentModuleNode(DjangoObjectType):
class Meta:
model = RecentModule
interfaces = (relay.Node,)
class TopicNode(DjangoObjectType):
pk = graphene.Int()
modules = DjangoFilterConnectionField(ModuleNode)
class Meta:
model = Topic
only_fields = [
'slug', 'title', 'teaser', 'description', 'vimeo_id', 'order', 'instructions'
]
filter_fields = {
'slug': ['exact', 'icontains', 'in'],
'title': ['exact', 'icontains', 'in'],
}
interfaces = (relay.Node,)
def resolve_pk(self, *args, **kwargs):
return self.id
def resolve_modules(self, *args, **kwargs):
return Module.get_by_parent(self)
class BookNode(DjangoObjectType):
pk = graphene.Int()
topics = DjangoFilterConnectionField(TopicNode)
class Meta:
model = Book
only_fields = [
'slug', 'title',
]
filter_fields = {
'slug': ['exact', 'icontains', 'in'],
'title': ['exact', 'icontains', 'in'],
}
interfaces = (relay.Node,)
def resolve_pk(self, *args, **kwargs):
return self.id
def resolve_topics(self, *args, **kwargs):
return Topic.get_by_parent(self)
class TopicConnection(relay.Connection):
class Meta:
node = TopicNode
logger = get_logger(__name__)
class BookQuery(object):
@ -250,7 +21,9 @@ class BookQuery(object):
content_block = relay.Node.Field(ContentBlockNode)
books = DjangoFilterConnectionField(BookNode)
topics = relay.ConnectionField(TopicConnection)
topics = DjangoFilterConnectionField(TopicNode)
# topics = relay.ConnectionField(TopicConnection)
# modules = relay.ConnectionField(ModuleConnection)
modules = DjangoFilterConnectionField(ModuleNode)
chapters = DjangoFilterConnectionField(ChapterNode)