Add properties to models and schema

This commit is contained in:
Ramon Wenger 2024-02-27 16:25:33 +01:00
parent c6e6491ef9
commit 5954151e2e
14 changed files with 92 additions and 21 deletions

View File

@ -9,11 +9,12 @@ from wagtail.search import index
from core.constants import DEFAULT_RICH_TEXT_FEATURES from core.constants import DEFAULT_RICH_TEXT_FEATURES
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from modelcluster.fields import ParentalKey
from core.mixins import GraphqlNodeMixin
@register_snippet @register_snippet
class Assignment(index.Indexed, TimeStampedModel): class Assignment(index.Indexed, TimeStampedModel, GraphqlNodeMixin):
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
assignment = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES) assignment = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES)
solution = RichTextField(null=True, blank=True, 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) user_created = models.BooleanField(default=False)
taskbase_id = models.CharField(max_length=255, null=True, blank=True) 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 = [ search_fields = [
index.AutocompleteField("title"), index.AutocompleteField("title"),
index.AutocompleteField("assignment"), index.AutocompleteField("assignment"),

View File

@ -26,7 +26,7 @@ class StudentSubmissionNode(DjangoObjectType):
def resolve_submission_feedback(root: StudentSubmission, info, **kwargs): def resolve_submission_feedback(root: StudentSubmission, info, **kwargs):
user = info.context.user user = info.context.user
if not hasattr(root, 'submission_feedback'): if not hasattr(root, "submission_feedback"):
return None return None
# teacher path # teacher path
@ -43,6 +43,7 @@ class StudentSubmissionNode(DjangoObjectType):
class AssignmentNode(DjangoObjectType): class AssignmentNode(DjangoObjectType):
submission = graphene.Field(StudentSubmissionNode) submission = graphene.Field(StudentSubmissionNode)
submissions = graphene.List(StudentSubmissionNode) submissions = graphene.List(StudentSubmissionNode)
path = graphene.String(required=True)
class Meta: class Meta:
model = Assignment model = Assignment
@ -50,16 +51,27 @@ class AssignmentNode(DjangoObjectType):
interfaces = (relay.Node,) interfaces = (relay.Node,)
def resolve_submission(self, info, **kwargs): 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): def resolve_submissions(self, info, **kwargs):
user = info.context.user user = info.context.user
if user.has_perm('users.can_manage_school_class_content'): 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 self.submissions.filter(
student__in=user.users_in_active_school_class()
).filter(final=True)
return [] return []
def resolve_solution(self, info, **kwargs): 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 return self.solution
else: else:
return None return None
@staticmethod
def resolve_path(root: Assignment, info, **kwargs):
return root.route

View File

@ -38,6 +38,10 @@ class Chapter(StrictHierarchyPage, GraphqlNodeMixin):
SchoolClass, related_name="hidden_chapter_descriptions" 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): def sync_title_visibility(self, school_class_template, school_class_to_sync):
if ( if (
self.title_hidden_for.filter(id=school_class_template.id).exists() self.title_hidden_for.filter(id=school_class_template.id).exists()

View File

@ -161,6 +161,10 @@ class ContentBlock(StrictHierarchyPage, GraphqlNodeMixin):
def module(self): def module(self):
return self.get_parent().get_parent().specific 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 # duplicate all attached Surveys and Assignments
def duplicate_attached_entities(self): def duplicate_attached_entities(self):
duplicate_entities_func = duplicate_entities_generator(self.module) duplicate_entities_func = duplicate_entities_generator(self.module)

View File

@ -20,7 +20,9 @@ class ModuleLevel(models.Model):
filter_attribute_type = models.CharField( filter_attribute_type = models.CharField(
max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT 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: class Meta:
verbose_name_plural = _("module Levels") verbose_name_plural = _("module Levels")
@ -36,7 +38,9 @@ class ModuleCategory(models.Model):
filter_attribute_type = models.CharField( filter_attribute_type = models.CharField(
max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT 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: class Meta:
verbose_name = _("module type") verbose_name = _("module type")
@ -120,6 +124,10 @@ class Module(StrictHierarchyPage):
def get_child_ids(self): def get_child_ids(self):
return self.get_children().values_list("id", flat=True) 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): def sync_from_school_class(self, school_class_template, school_class_to_sync):
# import here so we don't get a circular import error # import here so we don't get a circular import error
from books.models import Chapter, ContentBlock from books.models import Chapter, ContentBlock

View File

@ -71,8 +71,7 @@ class ChapterNode(DjangoObjectType):
@staticmethod @staticmethod
def resolve_path(root: Chapter, info, **kwargs): def resolve_path(root: Chapter, info, **kwargs):
module = root.get_parent() return root.route
return f"module/{module.slug}#{root.graphql_id}"
@staticmethod @staticmethod
def resolve_highlights(root: Chapter, info, **kwargs): def resolve_highlights(root: Chapter, info, **kwargs):

View File

@ -120,8 +120,7 @@ class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin):
@staticmethod @staticmethod
def resolve_path(root: ContentBlock, info, **kwargs): def resolve_path(root: ContentBlock, info, **kwargs):
module = root.get_parent().get_parent() return root.route
return f"module/{module.slug}#{root.graphql_id}"
@staticmethod @staticmethod
def resolve_highlights(root: ContentBlock, info, **kwargs): def resolve_highlights(root: ContentBlock, info, **kwargs):

View File

@ -58,8 +58,11 @@ class ModuleNode(DjangoObjectType):
category = graphene.Field(ModuleCategoryNode) category = graphene.Field(ModuleCategoryNode)
language = graphene.String() language = graphene.String()
highlights = graphene.List("notes.schema.HighlightNode") 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") my_bookmarks = graphene.List("notes.schema.BookmarkNode")
path = graphene.String()
def resolve_chapters(self, info, **kwargs): def resolve_chapters(self, info, **kwargs):
return Chapter.get_by_parent(self) return Chapter.get_by_parent(self)
@ -134,7 +137,7 @@ class ModuleNode(DjangoObjectType):
@staticmethod @staticmethod
def resolve_my_highlights(root: Module, info, **kwargs): def resolve_my_highlights(root: Module, info, **kwargs):
# todo: is this too expensive, query-wise # 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( highlights = Highlight.objects.filter(user=info.context.user).filter(
page__in=pages page__in=pages
) )
@ -161,6 +164,10 @@ class ModuleNode(DjangoObjectType):
+ list(content_block_bookmarks) + list(content_block_bookmarks)
) )
@staticmethod
def resolve_path(root: Module, info, **kwargs):
return root.route
class RecentModuleNode(DjangoObjectType): class RecentModuleNode(DjangoObjectType):
class Meta: class Meta:

