diff --git a/server/assignments/models.py b/server/assignments/models.py index 42bdb32e..2aa6237f 100644 --- a/server/assignments/models.py +++ b/server/assignments/models.py @@ -9,11 +9,12 @@ from wagtail.search import index from core.constants import DEFAULT_RICH_TEXT_FEATURES from django.utils.translation import gettext_lazy as _ -from modelcluster.fields import ParentalKey + +from core.mixins import GraphqlNodeMixin @register_snippet -class Assignment(index.Indexed, TimeStampedModel): +class Assignment(index.Indexed, TimeStampedModel, GraphqlNodeMixin): title = models.CharField(max_length=255) assignment = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES) solution = RichTextField(null=True, blank=True, features=DEFAULT_RICH_TEXT_FEATURES) @@ -27,6 +28,10 @@ class Assignment(index.Indexed, TimeStampedModel): user_created = models.BooleanField(default=False) taskbase_id = models.CharField(max_length=255, null=True, blank=True) + @property + def route(self): + return f"module/{self.module.slug}#{self.graphql_id}" + search_fields = [ index.AutocompleteField("title"), index.AutocompleteField("assignment"), diff --git a/server/assignments/schema/types.py b/server/assignments/schema/types.py index 2ac7814d..84da9071 100644 --- a/server/assignments/schema/types.py +++ b/server/assignments/schema/types.py @@ -26,7 +26,7 @@ class StudentSubmissionNode(DjangoObjectType): def resolve_submission_feedback(root: StudentSubmission, info, **kwargs): user = info.context.user - if not hasattr(root, 'submission_feedback'): + if not hasattr(root, "submission_feedback"): return None # teacher path @@ -43,6 +43,7 @@ class StudentSubmissionNode(DjangoObjectType): class AssignmentNode(DjangoObjectType): submission = graphene.Field(StudentSubmissionNode) submissions = graphene.List(StudentSubmissionNode) + path = graphene.String(required=True) class Meta: model = Assignment @@ -50,16 +51,27 @@ class AssignmentNode(DjangoObjectType): interfaces = (relay.Node,) def resolve_submission(self, info, **kwargs): - return self.submissions.filter(student=info.context.user).first() # returns None if it doesn't exist yet + return self.submissions.filter( + student=info.context.user + ).first() # returns None if it doesn't exist yet def resolve_submissions(self, info, **kwargs): user = info.context.user - if user.has_perm('users.can_manage_school_class_content'): - return self.submissions.filter(student__in=user.users_in_active_school_class()).filter(final=True) + if user.has_perm("users.can_manage_school_class_content"): + return self.submissions.filter( + student__in=user.users_in_active_school_class() + ).filter(final=True) return [] def resolve_solution(self, info, **kwargs): - if (info.context.user.is_teacher() or are_solutions_enabled_for(info.context.user, self.module)) and self.solution is not None: + if ( + info.context.user.is_teacher() + or are_solutions_enabled_for(info.context.user, self.module) + ) and self.solution is not None: return self.solution else: return None + + @staticmethod + def resolve_path(root: Assignment, info, **kwargs): + return root.route diff --git a/server/books/models/chapter.py b/server/books/models/chapter.py index 1305fb20..9332d30d 100644 --- a/server/books/models/chapter.py +++ b/server/books/models/chapter.py @@ -38,6 +38,10 @@ class Chapter(StrictHierarchyPage, GraphqlNodeMixin): SchoolClass, related_name="hidden_chapter_descriptions" ) + @property + def route(self): + return f"module/{self.get_parent().slug}#{self.graphql_id}" + def sync_title_visibility(self, school_class_template, school_class_to_sync): if ( self.title_hidden_for.filter(id=school_class_template.id).exists() diff --git a/server/books/models/contentblock.py b/server/books/models/contentblock.py index b8d26a6b..14132123 100644 --- a/server/books/models/contentblock.py +++ b/server/books/models/contentblock.py @@ -161,6 +161,10 @@ class ContentBlock(StrictHierarchyPage, GraphqlNodeMixin): def module(self): return self.get_parent().get_parent().specific + @property + def route(self): # path is probably used by treebeard + return f"module/{self.module.slug}#{self.graphql_id}" + # duplicate all attached Surveys and Assignments def duplicate_attached_entities(self): duplicate_entities_func = duplicate_entities_generator(self.module) diff --git a/server/books/models/module.py b/server/books/models/module.py index 3f374c94..745a85f1 100644 --- a/server/books/models/module.py +++ b/server/books/models/module.py @@ -20,7 +20,9 @@ class ModuleLevel(models.Model): filter_attribute_type = models.CharField( max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT ) - order = models.PositiveIntegerField(null=False, blank=False, default=99, help_text='Order in the Dropdown List') + order = models.PositiveIntegerField( + null=False, blank=False, default=99, help_text="Order in the Dropdown List" + ) class Meta: verbose_name_plural = _("module Levels") @@ -36,7 +38,9 @@ class ModuleCategory(models.Model): filter_attribute_type = models.CharField( max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT ) - order = models.PositiveIntegerField(null=False, blank=False, default=99, help_text='Order in the Dropdown List') + order = models.PositiveIntegerField( + null=False, blank=False, default=99, help_text="Order in the Dropdown List" + ) class Meta: verbose_name = _("module type") @@ -120,6 +124,10 @@ class Module(StrictHierarchyPage): def get_child_ids(self): return self.get_children().values_list("id", flat=True) + @property + def route(self): + return f"module/{self.slug}" + def sync_from_school_class(self, school_class_template, school_class_to_sync): # import here so we don't get a circular import error from books.models import Chapter, ContentBlock diff --git a/server/books/schema/nodes/chapter.py b/server/books/schema/nodes/chapter.py index 5669afc4..83028916 100644 --- a/server/books/schema/nodes/chapter.py +++ b/server/books/schema/nodes/chapter.py @@ -71,8 +71,7 @@ class ChapterNode(DjangoObjectType): @staticmethod def resolve_path(root: Chapter, info, **kwargs): - module = root.get_parent() - return f"module/{module.slug}#{root.graphql_id}" + return root.route @staticmethod def resolve_highlights(root: Chapter, info, **kwargs): diff --git a/server/books/schema/nodes/content.py b/server/books/schema/nodes/content.py index eee3617c..0de7a29a 100644 --- a/server/books/schema/nodes/content.py +++ b/server/books/schema/nodes/content.py @@ -120,8 +120,7 @@ class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin): @staticmethod def resolve_path(root: ContentBlock, info, **kwargs): - module = root.get_parent().get_parent() - return f"module/{module.slug}#{root.graphql_id}" + return root.route @staticmethod def resolve_highlights(root: ContentBlock, info, **kwargs): diff --git a/server/books/schema/nodes/module.py b/server/books/schema/nodes/module.py index 59563938..523d078b 100644 --- a/server/books/schema/nodes/module.py +++ b/server/books/schema/nodes/module.py @@ -58,8 +58,11 @@ class ModuleNode(DjangoObjectType): category = graphene.Field(ModuleCategoryNode) language = graphene.String() highlights = graphene.List("notes.schema.HighlightNode") - my_highlights = graphene.List("notes.schema.HighlightNode") + my_highlights = graphene.List( + graphene.NonNull("notes.schema.HighlightNode"), required=True + ) my_bookmarks = graphene.List("notes.schema.BookmarkNode") + path = graphene.String() def resolve_chapters(self, info, **kwargs): return Chapter.get_by_parent(self) @@ -134,7 +137,7 @@ class ModuleNode(DjangoObjectType): @staticmethod def resolve_my_highlights(root: Module, info, **kwargs): # todo: is this too expensive, query-wise - pages = Page.objects.live().descendant_of(root) + pages = list(Page.objects.live().descendant_of(root)) + [root] highlights = Highlight.objects.filter(user=info.context.user).filter( page__in=pages ) @@ -161,6 +164,10 @@ class ModuleNode(DjangoObjectType): + list(content_block_bookmarks) ) + @staticmethod + def resolve_path(root: Module, info, **kwargs): + return root.route + class RecentModuleNode(DjangoObjectType): class Meta: diff --git a/server/books/schema/nodes/topic.py b/server/books/schema/nodes/topic.py index 8043f0ca..f7c12c3b 100644 --- a/server/books/schema/nodes/topic.py +++ b/server/books/schema/nodes/topic.py @@ -19,7 +19,7 @@ class NotFound(graphene.ObjectType): class TopicNode(DjangoObjectType): pk = graphene.Int() - modules = graphene.List("books.schema.nodes.ModuleNode") + modules = graphene.List(graphene.NonNull("books.schema.nodes.ModuleNode")) highlights = graphene.List("notes.schema.HighlightNode") class Meta: diff --git a/server/notes/schema.py b/server/notes/schema.py index 58384f61..0ff12d6d 100644 --- a/server/notes/schema.py +++ b/server/notes/schema.py @@ -32,6 +32,7 @@ class NoteNode(DjangoObjectType): class ContentBlockBookmarkNode(DjangoObjectType): uuid = graphene.UUID() note = graphene.Field(NoteNode) + path = graphene.String() class Meta: model = ContentBlockBookmark @@ -39,17 +40,27 @@ class ContentBlockBookmarkNode(DjangoObjectType): filter_fields = [] interfaces = (relay.Node,) + @staticmethod + def resolve_path(root: ContentBlockBookmark, info, **kwargs): + return root.content_block.route + class ModuleBookmarkNode(DjangoObjectType): note = graphene.Field(NoteNode) + path = graphene.String() class Meta: model = ModuleBookmark fields = "__all__" + @staticmethod + def resolve_path(root: ModuleBookmark, info, **kwargs): + return root.module.route + class ChapterBookmarkNode(DjangoObjectType): note = graphene.Field(NoteNode) + path = graphene.String() class Meta: model = ChapterBookmark @@ -57,6 +68,10 @@ class ChapterBookmarkNode(DjangoObjectType): filter_fields = [] interfaces = (relay.Node,) + @staticmethod + def resolve_path(root: ChapterBookmark, info, **kwargs): + return root.chapter.route + class InstrumentBookmarkNode(DjangoObjectType): uuid = graphene.UUID() diff --git a/server/schema.graphql b/server/schema.graphql index 53051102..52bc1506 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -92,6 +92,7 @@ type SurveyNode implements Node { answers(offset: Int, before: String, after: String, first: Int, last: Int): AnswerNodeConnection! pk: Int answer: AnswerNode + path: String! } type ModuleNode implements ModuleInterface { @@ -130,8 +131,9 @@ type ModuleNode implements ModuleInterface { snapshots: [SnapshotNode] language: String highlights: [HighlightNode] - myHighlights: [HighlightNode] + myHighlights: [HighlightNode!]! myBookmarks: [BookmarkNode] + path: String } interface ModuleInterface { @@ -161,7 +163,7 @@ type TopicNode implements Node { """The ID of the object""" id: ID! pk: Int - modules: [ModuleNode] + modules: [ModuleNode!] highlights: [HighlightNode] } @@ -360,6 +362,7 @@ type ContentBlockBookmarkNode implements Node { note: NoteNode uuid: UUID contentBlock: ContentBlockNode! + path: String } type NoteNode implements Node { @@ -379,6 +382,7 @@ type ModuleBookmarkNode { user: PrivateUserNode! note: NoteNode module: ModuleNode! + path: String } type ChapterBookmarkNode implements Node { @@ -387,6 +391,7 @@ type ChapterBookmarkNode implements Node { user: PrivateUserNode! note: NoteNode chapter: ChapterNode! + path: String } type ChapterNode implements Node & ChapterInterface { @@ -499,6 +504,7 @@ type AssignmentNode implements Node { taskbaseId: String submissions: [StudentSubmissionNode] submission: StudentSubmissionNode + path: String! } """ @@ -963,7 +969,7 @@ type InstrumentNodeEdge { } type ActivityNode { - topics: [TopicNode] + topics: [TopicNode!] } """Debugging information for the current query.""" diff --git a/server/surveys/models.py b/server/surveys/models.py index 3e157fc3..e0a1e25e 100644 --- a/server/surveys/models.py +++ b/server/surveys/models.py @@ -5,9 +5,11 @@ from wagtail.snippets.models import register_snippet from wagtail.search import index from modelcluster.fields import ParentalKey +from core.mixins import GraphqlNodeMixin + @register_snippet -class Survey(models.Model, index.Indexed): +class Survey(models.Model, index.Indexed, GraphqlNodeMixin): title = models.CharField(max_length=255) module = models.ForeignKey( "books.Module", @@ -18,6 +20,10 @@ class Survey(models.Model, index.Indexed): ) data = JSONField() + @property + def route(self): + return f"module/{self.module.slug}#{self.graphql_id}" + search_fields = [ index.AutocompleteField("title"), index.AutocompleteField("module__meta_title"), diff --git a/server/surveys/schema.py b/server/surveys/schema.py index 9e5cdc37..f67efe45 100644 --- a/server/surveys/schema.py +++ b/server/surveys/schema.py @@ -23,6 +23,7 @@ class AnswerNode(DjangoObjectType): class SurveyNode(DjangoObjectType): pk = graphene.Int() answer = graphene.Field(AnswerNode) + path = graphene.String(required=True) class Meta: model = Survey @@ -39,6 +40,11 @@ class SurveyNode(DjangoObjectType): except Answer.DoesNotExist: return None + @staticmethod + def resolve_path(root: Survey, info, **kwargs): + return root.route + + class SurveysQuery(object): survey = graphene.Field(SurveyNode, id=graphene.ID()) surveys = DjangoFilterConnectionField(SurveyNode) diff --git a/server/users/schema/types.py b/server/users/schema/types.py index 7095160e..be2809b2 100644 --- a/server/users/schema/types.py +++ b/server/users/schema/types.py @@ -242,4 +242,4 @@ class UpdateError(graphene.ObjectType): class ActivityNode(graphene.ObjectType): - topics = graphene.List("books.schema.nodes.TopicNode") + topics = graphene.List(graphene.NonNull("books.schema.nodes.TopicNode"))