Add mutation to apply a snapshot

Also add unit test
This commit is contained in:
Ramon Wenger 2021-04-24 19:59:04 +02:00
parent 96528e8926
commit 8d6f30b2d2
5 changed files with 107 additions and 23 deletions

View File

@ -120,3 +120,19 @@ class ContentBlockSnapshot(ContentBlock):
null=True, null=True,
related_name='custom_content_blocks' related_name='custom_content_blocks'
) )
def to_regular_content_block(self, owner, school_class):
cb = ContentBlock(
contents=self.contents,
type=self.type,
title=self.title,
owner=owner
)
self.add_sibling(instance=cb, pos='right')
# some wagtail magic
revision = cb.save_revision()
revision.publish()
cb.visible_for.add(school_class)
cb.save()
return cb

View File

@ -1,7 +1,7 @@
from books.schema.mutations.chapter import UpdateChapterVisibility from books.schema.mutations.chapter import UpdateChapterVisibility
from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock
from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility
from books.schema.mutations.snapshot import CreateSnapshot from books.schema.mutations.snapshot import CreateSnapshot, ApplySnapshot
from books.schema.mutations.topic import UpdateLastTopic from books.schema.mutations.topic import UpdateLastTopic
@ -15,3 +15,4 @@ class BookMutations(object):
update_chapter_visibility = UpdateChapterVisibility.Field() update_chapter_visibility = UpdateChapterVisibility.Field()
sync_module_visibility = SyncModuleVisibility.Field() sync_module_visibility = SyncModuleVisibility.Field()
create_snapshot = CreateSnapshot.Field() create_snapshot = CreateSnapshot.Field()
apply_snapshot = ApplySnapshot.Field()

View File

@ -2,7 +2,7 @@ import graphene
from graphene import relay from graphene import relay
from api.utils import get_object from api.utils import get_object
from books.models import Module from books.models import Module, ContentBlock
from books.models.snapshot import Snapshot from books.models.snapshot import Snapshot
from books.schema.nodes import SnapshotNode from books.schema.nodes import SnapshotNode
from users.models import SchoolClass from users.models import SchoolClass
@ -25,3 +25,32 @@ class CreateSnapshot(relay.ClientIDMutation):
selected_class = get_object(SchoolClass, selected_class_id) selected_class = get_object(SchoolClass, selected_class_id)
snapshot = Snapshot.objects.create_snapshot(module, selected_class, user) snapshot = Snapshot.objects.create_snapshot(module, selected_class, user)
return cls(snapshot=snapshot, success=True) return cls(snapshot=snapshot, success=True)
class ApplySnapshot(relay.ClientIDMutation):
class Input:
snapshot = graphene.ID(required=True)
selected_class = graphene.ID(required=True)
success = graphene.Boolean()
@classmethod
def mutate_and_get_payload(cls, root, info, **args):
snapshot_id = args.get('snapshot')
snapshot = get_object(Snapshot, snapshot_id)
user = info.context.user
selected_class_id = args.get('selected_class')
selected_class = get_object(SchoolClass, selected_class_id)
if not selected_class.users.filter(username=user.username).exists() or not user.is_teacher():
raise PermissionError('Not allowed')
for content_block in snapshot.hidden_content_blocks.all():
content_block.hidden_for.add(selected_class)
for custom_content_block in snapshot.custom_content_blocks.all():
custom_content_block.to_regular_content_block(user, selected_class)
for chapter_snapshot in snapshot.chapters.through.objects.all():
chapter = chapter_snapshot.chapter
if chapter_snapshot.title_hidden:
chapter.title_hidden_for.add(selected_class)
if chapter_snapshot.description_hidden:
chapter.description_hidden_for.add(selected_class)
return cls(success=True)

View File