View File

@ -19,7 +19,7 @@ class NotFound(graphene.ObjectType):
class TopicNode(DjangoObjectType): class TopicNode(DjangoObjectType):
pk = graphene.Int() 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") highlights = graphene.List("notes.schema.HighlightNode")
class Meta: class Meta:

View File

@ -32,6 +32,7 @@ class NoteNode(DjangoObjectType):
class ContentBlockBookmarkNode(DjangoObjectType): class ContentBlockBookmarkNode(DjangoObjectType):
uuid = graphene.UUID() uuid = graphene.UUID()
note = graphene.Field(NoteNode) note = graphene.Field(NoteNode)
path = graphene.String()
class Meta: class Meta:
model = ContentBlockBookmark model = ContentBlockBookmark
@ -39,17 +40,27 @@ class ContentBlockBookmarkNode(DjangoObjectType):
filter_fields = [] filter_fields = []
interfaces = (relay.Node,) interfaces = (relay.Node,)
@staticmethod
def resolve_path(root: ContentBlockBookmark, info, **kwargs):
return root.content_block.route
class ModuleBookmarkNode(DjangoObjectType): class ModuleBookmarkNode(DjangoObjectType):
note = graphene.Field(NoteNode) note = graphene.Field(NoteNode)
path = graphene.String()
class Meta: class Meta:
model = ModuleBookmark model = ModuleBookmark
fields = "__all__" fields = "__all__"
@staticmethod
def resolve_path(root: ModuleBookmark, info, **kwargs):
return root.module.route
class ChapterBookmarkNode(DjangoObjectType): class ChapterBookmarkNode(DjangoObjectType):
note = graphene.Field(NoteNode) note = graphene.Field(NoteNode)
path = graphene.String()
class Meta: class Meta:
model = ChapterBookmark model = ChapterBookmark
@ -57,6 +68,10 @@ class ChapterBookmarkNode(DjangoObjectType):
filter_fields = [] filter_fields = []
interfaces = (relay.Node,) interfaces = (relay.Node,)
@staticmethod
def resolve_path(root: ChapterBookmark, info, **kwargs):
return root.chapter.route
class InstrumentBookmarkNode(DjangoObjectType): class InstrumentBookmarkNode(DjangoObjectType):
uuid = graphene.UUID() uuid = graphene.UUID()

View File

