From 24c88e84ffc3852a8dda1791c236ec0ea99c49b2 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 10 May 2021 14:05:14 +0200 Subject: [PATCH] Add ability to share a snapshot --- .../components/modules/SnapshotCreated.vue | 2 +- .../src/components/modules/SnapshotHeader.vue | 6 +- .../components/modules/SnapshotListItem.vue | 62 ++++++++++++---- .../graphql/gql/mutations/createSnapshot.gql | 6 +- .../graphql/gql/mutations/snapshots/share.gql | 9 +++ .../graphql/gql/queries/moduleSnapshots.gql | 3 + .../graphql/gql/queries/snapshots/detail.gql | 5 +- client/src/pages/snapshot/snapshots.vue | 15 +++- server/books/schema/nodes/module.py | 4 +- server/books/schema/nodes/snapshot.py | 11 +++ server/books/tests/test_snapshots.py | 72 ++++++++++++++++--- server/schema.graphql | 17 ++++- 12 files changed, 169 insertions(+), 43 deletions(-) create mode 100644 client/src/graphql/gql/mutations/snapshots/share.gql diff --git a/client/src/components/modules/SnapshotCreated.vue b/client/src/components/modules/SnapshotCreated.vue index 3b73543f..2d75f969 100644 --- a/client/src/components/modules/SnapshotCreated.vue +++ b/client/src/components/modules/SnapshotCreated.vue @@ -9,7 +9,7 @@
{{ snapshot.title }} - {{ created }} - {{ snapshot.creator.firstName }} {{ snapshot.creator.lastName }} + {{ created }} - {{ snapshot.creator }}
diff --git a/client/src/components/modules/SnapshotHeader.vue b/client/src/components/modules/SnapshotHeader.vue index 82ed2531..6f108b5b 100644 --- a/client/src/components/modules/SnapshotHeader.vue +++ b/client/src/components/modules/SnapshotHeader.vue @@ -2,7 +2,7 @@

Snapshot {{ id }}

