Merged in feature/nested-block (pull request #26)
Feature/nested block Approved-by: Ramon Wenger <ramon.wenger@iterativ.ch>
This commit is contained in:
commit
a1234f6688
|
|
@ -3,19 +3,20 @@
|
||||||
<div class="content-block" :class="specialClass">
|
<div class="content-block" :class="specialClass">
|
||||||
<div class="content-block__actions">
|
<div class="content-block__actions">
|
||||||
<visibility-action
|
<visibility-action
|
||||||
|
v-if="!contentBlock.indent"
|
||||||
:block="contentBlock"></visibility-action>
|
:block="contentBlock"></visibility-action>
|
||||||
<a @click="editContentBlock()" v-if="contentBlock.mine" class="content-block__action-button">
|
<a @click="editContentBlock()" v-if="canEditContentBlock" class="content-block__action-button">
|
||||||
<pen-icon class="content-block__action-icon action-icon"></pen-icon>
|
<pen-icon class="content-block__action-icon action-icon"></pen-icon>
|
||||||
</a>
|
</a>
|
||||||
<a @click="deleteContentBlock(contentBlock.id)" v-if="contentBlock.mine" class="content-block__action-button">
|
<a @click="deleteContentBlock(contentBlock.id)" v-if="canEditContentBlock" class="content-block__action-button">
|
||||||
<trash-icon class="content-block__action-icon action-icon"></trash-icon>
|
<trash-icon class="content-block__action-icon action-icon"></trash-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 v-if="instrumentLabel !== ''" class="content-block__instrument-label">{{instrumentLabel}}</h3>
|
<h3 v-if="instrumentLabel !== ''" class="content-block__instrument-label">{{instrumentLabel}}</h3>
|
||||||
<h4 class="content-block__title">{{contentBlock.title}}</h4>
|
<h4 class="content-block__title" v-if="!contentBlock.indent">{{contentBlock.title}}</h4>
|
||||||
|
|
||||||
<component v-for="component in contentBlock.contents"
|
<component v-for="component in contentBlocksWithContentLists.contents"
|
||||||
:key="component.id"
|
:key="component.id"
|
||||||
:is="component.type"
|
:is="component.type"
|
||||||
v-bind="component">
|
v-bind="component">
|
||||||
|
|
@ -23,7 +24,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<add-content-block-button :after="contentBlock.id"></add-content-block-button>
|
<add-content-block-button :after="contentBlock.id" v-if="!contentBlock.indent"></add-content-block-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -41,6 +42,7 @@
|
||||||
import InfogramBlock from '@/components/content-blocks/InfogramBlock';
|
import InfogramBlock from '@/components/content-blocks/InfogramBlock';
|
||||||
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
|
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
|
||||||
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
|
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
|
||||||
|
import ContentListBlock from '@/components/content-blocks/ContentListBlock';
|
||||||
import Assignment from '@/components/content-blocks/assignment/Assignment';
|
import Assignment from '@/components/content-blocks/assignment/Assignment';
|
||||||
import Solution from '@/components/content-blocks/Solution';
|
import Solution from '@/components/content-blocks/Solution';
|
||||||
import AddContentBlockButton from '@/components/AddContentBlockButton';
|
import AddContentBlockButton from '@/components/AddContentBlockButton';
|
||||||
|
|
@ -59,6 +61,7 @@
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['contentBlock', 'parent'],
|
props: ['contentBlock', 'parent'],
|
||||||
|
name: 'content-block',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
'text_block': TextBlock,
|
'text_block': TextBlock,
|
||||||
|
|
@ -72,6 +75,7 @@
|
||||||
'infogram_block': InfogramBlock,
|
'infogram_block': InfogramBlock,
|
||||||
'genially_block': GeniallyBlock,
|
'genially_block': GeniallyBlock,
|
||||||
'subtitle': SubtitleBlock,
|
'subtitle': SubtitleBlock,
|
||||||
|
'content_list': ContentListBlock,
|
||||||
Solution,
|
Solution,
|
||||||
Assignment,
|
Assignment,
|
||||||
Task,
|
Task,
|
||||||
|
|
@ -93,6 +97,59 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return `Instrument - ${instruments[contentType]}`
|
return `Instrument - ${instruments[contentType]}`
|
||||||
|
},
|
||||||
|
canEditContentBlock() {
|
||||||
|
return this.contentBlock.mine && !this.contentBlock.indent;
|
||||||
|
},
|
||||||
|
contentBlocksWithContentLists() {
|
||||||
|
/*
|
||||||
|
collects all conent_list_items in content_lists:
|
||||||
|
{
|
||||||
|
text_block,
|
||||||
|
content_list_item: [contents...],
|
||||||
|
content_list_item: [contents...],
|
||||||
|
text_block
|
||||||
|
} becomes
|
||||||
|
{
|
||||||
|
text_block,
|
||||||
|
content_list: [content_list_item: [contents...], content_list_item: [contents...]],
|
||||||
|
text_block
|
||||||
|
}
|
||||||
|
if there's only a single content_list_item it should not be displayed as list like so
|
||||||
|
{
|
||||||
|
text_block,
|
||||||
|
content_list_item: [text_block, image_block],
|
||||||
|
} becomes
|
||||||
|
{
|
||||||
|
text_block,
|
||||||
|
text_block,
|
||||||
|
image_block
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let contentList = [];
|
||||||
|
let newContent = this.contentBlock.contents.reduce((newContents, content, index) => {
|
||||||
|
// collect content_list_items
|
||||||
|
if (content.type === 'content_list_item') {
|
||||||
|
contentList = [...contentList, content]
|
||||||
|
if (index === this.contentBlock.contents.length - 1) { // content is last element of contents array
|
||||||
|
return [...newContents, ...this.createContentListOrBlocks(contentList)];
|
||||||
|
}
|
||||||
|
return newContents
|
||||||
|
} else {
|
||||||
|
// handle all other items and reset current content_list if necessary
|
||||||
|
if (contentList.length !== 0) {
|
||||||
|
newContents = [...newContents, ...this.createContentListOrBlocks(contentList), content];
|
||||||
|
contentList = [];
|
||||||
|
return newContents;
|
||||||
|
} else {
|
||||||
|
return [...newContents, content]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return Object.assign({}, this.contentBlock, {
|
||||||
|
contents: newContent
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -125,9 +182,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
createContentListOrBlocks(contentList) {
|
||||||
|
// if list contains only one item, return blocks
|
||||||
|
if (contentList.length === 1) {
|
||||||
|
return contentList[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
type: 'content_list',
|
||||||
|
contents: contentList,
|
||||||
|
id: contentList[0].id
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showVisibility: false
|
showVisibility: false
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<div class="content-list-block__container">
|
||||||
|
<div class="content-list-wrapper">
|
||||||
|
<ol class="content-list">
|
||||||
|
<li class="content-list__item contentlist-item" :key="contentBlock.id" v-for="contentBlock in contentBlocks">
|
||||||
|
<content-block :contentBlock="contentBlock"></content-block>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// import ContentBlock from '@/components/ContentBlock';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['contents', 'parent'],
|
||||||
|
name: 'content-block-list',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
// https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
|
||||||
|
ContentBlock: () => import('@/components/ContentBlock')
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
contentBlocks() {
|
||||||
|
return this.contents.map(contentBlock => {
|
||||||
|
return Object.assign({}, contentBlock, {
|
||||||
|
contents: [...contentBlock.value],
|
||||||
|
indent: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/styles/_variables.scss";
|
||||||
|
@import "@/styles/_mixins.scss";
|
||||||
|
|
||||||
|
.content-list-wrapper {
|
||||||
|
.content-list {
|
||||||
|
/* https://stackoverflow.com/questions/1632005/ordered-list-html-lower-alpha-with-right-parentheses */
|
||||||
|
counter-reset: list;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
list-style: none;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 2*15px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
font-weight: 600;
|
||||||
|
left: 0;
|
||||||
|
color: $color-brand;
|
||||||
|
content: counter(list, lower-alpha) ") ";
|
||||||
|
counter-increment: list;
|
||||||
|
line-height: 27px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/styles/_variables.scss";
|
@import "@/styles/_variables.scss";
|
||||||
.subtitle {
|
.subtitle {
|
||||||
|
padding-top: 1px;
|
||||||
margin-bottom: $large-spacing;
|
margin-bottom: $large-spacing;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -18,59 +18,67 @@ class GenericStreamFieldType(Scalar):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def serialize(stream_value):
|
def serialize(stream_value):
|
||||||
stream_data = stream_value.stream_data
|
stream_data = stream_value.stream_data
|
||||||
|
return augment_fields(stream_data)
|
||||||
for d in stream_data:
|
|
||||||
if isinstance(d, dict):
|
|
||||||
_type = d['type']
|
|
||||||
if _type == 'image_block':
|
|
||||||
_value = d['value']
|
|
||||||
value = {
|
|
||||||
# 'value': _value,
|
|
||||||
# 'id': d['id'],
|
|
||||||
'path': Image.objects.get(id=_value).file.url
|
|
||||||
}
|
|
||||||
d['value'] = value
|
|
||||||
if _type == 'assignment':
|
|
||||||
_value = d['value']
|
|
||||||
assignment_id = _value['assignment_id']
|
|
||||||
try:
|
|
||||||
assignment = Assignment.objects.get(pk=assignment_id)
|
|
||||||
value = {
|
|
||||||
'title': assignment.title,
|
|
||||||
'assignment': assignment.assignment,
|
|
||||||
'id': to_global_id('AssignmentNode', assignment.pk)
|
|
||||||
}
|
|
||||||
d['value'] = value
|
|
||||||
except Assignment.DoesNotExist:
|
|
||||||
logger.error('Assignment {} does not exist'.format(assignment_id))
|
|
||||||
if _type == 'basic_knowledge' or _type == 'instrument':
|
|
||||||
_value = d['value']
|
|
||||||
basic_knowledge = BasicKnowledge.objects.get(pk=_value['basic_knowledge'])
|
|
||||||
_value.update({
|
|
||||||
'slug': basic_knowledge.slug
|
|
||||||
})
|
|
||||||
d['value'] = _value
|
|
||||||
|
|
||||||
# value = dict(d['value'])
|
|
||||||
# if 'document' in value:
|
|
||||||
# value['document'] = Document.objects.get(id=value['document']).file.url
|
|
||||||
# if 'image' in value:
|
|
||||||
# value['image'] = Image.objects.get(id=value['image']).file.url
|
|
||||||
|
|
||||||
# else:
|
|
||||||
# _type = d[0]
|
|
||||||
# value = dict(d[1])
|
|
||||||
# if 'document' in value:
|
|
||||||
# value['document'] = value['document'].file.url
|
|
||||||
# if 'image' in value:
|
|
||||||
# value['image'] = value['image'].file.url
|
|
||||||
|
|
||||||
return stream_data
|
|
||||||
|
|
||||||
# by_api = stream_value.stream_block.get_api_representation(stream_value)
|
# by_api = stream_value.stream_block.get_api_representation(stream_value)
|
||||||
# return by_api
|
# return by_api
|
||||||
|
|
||||||
|
|
||||||
|
def augment_fields(stream_data):
|
||||||
|
for data in stream_data:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
_type = data['type']
|
||||||
|
if _type == 'image_block':
|
||||||
|
_value = data['value']
|
||||||
|
value = {
|
||||||
|
# 'value': _value,
|
||||||
|
# 'id': d['id'],
|
||||||
|
'path': Image.objects.get(id=_value).file.url
|
||||||
|
}
|
||||||
|
data['value'] = value
|
||||||
|
if _type == 'assignment':
|
||||||
|
_value = data['value']
|
||||||
|
assignment_id = _value['assignment_id']
|
||||||
|
try:
|
||||||
|
assignment = Assignment.objects.get(pk=assignment_id)
|
||||||
|
value = {
|
||||||
|
'title': assignment.title,
|
||||||
|
'assignment': assignment.assignment,
|
||||||
|
'id': to_global_id('AssignmentNode', assignment.pk)
|
||||||
|
}
|
||||||
|
data['value'] = value
|
||||||
|
except Assignment.DoesNotExist:
|
||||||
|
logger.error('Assignment {} does not exist'.format(assignment_id))
|
||||||
|
if _type == 'basic_knowledge' or _type == 'instrument':
|
||||||
|
_value = data['value']
|
||||||
|
basic_knowledge = BasicKnowledge.objects.get(pk=_value['basic_knowledge'])
|
||||||
|
_value.update({
|
||||||
|
'slug': basic_knowledge.slug
|
||||||
|
})
|
||||||
|
data['value'] = _value
|
||||||
|
|
||||||
|
# value = dict(d['value'])
|
||||||
|
# if 'document' in value:
|
||||||
|
# value['document'] = Document.objects.get(id=value['document']).file.url
|
||||||
|
# if 'image' in value:
|
||||||
|
# value['image'] = Image.objects.get(id=value['image']).file.url
|
||||||
|
|
||||||
|
# else:
|
||||||
|
# _type = d[0]
|
||||||
|
# value = dict(d[1])
|
||||||
|
# if 'document' in value:
|
||||||
|
# value['document'] = value['document'].file.url
|
||||||
|
# if 'image' in value:
|
||||||
|
# value['image'] = value['image'].file.url
|
||||||
|
|
||||||
|
if _type == 'content_list_item':
|
||||||
|
item_data = data['value']
|
||||||
|
data['value'] = augment_fields(item_data)
|
||||||
|
|
||||||
|
return stream_data
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(StreamField)
|
@convert_django_field.register(StreamField)
|
||||||
def convert_stream_field(field, registry=None):
|
def convert_stream_field(field, registry=None):
|
||||||
return GenericStreamFieldType(description=field.help_text, required=not field.null)
|
return GenericStreamFieldType(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface, ObjectList, StreamFieldPanel
|
from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface, ObjectList, StreamFieldPanel
|
||||||
|
from wagtail.core.blocks import StreamBlock
|
||||||
from wagtail.core.fields import StreamField
|
from wagtail.core.fields import StreamField
|
||||||
from wagtail.images.blocks import ImageChooserBlock
|
from wagtail.images.blocks import ImageChooserBlock
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ class ContentBlock(StrictHierarchyPage):
|
||||||
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_content_blocks')
|
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_content_blocks')
|
||||||
user_created = models.BooleanField(default=False)
|
user_created = models.BooleanField(default=False)
|
||||||
|
|
||||||
contents = StreamField([
|
content_blocks = [
|
||||||
('text_block', TextBlock()),
|
('text_block', TextBlock()),
|
||||||
('basic_knowledge', BasicKnowledgeBlock()),
|
('basic_knowledge', BasicKnowledgeBlock()),
|
||||||
('assignment', AssignmentBlock()),
|
('assignment', AssignmentBlock()),
|
||||||
|
|
@ -46,8 +47,11 @@ class ContentBlock(StrictHierarchyPage):
|
||||||
('document_block', DocumentBlock()),
|
('document_block', DocumentBlock()),
|
||||||
('infogram_block', InfogramBlock()),
|
('infogram_block', InfogramBlock()),
|
||||||
('genially_block', GeniallyBlock()),
|
('genially_block', GeniallyBlock()),
|
||||||
('subtitle', SubtitleBlock()),
|
('subtitle', SubtitleBlock())
|
||||||
], null=True, blank=True)
|
]
|
||||||
|
|
||||||
|
content_list_item = StreamBlock(content_blocks)
|
||||||
|
contents = StreamField(content_blocks + [('content_list_item', content_list_item)], null=True, blank=True)
|
||||||
|
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue