Add "add room entry" mutation with serializer
This commit is contained in:
parent
1ef4c6d8e1
commit
cb0f96f81e
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue