diff --git a/server/api/schema.py b/server/api/schema.py index ca6ff5aa..57a45751 100644 --- a/server/api/schema.py +++ b/server/api/schema.py @@ -4,7 +4,9 @@ from graphene import relay from graphene_django.debug import DjangoDebug # noinspection PyUnresolvedReferences -from api import graphene_wagtail # Keep this import exactly here, it's necessary for StreamField conversion +from api import ( + graphene_wagtail, +) # Keep this import exactly here, it's necessary for StreamField conversion from assignments.schema.mutations import AssignmentMutations from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery from basicknowledge.queries import InstrumentQuery @@ -25,20 +27,42 @@ from rooms.schema import RoomsQuery, ModuleRoomsQuery from users.schema import AllUsersQuery, UsersQuery, ProfileMutations -class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, - StudentSubmissionQuery, InstrumentQuery, PortfolioQuery, SurveysQuery, AllNewsTeasersQuery, - graphene.ObjectType): +class Query( + UsersQuery, + AllUsersQuery, + ModuleRoomsQuery, + RoomsQuery, + ObjectivesQuery, + BookQuery, + AssignmentsQuery, + StudentSubmissionQuery, + InstrumentQuery, + PortfolioQuery, + SurveysQuery, + AllNewsTeasersQuery, + graphene.ObjectType, +): node = relay.Node.Field() if settings.DEBUG: - debug = graphene.Field(DjangoDebug, name='_debug') + debug = graphene.Field(DjangoDebug, name="_debug") -class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, OauthMutations, - PortfolioMutations, ProfileMutations, SurveyMutations, NoteMutations, SpellCheckMutations, - graphene.ObjectType): +class Mutation( + BookMutations, + RoomMutations, + AssignmentMutations, + ObjectiveMutations, + OauthMutations, + PortfolioMutations, + ProfileMutations, + SurveyMutations, + NoteMutations, + SpellCheckMutations, + graphene.ObjectType, +): if settings.DEBUG: - debug = graphene.Field(DjangoDebug, name='_debug') + debug = graphene.Field(DjangoDebug, name="_debug") schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/server/books/schema/nodes/module.py b/server/books/schema/nodes/module.py index 69dd96c3..abf92593 100644 --- a/server/books/schema/nodes/module.py +++ b/server/books/schema/nodes/module.py @@ -1,4 +1,5 @@ import graphene +from wagtail.models import Page from assignments.models import StudentSubmission from assignments.schema.types import AssignmentNode, StudentSubmissionNode from books.models import Chapter, ContentBlock, Module, RecentModule @@ -10,7 +11,12 @@ from django.db.models import Q from graphene import relay from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField -from notes.models import ChapterBookmark, ContentBlockBookmark, ModuleBookmark +from notes.models import ( + ChapterBookmark, + ContentBlockBookmark, + Highlight, + ModuleBookmark, +) from objectives.schema import ObjectiveGroupNode from surveys.models import Answer from surveys.schema import AnswerNode @@ -57,6 +63,7 @@ class ModuleNode(DjangoObjectType): category = graphene.Field(ModuleCategoryNode) language = graphene.String() highlights = graphene.List("notes.schema.HighlightNode") + all_highlights = graphene.List("notes.schema.HighlightNode") def resolve_chapters(self, info, **kwargs): return Chapter.get_by_parent(self) @@ -128,6 +135,15 @@ class ModuleNode(DjangoObjectType): def resolve_highlights(root: Module, info, **kwargs): return root.highlights.filter(user=info.context.user) + @staticmethod + def resolve_all_highlights(root: Module, info, **kwargs): + # todo: is this too expensive, query-wise + pages = Page.objects.live().descendant_of(root) + highlights = Highlight.objects.filter(user=info.context.user).filter( + page__in=pages + ) + return highlights + class RecentModuleNode(DjangoObjectType): class Meta: diff --git a/server/books/schema/nodes/topic.py b/server/books/schema/nodes/topic.py index 91c3806f..8043f0ca 100644 --- a/server/books/schema/nodes/topic.py +++ b/server/books/schema/nodes/topic.py @@ -1,10 +1,13 @@ import graphene +from wagtail.models import Page from books.models import Module, Topic from books.schema.nodes import ModuleNode from graphene import relay from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField +from notes.models import Highlight + class NotFoundFailure: reason = "Not Found" @@ -16,7 +19,8 @@ class NotFound(graphene.ObjectType): class TopicNode(DjangoObjectType): pk = graphene.Int() - modules = DjangoFilterConnectionField("books.schema.nodes.ModuleNode") + modules = graphene.List("books.schema.nodes.ModuleNode") + highlights = graphene.List("notes.schema.HighlightNode") class Meta: model = Topic @@ -45,6 +49,12 @@ class TopicNode(DjangoObjectType): ids += list(translation.get_child_ids()) return Module.objects.filter(id__in=ids).live() + @staticmethod + def resolve_highlights(root: Topic, info, **kwargs): + # todo: is this too expensive, query-wise? + pages = Page.objects.live().descendant_of(root) + return Highlight.objects.filter(user=info.context.user).filter(page__in=pages) + class TopicOr404Node(graphene.Union): class Meta: diff --git a/server/schema.graphql b/server/schema.graphql index e1641cac..3d79e3cd 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -53,6 +53,8 @@ type Query { allUsers(offset: Int, before: String, after: String, first: Int, last: Int, username: String, email: String): PrivateUserNodeConnection myActivity(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 myInstrumentActivity(offset: Int, before: String, after: String, first: Int, last: Int, slug: String): InstrumentNodeConnection + myActivities: ActivityNode + something: String! _debug: DjangoDebug } @@ -129,6 +131,7 @@ type ModuleNode implements ModuleInterface { snapshots: [SnapshotNode] language: String highlights: [HighlightNode] + allHighlights: [HighlightNode] } interface ModuleInterface { @@ -158,107 +161,25 @@ type TopicNode implements Node { """The ID of the object""" id: ID! pk: Int - 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 + modules: [ModuleNode] + highlights: [HighlightNode] } -type ModuleNodeConnection { - """Pagination data for this connection.""" - pageInfo: PageInfo! - - """Contains the nodes in this connection.""" - edges: [ModuleNodeEdge]! -} - -""" -The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. -""" -type PageInfo { - """When paginating forwards, are there more items?""" - hasNextPage: Boolean! - - """When paginating backwards, are there more items?""" - hasPreviousPage: Boolean! - - """When paginating backwards, the cursor to continue.""" - startCursor: String - - """When paginating forwards, the cursor to continue.""" - endCursor: String -} - -"""A Relay edge containing a `ModuleNode` and its cursor.""" -type ModuleNodeEdge { - """The item at the end of the edge""" - node: ModuleNode - - """A cursor for use in pagination""" - cursor: String! -} - -type ModuleLevelNode implements Node { +type HighlightNode implements Node { """The ID of the object""" id: ID! - name: String! - filterAttributeType: BooksModuleLevelFilterAttributeTypeChoices! - - """Order in the Dropdown List""" - order: Int! - moduleSet(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! + user: PrivateUserNode! + page: HighlightableNode + contentIndex: Int + contentUuid: UUID + paragraphIndex: Int! + startPosition: Int! + selectionLength: Int! + text: String! + note: NoteNode + color: String! } -"""An enumeration.""" -enum BooksModuleLevelFilterAttributeTypeChoices { - """All""" - ALL - - """Exact""" - EXACT -} - -type ModuleCategoryNode implements Node { - """The ID of the object""" - id: ID! - name: String! - filterAttributeType: BooksModuleCategoryFilterAttributeTypeChoices! - - """Order in the Dropdown List""" - order: Int! - moduleSet(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! -} - -"""An enumeration.""" -enum BooksModuleCategoryFilterAttributeTypeChoices { - """All""" - ALL - - """Exact""" - EXACT -} - -type AssignmentNode implements Node { - """The ID of the object""" - id: ID! - created: DateTime! - modified: DateTime! - title: String! - assignment: String! - solution: String - deleted: Boolean! - owner: PrivateUserNode - module: ModuleNode! - userCreated: Boolean! - taskbaseId: String - submissions: [StudentSubmissionNode] - submission: StudentSubmissionNode -} - -""" -The `DateTime` scalar type represents a DateTime -value as specified by -[iso8601](https://en.wikipedia.org/wiki/ISO_8601). -""" -scalar DateTime - type PrivateUserNode implements Node { firstName: String! lastName: String! @@ -298,6 +219,60 @@ type PrivateUserNode implements Node { readOnly: Boolean } +type ModuleLevelNode implements Node { + """The ID of the object""" + id: ID! + name: String! + filterAttributeType: BooksModuleLevelFilterAttributeTypeChoices! + + """Order in the Dropdown List""" + order: Int! + moduleSet(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! +} + +"""An enumeration.""" +enum BooksModuleLevelFilterAttributeTypeChoices { + """All""" + ALL + + """Exact""" + EXACT +} + +type ModuleNodeConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [ModuleNodeEdge]! +} + +""" +The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. +""" +type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String +} + +"""A Relay edge containing a `ModuleNode` and its cursor.""" +type ModuleNodeEdge { + """The item at the end of the edge""" + node: ModuleNode + + """A cursor for use in pagination""" + cursor: String! +} + type TeamNode implements Node { """The ID of the object""" id: ID! @@ -345,6 +320,194 @@ type ClassMemberNode { isMe: Boolean } +union HighlightableNode = ContentBlockNode | InstrumentNode | ModuleNode | ChapterNode + +type ContentBlockNode implements Node & ContentBlockInterface { + title: String + + """ + Der Name der Seite, wie er in URLs angezeigt werden soll, z.B. http://domain.com/blog/[my-slug]/ + """ + slug: String! + hiddenFor: [SchoolClassNode] + visibleFor: [SchoolClassNode] + userCreated: Boolean! + contents: GenericStreamFieldType + type: String! + + """The ID of the object""" + id: ID! + mine: Boolean + bookmarks: [ContentBlockBookmarkNode] + originalCreator: PublicUserNode + instrumentCategory: InstrumentCategoryNode + path: String + highlights: [HighlightNode] +} + +interface ContentBlockInterface { + title: String + contents: GenericStreamFieldType + type: String! +} + +scalar GenericStreamFieldType + +type ContentBlockBookmarkNode implements Node { + """The ID of the object""" + id: ID! + user: PrivateUserNode! + note: NoteNode + uuid: UUID + contentBlock: ContentBlockNode! +} + +type NoteNode implements Node { + """The ID of the object""" + id: ID! + text: String! + contentblockbookmark: ContentBlockBookmarkNode + modulebookmark: ModuleBookmarkNode + chapterbookmark: ChapterBookmarkNode + instrumentbookmark: InstrumentBookmarkNode + highlight: HighlightNode + pk: Int +} + +type ModuleBookmarkNode { + id: ID! + user: PrivateUserNode! + note: NoteNode + module: ModuleNode! +} + +type ChapterBookmarkNode implements Node { + """The ID of the object""" + id: ID! + user: PrivateUserNode! + note: NoteNode + chapter: ChapterNode! +} + +type ChapterNode implements Node & ChapterInterface { + title: String + + """ + Der Name der Seite, wie er in URLs angezeigt werden soll, z.B. http://domain.com/blog/[my-slug]/ + """ + slug: String! + description: String + titleHiddenFor: [SchoolClassNode] + descriptionHiddenFor: [SchoolClassNode] + + """The ID of the object""" + id: ID! + bookmark: ChapterBookmarkNode + contentBlocks: [ContentBlockNode] + path: String + highlights: [HighlightNode] +} + +interface ChapterInterface { + description: String + title: String +} + +type InstrumentBookmarkNode implements Node { + """The ID of the object""" + id: ID! + user: PrivateUserNode! + note: NoteNode + uuid: UUID + instrument: InstrumentNode! +} + +""" +Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects +in fields, resolvers and input. +""" +scalar UUID + +type InstrumentNode implements Node { + """Der Seitentitel, der öffentlich angezeigt werden soll""" + title: String! + + """ + Der Name der Seite, wie er in URLs angezeigt werden soll, z.B. http://domain.com/blog/[my-slug]/ + """ + slug: String! + intro: String! + contents: GenericStreamFieldType + + """The ID of the object""" + id: ID! + bookmarks: [InstrumentBookmarkNode] + type: InstrumentTypeNode + language: String + highlights: [HighlightNode] +} + +type InstrumentTypeNode implements Node { + """The ID of the object""" + id: ID! + name: String! + category: InstrumentCategoryNode + type: String! +} + +type InstrumentCategoryNode implements Node { + """The ID of the object""" + id: ID! + name: String! + background: String! + foreground: String! + types: [InstrumentTypeNode] +} + +type ModuleCategoryNode implements Node { + """The ID of the object""" + id: ID! + name: String! + filterAttributeType: BooksModuleCategoryFilterAttributeTypeChoices! + + """Order in the Dropdown List""" + order: Int! + moduleSet(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! +} + +"""An enumeration.""" +enum BooksModuleCategoryFilterAttributeTypeChoices { + """All""" + ALL + + """Exact""" + EXACT +} + +type AssignmentNode implements Node { + """The ID of the object""" + id: ID! + created: DateTime! + modified: DateTime! + title: String! + assignment: String! + solution: String + deleted: Boolean! + owner: PrivateUserNode + module: ModuleNode! + userCreated: Boolean! + taskbaseId: String + submissions: [StudentSubmissionNode] + submission: StudentSubmissionNode +} + +""" +The `DateTime` scalar type represents a DateTime +value as specified by +[iso8601](https://en.wikipedia.org/wiki/ISO_8601). +""" +scalar DateTime + type StudentSubmissionNode implements Node { """The ID of the object""" id: ID! @@ -453,11 +616,6 @@ type SnapshotChapterNode implements Node & ChapterInterface { titleHidden: Boolean } -interface ChapterInterface { - description: String - title: String -} - type SnapshotContentBlockNode implements Node & ContentBlockInterface { """The ID of the object""" id: ID! @@ -467,14 +625,6 @@ type SnapshotContentBlockNode implements Node & ContentBlockInterface { hidden: Boolean } -interface ContentBlockInterface { - title: String - contents: GenericStreamFieldType - type: String! -} - -scalar GenericStreamFieldType - type ContentBlockNodeConnection { """Pagination data for this connection.""" pageInfo: PageInfo! @@ -492,152 +642,6 @@ type ContentBlockNodeEdge { cursor: String! } -type ContentBlockNode implements Node & ContentBlockInterface { - title: String - - """ - Der Name der Seite, wie er in URLs angezeigt werden soll, z.B. http://domain.com/blog/[my-slug]/ - """ - slug: String! - hiddenFor: [SchoolClassNode] - visibleFor: [SchoolClassNode] - userCreated: Boolean! - contents: GenericStreamFieldType - type: String! - - """The ID of the object""" - id: ID! - mine: Boolean - bookmarks: [ContentBlockBookmarkNode] - originalCreator: PublicUserNode - instrumentCategory: InstrumentCategoryNode - path: String - highlights: [HighlightNode] -} - -type ContentBlockBookmarkNode implements Node { - """The ID of the object""" - id: ID! - user: PrivateUserNode! - note: NoteNode - uuid: UUID - contentBlock: ContentBlockNode! -} - -type NoteNode implements Node { - """The ID of the object""" - id: ID! - text: String! - contentblockbookmark: ContentBlockBookmarkNode - modulebookmark: ModuleBookmarkNode - chapterbookmark: ChapterBookmarkNode - instrumentbookmark: InstrumentBookmarkNode - highlight: HighlightNode - pk: Int -} - -type ModuleBookmarkNode { - id: ID! - user: PrivateUserNode! - note: NoteNode - module: ModuleNode! -} - -type ChapterBookmarkNode implements Node { - """The ID of the object""" - id: ID! - user: PrivateUserNode! - note: NoteNode - chapter: ChapterNode! -} - -type ChapterNode implements Node & ChapterInterface { - title: String - - """ - Der Name der Seite, wie er in URLs angezeigt werden soll, z.B. http://domain.com/blog/[my-slug]/ - """ - slug: String! - description: String - titleHiddenFor: [SchoolClassNode] - descriptionHiddenFor: [SchoolClassNode] - - """The ID of the object""" - id: ID! - bookmark: ChapterBookmarkNode - contentBlocks: [ContentBlockNode] - path: String - highlights: [HighlightNode] -} - -type HighlightNode implements Node { - """The ID of the object""" - id: ID! - user: PrivateUserNode! - page: HighlightableNode - contentIndex: Int - contentUuid: UUID - paragraphIndex: Int! - startPosition: Int! - selectionLength: Int! - text: String! - note: NoteNode - color: String! -} - -union HighlightableNode = ContentBlockNode | InstrumentNode | ModuleNode | ChapterNode - -type InstrumentNode implements Node { - """Der Seitentitel, der öffentlich angezeigt werden soll""" - title: String! - - """ - Der Name der Seite, wie er in URLs angezeigt werden soll, z.B. http://domain.com/blog/[my-slug]/ - """ - slug: String! - intro: String! - contents: GenericStreamFieldType - - """The ID of the object""" - id: ID! - bookmarks: [InstrumentBookmarkNode] - type: InstrumentTypeNode - language: String - highlights: [HighlightNode] -} - -type InstrumentBookmarkNode implements Node { - """The ID of the object""" - id: ID! - user: PrivateUserNode! - note: NoteNode - uuid: UUID - instrument: InstrumentNode! -} - -""" -Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects -in fields, resolvers and input. -""" -scalar UUID - -type InstrumentTypeNode implements Node { - """The ID of the object""" - id: ID! - name: String! - category: InstrumentCategoryNode - type: String! -} - -type InstrumentCategoryNode implements Node { - """The ID of the object""" - id: ID! - name: String! - background: String! - foreground: String! - types: [InstrumentTypeNode] -} - type SnapshotObjectiveGroupNode implements Node { """The ID of the object""" id: ID! @@ -973,6 +977,10 @@ type InstrumentNodeEdge { cursor: String! } +type ActivityNode { + topics: [TopicNode] +} + """Debugging information for the current query.""" type DjangoDebug { """Executed SQL queries for this API query.""" diff --git a/server/users/schema/queries.py b/server/users/schema/queries.py index fd9b4e65..46923977 100644 --- a/server/users/schema/queries.py +++ b/server/users/schema/queries.py @@ -2,9 +2,10 @@ import graphene from basicknowledge.models import BasicKnowledge from books.models import Module from graphene_django.filter import DjangoFilterConnectionField +from books.models.topic import Topic from users.models import User -from .types import PrivateUserNode +from .types import ActivityNode, PrivateUserNode class UsersQuery(object): @@ -14,6 +15,7 @@ class UsersQuery(object): my_instrument_activity = DjangoFilterConnectionField( "basicknowledge.queries.InstrumentNode" ) + my_activities = graphene.Field(ActivityNode) def resolve_me(self, info, **kwargs): return info.context.user @@ -30,6 +32,10 @@ class UsersQuery(object): def resolve_my_instrument_activity(self, info, **kwargs): return BasicKnowledge.objects.filter(instrumentbookmark__user=info.context.user) + def resolve_my_activities(self, info, **kwargs): + topics = Topic.objects.all() + return {"topics": topics} + class AllUsersQuery(object): me = graphene.Field(PrivateUserNode) diff --git a/server/users/schema/types.py b/server/users/schema/types.py index 22c21429..7095160e 100644 --- a/server/users/schema/types.py +++ b/server/users/schema/types.py @@ -239,3 +239,7 @@ class FieldError(graphene.ObjectType): class UpdateError(graphene.ObjectType): field = graphene.String() errors = graphene.List(FieldError) + + +class ActivityNode(graphene.ObjectType): + topics = graphene.List("books.schema.nodes.TopicNode")