@ -26,16 +26,17 @@ class ChapterNode(DjangoObjectType):
def resolve_content_blocks(self, info, **kwargs): def resolve_content_blocks(self, info, **kwargs):
user = info.context.user user = info.context.user
school_classes = user.school_classes.values_list('pk') school_classes = user.school_classes.values_list('pk', flat=True)
by_parent = ContentBlock.get_by_parent(self) \ by_parent = ContentBlock.get_by_parent(self) \
.prefetch_related('visible_for') \ .filter(contentblocksnapshot__isnull=True) # exclude snapshots
.prefetch_related('hidden_for') # .prefetch_related('visible_for') \
# .prefetch_related('hidden_for')
# don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret # don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret
default_blocks = Q(user_created=False) default_blocks = Q(user_created=False)
owned_by_user = Q(user_created=True, owner=user) owned_by_user = Q(user_created=True, owner=user)
teacher_created_and_visible = Q(Q(user_created=True) & Q(visible_for__in=school_classes)) teacher_created_and_visible = Q(Q(user_created=True) & Q(visible_for__pk__in=school_classes))
if user.can_manage_school_class_content: # teacher if user.can_manage_school_class_content: # teacher
return by_parent.filter(default_blocks | owned_by_user | teacher_created_and_visible).distinct() return by_parent.filter(default_blocks | owned_by_user | teacher_created_and_visible).distinct()

View File

@ -4,6 +4,7 @@ from graphql_relay import to_global_id, from_global_id
from api.schema import schema from api.schema import schema
from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory
from books.models import Snapshot
from users.models import User, SchoolClass from users.models import User, SchoolClass
from users.services import create_users from users.services import create_users
@ -20,6 +21,7 @@ query ModulesQuery($slug: String!) {
edges { edges {
node { node {
id id
title
visibleFor { visibleFor {
edges { edges {
node { node {
@ -73,6 +75,13 @@ mutation CreateSnapshot($input: CreateSnapshotInput!) {
} }
} }
""" """
APPLY_SNAPSHOT_MUTATION = """
mutation ApplySnapshot($input: ApplySnapshotInput!) {
applySnapshot(input: $input) {
success
}
}
"""
def edges_to_array(entity): def edges_to_array(entity):
@ -82,20 +91,22 @@ def edges_to_array(entity):
class CreateSnapshotTestCase(TestCase): class CreateSnapshotTestCase(TestCase):
def setUp(self): def setUp(self):
create_users() create_users()
self.skillbox_class = SchoolClass.objects.get(name='skillbox')
second_class = SchoolClass.objects.get(name='second_class')
# teacher will create snapshot # teacher will create snapshot
self.teacher = User.objects.get(username='teacher') self.teacher = User.objects.get(username='teacher')
self.module = ModuleFactory(slug='some-module') self.module = ModuleFactory(slug='some-module')
self.skillbox_class = SchoolClass.objects.get(name='skillbox')
# module M has a chapter # module M has a chapter
self.chapter = ChapterFactory(parent=self.module, slug='some-chapter') self.chapter = ChapterFactory(parent=self.module, slug='some-chapter')
# chapter has some content blocks a, b, c # chapter has some content blocks a, b, c
self.title_visible = 'visible' self.title_visible = 'visible'
self.title_hidden = 'hidden' self.title_hidden = 'hidden'
self.title_custom = 'custom' self.title_custom = 'custom'
self.visible_content_block = ContentBlockFactory(parent=self.chapter, module=self.module, self.visible_content_block = ContentBlockFactory(parent=self.chapter, module=self.module,
title=self.title_visible, slug='cb-a') title=self.title_visible, slug='cb-a')
self.hidden_content_block = ContentBlockFactory(parent=self.chapter, module=self.module, title=self.title_hidden, slug='cb-b') self.hidden_content_block = ContentBlockFactory(parent=self.chapter, module=self.module,
title=self.title_hidden, slug='cb-b')
# content block c is user created # content block c is user created
self.custom_content_block = ContentBlockFactory(parent=self.chapter, owner=self.teacher, user_created=True, self.custom_content_block = ContentBlockFactory(parent=self.chapter, owner=self.teacher, user_created=True,
module=self.module, title=self.title_custom, module=self.module, title=self.title_custom,
@ -114,9 +125,8 @@ class CreateSnapshotTestCase(TestCase):
# 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
def test_setup(self): def _test_module_visibility(self, client, school_class_name):
# make sure everything is setup correctly result = client.execute(MODULE_QUERY, variables={
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug 'slug': self.module.slug
}) })
self.assertIsNone(result.get('errors')) self.assertIsNone(result.get('errors'))
@ -124,16 +134,26 @@ class CreateSnapshotTestCase(TestCase):
chapter = edges_to_array(module.get('chapters'))[0] chapter = edges_to_array(module.get('chapters'))[0]
self.assertIsNotNone(chapter) self.assertIsNotNone(chapter)
content_blocks = edges_to_array(chapter.get('contentBlocks')) content_blocks = edges_to_array(chapter.get('contentBlocks'))
content_block_ids = [node['id'] for node in content_blocks] content_block_titles = [node['title'] for node in content_blocks]
self.assertTrue(to_global_id('ContentBlockNode', self.visible_content_block.id) in content_block_ids) self.assertTrue(self.title_visible in content_block_titles)
self.assertTrue(to_global_id('ContentBlockNode', self.hidden_content_block.id) in content_block_ids) self.assertTrue(self.title_hidden in content_block_titles)
self.assertTrue(to_global_id('ContentBlockNode', self.custom_content_block.id) in content_block_ids) self.assertTrue(self.title_custom in content_block_titles)
b = [node for node in content_blocks if hidden_node = [node for node in content_blocks if
node['id'] == to_global_id('ContentBlockNode', self.hidden_content_block.id)][0] node['title'] == self.title_hidden][0]
c = [node for node in content_blocks if custom_node = [node for node in content_blocks if
node['id'] == to_global_id('ContentBlockNode', self.custom_content_block.id)][0] node['title'] == self.title_custom][0]
self.assertTrue('skillbox' in [school_class['name'] for school_class in edges_to_array(b.get('hiddenFor'))]) # check if hidden node is hidden for this school class
self.assertTrue('skillbox' in [school_class['name'] for school_class in edges_to_array(c.get('visibleFor'))]) self.assertTrue(
school_class_name in [school_class['name'] for school_class in
edges_to_array(hidden_node.get('hiddenFor'))])
# check if the custom node is visible for this school class
self.assertTrue(
school_class_name in [school_class['name'] for school_class in
edges_to_array(custom_node.get('visibleFor'))])
def test_setup(self):
# make sure everything is setup correctly
self._test_module_visibility(self.client, 'skillbox')
def test_create_snapshot(self): def test_create_snapshot(self):
result = self.client.execute(CREATE_SNAPSHOT_MUTATION, variables={ result = self.client.execute(CREATE_SNAPSHOT_MUTATION, variables={
@ -154,4 +174,21 @@ class CreateSnapshotTestCase(TestCase):
self.assertEqual(content_blocks[0]['title'], self.title_visible) self.assertEqual(content_blocks[0]['title'], self.title_visible)
self.assertEqual(content_blocks[1]['title'], self.title_custom) self.assertEqual(content_blocks[1]['title'], self.title_custom)
def test_apply_snapshot(self):
self.snapshot = Snapshot.objects.create_snapshot(module=self.module, school_class=self.skillbox_class,
user=self.teacher)
self.assertEqual(Snapshot.objects.count(), 1)
school_class_name = 'second_class'
second_class = SchoolClass.objects.get(name=school_class_name)
request = RequestFactory().get('/')
teacher2 = User.objects.get(username='teacher2')
request.user = teacher2
client = Client(schema=schema, context_value=request)
result = client.execute(APPLY_SNAPSHOT_MUTATION, variables={
'input': {
'snapshot': to_global_id('SnapshotNode', self.snapshot.pk),
'selectedClass': to_global_id('SchoolClassNode', second_class.pk),
}
})
self.assertIsNone(result.get('errors'))
self._test_module_visibility(client, school_class_name)