From 8d6f30b2d2766b735b2a65a73fc2789215f4f064 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Sat, 24 Apr 2021 19:59:04 +0200 Subject: [PATCH] Add mutation to apply a snapshot Also add unit test --- server/books/models/contentblock.py | 16 +++++ server/books/schema/mutations/__init__.py | 3 +- server/books/schema/mutations/snapshot.py | 31 +++++++++- server/books/schema/nodes/chapter.py | 9 +-- server/books/tests/test_create_snapshot.py | 71 ++++++++++++++++------ 5 files changed, 107 insertions(+), 23 deletions(-) diff --git a/server/books/models/contentblock.py b/server/books/models/contentblock.py index 7ff57ebf..eebe0ff9 100644 --- a/server/books/models/contentblock.py +++ b/server/books/models/contentblock.py @@ -120,3 +120,19 @@ class ContentBlockSnapshot(ContentBlock): null=True, 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 diff --git a/server/books/schema/mutations/__init__.py b/server/books/schema/mutations/__init__.py index ba8f1c00..92cd8087 100644 --- a/server/books/schema/mutations/__init__.py +++ b/server/books/schema/mutations/__init__.py @@ -1,7 +1,7 @@ from books.schema.mutations.chapter import UpdateChapterVisibility from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock 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 @@ -15,3 +15,4 @@ class BookMutations(object): update_chapter_visibility = UpdateChapterVisibility.Field() sync_module_visibility = SyncModuleVisibility.Field() create_snapshot = CreateSnapshot.Field() + apply_snapshot = ApplySnapshot.Field() diff --git a/server/books/schema/mutations/snapshot.py b/server/books/schema/mutations/snapshot.py index ba149b1b..00338a72 100644 --- a/server/books/schema/mutations/snapshot.py +++ b/server/books/schema/mutations/snapshot.py @@ -2,7 +2,7 @@ import graphene from graphene import relay 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.schema.nodes import SnapshotNode from users.models import SchoolClass @@ -25,3 +25,32 @@ class CreateSnapshot(relay.ClientIDMutation): selected_class = get_object(SchoolClass, selected_class_id) snapshot = Snapshot.objects.create_snapshot(module, selected_class, user) 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) diff --git a/server/books/schema/nodes/chapter.py b/server/books/schema/nodes/chapter.py index 101a4966..941a7264 100644 --- a/server/books/schema/nodes/chapter.py +++ b/server/books/schema/nodes/chapter.py @@ -26,16 +26,17 @@ class ChapterNode(DjangoObjectType): def resolve_content_blocks(self, info, **kwargs): 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) \ - .prefetch_related('visible_for') \ - .prefetch_related('hidden_for') + .filter(contentblocksnapshot__isnull=True) # exclude snapshots + # .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 default_blocks = Q(user_created=False) 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 return by_parent.filter(default_blocks | owned_by_user | teacher_created_and_visible).distinct() diff --git a/server/books/tests/test_create_snapshot.py b/server/books/tests/test_create_snapshot.py index a0261d9f..d3938e31 100644 --- a/server/books/tests/test_create_snapshot.py +++ b/server/books/tests/test_create_snapshot.py @@ -4,6 +4,7 @@ from graphql_relay import to_global_id, from_global_id from api.schema import schema from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory +from books.models import Snapshot from users.models import User, SchoolClass from users.services import create_users @@ -20,6 +21,7 @@ query ModulesQuery($slug: String!) { edges { node { id + title visibleFor { edges { 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): @@ -82,20 +91,22 @@ def edges_to_array(entity): class CreateSnapshotTestCase(TestCase): def setUp(self): create_users() - self.skillbox_class = SchoolClass.objects.get(name='skillbox') - second_class = SchoolClass.objects.get(name='second_class') # teacher will create snapshot self.teacher = User.objects.get(username='teacher') self.module = ModuleFactory(slug='some-module') + self.skillbox_class = SchoolClass.objects.get(name='skillbox') + # module M has a chapter self.chapter = ChapterFactory(parent=self.module, slug='some-chapter') + # chapter has some content blocks a, b, c self.title_visible = 'visible' self.title_hidden = 'hidden' self.title_custom = 'custom' self.visible_content_block = ContentBlockFactory(parent=self.chapter, module=self.module, 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 self.custom_content_block = ContentBlockFactory(parent=self.chapter, owner=self.teacher, user_created=True, module=self.module, title=self.title_custom, @@ -114,9 +125,8 @@ class CreateSnapshotTestCase(TestCase): # we make a snapshot S of the module M # snapshot S looks like module M for school class X - def test_setup(self): - # make sure everything is setup correctly - result = self.client.execute(MODULE_QUERY, variables={ + def _test_module_visibility(self, client, school_class_name): + result = client.execute(MODULE_QUERY, variables={ 'slug': self.module.slug }) self.assertIsNone(result.get('errors')) @@ -124,16 +134,26 @@ class CreateSnapshotTestCase(TestCase): chapter = edges_to_array(module.get('chapters'))[0] self.assertIsNotNone(chapter) content_blocks = edges_to_array(chapter.get('contentBlocks')) - content_block_ids = [node['id'] for node in content_blocks] - self.assertTrue(to_global_id('ContentBlockNode', self.visible_content_block.id) in content_block_ids) - self.assertTrue(to_global_id('ContentBlockNode', self.hidden_content_block.id) in content_block_ids) - self.assertTrue(to_global_id('ContentBlockNode', self.custom_content_block.id) in content_block_ids) - b = [node for node in content_blocks if - node['id'] == to_global_id('ContentBlockNode', self.hidden_content_block.id)][0] - c = [node for node in content_blocks if - node['id'] == to_global_id('ContentBlockNode', self.custom_content_block.id)][0] - self.assertTrue('skillbox' in [school_class['name'] for school_class in edges_to_array(b.get('hiddenFor'))]) - self.assertTrue('skillbox' in [school_class['name'] for school_class in edges_to_array(c.get('visibleFor'))]) + content_block_titles = [node['title'] for node 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 + 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): 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[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)