Merged in feature/nested-block (pull request #26)

Feature/nested block

Approved-by: Ramon Wenger <ramon.wenger@iterativ.ch>
This commit is contained in:
Christian Cueni 2019-07-17 15:14:57 +00:00 committed by Ramon Wenger
commit a1234f6688
5 changed files with 205 additions and 58 deletions

View File

@ -3,19 +3,20 @@
<div class="content-block" :class="specialClass">
<div class="content-block__actions">
<visibility-action
v-if="!contentBlock.indent"
: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>
</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>
</a>
</div>
<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"
:is="component.type"
v-bind="component">
@ -23,7 +24,7 @@
</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>
@ -41,6 +42,7 @@
import InfogramBlock from '@/components/content-blocks/InfogramBlock';
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
import ContentListBlock from '@/components/content-blocks/ContentListBlock';
import Assignment from '@/components/content-blocks/assignment/Assignment';
import Solution from '@/components/content-blocks/Solution';
import AddContentBlockButton from '@/components/AddContentBlockButton';
@ -59,6 +61,7 @@
export default {
props: ['contentBlock', 'parent'],
name: 'content-block',
components: {
'text_block': TextBlock,
@ -72,6 +75,7 @@
'infogram_block': InfogramBlock,
'genially_block': GeniallyBlock,
'subtitle': SubtitleBlock,
'content_list': ContentListBlock,
Solution,
Assignment,
Task,
@ -93,6 +97,59 @@
}
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() {
return {
showVisibility: false

View File

@ -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>

View File

@ -11,6 +11,7 @@
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.subtitle {
padding-top: 1px;
margin-bottom: $large-spacing;
}
</style>

View File

@ -18,59 +18,67 @@ class GenericStreamFieldType(Scalar):
@staticmethod
def serialize(stream_value):
stream_data = stream_value.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
return augment_fields(stream_data)
# by_api = stream_value.stream_block.get_api_representation(stream_value)
# 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)
def convert_stream_field(field, registry=None):
return GenericStreamFieldType(description=field.help_text, required=not field.null)

View File

@ -2,6 +2,7 @@ import logging
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface, ObjectList, StreamFieldPanel
from wagtail.core.blocks import StreamBlock
from wagtail.core.fields import StreamField
from wagtail.images.blocks import ImageChooserBlock
@ -34,7 +35,7 @@ class ContentBlock(StrictHierarchyPage):
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_content_blocks')
user_created = models.BooleanField(default=False)
contents = StreamField([
content_blocks = [
('text_block', TextBlock()),
('basic_knowledge', BasicKnowledgeBlock()),
('assignment', AssignmentBlock()),
@ -46,8 +47,11 @@ class ContentBlock(StrictHierarchyPage):
('document_block', DocumentBlock()),
('infogram_block', InfogramBlock()),
('genially_block', GeniallyBlock()),
('subtitle', SubtitleBlock()),
], null=True, blank=True)
('subtitle', SubtitleBlock())
]
content_list_item = StreamBlock(content_blocks)
contents = StreamField(content_blocks + [('content_list_item', content_list_item)], null=True, blank=True)
type = models.CharField(
max_length=100,