Add unit tests to test id creation in custom content blocks

Relates to MS-919
This commit is contained in:
Ramon Wenger 2024-04-17 16:14:48 +02:00
parent 60b28ec022
commit 6ad39bcdb2
4 changed files with 528 additions and 238 deletions

View File

@ -1,15 +1,7 @@
# -*- 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 bleach
import re import re
import uuid
from typing import List, Union from typing import List, Union
@ -33,35 +25,45 @@ def get_previous_item(previous_contents: Union[StreamValue, List[dict]], item: d
contents = previous_contents.raw_data contents = previous_contents.raw_data
else: else:
contents = previous_contents contents = previous_contents
return next((c for c in contents if c.get('id', None) == item['id']), None) return next((c for c in contents if c.get("id", None) == item["id"]), None)
def handle_text(text): def handle_text(text):
is_list = bool(re.search(r'<ul>', text)) is_list = bool(re.search(r"<ul>", text))
if is_list: if is_list:
# let's assume this is formatted correctly already, otherwise bleach or the browser should strip / ignore it # let's assume this is formatted correctly already, otherwise bleach or the browser should strip / ignore it
return text return text
else: else:
parts = re.split(r'[\r\n]+', text) parts = re.split(r"[\r\n]+", text)
paragraphs = ['<p>{}</p>'.format(p.strip()) for p in parts] paragraphs = ["<p>{}</p>".format(p.strip()) for p in parts]
return '\n'.join(paragraphs) return "\n".join(paragraphs)
ALLOWED_BLOCKS = ( ALLOWED_BLOCKS = (
'text_block', "text_block",
'student_entry', "student_entry",
'image_url_block', "image_url_block",
'link_block', "link_block",
'video_block', "video_block",
'assignment', "assignment",
'document_block', "document_block",
'content_list_item', "content_list_item",
'subtitle', "subtitle",
'readonly' "readonly",
) )
def handle_content_block(content, context=None, module=None, allowed_blocks=ALLOWED_BLOCKS, previous_contents=None): def get_content_dict(content_type, id, value):
return {"type": content_type, "id": id, "value": value}
def handle_content_block(
content,
context=None,
module=None,
allowed_blocks=ALLOWED_BLOCKS,
previous_contents=None,
):
""" """
Handle different content types of a content block Handle different content types of a content block
@ -75,95 +77,98 @@ def handle_content_block(content, context=None, module=None, allowed_blocks=ALLO
""" """
# todo: add all the content blocks # todo: add all the content blocks
# todo: sanitize user inputs! # todo: sanitize user inputs!
if content['type'] not in allowed_blocks: if content["type"] not in allowed_blocks:
return return
if content['type'] == 'text_block': id = content.get("id")
return { if id is None:
'type': 'text_block', id = str(uuid.uuid4())
'value': {
'text': handle_text(bleach.clean(content['value']['text'], strip=True)) if content["type"] == "text_block":
}} content_type = "text_block"
elif content['type'] == 'assignment': value = {
"text": handle_text(bleach.clean(content["value"]["text"], strip=True))
}
return get_content_dict(
content_type=content_type,
id=id,
value=value,
)
elif content["type"] == "assignment":
if module is None: if module is None:
raise AssignmentParameterException('Module is missing for assignment') # todo: define better exception raise AssignmentParameterException(
"Module is missing for assignment"
) # todo: define better exception
if context is None: if context is None:
raise AssignmentParameterException('Context is missing for assignment') raise AssignmentParameterException("Context is missing for assignment")
value = content['value'] value = content["value"]
if value.get('id') is not None: if value.get("id") is not None:
assignment = get_object(Assignment, value.get('id')) assignment = get_object(Assignment, value.get("id"))
if assignment.user_created and assignment.owner == context.user: if assignment.user_created and assignment.owner == context.user:
assignment.title = value.get('title') assignment.title = value.get("title")
assignment.assignment = value.get('assignment') assignment.assignment = value.get("assignment")
assignment.save() assignment.save()
else: else:
assignment = Assignment.objects.create( assignment = Assignment.objects.create(
title=value.get('title'), title=value.get("title"),
assignment=value.get('assignment'), assignment=value.get("assignment"),
owner=context.user, owner=context.user,
module=module, module=module,
user_created=True user_created=True,
) )
return { content_type = "assignment"
'type': 'assignment', value = {"assignment_id": assignment.id}
'value': { return get_content_dict(content_type=content_type, id=id, value=value)
'assignment_id': assignment.id elif content["type"] == "image_url_block":
}} content_type = "image_url_block"
elif content['type'] == 'image_url_block': value = {"url": bleach.clean(content["value"]["url"])}
return { return get_content_dict(content_type=content_type, id=id, value=value)
'type': 'image_url_block', elif content["type"] == "link_block":
'value': { content_type = "link_block"
'url': bleach.clean(content['value']['url']) value = {
}} "text": bleach.clean(content["value"]["text"]),
elif content['type'] == 'link_block': "url": bleach.clean(content["value"]["url"]),
return {
'type': 'link_block',
'value': {
'text': bleach.clean(content['value']['text']),
'url': bleach.clean(content['value']['url'])
}
} }
elif content['type'] == 'video_block': return get_content_dict(content_type=content_type, id=id, value=value)
return { elif content["type"] == "video_block":
'type': 'video_block', content_type = "video_block"
'value': { value = {"url": bleach.clean(content["value"]["url"])}
'url': bleach.clean(content['value']['url']) return get_content_dict(content_type=content_type, id=id, value=value)
}} elif content["type"] == "document_block":
elif content['type'] == 'document_block': content_type = "document_block"
return { value = {"url": bleach.clean(content["value"]["url"])}
'type': 'document_block', return get_content_dict(content_type=content_type, id=id, value=value)
'value': { elif content["type"] == "subtitle":
'url': bleach.clean(content['value']['url']) content_type = "subtitle"
}} value = {"text": bleach.clean(content["value"]["text"])}
elif content['type'] == 'subtitle': return get_content_dict(content_type=content_type, id=id, value=value)
return { elif content["type"] == "content_list_item":
'type': 'subtitle', if content.get("id") is not None: # the list block existed already
'value': { previous_item = get_previous_item(
'text': bleach.clean(content['value']['text']) previous_contents=previous_contents, item=content
} )
} previous_content = previous_item.get("value")
elif content['type'] == 'content_list_item':
if content.get('id') is not None: # the list block existed already
previous_item = get_previous_item(previous_contents=previous_contents, item=content)
previous_content = previous_item.get('value')
else: else:
previous_content = None previous_content = None
value = [handle_content_block(c, context, module, previous_contents=previous_content) for c in value = [
content['contents']] handle_content_block(c, context, module, previous_contents=previous_content)
return {'type': 'content_list_item', for c in content["contents"]
'value': value, ]
'id': content.get('id')} content_type = "content_list_item"
elif content['type'] == 'readonly' and previous_contents is not None: return get_content_dict(content_type=content_type, id=id, value=value)
elif content["type"] == "readonly" and previous_contents is not None:
# get first item that matches the id # get first item that matches the id
# users can re-order readonly items, but we won't let them change them otherwise, so we just take the # users can re-order readonly items, but we won't let them change them otherwise, so we just take the
# item from before and ignore anything else # item from before and ignore anything else
previous_item = get_previous_item(previous_contents=previous_contents, item=content) previous_item = get_previous_item(
previous_contents=previous_contents, item=content
)
if previous_item is None: if previous_item is None:
raise ContentTypeException('Readonly item found that is not allowed here') raise ContentTypeException("Readonly item found that is not allowed here")
return previous_item return previous_item
raise ContentTypeException('Type did not match a case') raise ContentTypeException("Type did not match a case")
def set_user_defined_block_type(block_type): def set_user_defined_block_type(block_type):

View File

@ -0,0 +1,284 @@
import pytest
from api.utils import get_object
from assignments.models import Assignment
from books.factories import ChapterFactory, ContentBlockFactory, ModuleFactory
from books.models.contentblock import ContentBlock
from core.logger import get_logger
logger = get_logger(__name__)
pytestmark = pytest.mark.django_db
add_mutation = """
mutation AddContentBlock($input: AddContentBlockInput!) {
addContentBlock(input: $input) {
newContentBlock {
id
}
errors
clientMutationId
}
}
"""
update_mutation = """
mutation MutateContentBlock($input: MutateContentBlockInput!) {
mutateContentBlock(input: $input) {
contentBlock {
id
contents
title
}
}
}
"""
def create_block_input(content_type, value):
return {
"contents": {
"type": content_type,
"value": value,
},
"title": "some title",
"type": "NORMAL",
}
@pytest.mark.usefixtures("create_users")
class TestCreateCustomContentBlock:
def _add_content_block(self, client, input):
module = ModuleFactory()
chapter = ChapterFactory(parent=module)
content_block = ContentBlockFactory(parent=chapter, module=module)
after = content_block.graphql_id
logger.info(after)
result = client.execute(
add_mutation,
variables={
"input": {
"contentBlock": input,
"after": after,
}
},
)
assert result.errors is None
assert result.data.get("addContentBlock", None).get("errors", None) is None
assert ContentBlock.objects.count() == 2
new_content_block_data = result.data.get("addContentBlock").get(
"newContentBlock"
)
new_block_id = new_content_block_data.get("id")
new_content_block = get_object(ContentBlock, new_block_id)
return new_content_block
def _update_content_block(self, client, input):
update_result = client.execute(update_mutation, variables=input)
assert update_result.errors is None
assert ContentBlock.objects.count() == 2
updated_content_block_id = (
update_result.data.get("mutateContentBlock").get("contentBlock").get("id")
)
updated_content_block = get_object(ContentBlock, updated_content_block_id)
return updated_content_block
def test_add_custom_content_block(self, teacher, get_client):
content_block_title = "A new Custom Content Block"
content_block_input = {
"contents": [
{
"contents": [
{
"type": "text_block",
"value": {"text": "<p>Some nested text</p>"},
}
],
"type": "content_list_item",
}
],
"title": content_block_title,
"type": "NORMAL",
}
client = get_client(teacher)
new_content_block = self._add_content_block(
client=client, input=content_block_input
)
assert new_content_block.title == content_block_title
content_list_content = new_content_block.contents.raw_data[0]
logger.debug(content_list_content)
content_list_id = content_list_content.get("id")
assert content_list_id is not None
text_content = content_list_content.get("value")[0]
text_id = text_content.get("id")
assert text_id is not None
update_input = {
"input": {
"contentBlock": {
"contents": [
{
"contents": [
{
"type": "text_block",
"id": text_id,
"value": {"text": "<p>Some nested text</p>"},
},
{
"type": "text_block",
"value": {"text": "<p>Second nested text</p>"},
},
],
"id": content_list_id,
"type": "content_list_item",
}
],
"title": content_block_title,
"type": "NORMAL",
},
"id": new_content_block.graphql_id,
}
}
updated_content_block = self._update_content_block(client, update_input)
updated_content_list_content = updated_content_block.contents.raw_data[0]
updated_content_list_id = updated_content_list_content.get("id")
assert content_list_id == updated_content_list_id
updated_text_content = updated_content_list_content.get("value")[0]
updated_text_id = updated_text_content.get("id")
assert text_id == updated_text_id
added_text_content = updated_content_list_content.get("value")[1]
assert added_text_content.get("id") is not None
assert (
added_text_content.get("value").get("text") == "<p>Second nested text</p>"
)
def test_add_text_block_should_get_id(self, teacher, get_client):
client = get_client(teacher)
content_block_title = "A new Custom Content Block"
content_block_input = {
"contents": [
{
"value": {"text": "Some text"},
"type": "text_block",
}
],
"title": content_block_title,
"type": "NORMAL",
}
new_content_block = self._add_content_block(
client=client, input=content_block_input
)
assert new_content_block.title == content_block_title
text_block = new_content_block.contents.raw_data[0]
text_id = text_block.get("id")
assert text_id is not None
update_input = {
"input": {
"contentBlock": {
"contents": [
{
"id": text_id,
"type": "text_block",
"value": {"text": "<p>ein text</p>"},
}
],
"title": content_block_title,
"type": "NORMAL",
},
"id": new_content_block.graphql_id,
}
}
updated_content_block = self._update_content_block(client, update_input)
updated_text = updated_content_block.contents.raw_data[0]
assert updated_text.get("id") == text_id
def test_add_text_block(self, teacher, get_client):
content_type = "text_block"
value = {"text": "<p>some text</p>"}
client = get_client(teacher)
input = create_block_input(content_type=content_type, value=value)
new_content_block = self._add_content_block(client=client, input=input)
text_block = new_content_block.contents.raw_data[0]
assert text_block.get("id") is not None
assert text_block.get("value").get("text") == value.get("text")
def test_add_assignment(self, teacher, get_client):
content_type = "assignment"
value = {"title": "Assignment title", "assignment": "Assignment text"}
client = get_client(teacher)
input = create_block_input(content_type=content_type, value=value)
new_content_block = self._add_content_block(client=client, input=input)
assignment_block = new_content_block.contents.raw_data[0]
logger.debug(assignment_block)
assert assignment_block.get("id") is not None
assert assignment_block.get("value").get("assignment_id") is not None
assert Assignment.objects.count() == 1
assignment = Assignment.objects.get(
pk=assignment_block.get("value").get("assignment_id")
)
assert assignment.title == value.get("title")
assert assignment.assignment == value.get("assignment")
def test_add_image_block(self, teacher, get_client):
content_type = "image_url_block"
value = {"url": "https://hep-verlag.ch/some-image"}
client = get_client(teacher)
input = create_block_input(content_type=content_type, value=value)
new_content_block = self._add_content_block(client=client, input=input)
image_block = new_content_block.contents.raw_data[0]
assert image_block.get("id") is not None
assert image_block.get("value").get("url") == value.get("url")
def test_add_link_block(self, teacher, get_client):
content_type = "link_block"
value = {"text": "link of the text", "url": "https://hep-verlag.ch/some-image"}
client = get_client(teacher)
input = create_block_input(content_type=content_type, value=value)
new_content_block = self._add_content_block(client=client, input=input)
link_block = new_content_block.contents.raw_data[0]
assert link_block.get("id") is not None
assert link_block.get("value").get("text") == value.get("text")
assert link_block.get("value").get("url") == value.get("url")
def test_add_video_block(self, teacher, get_client):
content_type = "video_block"
value = {"url": "https://hep-verlag.ch/some-video"}
client = get_client(teacher)
input = create_block_input(content_type=content_type, value=value)
new_content_block = self._add_content_block(client=client, input=input)
video_block = new_content_block.contents.raw_data[0]
assert video_block.get("id") is not None
assert video_block.get("value").get("url") == value.get("url")
def test_add_document_block(self, teacher, get_client):
content_type = "document_block"
value = {"url": "https://hep-verlag.ch/some-document"}
client = get_client(teacher)
input = create_block_input(content_type=content_type, value=value)
new_content_block = self._add_content_block(client=client, input=input)
document_block = new_content_block.contents.raw_data[0]
assert document_block.get("id") is not None
assert document_block.get("value").get("url") == value.get("url")
def test_add_subtitle(self, teacher, get_client):
content_type = "subtitle"
value = {"text": "some subtitle"}
client = get_client(teacher)
input = create_block_input(content_type=content_type, value=value)
new_content_block = self._add_content_block(client=client, input=input)
subtitle_block = new_content_block.contents.raw_data[0]
assert subtitle_block.get("id") is not None
assert subtitle_block.get("value").get("text") == value.get("text")
# def test_add_readonly(self):
# content_type = "readonly"
# value = {"text": "some subtitle"}
# client = get_client(teacher)
# input = create_block_input(content_type=content_type, value=value)
# new_content_block = self._add_content_block(client=client, input=input)
# subtitle_block = new_content_block.contents.raw_data[0]
# assert subtitle_block.get("id") is not None
# assert subtitle_block.get("value").get("text") == value.get("text")

View File

@ -0,0 +1,145 @@
from django.test import RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id
from api.schema import schema
from books.factories import ModuleFactory
from books.models import Chapter, ContentBlock
from core.tests.base_test import SkillboxTestCase
from users.models import User, SchoolClass
class CustomContentTestCase(SkillboxTestCase):
def setUp(self):
self.module = ModuleFactory()
self.chapter = Chapter(title="Hello")
self.module.add_child(instance=self.chapter)
self.createDefault()
content_block = ContentBlock(title="bla", slug="bla")
self.chapter_id = to_global_id("ChapterNode", self.chapter.id)
self.chapter.specific.add_child(instance=content_block)
self.user = User.objects.get(username="teacher")
school_class2 = SchoolClass.objects.get(name="second_class")
school_class2.users.add(self.user)
school_class2.save()
request = RequestFactory().get("/")
request.user = self.user
assert content_block.id is not None
self.content_block_id = to_global_id("ContentBlockNode", content_block.id)
self.client = Client(schema=schema, context_value=request)
def test_custom_content_blocks(self):
self.assertEqual(self.user.school_classes.count(), 2)
chapterQuery = """
query ChapterQuery($id: ID!) {
chapter(id: $id) {
id
title
contentBlocks {
id
title
}
}
}
"""
result = self.get_client().execute(
chapterQuery, variables={"id": self.chapter_id}
)
self.assertIsNone(result.errors)
self.assertEqual(len(result.data.get("chapter").get("contentBlocks")), 1)
custom_content_block = ContentBlock(
title="own", slug="own", user_created=True, owner=self.user
)
self.chapter.specific.add_child(instance=custom_content_block)
result = self.get_client().execute(
chapterQuery, variables={"id": self.chapter_id}
)
self.assertEqual(len(result.data.get("chapter").get("contentBlocks")), 2)
for school_class in self.user.school_classes.all():
custom_content_block.visible_for.add(school_class)
result = self.get_client().execute(
chapterQuery, variables={"id": self.chapter_id}
)
self.assertEqual(len(result.data.get("chapter").get("contentBlocks")), 2)
def test_mutate_own_content_block(self):
query = """
query ContentBlockQuery($id: ID!) {
contentBlock(id: $id) {
contents
title
}
}
"""
res = self.get_client().execute(query, variables={"id": self.content_block_id})
self.assertIsNone(res.errors)
self.assertEqual(res.data["contentBlock"]["title"], "bla")
mutation = """
mutation MutateContentBlock($input: MutateContentBlockInput!) {
mutateContentBlock(input: $input) {
contentBlock {
contents
title
}
}
}
"""
new_content = {
"id": "",
"type": "text_block",
"value": {"text": "new text \n a new line"},
}
variables = {
"input": {
"id": self.content_block_id,
"contentBlock": {
"contents": [new_content],
"title": "new title",
"type": "NORMAL",
},
}
}
mutation_result = self.get_client().execute(mutation, variables=variables)
self.assertIsNone(mutation_result.errors)
content_block = mutation_result.data["mutateContentBlock"]["contentBlock"]
self.assertEqual(content_block["title"], "new title")
self.assertEqual(
content_block["contents"][0]["value"]["text"],
"<p>new text</p>\n<p>a new line</p>",
)
content_with_list = {
"id": "",
"type": "text_block",
"value": {"text": "<ul><li>Hallo</li><li>Velo</li></ul>"},
}
other_variables = {
"input": {
"id": self.content_block_id,
"contentBlock": {
"contents": [content_with_list],
"title": "title for list content",
"type": "NORMAL",
},
}
}
list_mutation_result = self.get_client().execute(
mutation, variables=other_variables
)
self.assertIsNone(list_mutation_result.errors)
content_block = list_mutation_result.data["mutateContentBlock"]["contentBlock"]
self.assertEqual(content_block["title"], "title for list content")
self.assertEqual(
content_block["contents"][0]["value"]["text"],
"<ul><li>Hallo</li><li>Velo</li></ul>",
)

View File

@ -1,144 +0,0 @@
from django.test import TestCase, RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id
from api.schema import schema
from books.factories import ModuleFactory
from books.models import Chapter, ContentBlock
from core.tests.base_test import SkillboxTestCase
from users.models import User, SchoolClass
from users.services import create_users
class OwnContentTestCase(SkillboxTestCase):
def setUp(self):
self.module = ModuleFactory()
self.chapter = Chapter(title='Hello')
self.module.add_child(instance=self.chapter)
self.createDefault()
content_block = ContentBlock(title='bla', slug='bla')
self.chapter_id = to_global_id('ChapterNode', self.chapter.id)
self.chapter.specific.add_child(instance=content_block)
self.user = User.objects.get(username='teacher')
school_class2 = SchoolClass.objects.get(name='second_class')
school_class2.users.add(self.user)
school_class2.save()
request = RequestFactory().get('/')
request.user = self.user
assert content_block.id is not None
self.content_block_id = to_global_id('ContentBlockNode', content_block.id)
self.client = Client(schema=schema, context_value=request)
def test_custom_content_blocks(self):
self.assertEqual(self.user.school_classes.count(), 2)
chapterQuery = """
query ChapterQuery($id: ID!) {
chapter(id: $id) {
id
title
contentBlocks {
id
title
}
}
}
"""
result = self.get_client().execute(chapterQuery, variables={
"id": self.chapter_id
})
self.assertIsNone(result.errors)
self.assertEqual(len(result.data.get('chapter').get('contentBlocks')), 1)
custom_content_block = ContentBlock(title='own', slug='own', user_created=True, owner=self.user)
self.chapter.specific.add_child(instance=custom_content_block)
result = self.get_client().execute(chapterQuery, variables={
"id": self.chapter_id
})
self.assertEqual(len(result.data.get('chapter').get('contentBlocks')), 2)
for school_class in self.user.school_classes.all():
custom_content_block.visible_for.add(school_class)
result = self.get_client().execute(chapterQuery, variables={
"id": self.chapter_id
})
self.assertEqual(len(result.data.get('chapter').get('contentBlocks')), 2)
def test_mutate_own_content_block(self):
query = """
query ContentBlockQuery($id: ID!) {
contentBlock(id: $id) {
contents
title
}
}
"""
res = self.get_client().execute(query, variables={'id': self.content_block_id})
self.assertIsNone(res.errors)
self.assertEqual(res.data['contentBlock']['title'], 'bla')
mutation = """
mutation MutateContentBlock($input: MutateContentBlockInput!) {
mutateContentBlock(input: $input) {
contentBlock {
contents
title
}
}
}
"""
new_content = {
'id': '',
'type': 'text_block',
'value': {
'text': 'new text \n a new line'
}
}
variables = {
'input': {
'id': self.content_block_id,
'contentBlock': {
'contents': [
new_content
],
'title': 'new title',
'type': 'NORMAL'
}
}
}
mutation_result = self.get_client().execute(mutation, variables=variables)
self.assertIsNone(mutation_result.errors)
content_block = mutation_result.data['mutateContentBlock']['contentBlock']
self.assertEqual(content_block['title'], 'new title')
self.assertEqual(content_block['contents'][0]['value']['text'], '<p>new text</p>\n<p>a new line</p>')
content_with_list = {
'id': '',
'type': 'text_block',
'value': {
'text': '<ul><li>Hallo</li><li>Velo</li></ul>'
}
}
other_variables = {
'input': {
'id': self.content_block_id,
'contentBlock': {
'contents': [
content_with_list
],
'title': 'title for list content',
'type': 'NORMAL'
}
}
}
list_mutation_result = self.get_client().execute(mutation, variables=other_variables)
self.assertIsNone(list_mutation_result.errors)
content_block = list_mutation_result.data['mutateContentBlock']['contentBlock']
self.assertEqual(content_block['title'], 'title for list content')
self.assertEqual(content_block['contents'][0]['value']['text'], '<ul><li>Hallo</li><li>Velo</li></ul>')