mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-12 09:13:14 +00:00
Merge branch 'feature/streamfield' of github.com:torchbox/wagtail into feature/streamfield
This commit is contained in:
commit
6babbe37f2
5 changed files with 250 additions and 14 deletions
|
|
@ -19,6 +19,9 @@ from django.forms.utils import ErrorList
|
|||
|
||||
import six
|
||||
|
||||
from wagtail.wagtailcore.utils import escape_script
|
||||
from wagtail.wagtailcore.rich_text import expand_db_html
|
||||
|
||||
# helpers for Javascript expression formatting
|
||||
|
||||
def indent(string, depth=1):
|
||||
|
|
@ -295,6 +298,14 @@ class CharBlock(FieldBlock):
|
|||
# TODO: some kwargs, such as max_length, and *possibly* things like help_text, should be passed to
|
||||
# the CharField constructor. Figure out a system for doing this
|
||||
|
||||
class RichTextBlock(FieldBlock):
|
||||
def __init__(self, **kwargs):
|
||||
from wagtail.wagtailcore.fields import RichTextArea
|
||||
super(RichTextBlock, self).__init__(CharField(widget=RichTextArea), **kwargs)
|
||||
|
||||
def render_basic(self, value):
|
||||
return mark_safe('<div class="rich-text">' + expand_db_html(value) + '</div>')
|
||||
|
||||
# =======
|
||||
# Chooser
|
||||
# =======
|
||||
|
|
@ -520,7 +531,7 @@ class ListBlock(Block):
|
|||
|
||||
return format_html(
|
||||
'<script type="text/template" id="{0}-newmember">{1}</script>',
|
||||
self.definition_prefix, list_member_html
|
||||
self.definition_prefix, mark_safe(escape_script(list_member_html))
|
||||
)
|
||||
|
||||
def js_initializer(self):
|
||||
|
|
@ -644,7 +655,7 @@ class BaseStreamBlock(Block):
|
|||
(
|
||||
self.definition_prefix,
|
||||
name,
|
||||
self.render_list_member(name, child_block.default, '__PREFIX__', '')
|
||||
mark_safe(escape_script(self.render_list_member(name, child_block.default, '__PREFIX__', '')))
|
||||
)
|
||||
for name, child_block in self.child_blocks.items()
|
||||
]
|
||||
|
|
|
|||
|
|
@ -80,11 +80,17 @@ For example, they don't assume the presence of a 'delete' button - it's up to th
|
|||
newMember._markAdded();
|
||||
}
|
||||
|
||||
function elementFromTemplate(template, newPrefix) {
|
||||
/* generate a jquery object ready to be inserted into the list, based on the passed HTML template string.
|
||||
'__PREFIX__' will be substituted with newPrefix, and script tags escaped as <-/script> will be un-escaped */
|
||||
return $(template.replace(/__PREFIX__/g, newPrefix).replace(/<-(-*)\/script>/g, '<$1/script>'));
|
||||
}
|
||||
|
||||
self.insertMemberBefore = function(otherMember, template) {
|
||||
newMemberPrefix = getNewMemberPrefix();
|
||||
|
||||
/* Create the new list member element with the real prefix substituted in */
|
||||
var elem = $(template.replace(/__PREFIX__/g, newMemberPrefix));
|
||||
var elem = elementFromTemplate(template, newMemberPrefix);
|
||||
otherMember.container.before(elem);
|
||||
var newMember = SequenceMember(self, newMemberPrefix);
|
||||
var index = otherMember.getIndex();
|
||||
|
|
@ -105,7 +111,7 @@ For example, they don't assume the presence of a 'delete' button - it's up to th
|
|||
newMemberPrefix = getNewMemberPrefix();
|
||||
|
||||
/* Create the new list member element with the real prefix substituted in */
|
||||
var elem = $(template.replace(/__PREFIX__/g, newMemberPrefix));
|
||||
var elem = elementFromTemplate(template, newMemberPrefix);
|
||||
otherMember.container.after(elem);
|
||||
var newMember = SequenceMember(self, newMemberPrefix);
|
||||
var index = otherMember.getIndex() + 1;
|
||||
|
|
@ -130,7 +136,7 @@ For example, they don't assume the presence of a 'delete' button - it's up to th
|
|||
newMemberPrefix = getNewMemberPrefix();
|
||||
|
||||
/* Create the new list member element with the real prefix substituted in */
|
||||
var elem = $(template.replace(/__PREFIX__/g, newMemberPrefix));
|
||||
var elem = elementFromTemplate(template, newMemberPrefix);
|
||||
list.prepend(elem);
|
||||
var newMember = SequenceMember(self, newMemberPrefix);
|
||||
|
||||
|
|
@ -150,7 +156,7 @@ For example, they don't assume the presence of a 'delete' button - it's up to th
|
|||
newMemberPrefix = getNewMemberPrefix();
|
||||
|
||||
/* Create the new list member element with the real prefix substituted in */
|
||||
var elem = $(template.replace(/__PREFIX__/g, newMemberPrefix));
|
||||
var elem = elementFromTemplate(template, newMemberPrefix);
|
||||
list.append(elem);
|
||||
var newMember = SequenceMember(self, newMemberPrefix);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django import template
|
||||
from django.contrib.humanize.templatetags.humanize import intcomma
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import get_navigation_menu_items, UserPagePermissionsProxy, PageViewRestriction
|
||||
from wagtail.wagtailcore.utils import camelcase_to_underscore
|
||||
from wagtail.wagtailcore.utils import camelcase_to_underscore, escape_script
|
||||
from wagtail.wagtailadmin.menu import admin_menu
|
||||
|
||||
|
||||
|
|
@ -136,7 +134,6 @@ def usage_count_enabled():
|
|||
|
||||
class EscapeScriptNode(template.Node):
|
||||
TAG_NAME = 'escapescript'
|
||||
SCRIPT_RE = re.compile(r'<(-*)/script>')
|
||||
|
||||
def __init__(self, nodelist):
|
||||
super(EscapeScriptNode, self).__init__()
|
||||
|
|
@ -144,8 +141,7 @@ class EscapeScriptNode(template.Node):
|
|||
|
||||
def render(self, context):
|
||||
out = self.nodelist.render(context)
|
||||
escaped_out = self.SCRIPT_RE.sub(r'<-\1/script>', out)
|
||||
return escaped_out
|
||||
return escape_script(out)
|
||||
|
||||
@classmethod
|
||||
def handle(cls, parser, token):
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ class TestStructBlock(unittest.TestCase):
|
|||
|
||||
def test_render(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField(label="Title"))
|
||||
link = blocks.FieldBlock(forms.URLField(label="Link"))
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
block = LinkBlock()
|
||||
html = block.render({
|
||||
|
|
@ -127,6 +127,27 @@ class TestStructBlock(unittest.TestCase):
|
|||
self.assertIn('<dt>link</dt>', html)
|
||||
self.assertIn('<dd>http://www.wagtail.io</dd>', html)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_render_unknown_field(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
block = LinkBlock()
|
||||
html = block.render({
|
||||
'title': "Wagtail site",
|
||||
'link': 'http://www.wagtail.io',
|
||||
'image': 10,
|
||||
})
|
||||
|
||||
self.assertIn('<dt>title</dt>', html)
|
||||
self.assertIn('<dd>Wagtail site</dd>', html)
|
||||
self.assertIn('<dt>link</dt>', html)
|
||||
self.assertIn('<dd>http://www.wagtail.io</dd>', html)
|
||||
|
||||
# Don't render the extra item
|
||||
self.assertNotIn('<dt>image</dt>', html)
|
||||
|
||||
def test_render_form(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
|
|
@ -144,6 +165,42 @@ class TestStructBlock(unittest.TestCase):
|
|||
self.assertIn('<div class="field url_field">', html)
|
||||
self.assertIn('<input id="mylink-link" name="mylink-link" type="url" value="http://www.wagtail.io" />', html)
|
||||
|
||||
def test_render_form_unknown_field(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField())
|
||||
link = blocks.FieldBlock(forms.URLField())
|
||||
|
||||
block = LinkBlock()
|
||||
html = block.render_form({
|
||||
'title': "Wagtail site",
|
||||
'link': 'http://www.wagtail.io',
|
||||
'image': 10,
|
||||
}, prefix='mylink')
|
||||
|
||||
self.assertIn('<div class="struct-block">', html)
|
||||
self.assertIn('<div class="field char_field">', html)
|
||||
self.assertIn('<input id="mylink-title" name="mylink-title" type="text" value="Wagtail site" />', html)
|
||||
self.assertIn('<div class="field url_field">', html)
|
||||
self.assertIn('<input id="mylink-link" name="mylink-link" type="url" value="http://www.wagtail.io" />', html)
|
||||
|
||||
# Don't render the extra field
|
||||
self.assertNotIn('mylink-image', html)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_render_form_uses_initial_values(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.FieldBlock(forms.CharField(initial="Torchbox"))
|
||||
link = blocks.FieldBlock(forms.URLField(initial="http://www.torchbox.com"))
|
||||
|
||||
block = LinkBlock()
|
||||
html = block.render_form({}, prefix='mylink')
|
||||
|
||||
self.assertIn('<div class="struct-block">', html)
|
||||
self.assertIn('<div class="field char_field">', html)
|
||||
self.assertIn('<input id="mylink-title" name="mylink-title" type="text" value="Torchbox" />', html)
|
||||
self.assertIn('<div class="field url_field">', html)
|
||||
self.assertIn('<input id="mylink-link" name="mylink-link" type="url" value="http://www.torchbox.com" />', html)
|
||||
|
||||
|
||||
class TestListBlock(unittest.TestCase):
|
||||
def test_initialise_with_class(self):
|
||||
|
|
@ -213,3 +270,159 @@ class TestListBlock(unittest.TestCase):
|
|||
self.assertIn('<input id="links-0-value-link" name="links-0-value-link" type="url" value="http://www.wagtail.io" />', html)
|
||||
self.assertIn('<input id="links-1-value-title" name="links-1-value-title" type="text" value="Django" />', html)
|
||||
self.assertIn('<input id="links-1-value-link" name="links-1-value-link" type="url" value="http://www.djangoproject.com" />', html)
|
||||
|
||||
|
||||
class TestStreamBlock(unittest.TestCase):
|
||||
def test_initialisation(self):
|
||||
block = blocks.StreamBlock([
|
||||
('heading', blocks.FieldBlock(forms.CharField())),
|
||||
('paragraph', blocks.FieldBlock(forms.CharField())),
|
||||
])
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['heading', 'paragraph'])
|
||||
|
||||
def test_initialisation_from_subclass(self):
|
||||
class ArticleBlock(blocks.StreamBlock):
|
||||
heading = blocks.FieldBlock(forms.CharField())
|
||||
paragraph = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
block = ArticleBlock()
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['heading', 'paragraph'])
|
||||
|
||||
def test_initialisation_from_subclass_with_extra(self):
|
||||
class ArticleBlock(blocks.StreamBlock):
|
||||
heading = blocks.FieldBlock(forms.CharField())
|
||||
paragraph = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
block = ArticleBlock([
|
||||
('intro', blocks.FieldBlock(forms.CharField()))
|
||||
])
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['heading', 'paragraph', 'intro'])
|
||||
|
||||
def test_initialisation_with_multiple_subclassses(self):
|
||||
class ArticleBlock(blocks.StreamBlock):
|
||||
heading = blocks.FieldBlock(forms.CharField())
|
||||
paragraph = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
class ArticleWithIntroBlock(ArticleBlock):
|
||||
intro = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
block = ArticleWithIntroBlock()
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['heading', 'paragraph', 'intro'])
|
||||
|
||||
@unittest.expectedFailure # Field order doesn't match inheritance order
|
||||
def test_initialisation_with_mixins(self):
|
||||
class ArticleBlock(blocks.StreamBlock):
|
||||
heading = blocks.FieldBlock(forms.CharField())
|
||||
paragraph = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
class IntroMixin(blocks.StreamBlock):
|
||||
intro = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
class ArticleWithIntroBlock(ArticleBlock, IntroMixin):
|
||||
pass
|
||||
|
||||
block = ArticleWithIntroBlock()
|
||||
|
||||
self.assertEqual(list(block.child_blocks.keys()), ['heading', 'paragraph', 'intro'])
|
||||
|
||||
def render_article(self, data):
|
||||
class ArticleBlock(blocks.StreamBlock):
|
||||
heading = blocks.FieldBlock(forms.CharField())
|
||||
paragraph = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
block = ArticleBlock()
|
||||
value = block.to_python(data)
|
||||
|
||||
return block.render(value)
|
||||
|
||||
def test_render(self):
|
||||
html = self.render_article([
|
||||
{
|
||||
'type': 'heading',
|
||||
'value': "My title",
|
||||
},
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'value': 'My first paragraph',
|
||||
},
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'value': 'My second paragraph',
|
||||
},
|
||||
])
|
||||
|
||||
self.assertIn('<div class="block-heading">My title</div>', html)
|
||||
self.assertIn('<div class="block-paragraph">My first paragraph</div>', html)
|
||||
self.assertIn('<div class="block-paragraph">My second paragraph</div>', html)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_render_unknown_type(self):
|
||||
# This can happen if a developer removes a type from their StreamBlock
|
||||
html = self.render_article([
|
||||
{
|
||||
'type': 'foo',
|
||||
'value': "Hello",
|
||||
},
|
||||
])
|
||||
|
||||
def render_form(self):
|
||||
class ArticleBlock(blocks.StreamBlock):
|
||||
heading = blocks.FieldBlock(forms.CharField())
|
||||
paragraph = blocks.FieldBlock(forms.CharField())
|
||||
|
||||
block = ArticleBlock()
|
||||
value = block.to_python([
|
||||
{
|
||||
'type': 'heading',
|
||||
'value': "My title",
|
||||
},
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'value': 'My first paragraph',
|
||||
},
|
||||
{
|
||||
'type': 'paragraph',
|
||||
'value': 'My second paragraph',
|
||||
},
|
||||
])
|
||||
return block.render_form(value, prefix='myarticle')
|
||||
|
||||
def test_render_form_wrapper_class(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<div class="sequence">', html)
|
||||
|
||||
def test_render_form_count_field(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input type="hidden" name="myarticle-count" id="myarticle-count" value="3">', html)
|
||||
|
||||
def test_render_form_delete_field(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input type="hidden" id="myarticle-0-deleted" name="myarticle-0-deleted" value="">', html)
|
||||
|
||||
def test_render_form_order_fields(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input type="hidden" id="myarticle-0-order" name="myarticle-0-order" value="0">', html)
|
||||
self.assertIn('<input type="hidden" id="myarticle-1-order" name="myarticle-1-order" value="1">', html)
|
||||
self.assertIn('<input type="hidden" id="myarticle-2-order" name="myarticle-2-order" value="2">', html)
|
||||
|
||||
def test_render_form_type_fields(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input type="hidden" id="myarticle-0-type" name="myarticle-0-type" value="heading">', html)
|
||||
self.assertIn('<input type="hidden" id="myarticle-1-type" name="myarticle-1-type" value="paragraph">', html)
|
||||
self.assertIn('<input type="hidden" id="myarticle-2-type" name="myarticle-2-type" value="paragraph">', html)
|
||||
|
||||
def test_render_form_value_fields(self):
|
||||
html = self.render_form()
|
||||
|
||||
self.assertIn('<input id="myarticle-0-value" name="myarticle-0-value" type="text" value="My title" />', html)
|
||||
self.assertIn('<input id="myarticle-1-value" name="myarticle-1-value" type="text" value="My first paragraph" />', html)
|
||||
self.assertIn('<input id="myarticle-2-value" name="myarticle-2-value" type="text" value="My second paragraph" />', html)
|
||||
|
|
|
|||
|
|
@ -36,3 +36,13 @@ def resolve_model_string(model_string, default_app=None):
|
|||
|
||||
else:
|
||||
raise LookupError("Can not resolve {0!r} into a model".format(model_string), model_string)
|
||||
|
||||
|
||||
SCRIPT_RE = re.compile(r'<(-*)/script>')
|
||||
def escape_script(text):
|
||||
"""
|
||||
Escape `</script>` tags in 'text' so that it can be placed within a `<script>` block without
|
||||
accidentally closing it. A '-' character will be inserted for each time it is escaped:
|
||||
`<-/script>`, `<--/script>` etc.
|
||||
"""
|
||||
return SCRIPT_RE.sub(r'<-\1/script>', text)
|
||||
|
|
|
|||
Loading…
Reference in a new issue