class DraftailRichTextArea(WidgetWithScript, widgets.HiddenInput): # this class's constructor accepts a 'features' kwarg accepts_features = True def get_panel(self): return RichTextFieldPanel def __init__(self, *args, **kwargs): # note: this constructor will receive an 'options' kwarg taken from the WAGTAILADMIN_RICH_TEXT_EDITORS setting, # but we don't currently recognise any options from there (other than 'features', which is passed here as a separate kwarg) kwargs.pop('options', None) self.options = {} self.features = kwargs.pop('features', None) if self.features is None: self.features = feature_registry.get_default_features() for feature in self.features: plugin = feature_registry.get_editor_plugin('draftail', feature) if plugin: plugin.construct_options(self.options) self.converter = ContentstateConverter(self.features) super().__init__(*args, **kwargs) def translate_value(self, value): # Convert database rich text representation to the format required by # the input field if value is None: value = '' return self.converter.from_database_format(value) def render(self, name, value, attrs=None): if attrs is None: attrs = {} attrs['data-draftail-input'] = True translated_value = self.translate_value(value) return super().render(name, translated_value, attrs) def render_js_init(self, id_, name, value): return "window.draftail.initEditor('#{id}', {opts}, document.currentScript)".format( id=id_, opts=json.dumps(self.options)) def value_from_datadict(self, data, files, name): original_value = super().value_from_datadict(data, files, name) if original_value is None: return None return self.converter.to_database_format(original_value) @property def media(self): return Media(js=[ 'wagtailadmin/js/draftail.js', ], css={'all': ['wagtailadmin/css/panels/draftail.css']})
class DraftailRichTextArea(WidgetWithScript, widgets.HiddenInput): # this class's constructor accepts a 'features' kwarg accepts_features = True def get_panel(self): return RichTextFieldPanel def __init__(self, *args, **kwargs): # note: this constructor will receive an 'options' kwarg taken from the WAGTAILADMIN_RICH_TEXT_EDITORS setting, # but we don't currently recognise any options from there (other than 'features', which is passed here as a separate kwarg) self.options = {} self.features = kwargs.pop('features', None) if self.features is None: self.features = feature_registry.get_default_features() for feature in self.features: plugin = feature_registry.get_editor_plugin('draftail', feature) if plugin: plugin.construct_options(self.options) self.converter = ContentstateConverter(self.features) super().__init__(*args, **kwargs) def translate_value(self, value): # Convert database rich text representation to the format required by # the input field if value is None: value = '' return self.converter.from_database_format(value) def render(self, name, value, attrs=None): if attrs is None: attrs = {} attrs['data-draftail-input'] = True translated_value = self.translate_value(value) return super().render(name, translated_value, attrs) def render_js_init(self, id_, name, value): return "window.draftail.initEditor('#{id}', {opts}, document.currentScript)".format( id=id_, opts=json.dumps(self.options)) def value_from_datadict(self, data, files, name): original_value = super().value_from_datadict(data, files, name) if original_value is None: return None return self.converter.to_database_format(original_value) @property def media(self): return Media(js=[ 'wagtailadmin/js/draftail.js', ], css={ 'all': ['wagtailadmin/css/panels/draftail.css'] })
def test_paragraphs_retain_keys(self): converter = ContentstateConverter(features=[]) contentState = json.dumps({ 'entityMap': {}, 'blocks': [ {'inlineStyleRanges': [], 'text': 'Hello world!', 'depth': 0, 'type': 'unstyled', 'key': '00000', 'entityRanges': []}, {'inlineStyleRanges': [], 'text': 'Goodbye world!', 'depth': 0, 'type': 'unstyled', 'key': '00001', 'entityRanges': []}, ] }) result = converter.to_database_format(contentState) self.assertHTMLEqual(result, ''' <p data-block-key='00000'>Hello world!</p> <p data-block-key='00001'>Goodbye world!</p> ''')
def test_reject_javascript_link(self): converter = ContentstateConverter(features=['link']) contentstate_json = json.dumps({ 'entityMap': { '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': "javascript:alert('oh no')"}} }, 'blocks': [ { 'inlineStyleRanges': [], 'text': 'an external link', 'depth': 0, 'type': 'unstyled', 'key': '00000', 'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}] }, ] }) result = converter.to_database_format(contentstate_json) self.assertEqual(result, '<p data-block-key="00000">an <a>external</a> link</p>')
def test_style_fallback(self): # Test a block which uses an invalid inline style, and will be removed converter = ContentstateConverter(features=[]) result = converter.to_database_format(json.dumps({ 'entityMap': {}, 'blocks': [ { 'inlineStyleRanges': [{'offset': 0, 'length': 12, 'style': 'UNDERLINE'}], 'text': 'Hello world!', 'depth': 0, 'type': 'unstyled', 'key': '00000', 'entityRanges': [] }, ] })) self.assertHTMLEqual(result, ''' <p data-block-key="00000"> Hello world! </p> ''')
def test_wrapped_block_retains_key(self): # Test a block which uses a wrapper correctly receives the key defined on the inner element converter = ContentstateConverter(features=['h1', 'ol', 'bold', 'italic']) result = converter.to_database_format(json.dumps({ 'entityMap': {}, 'blocks': [ {'inlineStyleRanges': [], 'text': 'The rules of Fight Club', 'depth': 0, 'type': 'header-one', 'key': '00000', 'entityRanges': []}, {'inlineStyleRanges': [], 'text': 'You do not talk about Fight Club.', 'depth': 0, 'type': 'ordered-list-item', 'key': '00001', 'entityRanges': []}, { 'inlineStyleRanges': [], 'text': 'You do not talk about Fight Club.', 'depth': 0, 'type': 'ordered-list-item', 'key': '00002', 'entityRanges': [] }, ] })) self.assertHTMLEqual(result, ''' <h1 data-block-key='00000'>The rules of Fight Club</h1> <ol> <li data-block-key='00001'>You do not talk about Fight Club.</li> <li data-block-key='00002'>You do not talk about Fight Club.</li> </ol> ''')
class DraftailRichTextArea(widgets.HiddenInput): template_name = 'wagtailadmin/widgets/draftail_rich_text_area.html' is_hidden = False # this class's constructor accepts a 'features' kwarg accepts_features = True def get_panel(self): return RichTextFieldPanel def __init__(self, *args, **kwargs): # note: this constructor will receive an 'options' kwarg taken from the WAGTAILADMIN_RICH_TEXT_EDITORS setting, # but we don't currently recognise any options from there (other than 'features', which is passed here as a separate kwarg) kwargs.pop('options', None) self.options = {} self._media = Media( js=[ versioned_static('wagtailadmin/js/draftail.js'), ], css={ 'all': [versioned_static('wagtailadmin/css/panels/draftail.css')] }) self.features = kwargs.pop('features', None) if self.features is None: self.features = feature_registry.get_default_features() for feature in self.features: plugin = feature_registry.get_editor_plugin('draftail', feature) if plugin: plugin.construct_options(self.options) self._media += plugin.media self.converter = ContentstateConverter(self.features) default_attrs = {'data-draftail-input': True} attrs = kwargs.get('attrs') if attrs: default_attrs.update(attrs) kwargs['attrs'] = default_attrs super().__init__(*args, **kwargs) def format_value(self, value): # Convert database rich text representation to the format required by # the input field value = super().format_value(value) if value is None: value = '' return self.converter.from_database_format(value) def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) context['widget']['options_json'] = json.dumps(self.options) return context def value_from_datadict(self, data, files, name): original_value = super().value_from_datadict(data, files, name) if original_value is None: return None return self.converter.to_database_format(original_value) @property def media(self): return self._media
class DraftailRichTextArea(widgets.HiddenInput): template_name = "wagtailadmin/widgets/draftail_rich_text_area.html" is_hidden = False # this class's constructor accepts a 'features' kwarg accepts_features = True # Draftail has its own commenting show_add_comment_button = False def __init__(self, *args, **kwargs): # note: this constructor will receive an 'options' kwarg taken from the WAGTAILADMIN_RICH_TEXT_EDITORS setting, # but we don't currently recognise any options from there (other than 'features', which is passed here as a separate kwarg) kwargs.pop("options", None) self.options = {} self.plugins = [] self.features = kwargs.pop("features", None) if self.features is None: self.features = feature_registry.get_default_features() for feature in self.features: plugin = feature_registry.get_editor_plugin("draftail", feature) if plugin is None: warnings.warn( f"Draftail received an unknown feature '{feature}'.", category=RuntimeWarning, ) else: plugin.construct_options(self.options) self.plugins.append(plugin) self.converter = ContentstateConverter(self.features) default_attrs = {"data-draftail-input": True} attrs = kwargs.get("attrs") if attrs: default_attrs.update(attrs) kwargs["attrs"] = default_attrs super().__init__(*args, **kwargs) def format_value(self, value): # Convert database rich text representation to the format required by # the input field value = super().format_value(value) if value is None: value = "" return self.converter.from_database_format(value) def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) context["widget"]["options_json"] = json.dumps(self.options) return context def value_from_datadict(self, data, files, name): original_value = super().value_from_datadict(data, files, name) if original_value is None: return None return self.converter.to_database_format(original_value) @cached_property def media(self): media = Media( js=[ versioned_static("wagtailadmin/js/draftail.js"), ], css={ "all": [versioned_static("wagtailadmin/css/panels/draftail.css")] }, ) for plugin in self.plugins: media += plugin.media return media
class DraftailRichTextArea(widgets.HiddenInput): template_name = 'wagtailadmin/widgets/draftail_rich_text_area.html' # this class's constructor accepts a 'features' kwarg accepts_features = True def get_panel(self): return RichTextFieldPanel def __init__(self, *args, **kwargs): # note: this constructor will receive an 'options' kwarg taken from the WAGTAILADMIN_RICH_TEXT_EDITORS setting, # but we don't currently recognise any options from there (other than 'features', which is passed here as a separate kwarg) kwargs.pop('options', None) self.options = {} self._media = Media(js=[ 'wagtailadmin/js/draftail.js', ], css={ 'all': ['wagtailadmin/css/panels/draftail.css'] }) self.features = kwargs.pop('features', None) if self.features is None: self.features = feature_registry.get_default_features() for feature in self.features: plugin = feature_registry.get_editor_plugin('draftail', feature) if plugin: plugin.construct_options(self.options) self._media += plugin.media self.converter = ContentstateConverter(self.features) default_attrs = {'data-draftail-input': True} attrs = kwargs.get('attrs') if attrs: default_attrs.update(attrs) kwargs['attrs'] = default_attrs super().__init__(*args, **kwargs) def format_value(self, value): # Convert database rich text representation to the format required by # the input field value = super().format_value(value) if value is None: value = '' return self.converter.from_database_format(value) def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) context['widget']['options_json'] = json.dumps(self.options) return context def value_from_datadict(self, data, files, name): original_value = super().value_from_datadict(data, files, name) if original_value is None: return None return self.converter.to_database_format(original_value) @property def media(self): return self._media