diff --git a/server/api/schema.py b/server/api/schema.py index f2bd4454..87fd323a 100644 --- a/server/api/schema.py +++ b/server/api/schema.py @@ -3,8 +3,8 @@ from django.conf import settings from graphene import relay from graphene_django.debug import DjangoDebug -# Keep this import exactly here, it's necessary for StreamField conversion -from api import graphene_wagtail +# noinspection PyUnresolvedReferences +from api import graphene_wagtail # Keep this import exactly here, it's necessary for StreamField conversion from book.schema.mutations import BookMutations from filteredbook.schema import BookQuery @@ -22,7 +22,6 @@ class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, graphene.ObjectT class Mutation(BookMutations, RoomMutations, graphene.ObjectType): - if settings.DEBUG: debug = graphene.Field(DjangoDebug, name='__debug') diff --git a/server/book/blocks.py b/server/book/blocks.py index 593fa3cc..f52553d8 100644 --- a/server/book/blocks.py +++ b/server/book/blocks.py @@ -5,39 +5,60 @@ DEFAULT_RICH_TEXT_FEATURES = ['bold', 'italic', 'link', 'ol', 'ul'] # link_block class LinkBlock(blocks.StructBlock): + class Meta: + icon = 'link' + text = blocks.TextBlock() url = blocks.URLBlock() # 'text_block' 'task' class TextBlock(blocks.StructBlock): + class Meta: + icon = 'doc-full' + text = blocks.RichTextBlock() # 'basic_knowledge' class BasicKnowledgeBlock(blocks.StructBlock): + class Meta: + icon = 'placeholder' + description = blocks.RichTextBlock() url = blocks.URLBlock() # 'image_url' class ImageUrlBlock(blocks.StructBlock): + class Meta: + icon = 'image' + title = blocks.RichTextBlock() url = blocks.URLBlock() # 'student_entry' class StudentEntryBlock(blocks.StructBlock): + class Meta: + icon = 'download' + task_text = blocks.RichTextBlock() # 'video_block' class VideoBlock(blocks.StructBlock): + class Meta: + icon = 'media' + url = blocks.URLBlock() # 'document_block' class DocumentBlock(blocks.StructBlock): + class Meta: + icon = 'doc-full' + url = blocks.URLBlock() # 'text_block' 'task' 'basic_knowledge' 'student_entry' 'image_block' diff --git a/server/book/models/contentblock.py b/server/book/models/contentblock.py index d645ef39..0d73be8d 100644 --- a/server/book/models/contentblock.py +++ b/server/book/models/contentblock.py @@ -33,15 +33,15 @@ class ContentBlock(StrictHierarchyPage): hidden_for = models.ManyToManyField(UserGroup) contents = StreamField([ - ('text_block', TextBlock(icon='doc-full')), - ('basic_knowledge', BasicKnowledgeBlock(icon='placeholder')), - ('student_entry', StudentEntryBlock(icon='download')), - ('image_block', ImageChooserBlock(icon='image')), - ('image_url_block', ImageUrlBlock(icon='image')), - ('link_block', LinkBlock(icon='link')), + ('text_block', TextBlock()), + ('basic_knowledge', BasicKnowledgeBlock()), + ('student_entry', StudentEntryBlock()), + ('image_block', ImageChooserBlock()), + ('image_url_block', ImageUrlBlock()), + ('link_block', LinkBlock()), ('task', TextBlock(icon='tick')), - ('video_block', VideoBlock(icon='media')), - ('document_block', DocumentBlock(icon='doc-full')), + ('video_block', VideoBlock()), + ('document_block', DocumentBlock()), ], null=True, blank=True) type = models.CharField( diff --git a/server/book/schema/mutations/__init__.py b/server/book/schema/mutations/__init__.py new file mode 100644 index 00000000..b9d933bb --- /dev/null +++ b/server/book/schema/mutations/__init__.py @@ -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 +from .contentblock import AddContentBlock, MutateContentBlock + + +class BookMutations(object): + mutate_content_block = MutateContentBlock.Field() + add_content_block = AddContentBlock.Field() \ No newline at end of file diff --git a/server/book/schema/mutations.py b/server/book/schema/mutations/contentblock.py similarity index 63% rename from server/book/schema/mutations.py rename to server/book/schema/mutations/contentblock.py index a00f9867..6f9060ba 100644 --- a/server/book/schema/mutations.py +++ b/server/book/schema/mutations/contentblock.py @@ -1,8 +1,6 @@ import json -import bleach import graphene -import re from django.core.exceptions import ValidationError from graphene import relay @@ -11,70 +9,7 @@ from book.models import ContentBlock, Chapter, UserGroup from book.schema.inputs import ContentBlockInput from book.schema.queries import ContentBlockNode - -def newlines_to_paragraphs(text): - parts = re.split(r'[\r\n]+', text) - paragraphs = ['

