Add ability to share a snapshot

This commit is contained in:
Ramon Wenger 2021-05-10 14:05:14 +02:00
parent ade00205e5
commit 24c88e84ff
12 changed files with 169 additions and 43 deletions

View File

@ -9,7 +9,7 @@
<div class="snapshot-created__content"> <div class="snapshot-created__content">
<div class="snapshot-created__entry"> <div class="snapshot-created__entry">
<span class="snapshot-created__title">{{ snapshot.title }}</span> <span class="snapshot-created__title">{{ snapshot.title }}</span>
<span class="snapshot-created__meta">{{ created }} - {{ snapshot.creator.firstName }} {{ snapshot.creator.lastName }}</span> <span class="snapshot-created__meta">{{ created }} - {{ snapshot.creator }}</span>
</div> </div>
</div> </div>
<div slot="footer"> <div slot="footer">

View File

@ -2,7 +2,7 @@
<div class="snapshot-header"> <div class="snapshot-header">
<h1>Snapshot {{ id }}</h1> <h1>Snapshot {{ id }}</h1>
<div class="snapshot-header__meta"> <div class="snapshot-header__meta">
{{ created }} {{ creator }} {{ created }} {{ snapshot.creator }}
</div> </div>
<section class="snapshot-header__section"> <section class="snapshot-header__section">
@ -86,10 +86,6 @@
created() { created() {
return dateformat(this.snapshot.created); return dateformat(this.snapshot.created);
}, },
creator() {
const {firstName, lastName} = this.snapshot.creator || {};
return `${firstName} ${lastName}`;
},
hiddenObjectives() { hiddenObjectives() {
return _getChange(this.snapshot, 'hiddenObjectives'); return _getChange(this.snapshot, 'hiddenObjectives');
}, },

View File

@ -1,42 +1,78 @@
<template> <template>
<div class="snapshot-list-item"> <div class="snapshot-list-item">
<span <router-link
:to="snapshotRoute"
class="snapshot-list-item__title" class="snapshot-list-item__title"
v-html="snapshot.title"/> v-html="snapshot.title"/>
<span <span
class="snapshot-list-item__date" class="snapshot-list-item__date"
v-html="created" /> v-html="meta"/>
<router-link <a
:to="snapshotRoute" class="snapshot-list-item__link"
class="snapshot-list-item__link">Mit Team teilen</router-link> v-if="snapshot.mine"
@click="share"
>
<template v-if="snapshot.shared">Nicht mehr teilen</template>
<template v-else>Mit Team teilen</template>
</a>
</div> </div>
</template> </template>
<script> <script>
import dateformat from '@/helpers/date-format'; import dateformat from '@/helpers/date-format';
import {SNAPSHOT_DETAIL} from '@/router/module.names'; import {SNAPSHOT_DETAIL} from '@/router/module.names';
import SHARE_SNAPSHOT_MUTATION from 'gql/mutations/snapshots/share.gql';
import gql from 'graphql-tag';
export default { export default {
props: { props: {
snapshot: { snapshot: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
} },
}, },
computed: { computed: {
created() { meta() {
return dateformat(this.snapshot.created); const created = dateformat(this.snapshot.created);
if (this.snapshot.mine) {
return created;
}
return `${created} - ${this.snapshot.creator}`;
}, },
snapshotRoute() { snapshotRoute() {
return { return {
name: SNAPSHOT_DETAIL, name: SNAPSHOT_DETAIL,
params: { params: {
id: this.snapshot.id id: this.snapshot.id,
} },
}; };
} },
} },
methods: {
share() {
this.$apollo.mutate({
mutation: SHARE_SNAPSHOT_MUTATION,
variables: {
input: {
snapshot: this.snapshot.id,
shared: !this.snapshot.shared,
},
},
update(store, {data: {shareSnapshot: {snapshot: {id, shared}}}}) {
store.writeFragment({
id,
fragment: gql`fragment SnapshotFragment on SnapshotNode { shared }`,
data: {
shared,
__typename: 'SnapshotNode'
}
});
}
});
},
},
}; };
</script> </script>

View File

@ -4,11 +4,7 @@ mutation CreateSnapshot($input: CreateSnapshotInput!) {
id id
title title
created created
creator { creator
username
firstName
lastName
}
} }
success success
} }

View File

