combo/tests/test_cells.py

674 lines
28 KiB
Python

import json
import mock
import os
import pytest
import requests
from combo.data.models import Page, CellBase, TextCell, LinkCell, JsonCellBase, JsonCell, ConfigJsonCell
from django.conf import settings
from django.db import connection
from django.forms.widgets import Media
from django.template import Context
from django.test import override_settings
from django.test.client import RequestFactory
from django.test.utils import CaptureQueriesContext
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from combo.data.library import get_cell_classes
from combo.utils import NothingInCacheException
from test_manager import admin_user, login
pytestmark = pytest.mark.django_db
def test_cell_reference():
page = Page()
page.save()
cell = TextCell()
cell.page = page
cell.text = 'foobar'
cell.order = 0
cell.save()
assert CellBase.get_cell(cell.get_reference()) == cell
def test_media():
class TextCelleWithMedia(TextCell):
class Media:
js = ['coincoin.js']
class Meta:
# to prevent error in Models metaclass as the current module is not
# in a registered applicatoin
app_label = 'data'
abstract = True
cells = [TextCelleWithMedia() for i in range(3)]
assert unicode(sum((cell.media for cell in cells), Media())) == u'<script type="text/javascript" src="/static/coincoin.js"></script>'
def test_additional_label():
page = Page()
page.save()
cell = TextCell()
cell.page = page
cell.text = '<p>foobar</p>'
cell.order = 0
cell.save()
assert cell.get_additional_label() == 'foobar'
cell = TextCell()
cell.page = page
cell.text = '<p>%s</p>' % 'foo'*30
cell.order = 0
cell.save()
assert len(cell.get_additional_label()) < 100
assert '...' in cell.get_additional_label()
def test_link_cell():
page = Page(title='example page', slug='example-page')
page.save()
cell = LinkCell()
cell.page = page
cell.title = 'Example Site'
cell.url = 'http://example.net/'
cell.order = 0
cell.save()
assert cell.get_additional_label() == 'Example Site'
ctx = {}
assert cell.render(ctx).strip() == '<a href="http://example.net/">Example Site</a>'
cell.title = ''
cell.save()
assert cell.render(ctx).strip() == '<a href="http://example.net/">http://example.net/</a>'
cell.link_page = page
cell.save()
assert cell.render(ctx).strip() == '<a href="/example-page/">example page</a>'
cell.title = 'altertitle'
cell.save()
assert cell.render(ctx).strip() == '<a href="/example-page/">altertitle</a>'
cell.anchor = 'anchor'
cell.save()
assert cell.render(ctx).strip() == '<a href="/example-page/#anchor">altertitle</a>'
cell.link_page = None
cell.save()
assert cell.render(ctx).strip() == '<a href="http://example.net/#anchor">altertitle</a>'
def test_variant_templates():
page = Page(title='example page', slug='example-page')
page.save()
cell = TextCell()
cell.page = page
cell.text = '<p>foobar</p>'
cell.order = 0
cell.save()
ctx = {}
assert cell.render(ctx).strip() == '<p>foobar</p>'
templates_settings = [settings.TEMPLATES[0].copy()]
templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
with override_settings(TEMPLATES=templates_settings):
assert cell.render(ctx).strip() == '<p>foobar</p>'
cell.slug = 'foobar'
cell.save()
assert cell.render(ctx).strip() == '<div class="XXX"><p>foobar</p></div>'
assert cell.render(ctx).strip() == '<p>foobar</p>'
def mocked_request(*args, **kwargs):
pass
def test_json_cell():
page = Page(title='example page', slug='example-page')
page.save()
cell = JsonCell()
cell.page = page
cell.url = 'http://example.net/'
cell.title = 'Example Site'
cell.order = 0
cell.save()
cell._json_content = None
assert cell.template_name == 'combo/json-error-cell.html'
cell._json_content = {}
assert cell.template_name == 'combo/json-cell.html'
cell._json_content = {'data': []}
assert cell.template_name == 'combo/json-cell.html'
cell._json_content = {'data': [{'url': 'xxx', 'text': 'xxx'}]}
assert cell.template_name == 'combo/json-list-cell.html'
cell._json_content = {'data': [{'foo': 'xxx', 'bar': 'xxx'}]}
assert cell.template_name == 'combo/json-cell.html'
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': [{'url': 'xxx', 'text': 'xxx'}]}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
context = cell.get_cell_extra_context({})
assert context['json'] == data
assert context['json_url'] == 'http://example.net/'
assert context['json_status'] == 200
assert 'json_error' not in context
requests_get.return_value = mock.Mock(status_code=204) # 204 : No Content
context = cell.get_cell_extra_context({})
assert context['json'] is None
assert context['json_url'] == 'http://example.net/'
assert context['json_status'] == 204
assert 'json_error' not in context
requests_get.return_value = mock.Mock(content='not found', status_code=404,
headers={})
context = cell.get_cell_extra_context({})
assert context['json'] is None
assert context['json_url'] == 'http://example.net/'
assert context['json_status'] == 404
assert 'json_error' not in context
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=404,
headers={'content-type': 'application/json'})
context = cell.get_cell_extra_context({})
assert context['json'] is None
assert context['json_url'] == 'http://example.net/'
assert context['json_status'] == 404
assert context['json_error'] == data
def mocked_requests_connection_error(*args, **kwargs):
raise requests.ConnectionError('boom')
requests_get.side_effect = mocked_requests_connection_error
context = cell.get_cell_extra_context({})
assert context['json'] is None
assert context['json_status'] == -1
assert context['json_url'] == 'http://example.net/'
assert context['json_error'] == 'boom'
assert isinstance(context['json_exception'], requests.ConnectionError)
cell.url = '' # no URL -> no request, no data, no status
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
context = cell.get_cell_extra_context({})
assert context['json'] is None
assert context['json_url'] == ''
assert 'json_status' not in context
assert 'json_error' not in context
with pytest.raises(NothingInCacheException):
cell.url = 'http://test3'
cell.render({})
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': [{'url': 'http://a.b', 'text': 'xxx'}]}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
cell.url = 'http://test4'
result = cell.render({'synchronous': True})
assert 'http://a.b' in result
assert requests_get.call_count == 1
cell.template_string = '{{json.data.0.text}}'
result = cell.render({'synchronous': True})
assert result == 'xxx'
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content='garbage', status_code=200)
cell.url = 'http://test5'
result = cell.render({'synchronous': True})
assert result == ''
context = cell.get_cell_extra_context({})
assert context['json'] is None
assert context['json_url'] == 'http://test5'
assert context['json_status'] == 200
assert context['json_error'] == 'invalid_json'
# URL is a template, using [variables]
cell.cache_duration = 10
data = {'data': [{'url': 'xxx', 'text': 'xxx'}]}
cell.url = 'http://testuser?[foobar]'
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
context = cell.get_cell_extra_context({'foobar': 'barfoo'})
assert context['json'] == data
assert context['json_url'] == 'http://testuser?barfoo'
assert context['json_status'] == 200
assert requests_get.call_args[0][0] == 'http://testuser?barfoo'
assert requests_get.call_args[1]['cache_duration'] == 10
assert requests_get.call_count == 1
with mock.patch('combo.utils.requests.get') as requests_get:
context = cell.get_cell_extra_context({})
# can't get URL, 'foobar' variable is missing
assert context['json'] == None
assert requests_get.call_count == 0
request = RequestFactory().get('/')
request.user = User(username='foo', email='foo@example.net')
cell.url = 'http://testuser?email=[user_email]'
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
context = cell.get_cell_extra_context({'request': request})
assert context['json'] == data
assert context['json_url'] == 'http://testuser?email=foo%40example.net'
assert requests_get.call_count == 1
assert requests_get.call_args[0][0] == 'http://testuser?email=foo%40example.net'
assert requests_get.call_args[1]['cache_duration'] == 10
def test_json_cell_with_varnames(app):
page = Page(title='example page', slug='index')
page.save()
cell = JsonCell()
cell.page = page
cell.title = 'Example Site'
cell.order = 0
cell.varnames_str = 'var1, var2, '
cell.url = 'http://foo?varone=[var1]&vartwo=[var2]'
cell.template_string = '/var1={{var1}}/var2={{var2}}/'
cell.save()
assert cell.varnames == ['var1', 'var2']
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': []}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
url = reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()})
resp = app.get(url + '?var1=foo&var2=bar') # request query string is here
assert requests_get.call_count == 1
assert requests_get.call_args[0][0] == 'http://foo?varone=foo&vartwo=bar'
assert '/var1=foo/' in resp.body
assert '/var2=bar/' in resp.body
def test_config_json_cell():
page = Page(title='example page', slug='example-page')
page.save()
request = RequestFactory().get('/')
with override_settings(JSON_CELL_TYPES={'foobar': {'name': 'Foobar', 'url': 'http://test/'}}):
cell = ConfigJsonCell()
cell.key = 'foobar'
cell.parameters = {'blah': 'plop'}
assert cell.get_label() == 'Foobar'
assert cell.url == 'http://test/'
assert cell.template_name == 'combo/json/foobar.html'
assert cell.css_class_names.split() == ['configjsoncell', 'foobar']
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps({'hello': 'world'}), status_code=200)
context = cell.get_cell_extra_context({'request': request})
assert context['json'] == {'hello': 'world'}
assert context['json_url'] == 'http://test/'
assert context['json_status'] == 200
assert context['parameters'] == {'blah': 'plop'}
assert context['blah'] == 'plop'
with override_settings(JSON_CELL_TYPES={'foobar': {
'name': 'Foobar', 'url': 'http://test/', 'cache_duration': 10}}):
cell = ConfigJsonCell()
cell.key = 'foobar'
cell.parameters = {'blah': 'plop'}
assert cell.get_label() == 'Foobar'
assert cell.url == 'http://test/'
assert cell.template_name == 'combo/json/foobar.html'
assert cell.cache_duration == 10
def test_config_json_cell_with_varnames(app):
page = Page(title='example page', slug='index')
page.save()
templates_settings = [settings.TEMPLATES[0].copy()]
templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
with override_settings(JSON_CELL_TYPES={
'test-config-json-cell': {
'name': 'Foobar',
'url': 'http://foo?varone=[var1]&vartwo=[var2]',
'varnames': ['var1', 'var2']
}},
TEMPLATES=templates_settings):
cell = ConfigJsonCell()
cell.key = 'test-config-json-cell'
cell.page = page
cell.title = 'Example Site'
cell.order = 0
cell.save()
assert cell.varnames == ['var1', 'var2']
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': []}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
url = reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()})
resp = app.get(url + '?var1=foo&var2=bar') # request query string is here
assert requests_get.call_count == 1
assert requests_get.call_args[0][0] == 'http://foo?varone=foo&vartwo=bar'
assert '/var1=foo/' in resp.body
assert '/var2=bar/' in resp.body
resp = app.get(url + '?var2=plop') # use var1 default value
assert requests_get.call_count == 1 # no request, var1 is missing
assert '/var1=/' in resp.body
assert '/var2=plop/' in resp.body
cell.parameters = {'var1': 'defaultvalue1'}
cell.save()
resp = app.get(url + '?var2=plop') # use var1 default value
assert requests_get.call_count == 2
assert requests_get.call_args[0][0] == 'http://foo?varone=defaultvalue1&vartwo=plop'
assert '/var1=defaultvalue1/' in resp.body
assert '/var2=plop/' in resp.body
def test_config_json_cell_with_param_in_url(app):
page = Page(title='example page', slug='index')
page.save()
templates_settings = [settings.TEMPLATES[0].copy()]
templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
with override_settings(JSON_CELL_TYPES={
'test-config-json-cell': {
'name': 'Foobar',
'url': 'http://foo?var=[identifier]',
'log_errors': False,
'timeout': 42,
'form': [
{
"varname": "identifier",
"type": "string",
"label": "Identifier"
}
]
}},
TEMPLATES=templates_settings):
cell = ConfigJsonCell()
cell.key = 'test-config-json-cell'
cell.parameters = {'identifier': 'plop'}
cell.page = page
cell.title = 'Example Site'
cell.order = 0
cell.save()
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': []}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
url = reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()})
resp = app.get(url)
assert requests_get.call_count == 1
assert requests_get.call_args[0][0] == 'http://foo?var=plop'
assert requests_get.call_args[-1]['log_errors'] == False
assert requests_get.call_args[-1]['timeout'] == 42
def test_json_force_async():
cell = JsonCellBase()
cell.url = 'http://example.net/test-force-async'
cell.template_string = '{{json.hello}}'
cell.force_async = True
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps({'hello': 'world'}), status_code=200)
with pytest.raises(NothingInCacheException):
cell.render({})
assert cell.render({'synchronous': True}) == 'world'
# check force async is effective
with pytest.raises(NothingInCacheException):
cell.render({})
# disable force_async
cell.force_async = False
assert cell.render({}) == 'world'
cell = JsonCellBase()
cell.url = 'http://example.net/test-force-async-2'
cell.template_string = '{{json.hello}}'
cell.force_async = False
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps({'hello': 'world2'}), status_code=200)
# raise if nothing in cache
with pytest.raises(NothingInCacheException):
cell.render({})
# force stuff in cache
assert cell.render({'synchronous': True}) == 'world2'
# rerun with stuff in cache
assert cell.render({}) == 'world2'
def test_config_json_cell_additional_url(app):
page = Page(title='example page', slug='index')
page.save()
templates_settings = [settings.TEMPLATES[0].copy()]
templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
with override_settings(JSON_CELL_TYPES={
'test-config-json-cell-2': {
'name': 'Foobar',
'url': 'http://foo',
'additional-data': [
{'key': 'plop', 'url': 'http://bar', 'log_errors': False, 'timeout': 42},
]
}},
TEMPLATES=templates_settings):
cell = ConfigJsonCell()
cell.key = 'test-config-json-cell-2'
cell.page = page
cell.title = 'Example Site'
cell.order = 0
cell.save()
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': 'toto'}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
url = reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()})
resp = app.get(url)
assert resp.body.strip() == '/var1=toto/var2=toto/'
assert len(requests_get.mock_calls) == 2
assert requests_get.mock_calls[0][1][0] == 'http://foo'
assert requests_get.mock_calls[0][-1]['log_errors'] == True
assert requests_get.mock_calls[0][-1]['timeout'] == None
assert requests_get.mock_calls[1][1][0] == 'http://bar'
assert requests_get.mock_calls[1][-1]['log_errors'] == False
assert requests_get.mock_calls[1][-1]['timeout'] == 42
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': 'toto'}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=404,
headers={'content-type': 'application/json'})
url = reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()})
resp = app.get(url)
assert resp.body.strip() == '/var1=/var2=/'
assert len(requests_get.mock_calls) == 2
assert requests_get.mock_calls[0][1][0] == 'http://foo'
assert requests_get.mock_calls[1][1][0] == 'http://bar'
context = cell.get_cell_extra_context({})
assert context['json'] == None
assert context['json_url'] == 'http://foo'
assert context['json_status'] == 404
assert context['json_error'] == data
assert context['plop'] == None
assert context['plop_url'] == 'http://bar'
assert context['plop_status'] == 404
assert context['plop_error'] == data
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': 'toto'}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
context = cell.get_cell_extra_context({})
assert context['json'] == data
assert context['json_url'] == 'http://foo'
assert context['json_status'] == 200
assert 'json_error' not in context
assert context['plop'] == data
assert context['plop_url'] == 'http://bar'
assert context['plop_status'] == 200
assert 'plop_error' not in context
# additional-data url depends on others results, with Django-syntax URL
with override_settings(JSON_CELL_TYPES={
'test-config-json-cell-2': {
'name': 'Foobar',
'url': 'http://foo',
'additional-data': [
{'key': 'plop', 'url': 'http://{{json.data}}', 'log_errors': False, 'timeout': 42},
{'key': 'plop2', 'url': '{% if plop %}http://{{json.data}}/{{plop.data}}{% endif %}', 'log_errors': False,
'timeout': 10},
]
}},
TEMPLATES=templates_settings):
cell = ConfigJsonCell()
cell.key = 'test-config-json-cell-2'
cell.page = page
cell.title = 'Example Site'
cell.order = 0
cell.save()
data = {'data': 'bar'}
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
url = reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()})
resp = app.get(url)
assert resp.body.strip() == '/var1=bar/var2=bar/'
assert len(requests_get.mock_calls) == 3
assert requests_get.mock_calls[0][1][0] == 'http://foo'
assert requests_get.mock_calls[0][-1]['log_errors'] == True
assert requests_get.mock_calls[0][-1]['timeout'] == None
assert requests_get.mock_calls[1][1][0] == 'http://bar'
assert requests_get.mock_calls[1][-1]['log_errors'] == False
assert requests_get.mock_calls[1][-1]['timeout'] == 42
assert requests_get.mock_calls[2][1][0] == 'http://bar/bar'
assert requests_get.mock_calls[2][-1]['log_errors'] == False
assert requests_get.mock_calls[2][-1]['timeout'] == 10
context = cell.get_cell_extra_context({})
assert context['json'] == data
assert context['json_url'] == 'http://foo'
assert context['json_status'] == 200
assert context['plop'] == data
assert context['plop_url'] == 'http://bar'
assert context['plop_status'] == 200
assert context['plop2'] == data
assert context['plop2_url'] == 'http://bar/bar'
assert context['plop2_status'] == 200
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=404,
headers={'content-type': 'application/json'})
url = reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()})
resp = app.get(url)
assert resp.body.strip() == '/var1=/var2=/'
# can not create plop and plop2 url: only one request for "json"
assert len(requests_get.mock_calls) == 2
assert requests_get.mock_calls[0][1][0] == 'http://foo'
context = cell.get_cell_extra_context({})
assert context['json'] == None
assert context['json_url'] == 'http://foo'
assert context['json_status'] == 404
assert context['json_error'] == data
assert context['plop'] == None
assert context['plop_url'] == 'http://'
assert context['plop_status'] == 404
assert context['plop_error'] == data
# plop2 url is empty, no request: None value, no status
assert context['plop2'] == None
assert context['plop2_url'] == ''
assert 'plop2_status' not in context
assert 'plop2_error' not in context
def test_config_json_invalid_key_cell():
page = Page(title='example page', slug='example-page')
page.save()
request = RequestFactory().get('/')
with override_settings(JSON_CELL_TYPES={'foobar': {'name': 'Foobar', 'url': 'http://test/'}}):
cell = ConfigJsonCell()
cell.key = 'foobar'
cell.parameters = {'blah': 'plop'}
cell.page = page
cell.order = 0
cell.save()
assert len(page.get_cells()) == 1
assert len(page.get_cells()) == 0
def test_page_cell_placeholder_restricted_visibility(app, admin_user):
page = Page(title='Test', slug='test', template_name='standard')
page.save()
json_cell = JsonCell(page=page, placeholder='content', order=0, url='http://example.com')
json_cell.template_string = '{% load combo %}{% placeholder "foobar" name="Foobar" %}'
json_cell.save()
TextCell(page=page, placeholder='foobar', text='<p>Public text</p>', order=0,
restricted_to_unlogged=True).save()
TextCell(page=page, placeholder='foobar', text='<p>Private text</p>',
order=1, public=False).save()
resp = app.get(reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.pk, 'cell_reference': json_cell.get_reference()}))
assert "<p>Public text</p>" in resp.content
assert "<p>Private text</p>" not in resp.content
app = login(app)
resp = app.get(reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.pk, 'cell_reference': json_cell.get_reference()}))
assert "<p>Public text</p>" not in resp.content
assert "<p>Private text</p>" in resp.content
def test_related_cell_types_tracking():
page = Page(title='example page', slug='example-page')
page.save()
assert page.related_cells['cell_types'] == []
TextCell(page=page, placeholder='content', order=0, text='hello').save()
assert Page.objects.get(id=page.id).related_cells['cell_types'] == ['data_textcell']
TextCell(page=page, placeholder='content', order=1, text='hello').save()
assert Page.objects.get(id=page.id).related_cells['cell_types'] == ['data_textcell']
LinkCell(page=page, placeholder='content', order=0, title='Test', url='http://example.net').save()
assert set(Page.objects.get(id=page.id).related_cells['cell_types']) == set(['data_textcell', 'data_linkcell'])
with CaptureQueriesContext(connection) as ctx:
assert len(CellBase.get_cells(page=Page.objects.get(id=page.id))) == 3
assert len(ctx.captured_queries) == 1 + 2
TextCell.objects.get(order=1).delete()
assert set(Page.objects.get(id=page.id).related_cells['cell_types']) == set(['data_textcell', 'data_linkcell'])
TextCell.objects.get(order=0).delete()
assert set(Page.objects.get(id=page.id).related_cells['cell_types']) == set(['data_linkcell'])
with CaptureQueriesContext(connection) as ctx:
assert len(CellBase.get_cells(page=Page.objects.get(id=page.id))) == 1
assert len(ctx.captured_queries) == 1 + 1
# remove tracker, check it is rebuilt correctly
page.related_cells = {}
page.save()
with CaptureQueriesContext(connection) as ctx:
assert len(CellBase.get_cells(page=Page.objects.get(id=page.id))) == 1
assert len(ctx.captured_queries) == len(get_cell_classes())
Page.objects.get(id=page.id).get_cells()
TextCell(page=page, placeholder='content', order=0, text='hello').save()
assert set(Page.objects.get(id=page.id).related_cells['cell_types']) == set(['data_textcell', 'data_linkcell'])