{}

'.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 +from .utils import handle_content_block class MutateContentBlock(relay.ClientIDMutation): @@ -109,7 +44,7 @@ class MutateContentBlock(relay.ClientIDMutation): content_block.title = title 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() @@ -157,8 +92,8 @@ class AddContentBlock(relay.ClientIDMutation): revision.publish() 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_contents) + new_content_block.contents = json.dumps( + list(map(handle_content_block, contents))) # can only do this after the content block has been saved new_content_block.save() return new_content_block @@ -180,8 +115,3 @@ class AddContentBlock(relay.ClientIDMutation): errors = ['Error: {}'.format(e)] return cls(new_content_block=None, errors=errors) - - -class BookMutations(object): - mutate_content_block = MutateContentBlock.Field() - add_content_block = AddContentBlock.Field() diff --git a/server/book/schema/mutations/utils.py b/server/book/schema/mutations/utils.py new file mode 100644 index 00000000..edba5b5e --- /dev/null +++ b/server/book/schema/mutations/utils.py @@ -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 + +import bleach +import re + + +def newlines_to_paragraphs(text): + parts = re.split(r'[\r\n]+', text) + paragraphs = ['

{}

'.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 diff --git a/server/rooms/inputs.py b/server/rooms/inputs.py index a4cd4e13..227ff64b 100644 --- a/server/rooms/inputs.py +++ b/server/rooms/inputs.py @@ -1,6 +1,7 @@ import graphene from graphene import InputObjectType +from book.schema.inputs import ContentElementInput from user.inputs import UserGroupInput @@ -17,3 +18,10 @@ class AddRoomArgument(RoomInput): class UpdateRoomArgument(RoomInput): 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) diff --git a/server/rooms/models.py b/server/rooms/models.py index a62ef5d0..a68e97d1 100644 --- a/server/rooms/models.py +++ b/server/rooms/models.py @@ -3,7 +3,7 @@ from django.db import models from django_extensions.db.models import TitleDescriptionModel, TitleSlugDescriptionModel 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 user.models import UserGroup @@ -30,9 +30,10 @@ class RoomEntry(TitleSlugDescriptionModel): subtitle = models.CharField(blank=True, null=False, max_length=255) contents = StreamField([ - ('text_block', TextBlock(icon='doc-full')), - ('image_url', ImageUrlBlock(icon='image')), - ('link_block', LinkBlock(icon='link')) + ('text_block', TextBlock()), + ('image_url', ImageUrlBlock()), + ('link_block', LinkBlock()), + ('video_block', VideoBlock()) ], null=True, blank=True) def __str__(self): diff --git a/server/rooms/mutations.py b/server/rooms/mutations.py index a5ba74ff..fec8cbc4 100644 --- a/server/rooms/mutations.py +++ b/server/rooms/mutations.py @@ -1,12 +1,11 @@ import graphene from graphene import relay -from graphql_relay import from_global_id from api.utils import get_object -from rooms.inputs import UpdateRoomArgument, AddRoomArgument -from rooms.models import Room -from rooms.schema import RoomNode -from rooms.serializers import RoomSerializer +from rooms.inputs import UpdateRoomArgument, AddRoomArgument, AddRoomEntryArgument +from rooms.models import Room, RoomEntry +from rooms.schema import RoomNode, RoomEntryNode +from rooms.serializers import RoomSerializer, RoomEntrySerializer from user.models import UserGroup @@ -56,7 +55,31 @@ class DeleteRoom(relay.ClientIDMutation): 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: update_room = UpdateRoom.Field() add_room = AddRoom.Field() delete_room = DeleteRoom.Field() + add_room_entry = AddRoomEntry.Field() diff --git a/server/rooms/serializers.py b/server/rooms/serializers.py index 334e5ea0..7e5f9f44 100644 --- a/server/rooms/serializers.py +++ b/server/rooms/serializers.py @@ -1,10 +1,35 @@ +import json + 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 Meta: model = Room - fields = ('id', 'title', 'description', 'slug', 'user_group', 'appearance', ) - read_only_fields = ('id', 'slug', ) + fields = ('id', 'title', 'description', 'slug', 'user_group', 'appearance',) + 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