From 6be6ab809251a716068c63475f650ec234dce44c Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Tue, 20 Sep 2022 23:07:02 +0200 Subject: [PATCH 01/18] Add mutation and unit test for duplication --- server/books/managers.py | 27 +++++++ server/books/models/contentblock.py | 3 + server/books/schema/mutations/__init__.py | 4 +- server/books/schema/mutations/contentblock.py | 19 +++++ .../tests/test_duplicate_content_blocks.py | 76 +++++++++++++++++++ 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 server/books/managers.py create mode 100644 server/books/tests/test_duplicate_content_blocks.py diff --git a/server/books/managers.py b/server/books/managers.py new file mode 100644 index 00000000..603ff304 --- /dev/null +++ b/server/books/managers.py @@ -0,0 +1,27 @@ +from wagtail.core.models import PageManager +from typing import TYPE_CHECKING + +from core.logger import get_logger + +if TYPE_CHECKING: + from books.models import ContentBlock + +logger = get_logger(__name__) + +class ContentBlockManager(PageManager): + def duplicate(self, content_block: "ContentBlock", user): + try: + new_content_block = self.model( + user_created=True, + owner=user, + contents=content_block.contents, + title=content_block.title, + type=content_block.type, + ) + content_block.add_sibling(instance=new_content_block, pos='right') + revision = new_content_block.save_revision() + revision.publish() + new_content_block.save() + return new_content_block + except Exception as e: + logger.warn(e) diff --git a/server/books/models/contentblock.py b/server/books/models/contentblock.py index bc744823..e6505022 100644 --- a/server/books/models/contentblock.py +++ b/server/books/models/contentblock.py @@ -6,6 +6,7 @@ from wagtail.core.blocks import StreamBlock from wagtail.core.fields import StreamField from wagtail.images.blocks import ImageChooserBlock +from books.managers import ContentBlockManager from core.wagtail_utils import get_default_settings from books.blocks import CMSDocumentBlock, SolutionBlock, TextBlock, BasicKnowledgeBlock, LinkBlock, VideoBlock, \ DocumentBlock, \ @@ -90,6 +91,8 @@ class ContentBlock(StrictHierarchyPage): parent_page_types = ['books.Chapter'] subpage_types = [] + objects = ContentBlockManager() + @property def module(self): return self.get_parent().get_parent().specific diff --git a/server/books/schema/mutations/__init__.py b/server/books/schema/mutations/__init__.py index 3b0c01e9..b0587618 100644 --- a/server/books/schema/mutations/__init__.py +++ b/server/books/schema/mutations/__init__.py @@ -1,5 +1,6 @@ from books.schema.mutations.chapter import UpdateChapterVisibility -from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock +from books.schema.mutations.contentblock import DuplicateContentBlock, MutateContentBlock, AddContentBlock, \ + DeleteContentBlock from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility from books.schema.mutations.snapshot import CreateSnapshot, ApplySnapshot, ShareSnapshot, UpdateSnapshot, DeleteSnapshot from books.schema.mutations.topic import UpdateLastTopic @@ -9,6 +10,7 @@ class BookMutations(object): mutate_content_block = MutateContentBlock.Field() add_content_block = AddContentBlock.Field() delete_content_block = DeleteContentBlock.Field() + duplicate_content_block = DuplicateContentBlock.Field() update_solution_visibility = UpdateSolutionVisibility.Field() update_last_module = UpdateLastModule.Field() update_last_topic = UpdateLastTopic.Field() diff --git a/server/books/schema/mutations/contentblock.py b/server/books/schema/mutations/contentblock.py index fda65367..43e2d39b 100644 --- a/server/books/schema/mutations/contentblock.py +++ b/server/books/schema/mutations/contentblock.py @@ -143,3 +143,22 @@ class DeleteContentBlock(relay.ClientIDMutation): return cls(success=True) except ContentBlock.DoesNotExist: return cls(success=False, errors='Content block not found') + + +class DuplicateContentBlock(relay.ClientIDMutation): + class Input: + id = graphene.ID(required=True) + + content_block = graphene.Field(ContentBlockNode) + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + id = from_global_id(kwargs.get('id'))[1] + user = info.context.user + + try: + content_block = ContentBlock.objects.get(pk=id) + new_content_block = ContentBlock.objects.duplicate(content_block=content_block, user=user) + return cls(content_block=new_content_block) + except ContentBlock.DoesNotExist: + return cls(content_block=None) diff --git a/server/books/tests/test_duplicate_content_blocks.py b/server/books/tests/test_duplicate_content_blocks.py new file mode 100644 index 00000000..12f891fb --- /dev/null +++ b/server/books/tests/test_duplicate_content_blocks.py @@ -0,0 +1,76 @@ +from graphql_relay import to_global_id + +from books.factories import ContentBlockFactory, ModuleFactory, ChapterFactory +from books.models import ContentBlock +from core.tests.base_test import SkillboxTestCase + +DUPLICATE_CONTENT_BLOCK_MUTATION = """ +mutation DuplicateContentBlockMutation($input: DuplicateContentBlockInput!) { + duplicateContentBlock(input: $input) { + contentBlock { + id + } + } +} +""" + +CONTENT_BLOCK_QUERY = """ +query ContentBlockQuery($slug: String!) { + module(slug: $slug) { + chapters { + id + contentBlocks { + id + title + type + } + } + } +} +""" + + +class DuplicateContentBlockTestCase(SkillboxTestCase): + def setUp(self) -> None: + self.createDefault() + # + self.slug = 'module' + self.module = ModuleFactory(slug=self.slug) + self.chapter = ChapterFactory(parent=self.module) + self.content_block = ContentBlock( + type=ContentBlock.NORMAL, + title='Title', + ) + self.chapter.add_child(instance=self.content_block) + + def test_duplicate_content_block(self): + result = self.get_client().execute(CONTENT_BLOCK_QUERY, variables={ + 'slug': self.slug + }) + self.assertIsNone(result.errors) + module = result.data.get('module') + chapter = module.get('chapters')[0] + content_blocks = chapter.get('contentBlocks') + self.assertEqual(len(content_blocks), 1) + self.assertEqual(ContentBlock.objects.count(), 1) + + result = self.get_client().execute(DUPLICATE_CONTENT_BLOCK_MUTATION, variables={ + 'input': { + "id": to_global_id('ContentBlockNode', self.content_block.id) + } + }) + self.assertIsNone(result.errors) + duplicate_content_block = result.data.get('duplicateContentBlock') + content_block = duplicate_content_block.get('contentBlock') + # self.assertEqual(content_block['title'], 'Title') + self.assertIsNotNone(content_block['id']) + + result = self.get_client().execute(CONTENT_BLOCK_QUERY, variables={ + 'slug': self.slug + }) + self.assertIsNone(result.errors) + module = result.data.get('module') + chapter = module.get('chapters')[0] + content_blocks = chapter.get('contentBlocks') + self.assertEqual(ContentBlock.objects.count(), 2) + self.assertEqual(len(content_blocks), 2) From 4693d2c01ae570a49c1ed7a1512fdb4892fe4c8b Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Wed, 21 Sep 2022 16:43:51 +0200 Subject: [PATCH 02/18] Add duplicate action to frontend --- .../modules/custom-content-block.spec.js | 8 +++ client/src/components/ContentBlock.vue | 60 +++++++++++++++++-- .../gql/mutations/duplicateContentBlock.gql | 11 ++++ server/books/managers.py | 2 +- server/schema.graphql | 11 ++++ 5 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 client/src/graphql/gql/mutations/duplicateContentBlock.gql diff --git a/client/cypress/integration/frontend/modules/custom-content-block.spec.js b/client/cypress/integration/frontend/modules/custom-content-block.spec.js index 21bf98c8..d569de30 100644 --- a/client/cypress/integration/frontend/modules/custom-content-block.spec.js +++ b/client/cypress/integration/frontend/modules/custom-content-block.spec.js @@ -61,6 +61,14 @@ describe('Custom Content Block', () => { cy.log('Opening More Menu'); cy.getByDataCy('more-options-link').click(); + cy.log('Duplicating Content Block'); + cy.getByDataCy('duplicate-content-block-link').click(); + + cy.get('.content-block').should('have.length', 2); + + cy.log('Opening More Menu'); + cy.getByDataCy('more-options-link').click(); + // check if content block is still there cy.log('Deleting Content Block'); cy.getByDataCy('delete-content-block-link').click(); diff --git a/client/src/components/ContentBlock.vue b/client/src/components/ContentBlock.vue index 77cd254f..2fabf50b 100644 --- a/client/src/components/ContentBlock.vue +++ b/client/src/components/ContentBlock.vue @@ -11,14 +11,27 @@ >
- + -