From 5e5b413afb2781bcba72fe3978ec409a554bf30f Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Tue, 23 Mar 2021 14:09:08 +0100 Subject: [PATCH] Add tests to cover more use cases --- .../frontend/sync-module-visibility.spec.js | 2 +- server/api/schema_public.py | 6 +- server/books/models/chapter.py | 20 ++ server/books/models/module.py | 10 + .../test_copy_visibility_for_other_class.py | 236 ++++++++++++++---- server/core/utils.py | 22 +- server/objectives/models.py | 22 +- 7 files changed, 262 insertions(+), 56 deletions(-) diff --git a/client/cypress/integration/frontend/sync-module-visibility.spec.js b/client/cypress/integration/frontend/sync-module-visibility.spec.js index d69b65b0..eb660e2e 100644 --- a/client/cypress/integration/frontend/sync-module-visibility.spec.js +++ b/client/cypress/integration/frontend/sync-module-visibility.spec.js @@ -47,7 +47,7 @@ describe('Apply module visibility', () => { }); }); - it('needs to be implemented', () => { + it('clicks through the UI', () => { // Cypress.config({ // baseUrl: 'http://localhost:8080', // }); diff --git a/server/api/schema_public.py b/server/api/schema_public.py index fac243dc..07af123a 100644 --- a/server/api/schema_public.py +++ b/server/api/schema_public.py @@ -7,17 +7,17 @@ from users.mutations_public import UserMutations from registration.mutations_public import RegistrationMutations -class Mutation(UserMutations, RegistrationMutations, graphene.ObjectType): +class PublicMutation(UserMutations, RegistrationMutations, graphene.ObjectType): if settings.DEBUG: debug = graphene.Field(DjangoDebug, name='_debug') -class Query(AllNewsTeasersQuery, graphene.ObjectType): +class PublicQuery(AllNewsTeasersQuery, graphene.ObjectType): node = graphene.relay.Node.Field() if settings.DEBUG: debug = graphene.Field(DjangoDebug, name='_debug') -schema = graphene.Schema(mutation=Mutation, query=Query) +schema = graphene.Schema(mutation=PublicMutation, query=PublicQuery) diff --git a/server/books/models/chapter.py b/server/books/models/chapter.py index 8f329601..e3a7e382 100644 --- a/server/books/models/chapter.py +++ b/server/books/models/chapter.py @@ -37,3 +37,23 @@ class Chapter(StrictHierarchyPage): title_hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_chapter_titles') description_hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_chapter_descriptions') + + def sync_title_visibility(self, school_class_template, school_class_to_sync): + if self.title_hidden_for.filter(id=school_class_template.id).exists() \ + and not self.title_hidden_for.filter(id=school_class_to_sync.id).exists(): + self.title_hidden_for.add(school_class_to_sync) + + if self.title_hidden_for.filter( + id=school_class_to_sync.id).exists() \ + and not self.title_hidden_for.filter(id=school_class_template.id).exists(): + self.title_hidden_for.remove(school_class_to_sync) + + def sync_description_visibility(self, school_class_template, school_class_to_sync): + if self.description_hidden_for.filter(id=school_class_template.id).exists() \ + and not self.description_hidden_for.filter(id=school_class_to_sync.id).exists(): + self.description_hidden_for.add(school_class_to_sync) + + if self.description_hidden_for.filter( + id=school_class_to_sync.id).exists() \ + and not self.description_hidden_for.filter(id=school_class_template.id).exists(): + self.description_hidden_for.remove(school_class_to_sync) diff --git a/server/books/models/module.py b/server/books/models/module.py index 94a058df..dc0a6462 100644 --- a/server/books/models/module.py +++ b/server/books/models/module.py @@ -86,6 +86,16 @@ class Module(StrictHierarchyPage): # add `school_class_to_sync` to these blocks' `visible for` content_block.visible_for.add(school_class_to_sync) + for chapter in chapters: + chapter.sync_title_visibility(school_class_template, school_class_to_sync) + chapter.sync_description_visibility(school_class_template, school_class_to_sync) + + objective_groups = self.objective_groups.all() + + for objective_group in objective_groups: + objective_group.sync_visibility(school_class_template, 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 6fd27a22..145b0462 100644 --- a/server/books/tests/test_copy_visibility_for_other_class.py +++ b/server/books/tests/test_copy_visibility_for_other_class.py @@ -1,38 +1,86 @@ -import logging - from django.test import TestCase, RequestFactory from graphene.test import Client from graphql_relay import to_global_id from api.schema import schema -from books.models import ContentBlock, Chapter from books.factories import ModuleFactory +from books.models import ContentBlock, Chapter +from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory from users.factories import SchoolClassFactory from users.models import User from users.services import create_users -CONTENT_BLOCK_QUERY = """ - query ContentBlockQuery($id: ID!) { - contentBlock(id: $id) { - hiddenFor { - edges { - node { - id - name - } - } - } - visibleFor { - edges { - node { - id - name - } - } - } - } - } - """ +TEMPLATE_CLASS_NAME = 'template-class' +SYNC_CLASS_NAME = 'class-to-be-synced' + +SCHOOL_CLASS_FRAGMENT = """ +fragment SchoolClassFragment on SchoolClassNode { + id + name +} +""" + +EDGES_FRAGMENT = SCHOOL_CLASS_FRAGMENT + """ +fragment SchoolClassNodeFragment on SchoolClassNodeConnection { + edges { + node { + ...SchoolClassFragment + } + } +} +""" + +CONTENT_BLOCK_QUERY = EDGES_FRAGMENT + """ +query ContentBlockQuery($id: ID!) { + contentBlock(id: $id) { + hiddenFor { + ...SchoolClassNodeFragment + } + visibleFor { + ...SchoolClassNodeFragment + } + } +} +""" + +CHAPTER_QUERY = EDGES_FRAGMENT + """ +query ChapterQuery($id: ID!) { + chapter(id: $id) { + id + titleHiddenFor { + ...SchoolClassNodeFragment + } + descriptionHiddenFor { + ...SchoolClassNodeFragment + } + } +} +""" + +OBJECTIVE_GROUP_QUERY = EDGES_FRAGMENT + """ +query ObjectiveGroupQuery($id: ID!) { + objectiveGroup(id: $id) { + hiddenFor { + ...SchoolClassNodeFragment + } + objectives { + edges { + node { + id + text + hiddenFor { + ...SchoolClassNodeFragment + } + visibleFor { + ...SchoolClassNodeFragment + } + } + } + } + } +} + +""" SYNC_MUTATION = """ mutation SyncMutationVisibility($input: SyncModuleVisibilityInput!) { @@ -60,24 +108,29 @@ class CopyVisibilityForClassesTestCase(TestCase): """ def setUp(self): - module = ModuleFactory(slug='some-module') - chapter = Chapter(title='Some Chapter') - module.add_child(instance=chapter) + # create users and school classes create_users() teacher = User.objects.get(username='teacher') student1 = User.objects.get(username='student1') student2 = User.objects.get(username='student2') # school class to be used as the pattern or model - template_school_class = SchoolClassFactory(name='template-class', users=[teacher, student1]) + template_school_class = SchoolClassFactory(name=TEMPLATE_CLASS_NAME, 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]) + school_class_to_be_synced = SchoolClassFactory(name=SYNC_CLASS_NAME, users=[teacher, student2]) + # create content + module = ModuleFactory(slug='some-module') + chapter = Chapter(title='Some Chapter') + module.add_child(instance=chapter) 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) + objective_group = ObjectiveGroupFactory(module=module) + objective = ObjectiveFactory(group=objective_group) + chapter.specific.add_child(instance=default_content_block) chapter.specific.add_child(instance=hidden_content_block) chapter.specific.add_child(instance=custom_content_block) @@ -90,6 +143,14 @@ class CopyVisibilityForClassesTestCase(TestCase): other_hidden_content_block.hidden_for.add(school_class_to_be_synced) other_custom_content_block.visible_for.add(school_class_to_be_synced) + # hide chapter title and description for student 1 in template-school-class + chapter.title_hidden_for.add(template_school_class) + chapter.description_hidden_for.add(template_school_class) + + # hide objectives for template-school-class + objective_group.hidden_for.add(template_school_class) + objective.hidden_for.add(template_school_class) + teacher_request = RequestFactory().get('/') teacher_request.user = teacher self.teacher_client = Client(schema=schema, context_value=teacher_request) @@ -102,10 +163,12 @@ class CopyVisibilityForClassesTestCase(TestCase): student2_request.user = student2 self.student2_client = Client(schema=schema, context_value=student2_request) + # set references 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.objective_group_id = to_global_id('ObjectiveGroupNode', objective_group.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) @@ -118,12 +181,21 @@ class CopyVisibilityForClassesTestCase(TestCase): }) return result + def _execute_sync(self): + self.teacher_client.execute(SYNC_MUTATION, variables={ + 'input': { + 'module': self.module.slug, + 'templateSchoolClass': to_global_id('SchoolClassNode', self.template_school_class.pk), + 'schoolClass': to_global_id('SchoolClassNode', self.school_class_to_be_synced.pk) + } + }) + def _test_in_sync(self): # 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)) + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) + self.assertTrue(SYNC_CLASS_NAME 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, @@ -139,8 +211,8 @@ class CopyVisibilityForClassesTestCase(TestCase): # 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)) + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], visible_for)) + self.assertTrue(SYNC_CLASS_NAME 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, @@ -153,14 +225,14 @@ class CopyVisibilityForClassesTestCase(TestCase): 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)) + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) + self.assertFalse(SYNC_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) 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)) + self.assertFalse(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) + self.assertTrue(SYNC_CLASS_NAME 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') @@ -168,14 +240,14 @@ class CopyVisibilityForClassesTestCase(TestCase): 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)) + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], visible_for)) + self.assertFalse(SYNC_CLASS_NAME 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)) + self.assertFalse(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], visible_for)) + self.assertTrue(SYNC_CLASS_NAME 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) @@ -183,11 +255,77 @@ class CopyVisibilityForClassesTestCase(TestCase): self._test_in_sync() def test_mutation(self): - self.teacher_client.execute(SYNC_MUTATION, variables={ - 'input': { - 'module': self.module.slug, - 'templateSchoolClass': to_global_id('SchoolClassNode', self.template_school_class.pk), - 'schoolClass': to_global_id('SchoolClassNode', self.school_class_to_be_synced.pk) - } - }) + self._execute_sync() self._test_in_sync() + + def test_chapter_visibility(self): + # chapter title and description hidden for class A + # chapter title and description visible for class B + # sync module + # chapter title and description hidden for class B + query = CHAPTER_QUERY + variables = {"id": self.chapter} + + result = self.student1_client.execute(query, variables=variables) + self.assertIsNone(result.get('errors')) + chapter = result.get('data').get('chapter') + title_hidden_for = chapter.get('titleHiddenFor').get('edges') + description_hidden_for = chapter.get('descriptionHiddenFor').get('edges') + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], title_hidden_for)) + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], description_hidden_for)) + self.assertTrue(SYNC_CLASS_NAME not in map(lambda x: x['node']['name'], title_hidden_for)) + self.assertTrue(SYNC_CLASS_NAME not in map(lambda x: x['node']['name'], description_hidden_for)) + + self._execute_sync() + + result = self.student1_client.execute(query, variables=variables) + self.assertIsNone(result.get('errors')) + chapter = result.get('data').get('chapter') + title_hidden_for = chapter.get('titleHiddenFor').get('edges') + description_hidden_for = chapter.get('descriptionHiddenFor').get('edges') + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], title_hidden_for)) + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], description_hidden_for)) + self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['node']['name'], title_hidden_for)) + self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['node']['name'], description_hidden_for)) + + def _objective_group_query(self): + query = OBJECTIVE_GROUP_QUERY + variables = {"id": self.objective_group_id} + return query, variables + + def _get_objective_group(self, client, query, variables): + result = client.execute(query, variables=variables) + self.assertIsNone(result.get('errors')) + + objective_group = result.get('data').get('objectiveGroup') + return objective_group + + def test_objective_group_visibility(self): + query, variables = self._objective_group_query() + objective_group = self._get_objective_group(self.student1_client, query, variables) + hidden_for = objective_group.get('hiddenFor').get('edges') + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) + self.assertTrue(SYNC_CLASS_NAME not in map(lambda x: x['node']['name'], hidden_for)) + + self._execute_sync() + + objective_group = self._get_objective_group(self.student1_client, query, variables) + hidden_for = objective_group.get('hiddenFor').get('edges') + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) + self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) + + def test_objective_visibility(self): + query, variables = self._objective_group_query() + objective_group = self._get_objective_group(self.student2_client, query, variables) + objective = objective_group.get('objectives').get('edges')[0]['node'] + hidden_for = objective.get('hiddenFor').get('edges') + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) + self.assertTrue(SYNC_CLASS_NAME not in map(lambda x: x['node']['name'], hidden_for)) + + self._execute_sync() + + objective_group = self._get_objective_group(self.student2_client, query, variables) + objective = objective_group.get('objectives').get('edges')[0]['node'] + hidden_for = objective.get('hiddenFor').get('edges') + self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) + self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['node']['name'], hidden_for)) diff --git a/server/core/utils.py b/server/core/utils.py index 2dc1b9db..5cfb4abd 100644 --- a/server/core/utils.py +++ b/server/core/utils.py @@ -37,7 +37,7 @@ def is_private_api_call_allowed(user, body): return True # logout, me and coupon resources are always allowed. Even if the user has no valid license - if re.search(r"mutation\s*.*\s*logout\s*{", body_unicode) or re.search(r"query\s*.*\s*me\s*{", body_unicode)\ + if re.search(r"mutation\s*.*\s*logout\s*{", body_unicode) or re.search(r"query\s*.*\s*me\s*{", body_unicode) \ or re.search(r"mutation\s*Coupon", body_unicode): return True @@ -48,3 +48,23 @@ def is_private_api_call_allowed(user, body): return False return True + + +def sync_hidden_for(model, school_class_template, school_class_to_sync): + if model.hidden_for.filter(id=school_class_template.id).exists() and not model.hidden_for.filter( + id=school_class_to_sync.id).exists(): + model.hidden_for.add(school_class_to_sync) + + if model.hidden_for.filter(id=school_class_to_sync.id).exists() and not model.hidden_for.filter( + id=school_class_template.id).exists(): + model.hidden_for.remove(school_class_to_sync) + + +def sync_visible_for(model, school_class_template, school_class_to_sync): + if model.visible_for.filter(id=school_class_template.id).exists() and not model.visible_for.filter( + id=school_class_to_sync.id).exists(): + model.visible_for.add(school_class_to_sync) + + if model.visible_for.filter(id=school_class_template.id).exists() and not model.visible_for.filter( + id=school_class_to_sync.id).exists(): + model.visible_for.add(school_class_to_sync) diff --git a/server/objectives/models.py b/server/objectives/models.py index 87bf5a86..7b8166b8 100644 --- a/server/objectives/models.py +++ b/server/objectives/models.py @@ -3,6 +3,7 @@ from django.db import models from django.db.models import F from books.models import Module +from core.utils import sync_visible_for, sync_hidden_for from users.models import SchoolClass @@ -21,14 +22,27 @@ class ObjectiveGroup(models.Model): (INTERDISCIPLINARY, 'Überfachliche Lernziele'), ) - title = models.CharField('title', blank=True, null=False, max_length=255, choices=TITLE_CHOICES, default=LANGUAGE_COMMUNICATION) - module = models.ForeignKey(Module, blank=False, null=False, on_delete=models.CASCADE, related_name='objective_groups') + title = models.CharField('title', blank=True, null=False, max_length=255, choices=TITLE_CHOICES, + default=LANGUAGE_COMMUNICATION) + module = models.ForeignKey(Module, blank=False, null=False, on_delete=models.CASCADE, + related_name='objective_groups') hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_objective_groups', blank=True) def __str__(self): return '{} - {}'.format(self.module, self.title) + def sync_visibility(self, school_class_template, school_class_to_sync): + # if self.hidden_for.filter(id=school_class_template.id).exists() and not self.hidden_for.filter(id=school_class_to_sync.id).exists(): + # self.hidden_for.add(school_class_to_sync) + # + # if self.hidden_for.filter(id=school_class_to_sync.id).exists() and not self.hidden_for.filter(id=school_class_template.id).exists(): + # self.hidden_for.remove(school_class_to_sync) + sync_hidden_for(self, school_class_template, school_class_to_sync) + + for objective in self.objectives.all(): + objective.sync_visibility(school_class_template, school_class_to_sync) + class Objective(models.Model): class Meta: @@ -46,3 +60,7 @@ class Objective(models.Model): def __str__(self): return 'Objective {}-{}'.format(self.id, self.text) + + def sync_visibility(self, school_class_template, school_class_to_sync): + sync_hidden_for(self, school_class_template, school_class_to_sync) + sync_visible_for(self, school_class_template, school_class_to_sync)