Merged in feature/duplicate-content-blocks-MS-570-29-09-2022 (pull request #120)
Feature/duplicate content blocks MS-570 29 09 2022 Approved-by: Lorenz Padberg
This commit is contained in:
commit
ecf01971b0
|
|
@ -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: '<p>Asdf</p>'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'assignment',
|
||||
value: {
|
||||
title: 'Ein Auftrag',
|
||||
assignment: 'Eine Beschreibung',
|
||||
id: 'abcd'
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
cy.mockGraphqlOps({
|
||||
operations
|
||||
});
|
||||
cy.visit('/module/some-module');
|
||||
|
||||
cy.getByDataCy('toggle-editing').click();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -11,14 +11,28 @@
|
|||
>
|
||||
<div
|
||||
class="block-actions"
|
||||
v-if="canEditContentBlock && editMode"
|
||||
v-if="canEditModule && !isInstrumentBlock"
|
||||
>
|
||||
<user-widget
|
||||
v-bind="me"
|
||||
class="block-actions__user-widget content-block__user-widget"
|
||||
v-if="isMine"
|
||||
/>
|
||||
<more-options-widget>
|
||||
<li class="popover-links__link">
|
||||
<li
|
||||
class="popover-links__link"
|
||||
v-if="!isInstrumentBlock"
|
||||
>
|
||||
<popover-link
|
||||
data-cy="duplicate-content-block-link"
|
||||
text="Duplizieren"
|
||||
@link-action="duplicateContentBlock(contentBlock)"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
class="popover-links__link"
|
||||
v-if="isMine"
|
||||
>
|
||||
<popover-link
|
||||
data-cy="delete-content-block-link"
|
||||
text="Löschen"
|
||||
|
|
@ -26,7 +40,10 @@
|
|||
/>
|
||||
</li>
|
||||
|
||||
<li class="popover-links__link">
|
||||
<li
|
||||
class="popover-links__link"
|
||||
v-if="isMine"
|
||||
>
|
||||
<popover-link
|
||||
text="Bearbeiten"
|
||||
@link-action="editContentBlock(contentBlock)"
|
||||
|
|
@ -83,13 +100,14 @@
|
|||
|
||||
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
|
||||
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
|
||||
import DUPLICATE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/duplicateContentBlock.gql';
|
||||
|
||||
import me from '@/mixins/me';
|
||||
|
||||
import {hidden} from '@/helpers/visibility';
|
||||
import {CONTENT_TYPE} from '@/consts/types';
|
||||
import PopoverLink from '@/components/ui/PopoverLink';
|
||||
import {removeAtIndex} from '@/graphql/immutable-operations';
|
||||
import {insertAtIndex, removeAtIndex} from '@/graphql/immutable-operations';
|
||||
import {EDIT_CONTENT_BLOCK_PAGE} from '@/router/module.names';
|
||||
import {instrumentCategory} from '@/helpers/instrumentType';
|
||||
|
||||
|
|
@ -163,7 +181,10 @@
|
|||
return {};
|
||||
},
|
||||
canEditContentBlock() {
|
||||
return this.contentBlock.mine && !this.contentBlock.indent;
|
||||
return this.isMine && !this.contentBlock.indent;
|
||||
},
|
||||
isMine() {
|
||||
return this.contentBlock.mine;
|
||||
},
|
||||
contentBlocksWithContentLists() {
|
||||
/*
|
||||
|
|
@ -218,6 +239,36 @@
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
duplicateContentBlock({id}) {
|
||||
const parent = this.parent;
|
||||
this.$apollo.mutate({
|
||||
mutation: DUPLICATE_CONTENT_BLOCK_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
update(store, {data: {duplicateContentBlock: {contentBlock}}}) {
|
||||
if (contentBlock) {
|
||||
const query = CHAPTER_QUERY;
|
||||
const variables = {
|
||||
id: parent.id,
|
||||
};
|
||||
const {chapter} = store.readQuery({query, variables});
|
||||
const index = chapter.contentBlocks.findIndex(contentBlock => 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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
@remove="$emit('remove', false)"
|
||||
/>
|
||||
|
||||
|
||||
<!-- Content Forms -->
|
||||
<content-form-section
|
||||
:title="title"
|
||||
|
|
@ -55,6 +56,16 @@
|
|||
const AssignmentForm = () => 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
#import "../fragments/contentBlockInterfaceParts.gql"
|
||||
#import "../fragments/contentBlockParts.gql"
|
||||
|
||||
mutation DuplicateContentBlock($input: DuplicateContentBlockInput!) {
|
||||
duplicateContentBlock(input: $input) {
|
||||
contentBlock {
|
||||
...ContentBlockInterfaceParts
|
||||
...ContentBlockParts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue