diff --git a/client/cypress/e2e/frontend/modules/duplicate-content-block.cy.ts b/client/cypress/e2e/frontend/modules/duplicate-content-block.cy.ts
new file mode 100644
index 00000000..f68e6644
--- /dev/null
+++ b/client/cypress/e2e/frontend/modules/duplicate-content-block.cy.ts
@@ -0,0 +1,62 @@
+import {getMinimalMe} from '../../../support/helpers';
+
+describe('Duplicate Content Block', () => {
+ beforeEach(() => {
+ cy.setup();
+ });
+ it.skip('works', () => {
+ // todo: does not work right now, as the cache does not seem to update for the 'inEditMode' local query. Need to
+ // make this work for the test to work right.
+ const operations = {
+ MeQuery: getMinimalMe({isTeacher: true}),
+ UpdateLastModule: {},
+ ModuleEditModeQuery: {
+ module: {
+ }
+ },
+ AssignmentQuery: {
+ assignment: {
+ title: 'Ein Assignment',
+ assignment: 'Eine Beschreibung'
+ }
+ },
+ ModuleDetailsQuery: {
+ module: {
+ chapters: [
+ {
+ contentBlocks: [
+ {
+ type: 'normal',
+ title: 'Hello there',
+ contents: [
+ {
+ type: 'text_block',
+ value: {
+ text: '
Asdf
'
+ }
+ },
+ {
+ type: 'assignment',
+ value: {
+ title: 'Ein Auftrag',
+ assignment: 'Eine Beschreibung',
+ id: 'abcd'
+ }
+ }
+
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ };
+ cy.mockGraphqlOps({
+ operations
+ });
+ cy.visit('/module/some-module');
+
+ cy.getByDataCy('toggle-editing').click();
+ });
+});
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..4f5cb45f 100644
--- a/client/src/components/ContentBlock.vue
+++ b/client/src/components/ContentBlock.vue
@@ -11,14 +11,28 @@
>
-
+
+
+
+
-
+
contentBlock.id === id);
+ const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock);
+ const data = {
+ chapter: {
+ ...chapter,
+ contentBlocks,
+ },
+ };
+ store.writeQuery({query, variables, data});
+ }
+ },
+ });
+
+ },
editContentBlock(contentBlock) {
const route = {
name: EDIT_CONTENT_BLOCK_PAGE,
diff --git a/client/src/components/content-block-form/ContentBlockForm.vue b/client/src/components/content-block-form/ContentBlockForm.vue
index dd933f50..19269ff9 100644
--- a/client/src/components/content-block-form/ContentBlockForm.vue
+++ b/client/src/components/content-block-form/ContentBlockForm.vue
@@ -138,7 +138,13 @@
import AddContentLink from '@/components/content-block-form/AddContentLink.vue';
import ContentElement from '@/components/content-block-form/ContentElement.vue';
- import {moveToIndex, removeAtIndex, replaceAtIndex, swapElements} from '@/graphql/immutable-operations';
+ import {
+ insertAtIndex,
+ moveToIndex,
+ removeAtIndex,
+ replaceAtIndex,
+ swapElements
+ } from '@/graphql/immutable-operations';
import {CHOOSER, transformInnerContents} from '@/components/content-block-form/helpers.js';
import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue';
@@ -147,7 +153,7 @@
// TODO: refactor this file, it's huuuuuge!
interface ContentBlockFormData {
- localContentBlock: any;
+ localContentBlock: any;
}
export default Vue.extend({
@@ -201,60 +207,42 @@
update(index: number, element: any, parent?: number) {
if (parent === undefined) {
// element is top level
- this.localContentBlock.contents = [
- ...this.localContentBlock.contents.slice(0, index),
- element,
- ...this.localContentBlock.contents.slice(index + 1),
- ];
+ this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, index, element);
} else {
const parentBlock = this.localContentBlock.contents[parent];
- this.localContentBlock.contents = [
- ...this.localContentBlock.contents.slice(0, parent),
- {
- ...parentBlock,
- contents: [
- ...parentBlock.contents.slice(0, index),
- element,
- ...parentBlock.contents.slice(index + 1),
- ],
- },
- ...this.localContentBlock.contents.slice(parent + 1),
- ];
+ const newElementContents = replaceAtIndex(parentBlock.contents, index, element);
+ const newBlock = {
+ ...parentBlock,
+ contents: newElementContents,
+ };
+ this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, parent, newBlock);
}
},
addBlock(afterOuterIndex: number, innerIndex?: number) {
if (innerIndex !== undefined) {
const block = this.localContentBlock.contents[afterOuterIndex];
- this.localContentBlock.contents = [
- ...this.localContentBlock.contents.slice(0, afterOuterIndex),
- {
- ...block,
- contents: [
- ...block.contents.slice(0, innerIndex + 1),
- {
- id: -1,
- type: CHOOSER,
- },
- ...block.contents.slice(innerIndex + 1),
- ],
- },
- ...this.localContentBlock.contents.slice(afterOuterIndex + 1),
- ];
- } else {
- this.localContentBlock.contents = [
- ...this.localContentBlock.contents.slice(0, afterOuterIndex + 1),
- {
+ const element = {
+ ...block,
+ contents: insertAtIndex(block.contents, innerIndex + 1, {
id: -1,
type: CHOOSER,
- includeListOption: true,
- },
- ...this.localContentBlock.contents.slice(afterOuterIndex + 1),
- ];
+ }),
+ };
+
+ this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, afterOuterIndex, element);
+ } else {
+ const element = {
+ id: -1,
+ type: CHOOSER,
+ includeListOption: true,
+ };
+
+ this.localContentBlock.contents = insertAtIndex(this.localContentBlock.contents, afterOuterIndex + 1, element);
}
},
remove(outer: number, inner?: number, askForConfirmation = true) {
- if(askForConfirmation) {
+ if (askForConfirmation) {
this.$modal.open('confirm')
.then(() => {
this.executeRemoval(outer, inner);
diff --git a/client/src/components/content-block-form/ContentElement.vue b/client/src/components/content-block-form/ContentElement.vue
index 26c24c3e..d59555a9 100644
--- a/client/src/components/content-block-form/ContentElement.vue
+++ b/client/src/components/content-block-form/ContentElement.vue
@@ -9,6 +9,7 @@
@remove="$emit('remove', false)"
/>
+
import(/* webpackChunkName: "content-forms" */'@/components/content-forms/AssignmentForm');
const TextForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/TipTap.vue');
const SubtitleForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/SubtitleForm');
+ // readonly blocks
+ const Assignment = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/assignment/Assignment');
+ const SurveyBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/SurveyBlock');
+ const Solution = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/Solution');
+ const ImageBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ImageBlock');
+ const Instruction = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/Instruction');
+ const ModuleRoomSlug = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ModuleRoomSlug');
+ const CmsDocumentBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/CmsDocumentBlock');
+ const ThinglinkBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ThinglinkBlock');
+ const InfogramBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/InfogramBlock');
const CHOOSER = 'content-block-element-chooser-widget';
@@ -91,6 +102,15 @@
AssignmentForm,
TextForm,
SubtitleForm,
+ SurveyBlock,
+ Solution,
+ ImageBlock,
+ Instruction,
+ ModuleRoomSlug,
+ CmsDocumentBlock,
+ InfogramBlock,
+ ThinglinkBlock,
+ Assignment
},
computed: {
@@ -153,7 +173,7 @@
};
case 'assignment':
return {
- component: 'assignment-form',
+ component: element.id ? 'assignment' : 'assignment-form', // prevent editing of existing assignments
title: 'Aufgabe & Ergebnis',
icon: 'speech-bubble-icon',
};
@@ -163,6 +183,46 @@
title: 'Dokument',
icon: 'document-icon',
};
+ case 'survey':
+ return {
+ component: 'survey-block',
+ title: 'Übung',
+ };
+ case 'solution':
+ return {
+ component: 'solution',
+ title: 'Lösung',
+ };
+ case 'image_block':
+ return {
+ component: 'image-block',
+ title: 'Bild',
+ };
+ case 'instruction':
+ return {
+ component: 'instruction',
+ title: 'Instruktion',
+ };
+ case 'module_room_slug':
+ return {
+ component: 'module-room-slug',
+ title: 'Raum',
+ };
+ case 'cms_document_block':
+ return {
+ component: 'cms-document-block',
+ title: 'Dokument',
+ };
+ case 'thinglink_block':
+ return {
+ component: 'thinglink-block',
+ title: 'Interaktive Grafik'
+ };
+ case 'infogram_block':
+ return {
+ component: 'infogram-block',
+ title: 'Interaktive Grafik'
+ };
}
return {
component: CHOOSER,
diff --git a/client/src/components/content-block-form/helpers.js b/client/src/components/content-block-form/helpers.js
index 583be25c..097ec7f9 100644
--- a/client/src/components/content-block-form/helpers.js
+++ b/client/src/components/content-block-form/helpers.js
@@ -1,5 +1,28 @@
export const CHOOSER = 'content-block-element-chooser-widget';
export const chooserFilter = value => value.type !== CHOOSER;
+export const USER_CONTENT_TYPES = ['subtitle', 'link_block', 'video_block', 'image_url_block', 'text_block', 'assignment', 'document_block'];
+
+/*
+ Users can only edit certain types of contents, the rest can only be re-ordered. We only care about their id, we won't
+ send anything else to the server about them
+ */
+export const simplifyContents = (contents) => {
+ return contents.map(c => {
+ if (USER_CONTENT_TYPES.includes(c.type)) {
+ return c;
+ }
+ if (c.type === 'content_list_item') {
+ return {
+ ...c,
+ contents: simplifyContents(c.contents)
+ };
+ }
+ return {
+ id: c.id,
+ type: 'readonly'
+ };
+ });
+};
export const cleanUpContents = (contents) => {
let filteredContents = contents
diff --git a/client/src/graphql/gql/mutations/duplicateContentBlock.gql b/client/src/graphql/gql/mutations/duplicateContentBlock.gql
new file mode 100644
index 00000000..dbefec53
--- /dev/null
+++ b/client/src/graphql/gql/mutations/duplicateContentBlock.gql
@@ -0,0 +1,11 @@
+#import "../fragments/contentBlockInterfaceParts.gql"
+#import "../fragments/contentBlockParts.gql"
+
+mutation DuplicateContentBlock($input: DuplicateContentBlockInput!) {
+ duplicateContentBlock(input: $input) {
+ contentBlock {
+ ...ContentBlockInterfaceParts
+ ...ContentBlockParts
+ }
+ }
+}
diff --git a/client/src/pages/editContentBlock.vue b/client/src/pages/editContentBlock.vue
index 2d379f9c..ef98c1c7 100644
--- a/client/src/pages/editContentBlock.vue
+++ b/client/src/pages/editContentBlock.vue
@@ -14,7 +14,7 @@
import EDIT_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/editContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import {setUserBlockType} from '@/helpers/content-block';
- import {cleanUpContents} from '@/components/content-block-form/helpers';
+ import {cleanUpContents, simplifyContents} from '@/components/content-block-form/helpers';
// import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
@@ -62,10 +62,11 @@
methods: {
save({title, contents, isAssignment, id}) {
- let cleanedContents = cleanUpContents(contents);
+ const cleanedContents = cleanUpContents(contents);
+ const simplifiedContents = simplifyContents(cleanedContents);
const contentBlock = {
title: title,
- contents: cleanedContents,
+ contents: simplifiedContents,
type: setUserBlockType(isAssignment),
};
const { slug } = this.$route.params;
diff --git a/server/books/factories.py b/server/books/factories.py
index ea40f8d7..531974c0 100644
--- a/server/books/factories.py
+++ b/server/books/factories.py
@@ -175,7 +175,7 @@ class ContentBlockFactory(BasePageFactory):
if stream_field_name in kwargs:
"""
stream_field_name is most likely 'contents'
- this means: if there is a property named contents, us the defined ones in this block.
+ this means: if there is a property named contents, use the defined ones in this block.
otherwise, go into the other block and randomize the contents
"""
for idx, resource in enumerate(kwargs[stream_field_name]):
diff --git a/server/books/managers.py b/server/books/managers.py
new file mode 100644
index 00000000..761d58b7
--- /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=f'{content_block.title} (Kopie)',
+ type=content_block.type,
+ )
+ content_block.add_sibling(instance=new_content_block, pos='left')
+ 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/inputs.py b/server/books/schema/inputs.py
index 31bd3a3e..99820e11 100644
--- a/server/books/schema/inputs.py
+++ b/server/books/schema/inputs.py
@@ -13,6 +13,7 @@ class InputTypes(graphene.Enum):
document_block = 'document_block'
content_list_item = 'content_list_item'
subtitle = 'subtitle'
+ readonly = 'readonly'
class ContentElementValueInput(InputObjectType):
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..0cb3a220 100644
--- a/server/books/schema/mutations/contentblock.py
+++ b/server/books/schema/mutations/contentblock.py
@@ -48,7 +48,7 @@ class MutateContentBlock(relay.ClientIDMutation):
content_block.title = title
if contents is not None:
- content_block.contents = json.dumps([handle_content_block(c, info.context, module) for c in contents])
+ content_block.contents = json.dumps([handle_content_block(c, info.context, module, previous_contents=content_block.contents) for c in contents if c is not None])
content_block.save()
@@ -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/schema/mutations/utils.py b/server/books/schema/mutations/utils.py
index 821e012f..2579e802 100644
--- a/server/books/schema/mutations/utils.py
+++ b/server/books/schema/mutations/utils.py
@@ -41,10 +41,11 @@ ALLOWED_BLOCKS = (
'document_block',
'content_list_item',
'subtitle',
+ 'readonly'
)
-def handle_content_block(content, context=None, module=None, allowed_blocks=ALLOWED_BLOCKS):
+def handle_content_block(content, context=None, module=None, allowed_blocks=ALLOWED_BLOCKS, previous_contents=None):
# todo: add all the content blocks
# todo: sanitize user inputs!
if content['type'] not in allowed_blocks:
@@ -64,9 +65,10 @@ def handle_content_block(content, context=None, module=None, allowed_blocks=ALLO
value = content['value']
if value.get('id') is not None:
assignment = get_object(Assignment, value.get('id'))
- assignment.title = value.get('title')
- assignment.assignment = value.get('assignment')
- assignment.save()
+ if assignment.user_created and assignment.owner == context.user:
+ assignment.title = value.get('title')
+ assignment.assignment = value.get('assignment')
+ assignment.save()
else:
assignment = Assignment.objects.create(
title=value.get('title'),
@@ -119,7 +121,12 @@ def handle_content_block(content, context=None, module=None, allowed_blocks=ALLO
'type': 'content_list_item',
'value': [handle_content_block(c, context, module) for c in content['contents']]
}
-
+ elif content['type'] == 'readonly' and previous_contents is not None:
+ # get first item that matches the id
+ # users can re-order readonly items, but we won't let them change them otherwise, so we just take the
+ # item from before and ignore anything else
+ previous_content = next((c for c in previous_contents.raw_data if c['id'] == content['id']), None)
+ return previous_content
return 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..7c049102
--- /dev/null
+++ b/server/books/tests/test_duplicate_content_blocks.py
@@ -0,0 +1,186 @@
+from graphql_relay import to_global_id
+from wagtail.core.fields import StreamField
+from wagtail.tests.utils.form_data import streamfield, nested_form_data, rich_text
+
+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)
+ self.assertTrue('Kopie' in content_blocks[0].get('title'))
+ self.assertTrue('Kopie' not in content_blocks[1].get('title'))
+
+ # def test_duplicate_non_editable_contents(self):
+ # # todo: find out how to create contents for tests, then re-enable
+ # # contents__0__text_block__text
+ # nested_form_data({
+ # 'content': streamfield([
+ # nested_form_data({
+ # 'text_block': [
+ # ('text', rich_text('Asdf'))
+ # ]
+ # })
+ # ])
+ # })
+ #
+ # contents = [
+ # nested_form_data({
+ # 'text_block': streamfield([
+ # ('text', 'Asdf')
+ # ])
+ # }),
+ # # {
+ # # "type": "text_block",
+ # # "value": {
+ # # "text": "Asdf"
+ # # },
+ # # },
+ # # {
+ # # "type": "assignment",
+ # # "value": {
+ # # "title": "Ein Auftragstitel",
+ # # "assignment": "Ein Auftrag",
+ # # },
+ # # },
+ # # {
+ # # "type": "image_block",
+ # # "value": {
+ # # "path": "/media/original_images/dummy_pZUH02q.jpg"
+ # # },
+ # # },
+ # # {
+ # # "type": "image_url_block",
+ # # "value": {
+ # # "title": "Asdf",
+ # # "url": "http://localhost:8000/media/images/dummy_pZUH02q.max-165x165.jpg"
+ # # },
+ # # },
+ # # {
+ # # "type": "link_block",
+ # # "value": {
+ # # "text": "Asdf",
+ # # "url": "https://iterativ.ch"
+ # # },
+ # # },
+ # # {
+ # # "type": "solution",
+ # # "value": {
+ # # "text": "Asdf",
+ # # },
+ # # },
+ # # {
+ # # "type": "video_block",
+ # # "value": {
+ # # "url": "https://www.youtube.com/watch?v=QxQBWR7sntI"
+ # # },
+ # # },
+ # # {
+ # # "type": "document_block",
+ # # "value": {
+ # # "url": "http://localhost:8000/media/images/dummy_pZUH02q.max-165x165.jpg"
+ # # },
+ # # },
+ # # {
+ # # "type": "infogram_block",
+ # # "value": {
+ # # "id": "4405271e-dbfb-407e-ac19-0a238cde393f",
+ # # "title": "Gerät Internetnutzung Jungen"
+ # # },
+ # # },
+ # # {
+ # # "type": "thinglink_block",
+ # # "value": {
+ # # "id": "1314204266449076227"
+ # # },
+ # # },
+ # # {
+ # # "type": "subtitle",
+ # # "value": {
+ # # "text": "Subtitle"
+ # # },
+ # # },
+ # # {
+ # # "type": "instruction",
+ # # "value": {
+ # # "url": "http://localhost:8000/media/images/dummy_pZUH02q.max-165x165.jpg",
+ # # "text": "Instruction",
+ # # },
+ # # },
+ # # {
+ # # "type": "module_room_slug",
+ # # "value": {
+ # # "title": "Raum",
+ # # },
+ # # },
+ # ]
+ # self.content_block.contents = contents
+ # self.assertTrue(False)
diff --git a/server/schema.graphql b/server/schema.graphql
index 7a19e840..0cb896a5 100644
--- a/server/schema.graphql
+++ b/server/schema.graphql
@@ -478,6 +478,16 @@ type DjangoDebugSQL {
encoding: String
}
+input DuplicateContentBlockInput {
+ id: ID!
+ clientMutationId: String
+}
+
+type DuplicateContentBlockPayload {
+ contentBlock: ContentBlockNode
+ clientMutationId: String
+}
+
type DuplicateName {
reason: String
}
@@ -694,6 +704,7 @@ type Mutation {
mutateContentBlock(input: MutateContentBlockInput!): MutateContentBlockPayload
addContentBlock(input: AddContentBlockInput!): AddContentBlockPayload
deleteContentBlock(input: DeleteContentBlockInput!): DeleteContentBlockPayload
+ duplicateContentBlock(input: DuplicateContentBlockInput!): DuplicateContentBlockPayload
updateSolutionVisibility(input: UpdateSolutionVisibilityInput!): UpdateSolutionVisibilityPayload
updateLastModule(input: UpdateLastModuleInput!): UpdateLastModulePayload
updateLastTopic(input: UpdateLastTopicInput!): UpdateLastTopicPayload