From 209838dadb6518281172032838a3ca5ace1e3554 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Fri, 5 Mar 2021 14:27:36 +0100 Subject: [PATCH] Add method for syncing school classes --- server/books/models/module.py | 30 +++- .../test_copy_visibility_for_other_class.py | 141 ++++++++++++++---- 2 files changed, 143 insertions(+), 28 deletions(-) diff --git a/server/books/models/module.py b/server/books/models/module.py index 159d923b..2785d3a1 100644 --- a/server/books/models/module.py +++ b/server/books/models/module.py @@ -10,8 +10,6 @@ from books.blocks import DEFAULT_RICH_TEXT_FEATURES from core.wagtail_utils import StrictHierarchyPage from users.models import SchoolClass -logger = logging.getLogger(__name__) - class Module(StrictHierarchyPage): class Meta: @@ -60,6 +58,34 @@ class Module(StrictHierarchyPage): def get_child_ids(self): return self.get_children().values_list('id', flat=True) + def sync_from_school_class(self, school_class_pattern, school_class_to_sync): + # import here so we don't get a circular import error + from books.models import Chapter, ContentBlock + + # get chapters of module + chapters = Chapter.get_by_parent(self) + content_block_query = ContentBlock.objects.none() + + # get content blocks of chapters + for chapter in chapters: + content_block_query = content_block_query.union(ContentBlock.get_by_parent(chapter)) + + # clear all `hidden for` and `visible for` for class `school_class_to_sync` + for content_block in school_class_to_sync.hidden_content_blocks.intersection(content_block_query): + content_block.hidden_for.remove(school_class_to_sync) + for content_block in school_class_to_sync.visible_content_blocks.intersection(content_block_query): + content_block.visible_for.remove(school_class_to_sync) + + # get all content blocks with `hidden for` for class `school_class_pattern` + for content_block in school_class_pattern.hidden_content_blocks.intersection(content_block_query): + # add `school_class_to_sync` to these blocks' `hidden for` + content_block.hidden_for.add(school_class_to_sync) + + # get all content blocks with `visible for` for class `school_class_pattern` + for content_block in school_class_pattern.visible_content_blocks.intersection(content_block_query): + # add `school_class_to_sync` to these blocks' `visible for` + content_block.visible_for.add(school_class_to_sync) + class RecentModule(models.Model): module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='recent_modules') diff --git a/server/books/tests/test_copy_visibility_for_other_class.py b/server/books/tests/test_copy_visibility_for_other_class.py index 39c3dcfd..60cff107 100644 --- a/server/books/tests/test_copy_visibility_for_other_class.py +++ b/server/books/tests/test_copy_visibility_for_other_class.py @@ -11,9 +11,46 @@ from users.factories import SchoolClassFactory from users.models import User from users.services import create_users -logger = logging.getLogger(__name__) +CONTENT_BLOCK_QUERY = """ + query ContentBlockQuery($id: ID!) { + contentBlock(id: $id) { + hiddenFor { + edges { + node { + id + name + } + } + } + visibleFor { + edges { + node { + id + name + } + } + } + } + } + """ + class CopyVisibilityForClassesTestCase(TestCase): + """ + what do we want to happen? + we have 3 public content blocks [X, Y, Z] + we have 2 custom content block [M, N] + we have 2 school classes [A, B] + one public content block is hidden for class A | [X, Y] + one custom content block is visible for class A | [M] + class B also sees two of three public content blocks, but one is different from what A sees | [X, Z] + class B doesn't see the custom content block, but another one | [N] + so A sees | [X, Y, M] + B sees | [X, Z, N] + we want to copy the settings from class A to class B + now class B sees the same content blocks as class A | [X, Y, N] + """ + def setUp(self): module = ModuleFactory() chapter = Chapter(title='Some Chapter') @@ -22,16 +59,28 @@ class CopyVisibilityForClassesTestCase(TestCase): teacher = User.objects.get(username='teacher') student1 = User.objects.get(username='student1') student2 = User.objects.get(username='student2') - school_class1 = SchoolClassFactory(name='hidden-class', users=[teacher, student1]) - school_class2 = SchoolClassFactory(name='default-class', users=[teacher, student2]) + # school class to be used as the pattern or model + template_school_class = SchoolClassFactory(name='template-class', users=[teacher, student1]) + # school class to be synced, e.g. adapted to be like the other + school_class_to_be_synced = SchoolClassFactory(name='class-to-be-synced', users=[teacher, student2]) default_content_block = ContentBlock(title='default block', slug='default') hidden_content_block = ContentBlock(title='hidden block', slug='hidden') + other_hidden_content_block = ContentBlock(title='other hidden block', slug='other-hidden') + custom_content_block = ContentBlock(title='custom block', slug='custom', owner=teacher) + other_custom_content_block = ContentBlock(title='other custom block', slug='other-custom', owner=teacher) chapter.specific.add_child(instance=default_content_block) chapter.specific.add_child(instance=hidden_content_block) + chapter.specific.add_child(instance=custom_content_block) + chapter.specific.add_child(instance=other_custom_content_block) + chapter.specific.add_child(instance=other_hidden_content_block) - hidden_content_block.hidden_for.add(school_class1) + hidden_content_block.hidden_for.add(template_school_class) + custom_content_block.visible_for.add(template_school_class) + + other_hidden_content_block.hidden_for.add(school_class_to_be_synced) + other_custom_content_block.visible_for.add(school_class_to_be_synced) teacher_request = RequestFactory().get('/') teacher_request.user = teacher @@ -45,9 +94,15 @@ class CopyVisibilityForClassesTestCase(TestCase): student2_request.user = student2 self.student2_client = Client(schema=schema, context_value=student2_request) + self.template_school_class = template_school_class + self.school_class_to_be_synced = school_class_to_be_synced + self.module = module self.chapter = to_global_id('ChapterNode', chapter.pk) self.default_content_block = to_global_id('ContentBlockNode', default_content_block.pk) self.hidden_content_block = to_global_id('ContentBlockNode', hidden_content_block.pk) + self.custom_content_block = to_global_id('ContentBlockNode', custom_content_block.pk) + self.other_custom_content_block = to_global_id('ContentBlockNode', other_custom_content_block.pk) + self.other_hidden_content_block = to_global_id('ContentBlockNode', other_hidden_content_block.pk) def _get_result(self, query, client, id): result = client.execute(query, variables={ @@ -55,29 +110,63 @@ class CopyVisibilityForClassesTestCase(TestCase): }) return result - def test_hidden_for_set_correctly(self): - self.assertEqual(ContentBlock.objects.count(), 2) + def test_hidden_for_and_visible_for_set_correctly(self): + self.assertEqual(ContentBlock.objects.count(), 5) - query = """ - query ContentBlockQuery($id: ID!) { - contentBlock(id: $id) { - hiddenFor { - edges { - node { - id - name - } - } - } - } - } - """ + hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.hidden_content_block) + hidden_for = hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges') + self.assertTrue('template-class' in map(lambda x: x['node']['name'], hidden_for)) + self.assertFalse('class-to-be-synced' in map(lambda x: x['node']['name'], hidden_for)) - result = self._get_result(query, self.teacher_client, self.hidden_content_block) - logger.info(result) - hiddenFor = result.get('data').get('contentBlock').get('hiddenFor').get('edges') - logger.info(hiddenFor) - self.assertTrue('hidden-class' in map(lambda x: x['node']['name'], hiddenFor)) - self.assertFalse('default-class' in map(lambda x: x['node']['name'], hiddenFor)) + other_hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, + self.other_hidden_content_block) + hidden_for = other_hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges') + self.assertFalse('template-class' in map(lambda x: x['node']['name'], hidden_for)) + self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], hidden_for)) + default_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.default_content_block) + hidden_for = default_result.get('data').get('contentBlock').get('hiddenFor').get('edges') + self.assertEqual(len(hidden_for), 0) + custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.custom_content_block) + visible_for = custom_result.get('data').get('contentBlock').get('visibleFor').get('edges') + self.assertTrue('template-class' in map(lambda x: x['node']['name'], visible_for)) + self.assertFalse('class-to-be-synced' in map(lambda x: x['node']['name'], visible_for)) + + other_custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, + self.other_custom_content_block) + visible_for = other_custom_result.get('data').get('contentBlock').get('visibleFor').get('edges') + self.assertFalse('template-class' in map(lambda x: x['node']['name'], visible_for)) + self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], visible_for)) + + def test_syncs_correctly(self): + self.module.sync_from_school_class(self.template_school_class, self.school_class_to_be_synced) + + # the hidden block is hidden for both now + hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.hidden_content_block) + hidden_for = hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges') + self.assertTrue('template-class' in map(lambda x: x['node']['name'], hidden_for)) + self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], hidden_for)) + + # the other hidden block is hidden for no one now + other_hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, + self.other_hidden_content_block) + hidden_for = other_hidden_result.get('data').get('contentBlock').get('hiddenFor').get('edges') + self.assertEqual(len(hidden_for), 0) + + # the default block is still hidden for no one + default_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.default_content_block) + hidden_for = default_result.get('data').get('contentBlock').get('hiddenFor').get('edges') + self.assertEqual(len(hidden_for), 0) + + # the custom block is visible for both + custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.custom_content_block) + visible_for = custom_result.get('data').get('contentBlock').get('visibleFor').get('edges') + self.assertTrue('template-class' in map(lambda x: x['node']['name'], visible_for)) + self.assertTrue('class-to-be-synced' in map(lambda x: x['node']['name'], visible_for)) + + # the other custom block is visible for no one + other_custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, + self.other_custom_content_block) + visible_for = other_custom_result.get('data').get('contentBlock').get('visibleFor').get('edges') + self.assertEqual(len(visible_for), 0)