Add "add room entry" mutation with serializer

This commit is contained in:
Ramon Wenger 2018-09-25 17:01:16 +02:00
parent 1ef4c6d8e1
commit cb0f96f81e
10 changed files with 191 additions and 97 deletions

View File

@ -3,8 +3,8 @@ from django.conf import settings
from graphene import relay from graphene import relay
from graphene_django.debug import DjangoDebug from graphene_django.debug import DjangoDebug
# Keep this import exactly here, it's necessary for StreamField conversion # noinspection PyUnresolvedReferences
from api import graphene_wagtail from api import graphene_wagtail # Keep this import exactly here, it's necessary for StreamField conversion
from book.schema.mutations import BookMutations from book.schema.mutations import BookMutations
from filteredbook.schema import BookQuery from filteredbook.schema import BookQuery
@ -22,7 +22,6 @@ class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, graphene.ObjectT
class Mutation(BookMutations, RoomMutations, graphene.ObjectType): class Mutation(BookMutations, RoomMutations, graphene.ObjectType):
if settings.DEBUG: if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='__debug') debug = graphene.Field(DjangoDebug, name='__debug')

View File

@ -5,39 +5,60 @@ DEFAULT_RICH_TEXT_FEATURES = ['bold', 'italic', 'link', 'ol', 'ul']
# link_block # link_block
class LinkBlock(blocks.StructBlock): class LinkBlock(blocks.StructBlock):
class Meta:
icon = 'link'
text = blocks.TextBlock() text = blocks.TextBlock()
url = blocks.URLBlock() url = blocks.URLBlock()
# 'text_block' 'task' # 'text_block' 'task'
class TextBlock(blocks.StructBlock): class TextBlock(blocks.StructBlock):
class Meta:
icon = 'doc-full'
text = blocks.RichTextBlock() text = blocks.RichTextBlock()
# 'basic_knowledge' # 'basic_knowledge'
class BasicKnowledgeBlock(blocks.StructBlock): class BasicKnowledgeBlock(blocks.StructBlock):
class Meta:
icon = 'placeholder'
description = blocks.RichTextBlock() description = blocks.RichTextBlock()
url = blocks.URLBlock() url = blocks.URLBlock()
# 'image_url' # 'image_url'
class ImageUrlBlock(blocks.StructBlock): class ImageUrlBlock(blocks.StructBlock):
class Meta:
icon = 'image'
title = blocks.RichTextBlock() title = blocks.RichTextBlock()
url = blocks.URLBlock() url = blocks.URLBlock()
# 'student_entry' # 'student_entry'
class StudentEntryBlock(blocks.StructBlock): class StudentEntryBlock(blocks.StructBlock):
class Meta:
icon = 'download'
task_text = blocks.RichTextBlock() task_text = blocks.RichTextBlock()
# 'video_block' # 'video_block'
class VideoBlock(blocks.StructBlock): class VideoBlock(blocks.StructBlock):
class Meta:
icon = 'media'
url = blocks.URLBlock() url = blocks.URLBlock()
# 'document_block' # 'document_block'
class DocumentBlock(blocks.StructBlock): class DocumentBlock(blocks.StructBlock):
class Meta:
icon = 'doc-full'
url = blocks.URLBlock() url = blocks.URLBlock()
# 'text_block' 'task' 'basic_knowledge' 'student_entry' 'image_block' # 'text_block' 'task' 'basic_knowledge' 'student_entry' 'image_block'

View File

@ -33,15 +33,15 @@ class ContentBlock(StrictHierarchyPage):
hidden_for = models.ManyToManyField(UserGroup) hidden_for = models.ManyToManyField(UserGroup)
contents = StreamField([ contents = StreamField([
('text_block', TextBlock(icon='doc-full')), ('text_block', TextBlock()),
('basic_knowledge', BasicKnowledgeBlock(icon='placeholder')), ('basic_knowledge', BasicKnowledgeBlock()),
('student_entry', StudentEntryBlock(icon='download')), ('student_entry', StudentEntryBlock()),
('image_block', ImageChooserBlock(icon='image')), ('image_block', ImageChooserBlock()),
('image_url_block', ImageUrlBlock(icon='image')), ('image_url_block', ImageUrlBlock()),
('link_block', LinkBlock(icon='link')), ('link_block', LinkBlock()),
('task', TextBlock(icon='tick')), ('task', TextBlock(icon='tick')),
('video_block', VideoBlock(icon='media')), ('video_block', VideoBlock()),
('document_block', DocumentBlock(icon='doc-full')), ('document_block', DocumentBlock()),
], null=True, blank=True) ], null=True, blank=True)
type = models.CharField( type = models.CharField(

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
#
# Iterativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2018 Iterativ GmbH. All rights reserved.
#
# Created on 25.09.18
# @author: Ramon Wenger <ramon.wenger@iterativ.ch>
from .contentblock import AddContentBlock, MutateContentBlock
class BookMutations(object):
mutate_content_block = MutateContentBlock.Field()
add_content_block = AddContentBlock.Field()

View File

@ -1,8 +1,6 @@
import json import json
import bleach
import graphene import graphene
import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from graphene import relay from graphene import relay
@ -11,70 +9,7 @@ from book.models import ContentBlock, Chapter, UserGroup
from book.schema.inputs import ContentBlockInput from book.schema.inputs import ContentBlockInput
from book.schema.queries import ContentBlockNode from book.schema.queries import ContentBlockNode
from .utils import handle_content_block
def newlines_to_paragraphs(text):
parts = re.split(r'[\r\n]+', text)
paragraphs = ['<p>{}</p>'.format(p.strip()) for p in parts]
return '\n'.join(paragraphs)
ALLOWED_BLOCKS = (
'text_block',
'student_entry',
'image_url_block',
'link_block',
'video_block',
'document_block',
)
def handle_content_blocks(content_data, allowed_blocks=ALLOWED_BLOCKS):
new_contents = []
for content in content_data:
# todo: add all the content blocks
# todo: sanitize user inputs!
if content['type'] not in allowed_blocks:
continue
if content['type'] == 'text_block':
new_contents.append({
'type': 'text_block',
'value': {
'text': newlines_to_paragraphs(bleach.clean(content['value']['text'], strip=True))
}})
elif content['type'] == 'student_entry':
pass
elif content['type'] == 'image_url_block':
new_contents.append({
'type': 'image_url_block',
'value': {
'url': bleach.clean(content['value']['url'])
}})
elif content['type'] == 'link_block':
new_contents.append({
'type': 'link_block',
'value': {
'text': bleach.clean(content['value']['text']),
'url': bleach.clean(content['value']['url'])
}
})
elif content['type'] == 'task':
pass
elif content['type'] == 'video_block':
new_contents.append({
'type': 'video_block',
'value': {
'url': bleach.clean(content['value']['url'])
}})
elif content['type'] == 'document_block':
new_contents.append({
'type': 'document_block',
'value': {
'url': bleach.clean(content['value']['url'])
}})
return new_contents
class MutateContentBlock(relay.ClientIDMutation): class MutateContentBlock(relay.ClientIDMutation):
@ -109,7 +44,7 @@ class MutateContentBlock(relay.ClientIDMutation):
content_block.title = title content_block.title = title
if contents is not None: if contents is not None:
content_block.contents = json.dumps(handle_content_blocks(contents)) content_block.contents = json.dumps(list(map(handle_content_block, contents)))
content_block.save() content_block.save()
@ -157,8 +92,8 @@ class AddContentBlock(relay.ClientIDMutation):
revision.publish() revision.publish()
new_content_block.save() new_content_block.save()
new_contents = handle_content_blocks(contents) # can only do this after the content block has been saved new_content_block.contents = json.dumps(
new_content_block.contents = json.dumps(new_contents) list(map(handle_content_block, contents))) # can only do this after the content block has been saved
new_content_block.save() new_content_block.save()
return new_content_block return new_content_block
@ -180,8 +115,3 @@ class AddContentBlock(relay.ClientIDMutation):
errors = ['Error: {}'.format(e)] errors = ['Error: {}'.format(e)]
return cls(new_content_block=None, errors=errors) return cls(new_content_block=None, errors=errors)
class BookMutations(object):
mutate_content_block = MutateContentBlock.Field()
add_content_block = AddContentBlock.Field()

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
#
# Iterativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2018 Iterativ GmbH. All rights reserved.
#
# Created on 25.09.18
# @author: Ramon Wenger <ramon.wenger@iterativ.ch>
import bleach
import re
def newlines_to_paragraphs(text):
parts = re.split(r'[\r\n]+', text)
paragraphs = ['<p>{}</p>'.format(p.strip()) for p in parts]
return '\n'.join(paragraphs)
ALLOWED_BLOCKS = (
'text_block',
'student_entry',
'image_url_block',
'link_block',
'video_block',
'document_block',
)
def handle_content_block(content, allowed_blocks=ALLOWED_BLOCKS):
# todo: add all the content blocks
# todo: sanitize user inputs!
if content['type'] not in allowed_blocks:
return
if content['type'] == 'text_block':
return {
'type': 'text_block',
'value': {
'text': newlines_to_paragraphs(bleach.clean(content['value']['text'], strip=True))
}}
elif content['type'] == 'student_entry':
return
elif content['type'] == 'image_url_block':
return {
'type': 'image_url_block',
'value': {
'url': bleach.clean(content['value']['url'])
}}
elif content['type'] == 'link_block':
return {
'type': 'link_block',
'value': {
'text': bleach.clean(content['value']['text']),
'url': bleach.clean(content['value']['url'])
}
}
elif content['type'] == 'video_block':
return {
'type': 'video_block',
'value': {
'url': bleach.clean(content['value']['url'])
}}
elif content['type'] == 'document_block':
return {
'type': 'document_block',
'value': {
'url': bleach.clean(content['value']['url'])
}}
return None

View File

@ -1,6 +1,7 @@
import graphene import graphene
from graphene import InputObjectType from graphene import InputObjectType
from book.schema.inputs import ContentElementInput
from user.inputs import UserGroupInput from user.inputs import UserGroupInput
@ -17,3 +18,10 @@ class AddRoomArgument(RoomInput):
class UpdateRoomArgument(RoomInput): class UpdateRoomArgument(RoomInput):
id = graphene.ID(required=True) id = graphene.ID(required=True)
class AddRoomEntryArgument(InputObjectType):
title = graphene.String(required=True)
subtitle = graphene.String()
contents = graphene.List(ContentElementInput)
room = graphene.ID(required=True)

View File

@ -3,7 +3,7 @@ from django.db import models
from django_extensions.db.models import TitleDescriptionModel, TitleSlugDescriptionModel from django_extensions.db.models import TitleDescriptionModel, TitleSlugDescriptionModel
from wagtail.core.fields import StreamField from wagtail.core.fields import StreamField
from book.blocks import ImageUrlBlock, LinkBlock from book.blocks import ImageUrlBlock, LinkBlock, VideoBlock
from book.models import ContentBlock, TextBlock from book.models import ContentBlock, TextBlock
from user.models import UserGroup from user.models import UserGroup
@ -30,9 +30,10 @@ class RoomEntry(TitleSlugDescriptionModel):
subtitle = models.CharField(blank=True, null=False, max_length=255) subtitle = models.CharField(blank=True, null=False, max_length=255)
contents = StreamField([ contents = StreamField([
('text_block', TextBlock(icon='doc-full')), ('text_block', TextBlock()),
('image_url', ImageUrlBlock(icon='image')), ('image_url', ImageUrlBlock()),
('link_block', LinkBlock(icon='link')) ('link_block', LinkBlock()),
('video_block', VideoBlock())
], null=True, blank=True) ], null=True, blank=True)
def __str__(self): def __str__(self):

View File

@ -1,12 +1,11 @@
import graphene import graphene
from graphene import relay from graphene import relay
from graphql_relay import from_global_id
from api.utils import get_object from api.utils import get_object
from rooms.inputs import UpdateRoomArgument, AddRoomArgument from rooms.inputs import UpdateRoomArgument, AddRoomArgument, AddRoomEntryArgument
from rooms.models import Room from rooms.models import Room, RoomEntry
from rooms.schema import RoomNode from rooms.schema import RoomNode, RoomEntryNode
from rooms.serializers import RoomSerializer from rooms.serializers import RoomSerializer, RoomEntrySerializer
from user.models import UserGroup from user.models import UserGroup
@ -56,7 +55,31 @@ class DeleteRoom(relay.ClientIDMutation):
return cls(success=True) return cls(success=True)
class AddRoomEntry(relay.ClientIDMutation):
class Input:
room_entry = graphene.Argument(AddRoomEntryArgument)
room_entry = graphene.Field(RoomEntryNode)
errors = graphene.List(graphene.String)
@classmethod
def mutate_and_get_payload(cls, *args, **kwargs):
room_entry_data = kwargs.get('room_entry')
room_entry_data['room'] = get_object(Room, room_entry_data.get('room')).id
serializer = RoomEntrySerializer(data=room_entry_data)
if serializer.is_valid():
serializer.save()
return cls(room_entry=serializer.instance)
return cls(room_entry=None, errors=['{}: {}'.format(key, value) for key, value in serializer.errors.items()])
class RoomMutations: class RoomMutations:
update_room = UpdateRoom.Field() update_room = UpdateRoom.Field()
add_room = AddRoom.Field() add_room = AddRoom.Field()
delete_room = DeleteRoom.Field() delete_room = DeleteRoom.Field()
add_room_entry = AddRoomEntry.Field()

View File

@ -1,10 +1,35 @@
import json
from rest_framework import serializers from rest_framework import serializers
from rooms.models import Room from book.schema.mutations.utils import handle_content_block
from rooms.models import Room, RoomEntry
class ContentsSerializer(serializers.Field):
def to_internal_value(self, data):
return json.dumps(list(map(handle_content_block, data)))
class RoomSerializer(serializers.ModelSerializer): class RoomSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Room model = Room
fields = ('id', 'title', 'description', 'slug', 'user_group', 'appearance', ) fields = ('id', 'title', 'description', 'slug', 'user_group', 'appearance',)
read_only_fields = ('id', 'slug', ) read_only_fields = ('id', 'slug',)
class RoomEntrySerializer(serializers.ModelSerializer):
contents = ContentsSerializer()
class Meta:
model = RoomEntry
fields = ('room', 'author', 'subtitle', 'title', 'contents')
read_only_fields = ('id', 'slug',)
def validate_contents(self, value):
return value
def create(self, validated_data):
room_entry = RoomEntry(**validated_data)
room_entry.save()
return room_entry