@ -0,0 +1,9 @@
mutation ShareSnapshot($input: ShareSnapshotInput!) {
shareSnapshot(input: $input) {
success
snapshot {
id
shared
}
}
}

View File

@ -10,6 +10,9 @@ query ModuleSnapshotsQuery($slug: String!) {
id id
title title
created created
mine
shared
creator
} }
} }
} }

View File

@ -14,10 +14,7 @@ query SnapshotDetail($id: ID!) {
hiddenContentBlocks hiddenContentBlocks
hiddenObjectives hiddenObjectives
} }
creator { creator
firstName
lastName
}
chapters { chapters {
id id
description description

View File

@ -7,13 +7,12 @@
@select="selectedLink=$event" @select="selectedLink=$event"
/> />
<div <div
class="snapshots__list" class="snapshots__list">
v-if="selectedLink === 'mine'">
<snapshot-list-item <snapshot-list-item
:key="snapshot.id" :key="snapshot.id"
:snapshot="snapshot" :snapshot="snapshot"
class="snapshots__snapshot" class="snapshots__snapshot"
v-for="snapshot in module.snapshots" v-for="snapshot in snapshots"
/> />
</div> </div>
</div> </div>
@ -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: { apollo: {
module: { module: {
query: MODULE_SNAPSHOTS_QUERY, query: MODULE_SNAPSHOTS_QUERY,

View File

@ -1,4 +1,5 @@
import graphene import graphene
from django.db.models import Q
from graphene import relay from graphene import relay
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
@ -86,7 +87,8 @@ class ModuleNode(DjangoObjectType):
@staticmethod @staticmethod
def resolve_snapshots(parent, info, **kwargs): 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): class RecentModuleNode(DjangoObjectType):

View File

@ -72,6 +72,9 @@ class SnapshotNode(DjangoObjectType):
meta_title = graphene.String() meta_title = graphene.String()
hero_image = graphene.String() hero_image = graphene.String()
changes = graphene.Field(SnapshotChangesNode) changes = graphene.Field(SnapshotChangesNode)
mine = graphene.Boolean()
shared = graphene.Boolean(required=True)
creator = graphene.String(required=True)
class Meta: class Meta:
model = Snapshot model = Snapshot
@ -111,3 +114,11 @@ class SnapshotNode(DjangoObjectType):
'hidden_content_blocks': parent.hidden_content_blocks.count(), 'hidden_content_blocks': parent.hidden_content_blocks.count(),
'new_content_blocks': parent.custom_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}'

View File

@ -6,6 +6,7 @@ from api.schema import schema
from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory
from books.models import Snapshot, ChapterSnapshot from books.models import Snapshot, ChapterSnapshot
from core.tests.base_test import SkillboxTestCase from core.tests.base_test import SkillboxTestCase
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
from users.factories import SchoolClassFactory from users.factories import SchoolClassFactory
from users.models import User, SchoolClass from users.models import User, SchoolClass
@ -14,6 +15,18 @@ query ModulesQuery($slug: String, $id: ID) {
module(slug: $slug, id: $id) { module(slug: $slug, id: $id) {
id id
title title
objectiveGroups {
objectives {
id
text
hiddenFor {
name
}
visibleFor {
name
}
}
}
chapters { chapters {
id id
contentBlocks { contentBlocks {
@ -40,6 +53,11 @@ mutation CreateSnapshot($input: CreateSnapshotInput!) {
creator { creator {
username username
} }
objectiveGroups {
objectives {
text
}
}
chapters { chapters {
id id
descriptionHidden descriptionHidden
@ -152,31 +170,55 @@ class CreateSnapshotTestCase(SkillboxTestCase):
# we make a snapshot S of the module M # we make a snapshot S of the module M
# snapshot S looks like module M for school class X # 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): def _test_module_visibility(self, client, school_class_name):
result = client.execute(MODULE_QUERY, variables={ result = client.execute(MODULE_QUERY, variables={
'slug': self.module.slug 'slug': self.module.slug
}) })
self.assertIsNone(result.get('errors')) self.assertIsNone(result.get('errors'))
module = result.get('data').get('module') module = result.get('data').get('module')
chapter = edges_to_array(module.get('chapters'))[0] chapter = module.get('chapters')[0]
self.assertIsNotNone(chapter) self.assertIsNotNone(chapter)
content_blocks = chapter.get('contentBlocks') 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_visible in content_block_titles)
self.assertTrue(self.title_hidden in content_block_titles) self.assertTrue(self.title_hidden in content_block_titles)
self.assertTrue(self.title_custom in content_block_titles) self.assertTrue(self.title_custom in content_block_titles)
hidden_node = [node for node in content_blocks if hidden_content_block = [content_block for content_block in content_blocks if
node['title'] == self.title_hidden][0] content_block['title'] == self.title_hidden][0]
custom_node = [node for node in content_blocks if custom_content_block = [content_block for content_block in content_blocks if
node['title'] == self.title_custom][0] content_block['title'] == self.title_custom][0]
# check if hidden node is hidden for this school class # check if hidden content block is hidden for this school class
self.assertTrue( self.assertTrue(
school_class_name in [school_class['name'] for school_class in school_class_name in [school_class['name'] for school_class in
hidden_node.get('hiddenFor')]) hidden_content_block.get('hiddenFor')])
# check if the custom node is visible for this school class # check if the custom content block is visible for this school class
self.assertTrue( self.assertTrue(
school_class_name in [school_class['name'] for school_class in 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): def test_setup(self):
# make sure everything is setup correctly # make sure everything is setup correctly
@ -200,6 +242,7 @@ class CreateSnapshotTestCase(SkillboxTestCase):
self.assertFalse(chapter['descriptionHidden']) self.assertFalse(chapter['descriptionHidden'])
_, chapter_id = from_global_id(chapter['id']) _, chapter_id = from_global_id(chapter['id'])
self.assertEqual(int(chapter_id), self.chapter.id) self.assertEqual(int(chapter_id), self.chapter.id)
content_blocks = chapter['contentBlocks'] content_blocks = chapter['contentBlocks']
self.assertEqual(len(content_blocks), 3) self.assertEqual(len(content_blocks), 3)
visible, hidden, custom = content_blocks visible, hidden, custom = content_blocks
@ -211,6 +254,15 @@ class CreateSnapshotTestCase(SkillboxTestCase):
self.assertEqual(custom['hidden'], False) self.assertEqual(custom['hidden'], False)
self.assertEqual(ChapterSnapshot.objects.count(), 2) 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): def test_apply_snapshot(self):
self.snapshot = Snapshot.objects.create_snapshot(module=self.module, school_class=self.skillbox_class, self.snapshot = Snapshot.objects.create_snapshot(module=self.module, school_class=self.skillbox_class,
user=self.teacher) user=self.teacher)

View File

@ -419,6 +419,7 @@ type CustomMutation {
syncModuleVisibility(input: SyncModuleVisibilityInput!): SyncModuleVisibilityPayload syncModuleVisibility(input: SyncModuleVisibilityInput!): SyncModuleVisibilityPayload
createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload
applySnapshot(input: ApplySnapshotInput!): ApplySnapshotPayload applySnapshot(input: ApplySnapshotInput!): ApplySnapshotPayload
shareSnapshot(input: ShareSnapshotInput!): ShareSnapshotPayload
_debug: DjangoDebug _debug: DjangoDebug
} }
@ -882,6 +883,18 @@ type SchoolClassNodeEdge {
cursor: String! cursor: String!
} }
input ShareSnapshotInput {
snapshot: ID!
shared: Boolean!
clientMutationId: String
}
type ShareSnapshotPayload {
success: Boolean!
snapshot: SnapshotNode
clientMutationId: String
}
type SnapshotChangesNode { type SnapshotChangesNode {
hiddenObjectives: Int! hiddenObjectives: Int!
newObjectives: Int! newObjectives: Int!
@ -912,11 +925,13 @@ type SnapshotNode implements Node {
chapters: [SnapshotChapterNode] chapters: [SnapshotChapterNode]
hiddenContentBlocks(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ContentBlockNodeConnection! hiddenContentBlocks(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ContentBlockNodeConnection!
created: DateTime! created: DateTime!
creator: UserNode creator: String!
shared: Boolean!
title: String title: String
metaTitle: String metaTitle: String
heroImage: String heroImage: String
changes: SnapshotChangesNode changes: SnapshotChangesNode
mine: Boolean
} }
input SpellCheckInput { input SpellCheckInput {