Skip to content

Commit

Permalink
Support building a custom hallo plugins list from a 'features' kwarg …
Browse files Browse the repository at this point in the history
…on RichTextField
  • Loading branch information
gasman committed Aug 10, 2017
1 parent 31cf4b6 commit 3532f86
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 14 deletions.
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-07-13 22:20
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import wagtail.wagtailcore.fields


class Migration(migrations.Migration):

dependencies = [
('wagtailcore', '0040_page_draft_title'),
('tests', '0018_multiselect_form_field'),
]

operations = [
migrations.CreateModel(
name='RichTextFieldWithFeaturesPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
('body', wagtail.wagtailcore.fields.RichTextField()),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
]
9 changes: 9 additions & 0 deletions wagtail/tests/testapp/models.py
Expand Up @@ -911,6 +911,15 @@ class CustomRichBlockFieldPage(Page):
] ]




class RichTextFieldWithFeaturesPage(Page):
body = RichTextField(features=['blockquote', 'embed', 'made-up-feature'])

content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('body'),
]


# a page that only contains RichTextField within an InlinePanel, # a page that only contains RichTextField within an InlinePanel,
# to test that the inline child's form media gets pulled through # to test that the inline child's form media gets pulled through
class SectionedRichTextPageSection(Orderable): class SectionedRichTextPageSection(Orderable):
Expand Down
3 changes: 2 additions & 1 deletion wagtail/tests/testapp/wagtail_hooks.py
Expand Up @@ -5,6 +5,7 @@
from django.http import HttpResponse from django.http import HttpResponse


from wagtail.wagtailadmin.menu import MenuItem from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailadmin.rich_text import HalloPlugin
from wagtail.wagtailadmin.search import SearchArea from wagtail.wagtailadmin.search import SearchArea
from wagtail.wagtailcore import hooks from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.whitelist import allow_without_attributes, attribute_rule, check_url from wagtail.wagtailcore.whitelist import allow_without_attributes, attribute_rule, check_url
Expand Down Expand Up @@ -96,5 +97,5 @@ def polite_pages_only(parent_page, pages, request):
@hooks.register('register_rich_text_features') @hooks.register('register_rich_text_features')
def register_blockquote_feature(features): def register_blockquote_feature(features):
features.register_editor_plugin( features.register_editor_plugin(
'hallo', 'blockquote', {'plugin_name': 'halloblockquote'} 'hallo', 'blockquote', HalloPlugin(name='halloblockquote')
) )
53 changes: 44 additions & 9 deletions wagtail/wagtailadmin/rich_text.py
Expand Up @@ -9,15 +9,30 @@


from wagtail.utils.widgets import WidgetWithScript from wagtail.utils.widgets import WidgetWithScript
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel
from wagtail.wagtailcore.rich_text import DbWhitelister, expand_db_html from wagtail.wagtailcore.rich_text import DbWhitelister, expand_db_html, features




class HalloRichTextArea(WidgetWithScript, widgets.Textarea): class HalloRichTextArea(WidgetWithScript, widgets.Textarea):
# this class's constructor accepts a 'features' kwarg
accepts_features = True

def get_panel(self): def get_panel(self):
return RichTextFieldPanel return RichTextFieldPanel


def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.options = kwargs.pop('options', None) self.options = kwargs.pop('options', None)

self.features = kwargs.pop('features', None)
if self.features is None:
self.plugins = None
else:
# construct a list of plugin objects, by querying the feature registry
# and keeping the non-null responses from get_editor_plugin
self.plugins = filter(None, [
features.get_editor_plugin('hallo', feature_name)
for feature_name in self.features
])

super(HalloRichTextArea, self).__init__(*args, **kwargs) super(HalloRichTextArea, self).__init__(*args, **kwargs)


def render(self, name, value, attrs=None): def render(self, name, value, attrs=None):
Expand All @@ -28,15 +43,19 @@ def render(self, name, value, attrs=None):
return super(HalloRichTextArea, self).render(name, translated_value, attrs) return super(HalloRichTextArea, self).render(name, translated_value, attrs)


def render_js_init(self, id_, name, value): def render_js_init(self, id_, name, value):
try: if self.options is not None and 'plugins' in self.options:
plugins = self.options['plugins'] plugin_data = self.options['plugins']
except (TypeError, KeyError): elif self.plugins is not None:
plugin_data = {}
for plugin in self.plugins:
plugin.construct_plugins_list(plugin_data)
else:
# no plugin list specified, so initialise without a plugins arg # no plugin list specified, so initialise without a plugins arg
# (so that it'll pick up the globally-defined halloPlugins list instead) # (so that it'll pick up the globally-defined halloPlugins list instead)
return "makeHalloRichTextEditable({0});".format(json.dumps(id_)) return "makeHalloRichTextEditable({0});".format(json.dumps(id_))


return "makeHalloRichTextEditable({0}, {1});".format( return "makeHalloRichTextEditable({0}, {1});".format(
json.dumps(id_), json.dumps(plugins) json.dumps(id_), json.dumps(plugin_data)
) )


def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
Expand All @@ -56,20 +75,36 @@ def media(self):
]) ])