@ -92,6 +92,7 @@ type SurveyNode implements Node {
answers(offset: Int, before: String, after: String, first: Int, last: Int): AnswerNodeConnection! answers(offset: Int, before: String, after: String, first: Int, last: Int): AnswerNodeConnection!
pk: Int pk: Int
answer: AnswerNode answer: AnswerNode
path: String!
} }
type ModuleNode implements ModuleInterface { type ModuleNode implements ModuleInterface {
@ -130,8 +131,9 @@ type ModuleNode implements ModuleInterface {
snapshots: [SnapshotNode] snapshots: [SnapshotNode]
language: String language: String
highlights: [HighlightNode] highlights: [HighlightNode]
myHighlights: [HighlightNode] myHighlights: [HighlightNode!]!
myBookmarks: [BookmarkNode] myBookmarks: [BookmarkNode]
path: String
} }
interface ModuleInterface { interface ModuleInterface {
@ -161,7 +163,7 @@ type TopicNode implements Node {
"""The ID of the object""" """The ID of the object"""
id: ID! id: ID!
pk: Int pk: Int
modules: [ModuleNode] modules: [ModuleNode!]
highlights: [HighlightNode] highlights: [HighlightNode]
} }
@ -360,6 +362,7 @@ type ContentBlockBookmarkNode implements Node {
note: NoteNode note: NoteNode
uuid: UUID uuid: UUID
contentBlock: ContentBlockNode! contentBlock: ContentBlockNode!
path: String
} }
type NoteNode implements Node { type NoteNode implements Node {
@ -379,6 +382,7 @@ type ModuleBookmarkNode {
user: PrivateUserNode! user: PrivateUserNode!
note: NoteNode note: NoteNode
module: ModuleNode! module: ModuleNode!
path: String
} }
type ChapterBookmarkNode implements Node { type ChapterBookmarkNode implements Node {
@ -387,6 +391,7 @@ type ChapterBookmarkNode implements Node {
user: PrivateUserNode! user: PrivateUserNode!
note: NoteNode note: NoteNode
chapter: ChapterNode! chapter: ChapterNode!
path: String
} }
type ChapterNode implements Node & ChapterInterface { type ChapterNode implements Node & ChapterInterface {
@ -499,6 +504,7 @@ type AssignmentNode implements Node {
taskbaseId: String taskbaseId: String
submissions: [StudentSubmissionNode] submissions: [StudentSubmissionNode]
submission: StudentSubmissionNode submission: StudentSubmissionNode
path: String!
} }
""" """
@ -963,7 +969,7 @@ type InstrumentNodeEdge {
} }
type ActivityNode { type ActivityNode {
topics: [TopicNode] topics: [TopicNode!]
} }
"""Debugging information for the current query.""" """Debugging information for the current query."""

View File

@ -5,9 +5,11 @@ from wagtail.snippets.models import register_snippet
from wagtail.search import index from wagtail.search import index
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from core.mixins import GraphqlNodeMixin
@register_snippet @register_snippet
class Survey(models.Model, index.Indexed): class Survey(models.Model, index.Indexed, GraphqlNodeMixin):
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
module = models.ForeignKey( module = models.ForeignKey(
"books.Module", "books.Module",
@ -18,6 +20,10 @@ class Survey(models.Model, index.Indexed):
) )
data = JSONField() data = JSONField()
@property
def route(self):
return f"module/{self.module.slug}#{self.graphql_id}"
search_fields = [ search_fields = [
index.AutocompleteField("title"), index.AutocompleteField("title"),
index.AutocompleteField("module__meta_title"), index.AutocompleteField("module__meta_title"),

View File

@ -23,6 +23,7 @@ class AnswerNode(DjangoObjectType):
class SurveyNode(DjangoObjectType): class SurveyNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
answer = graphene.Field(AnswerNode) answer = graphene.Field(AnswerNode)
path = graphene.String(required=True)
class Meta: class Meta:
model = Survey model = Survey
@ -39,6 +40,11 @@ class SurveyNode(DjangoObjectType):
except Answer.DoesNotExist: except Answer.DoesNotExist:
return None return None
@staticmethod
def resolve_path(root: Survey, info, **kwargs):
return root.route
class SurveysQuery(object): class SurveysQuery(object):
survey = graphene.Field(SurveyNode, id=graphene.ID()) survey = graphene.Field(SurveyNode, id=graphene.ID())
surveys = DjangoFilterConnectionField(SurveyNode) surveys = DjangoFilterConnectionField(SurveyNode)

View File

@ -242,4 +242,4 @@ class UpdateError(graphene.ObjectType):
class ActivityNode(graphene.ObjectType): class ActivityNode(graphene.ObjectType):
topics = graphene.List("books.schema.nodes.TopicNode") topics = graphene.List(graphene.NonNull("books.schema.nodes.TopicNode"))