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__entry">
<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 slot="footer">

View File

@ -2,7 +2,7 @@
<div class="snapshot-header">
<h1>Snapshot {{ id }}</h1>
<div class="snapshot-header__meta">
{{ created }} {{ creator }}
{{ created }} {{ snapshot.creator }}
</div>
<section class="snapshot-header__section">
@ -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');
},

View File

@ -1,42 +1,78 @@
<template>
<div class="snapshot-list-item">
<span
<router-link
:to="snapshotRoute"
class="snapshot-list-item__title"
v-html="snapshot.title"/>
<span
class="snapshot-list-item__date"
v-html="created" />
<router-link
:to="snapshotRoute"
class="snapshot-list-item__link">Mit Team teilen</router-link>
v-html="meta"/>
<a
class="snapshot-list-item__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>
</template>
<script>
import dateformat from '@/helpers/date-format';
import {SNAPSHOT_DETAIL} from '@/router/module.names';
import SHARE_SNAPSHOT_MUTATION from 'gql/mutations/snapshots/share.gql';
import gql from 'graphql-tag';
export default {
props: {
snapshot: {
type: Object,
default: () => ({})
}
default: () => ({}),
},
},
computed: {
created() {
return dateformat(this.snapshot.created);
meta() {
const created = dateformat(this.snapshot.created);
if (this.snapshot.mine) {
return created;
}
return `${created} - ${this.snapshot.creator}`;
},
snapshotRoute() {
return {
name: SNAPSHOT_DETAIL,
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>

View File

@ -4,11 +4,7 @@ mutation CreateSnapshot($input: CreateSnapshotInput!) {
id
title
created
creator {
username
firstName
lastName
}
creator
}
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
title
created
mine
shared
creator
}
}
}

View File

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

View File

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

View File

@ -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):

View File

@ -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}'

View File

@ -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)

View File

@ -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 {