class HalloPlugin(object):
def __init__(self, **kwargs):
self.name = kwargs.pop('name', None)
self.options = kwargs.pop('options', {})

def construct_plugins_list(self, plugins):
if self.name is not None:
plugins[self.name] = self.options


DEFAULT_RICH_TEXT_EDITORS = { DEFAULT_RICH_TEXT_EDITORS = {
'default': { 'default': {
'WIDGET': 'wagtail.wagtailadmin.rich_text.HalloRichTextArea' 'WIDGET': 'wagtail.wagtailadmin.rich_text.HalloRichTextArea'
} }
} }




def get_rich_text_editor_widget(name='default'): def get_rich_text_editor_widget(name='default', features=None):
editor_settings = getattr(settings, 'WAGTAILADMIN_RICH_TEXT_EDITORS', DEFAULT_RICH_TEXT_EDITORS) editor_settings = getattr(settings, 'WAGTAILADMIN_RICH_TEXT_EDITORS', DEFAULT_RICH_TEXT_EDITORS)


editor = editor_settings[name] editor = editor_settings[name]
options = editor.get('OPTIONS', None) options = editor.get('OPTIONS', None)
cls = import_string(editor['WIDGET'])

kwargs = {}

if options is not None:
kwargs['options'] = options


if options is None: if getattr(cls, 'accepts_features', False):
return import_string(editor['WIDGET'])() kwargs['features'] = features


return import_string(editor['WIDGET'])(options=options) return cls(**kwargs)
36 changes: 36 additions & 0 deletions wagtail/wagtailadmin/tests/test_rich_text.py
Expand Up @@ -265,3 +265,39 @@ def test_custom_editor_in_rich_text_block(self):


# Check that the custom plugin options are being passed in the hallo initialiser # Check that the custom plugin options are being passed in the hallo initialiser
self.assertIn('makeHalloRichTextEditable("body", {"halloheadings": {"formatBlocks": ["p"]}});', form_html) self.assertIn('makeHalloRichTextEditable("body", {"halloheadings": {"formatBlocks": ["p"]}});', form_html)


class TestHalloJsWithFeaturesKwarg(BaseRichTextEditHandlerTestCase, WagtailTestUtils):

def setUp(self):
super(TestHalloJsWithFeaturesKwarg, self).setUp()

# Find root page
self.root_page = Page.objects.get(id=2)

self.login()

def test_features_list_on_rich_text_field(self):
response = self.client.get(reverse(
'wagtailadmin_pages:add', args=('tests', 'richtextfieldwithfeaturespage', self.root_page.id)
))

# Check status code
self.assertEqual(response.status_code, 200)

# Check that the custom plugin options are being passed in the hallo initialiser
self.assertContains(response, '"halloblockquote":')
self.assertContains(response, '"hallowagtailembeds":')
self.assertNotContains(response, '"halloheadings":')
self.assertNotContains(response, '"hallowagtailimage":')

def test_features_list_on_rich_text_block(self):
block = RichTextBlock(features=['blockquote', 'embed', 'made-up-feature'])

form_html = block.render_form(block.to_python("<p>hello</p>"), 'body')

# Check that the custom plugin options are being passed in the hallo initialiser
self.assertIn('"halloblockquote":', form_html)
self.assertIn('"hallowagtailembeds":', form_html)
self.assertNotIn('"halloheadings":', form_html)
self.assertNotIn('"hallowagtailimage":', form_html)
8 changes: 6 additions & 2 deletions wagtail/wagtailcore/blocks/field_block.py
Expand Up @@ -450,9 +450,10 @@ class Meta:


class RichTextBlock(FieldBlock): class RichTextBlock(FieldBlock):


def __init__(self, required=True, help_text=None, editor='default', **kwargs): def __init__(self, required=True, help_text=None, editor='default', features=None, **kwargs):
self.field_options = {'required': required, 'help_text': help_text} self.field_options = {'required': required, 'help_text': help_text}
self.editor = editor self.editor = editor
self.features = features
super(RichTextBlock, self).__init__(**kwargs) super(RichTextBlock, self).__init__(**kwargs)


def get_default(self): def get_default(self):
Expand All @@ -474,7 +475,10 @@ def get_prep_value(self, value):
@cached_property @cached_property
def field(self): def field(self):
from wagtail.wagtailadmin.rich_text import get_rich_text_editor_widget from wagtail.wagtailadmin.rich_text import get_rich_text_editor_widget
return forms.CharField(widget=get_rich_text_editor_widget(self.editor), **self.field_options) return forms.CharField(
widget=get_rich_text_editor_widget(self.editor, features=self.features),
**self.field_options
)