- {{ created }} – {{ creator }} + {{ created }} – {{ snapshot.creator }}
@@ -86,10 +86,6 @@ created() { return dateformat(this.snapshot.created); }, - creator() { - const {firstName, lastName} = this.snapshot.creator || {}; - return `${firstName} ${lastName}`; - }, hiddenObjectives() { return _getChange(this.snapshot, 'hiddenObjectives'); }, diff --git a/client/src/components/modules/SnapshotListItem.vue b/client/src/components/modules/SnapshotListItem.vue index 2fa902cd..c198b292 100644 --- a/client/src/components/modules/SnapshotListItem.vue +++ b/client/src/components/modules/SnapshotListItem.vue @@ -1,42 +1,78 @@ diff --git a/client/src/graphql/gql/mutations/createSnapshot.gql b/client/src/graphql/gql/mutations/createSnapshot.gql index 2b88a88a..99064f40 100644 --- a/client/src/graphql/gql/mutations/createSnapshot.gql +++ b/client/src/graphql/gql/mutations/createSnapshot.gql @@ -4,11 +4,7 @@ mutation CreateSnapshot($input: CreateSnapshotInput!) { id title created - creator { - username - firstName - lastName - } + creator } success } diff --git a/client/src/graphql/gql/mutations/snapshots/share.gql b/client/src/graphql/gql/mutations/snapshots/share.gql new file mode 100644 index 00000000..5abd7aca --- /dev/null +++ b/client/src/graphql/gql/mutations/snapshots/share.gql @@ -0,0 +1,9 @@ +mutation ShareSnapshot($input: ShareSnapshotInput!) { + shareSnapshot(input: $input) { + success + snapshot { + id + shared + } + } +} diff --git a/client/src/graphql/gql/queries/moduleSnapshots.gql b/client/src/graphql/gql/queries/moduleSnapshots.gql index 9ce5d249..d85bb779 100644 --- a/client/src/graphql/gql/queries/moduleSnapshots.gql +++ b/client/src/graphql/gql/queries/moduleSnapshots.gql @@ -10,6 +10,9 @@ query ModuleSnapshotsQuery($slug: String!) { id title created + mine + shared + creator } } } diff --git a/client/src/graphql/gql/queries/snapshots/detail.gql b/client/src/graphql/gql/queries/snapshots/detail.gql index 69f73007..27d1570a 100644 --- a/client/src/graphql/gql/queries/snapshots/detail.gql +++ b/client/src/graphql/gql/queries/snapshots/detail.gql @@ -14,10 +14,7 @@ query SnapshotDetail($id: ID!) { hiddenContentBlocks hiddenObjectives } - creator { - firstName - lastName - } + creator chapters { id description diff --git a/client/src/pages/snapshot/snapshots.vue b/client/src/pages/snapshot/snapshots.vue index 2a857238..10d90b51 100644 --- a/client/src/pages/snapshot/snapshots.vue +++ b/client/src/pages/snapshot/snapshots.vue @@ -7,13 +7,12 @@ @select="selectedLink=$event" />
+ class="snapshots__list">
@@ -39,6 +38,16 @@ }; }, + computed: { + snapshots() { + if (this.selectedLink === 'mine') { + return this.module.snapshots.filter(snapshot => snapshot.mine); + } else { + return this.module.snapshots.filter(snapshot => snapshot.shared); + } + } + }, + apollo: { module: { query: MODULE_SNAPSHOTS_QUERY, diff --git a/server/books/schema/nodes/module.py b/server/books/schema/nodes/module.py index 7156658a..1d8d6cd3 100644 --- a/server/books/schema/nodes/module.py +++ b/server/books/schema/nodes/module.py @@ -1,4 +1,5 @@ import graphene +from django.db.models import Q from graphene import relay from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField @@ -86,7 +87,8 @@ class ModuleNode(DjangoObjectType): @staticmethod def resolve_snapshots(parent, info, **kwargs): - return parent.snapshots.filter(creator=info.context.user) + user = info.context.user + return parent.snapshots.filter(Q(creator=user) | Q(Q(creator__team=user.team ) & Q(shared=True))) class RecentModuleNode(DjangoObjectType): diff --git a/server/books/schema/nodes/snapshot.py b/server/books/schema/nodes/snapshot.py index 95542bb4..5df5041f 100644 --- a/server/books/schema/nodes/snapshot.py +++ b/server/books/schema/nodes/snapshot.py @@ -72,6 +72,9 @@ class SnapshotNode(DjangoObjectType): meta_title = graphene.String() hero_image = graphene.String() changes = graphene.Field(SnapshotChangesNode) + mine = graphene.Boolean() + shared = graphene.Boolean(required=True) + creator = graphene.String(required=True) class Meta: model = Snapshot @@ -111,3 +114,11 @@ class SnapshotNode(DjangoObjectType): 'hidden_content_blocks': parent.hidden_content_blocks.count(), 'new_content_blocks': parent.custom_content_blocks.count() } + + @staticmethod + def resolve_mine(parent, info, **kwargs): + return parent.creator == info.context.user + + @staticmethod + def resolve_creator(parent, info, **kwargs): + return f'{parent.creator.first_name} {parent.creator.last_name}' diff --git a/server/books/tests/test_snapshots.py b/server/books/tests/test_snapshots.py index e0a34ff8..641289d7 100644 --- a/server/books/tests/test_snapshots.py +++ b/server/books/tests/test_snapshots.py @@ -6,6 +6,7 @@ from api.schema import schema from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory from books.models import Snapshot, ChapterSnapshot from core.tests.base_test import SkillboxTestCase +from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory from users.factories import SchoolClassFactory from users.models import User, SchoolClass @@ -14,6 +15,18 @@ query ModulesQuery($slug: String, $id: ID) { module(slug: $slug, id: $id) { id title + objectiveGroups { + objectives { + id + text + hiddenFor { + name + } + visibleFor { + name + } + } + } chapters { id contentBlocks { @@ -40,6 +53,11 @@ mutation CreateSnapshot($input: CreateSnapshotInput!) { creator { username } + objectiveGroups { + objectives { + text + } + } chapters { id descriptionHidden @@ -152,31 +170,55 @@ class CreateSnapshotTestCase(SkillboxTestCase): # we make a snapshot S of the module M # snapshot S looks like module M for school class X + objective_group = ObjectiveGroupFactory(module=self.module) + self.visible_objective = ObjectiveFactory(text='visible-objective', group=objective_group) + self.hidden_objective = ObjectiveFactory(text='hidden-objective', group=objective_group) + self.custom_objective = ObjectiveFactory(text='custom-objective', group=objective_group, owner=self.teacher) + + self.hidden_objective.hidden_for.add(self.skillbox_class) + self.custom_objective.visible_for.add(self.skillbox_class) + def _test_module_visibility(self, client, school_class_name): result = client.execute(MODULE_QUERY, variables={ 'slug': self.module.slug }) self.assertIsNone(result.get('errors')) module = result.get('data').get('module') - chapter = edges_to_array(module.get('chapters'))[0] + chapter = module.get('chapters')[0] self.assertIsNotNone(chapter) content_blocks = chapter.get('contentBlocks') - content_block_titles = [node['title'] for node in content_blocks] + content_block_titles = [content_block['title'] for content_block in content_blocks] self.assertTrue(self.title_visible in content_block_titles) self.assertTrue(self.title_hidden in content_block_titles) self.assertTrue(self.title_custom in content_block_titles) - hidden_node = [node for node in content_blocks if - node['title'] == self.title_hidden][0] - custom_node = [node for node in content_blocks if - node['title'] == self.title_custom][0] - # check if hidden node is hidden for this school class + hidden_content_block = [content_block for content_block in content_blocks if + content_block['title'] == self.title_hidden][0] + custom_content_block = [content_block for content_block in content_blocks if + content_block['title'] == self.title_custom][0] + # check if hidden content block is hidden for this school class self.assertTrue( school_class_name in [school_class['name'] for school_class in - hidden_node.get('hiddenFor')]) - # check if the custom node is visible for this school class + hidden_content_block.get('hiddenFor')]) + # check if the custom content block is visible for this school class self.assertTrue( school_class_name in [school_class['name'] for school_class in - custom_node.get('visibleFor')]) + custom_content_block.get('visibleFor')]) + + objectives = module['objectiveGroups'][0]['objectives'] + + hidden_objective = [objective for objective in objectives if + objective['text'] == self.hidden_objective.text][0] + custom_objective = [objective for objective in objectives if + objective['text'] == self.custom_objective.text][0] + + # check if hidden objective is hidden for this school class + self.assertTrue( + school_class_name in [school_class['name'] for school_class in + hidden_objective.get('hiddenFor')]) + # check if the custom objective is visible for this school class + self.assertTrue( + school_class_name in [school_class['name'] for school_class in + custom_objective.get('visibleFor')]) def test_setup(self): # make sure everything is setup correctly @@ -200,6 +242,7 @@ class CreateSnapshotTestCase(SkillboxTestCase): self.assertFalse(chapter['descriptionHidden']) _, chapter_id = from_global_id(chapter['id']) self.assertEqual(int(chapter_id), self.chapter.id) + content_blocks = chapter['contentBlocks'] self.assertEqual(len(content_blocks), 3) visible, hidden, custom = content_blocks @@ -211,6 +254,15 @@ class CreateSnapshotTestCase(SkillboxTestCase): self.assertEqual(custom['hidden'], False) self.assertEqual(ChapterSnapshot.objects.count(), 2) + visible, hidden, custom = snapshot['objectiveGroups'][0]['objectives'] + self.assertEqual(visible['text'], self.visible_objective.text) + self.assertEqual(visible['hidden'], False) + self.assertEqual(hidden['text'], self.hidden_objective.text) + self.assertEqual(hidden['hidden'], True) + self.assertEqual(custom['text'], self.custom_objective.text) + self.assertEqual(custom['hidden'], False) + + def test_apply_snapshot(self): self.snapshot = Snapshot.objects.create_snapshot(module=self.module, school_class=self.skillbox_class, user=self.teacher) diff --git a/server/schema.graphql b/server/schema.graphql index 44c1600a..ffc9812b 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -419,6 +419,7 @@ type CustomMutation { syncModuleVisibility(input: SyncModuleVisibilityInput!): SyncModuleVisibilityPayload createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload applySnapshot(input: ApplySnapshotInput!): ApplySnapshotPayload + shareSnapshot(input: ShareSnapshotInput!): ShareSnapshotPayload _debug: DjangoDebug } @@ -882,6 +883,18 @@ type SchoolClassNodeEdge { cursor: String! } +input ShareSnapshotInput { + snapshot: ID! + shared: Boolean! + clientMutationId: String +} + +type ShareSnapshotPayload { + success: Boolean! + snapshot: SnapshotNode + clientMutationId: String +} + type SnapshotChangesNode { hiddenObjectives: Int! newObjectives: Int! @@ -912,11 +925,13 @@ type SnapshotNode implements Node { chapters: [SnapshotChapterNode] hiddenContentBlocks(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ContentBlockNodeConnection! created: DateTime! - creator: UserNode + creator: String! + shared: Boolean! title: String metaTitle: String heroImage: String changes: SnapshotChangesNode + mine: Boolean } input SpellCheckInput {