def value_for_form(self, value): def value_for_form(self, value):
# Rich text editors take the source-HTML string as input (and takes care # Rich text editors take the source-HTML string as input (and takes care
Expand Down
4 changes: 3 additions & 1 deletion wagtail/wagtailcore/fields.py
Expand Up @@ -12,11 +12,13 @@
class RichTextField(models.TextField): class RichTextField(models.TextField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.editor = kwargs.pop('editor', 'default') self.editor = kwargs.pop('editor', 'default')
self.features = kwargs.pop('features', None)
# TODO: preserve 'editor' and 'features' when deconstructing for migrations
super(RichTextField, self).__init__(*args, **kwargs) super(RichTextField, self).__init__(*args, **kwargs)


def formfield(self, **kwargs): def formfield(self, **kwargs):
from wagtail.wagtailadmin.rich_text import get_rich_text_editor_widget from wagtail.wagtailadmin.rich_text import get_rich_text_editor_widget
defaults = {'widget': get_rich_text_editor_widget(self.editor)} defaults = {'widget': get_rich_text_editor_widget(self.editor, features=self.features)}
defaults.update(kwargs) defaults.update(kwargs)
return super(RichTextField, self).formfield(**defaults) return super(RichTextField, self).formfield(**defaults)


Expand Down
2 changes: 1 addition & 1 deletion wagtail/wagtailcore/tests/test_rich_text.py
Expand Up @@ -152,7 +152,7 @@ def test_register_rich_text_features_hook(self):
# plugin, via the register_rich_text_features hook; test that we can retrieve it here # plugin, via the register_rich_text_features hook; test that we can retrieve it here
features = FeatureRegistry() features = FeatureRegistry()
blockquote = features.get_editor_plugin('hallo', 'blockquote') blockquote = features.get_editor_plugin('hallo', 'blockquote')
self.assertEqual(blockquote['plugin_name'], 'halloblockquote') self.assertEqual(blockquote.name, 'halloblockquote')


def test_missing_editor_plugin_returns_none(self): def test_missing_editor_plugin_returns_none(self):
features = FeatureRegistry() features = FeatureRegistry()
Expand Down
9 changes: 9 additions & 0 deletions wagtail/wagtaildocs/wagtail_hooks.py
Expand Up @@ -10,6 +10,7 @@
from django.utils.translation import ungettext from django.utils.translation import ungettext


from wagtail.wagtailadmin.menu import MenuItem from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailadmin.rich_text import HalloPlugin
from wagtail.wagtailadmin.search import SearchArea from wagtail.wagtailadmin.search import SearchArea
from wagtail.wagtailadmin.site_summary import SummaryItem from wagtail.wagtailadmin.site_summary import SummaryItem
from wagtail.wagtailcore import hooks from wagtail.wagtailcore import hooks
Expand Down Expand Up @@ -74,6 +75,14 @@ def editor_js():
) )




@hooks.register('register_rich_text_features')
def register_embed_feature(features):
features.register_editor_plugin(
'hallo', 'document-link',
HalloPlugin(name='hallowagtaildoclink')
)


@hooks.register('register_rich_text_link_handler') @hooks.register('register_rich_text_link_handler')
def register_document_link_handler(): def register_document_link_handler():
return ('document', DocumentLinkHandler) return ('document', DocumentLinkHandler)
Expand Down
9 changes: 9 additions & 0 deletions wagtail/wagtailembeds/wagtail_hooks.py
Expand Up @@ -5,6 +5,7 @@
from django.core import urlresolvers from django.core import urlresolvers
from django.utils.html import format_html from django.utils.html import format_html


from wagtail.wagtailadmin.rich_text import HalloPlugin
from wagtail.wagtailcore import hooks from wagtail.wagtailcore import hooks
from wagtail.wagtailembeds import urls from wagtail.wagtailembeds import urls
from wagtail.wagtailembeds.rich_text import MediaEmbedHandler from wagtail.wagtailembeds.rich_text import MediaEmbedHandler
Expand Down Expand Up @@ -32,6 +33,14 @@ def editor_js():
) )




@hooks.register('register_rich_text_features')
def register_embed_feature(features):
features.register_editor_plugin(
'hallo', 'embed',
HalloPlugin(name='hallowagtailembeds')
)


@hooks.register('register_rich_text_embed_handler') @hooks.register('register_rich_text_embed_handler')
def register_media_embed_handler(): def register_media_embed_handler():
return ('media', MediaEmbedHandler) return ('media', MediaEmbedHandler)
9 changes: 9 additions & 0 deletions wagtail/wagtailimages/wagtail_hooks.py
Expand Up @@ -8,6 +8,7 @@
from django.utils.translation import ungettext from django.utils.translation import ungettext


from wagtail.wagtailadmin.menu import MenuItem from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailadmin.rich_text import HalloPlugin
from wagtail.wagtailadmin.search import SearchArea from wagtail.wagtailadmin.search import SearchArea
from wagtail.wagtailadmin.site_summary import SummaryItem from wagtail.wagtailadmin.site_summary import SummaryItem
from wagtail.wagtailcore import hooks from wagtail.wagtailcore import hooks
Expand Down Expand Up @@ -66,6 +67,14 @@ def editor_js():
) )




@hooks.register('register_rich_text_features')
def register_image_feature(features):
features.register_editor_plugin(
'hallo', 'image',
HalloPlugin(name='hallowagtailimage')
)


@hooks.register('register_image_operations') @hooks.register('register_image_operations')
def register_image_operations(): def register_image_operations():
return [ return [
Expand Down

0 comments on commit 3532f86

Please sign in to comment.