diff --git a/wagtail/wagtailcore/blocks.py b/wagtail/wagtailcore/blocks.py index 8d45a9df2..d977056ec 100644 --- a/wagtail/wagtailcore/blocks.py +++ b/wagtail/wagtailcore/blocks.py @@ -225,6 +225,12 @@ class Block(six.with_metaclass(BaseBlock, object)): """ return force_text(value) + def get_searchable_content(self, value): + """ + Returns a list of strings containing text content within this block to be used in a search engine. + """ + return [] + def __eq__(self, other): """ The deep_deconstruct method in django.db.migrations.autodetector.MigrationAutodetector does not @@ -347,17 +353,23 @@ class FieldBlock(Block): def clean(self, value): return self.field.clean(value) + class CharBlock(FieldBlock): def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs): # TODO: decide what to do about 'label' and 'initial' parameters to the form field self.field = forms.CharField(required=required, help_text=help_text, max_length=max_length, min_length=min_length) super(CharBlock, self).__init__(**kwargs) + def get_searchable_content(self, value): + return [force_text(value)] + + class URLBlock(FieldBlock): def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs): self.field = forms.URLField(required=required, help_text=help_text, max_length=max_length, min_length=min_length) super(URLBlock, self).__init__(**kwargs) + class RichTextBlock(FieldBlock): @cached_property def field(self): @@ -367,6 +379,10 @@ class RichTextBlock(FieldBlock): def render_basic(self, value): return mark_safe('
' + expand_db_html(value) + '
') + def get_searchable_content(self, value): + return [force_text(value)] + + class RawHTMLBlock(FieldBlock): def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs): self.field = forms.CharField( @@ -542,6 +558,14 @@ class BaseStructBlock(Block): for name, val in value.items() ]) + def get_searchable_content(self, value): + content = [] + + for name, block in self.child_blocks.items(): + content.extend(block.get_searchable_content(value.get(name, block.meta.default))) + + return content + def deconstruct(self): """ Always deconstruct StructBlock instances as if they were plain StructBlocks with all of the @@ -747,6 +771,14 @@ class ListBlock(Block): ) return format_html("", children) + def get_searchable_content(self, value): + content = [] + + for child_value in value: + content.extend(self.child_block.get_searchable_content(child_value)) + + return content + # =========== # StreamBlock @@ -925,6 +957,14 @@ class BaseStreamBlock(Block): [(child, child.block_type) for child in value] ) + def get_searchable_content(self, value): + content = [] + + for child in value: + content.extend(child.block.get_searchable_content(child.value)) + + return content + def deconstruct(self): """ Always deconstruct StreamBlock instances as if they were plain StreamBlocks with all of the diff --git a/wagtail/wagtailcore/fields.py b/wagtail/wagtailcore/fields.py index ede90dacd..d139a3683 100644 --- a/wagtail/wagtailcore/fields.py +++ b/wagtail/wagtailcore/fields.py @@ -82,3 +82,6 @@ class StreamField(with_metaclass(models.SubfieldBase, models.Field)): def value_to_string(self, obj): value = self._get_val_from_obj(obj) return self.get_prep_value(value) + + def get_searchable_content(self, value): + return self.stream_block.get_searchable_content(value) diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py index 6d9fd0115..17bf1b81e 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -35,6 +35,12 @@ class TestFieldBlock(unittest.TestCase): self.assertIn('This field is required.', html) + def test_charfield_searchable_content(self): + block = blocks.CharBlock() + content = block.get_searchable_content("Hello world!") + + self.assertEqual(content, ["Hello world!"]) + def test_choicefield_render(self): class ChoiceBlock(blocks.FieldBlock): field = forms.ChoiceField(choices=( @@ -62,6 +68,19 @@ class TestFieldBlock(unittest.TestCase): self.assertIn('', html) self.assertIn('', html) + @unittest.expectedFailure # Returning "choice-1" instead of "Choice 1" + def test_choicefield_searchable_content(self): + class ChoiceBlock(blocks.FieldBlock): + field = forms.ChoiceField(choices=( + ('choice-1', "Choice 1"), + ('choice-2', "Choice 2"), + )) + + block = ChoiceBlock() + content = block.get_searchable_content("choice-1") + + self.assertEqual(content, ["Choice 1"]) + class TestMeta(unittest.TestCase): def test_set_template_with_meta(self): @@ -262,6 +281,19 @@ class TestStructBlock(unittest.TestCase): block = LinkBlock() self.assertIn('', block.all_html_declarations()) + def test_searchable_content(self): + class LinkBlock(blocks.StructBlock): + title = blocks.CharBlock() + link = blocks.URLBlock() + + block = LinkBlock() + content = block.get_searchable_content({ + 'title': "Wagtail site", + 'link': 'http://www.wagtail.io', + }) + + self.assertEqual(content, ["Wagtail site"]) + class TestListBlock(unittest.TestCase): def test_initialise_with_class(self): @@ -398,6 +430,25 @@ class TestListBlock(unittest.TestCase): block = blocks.ListBlock(CharBlockWithDeclarations()) self.assertIn('', block.all_html_declarations()) + def test_searchable_content(self): + class LinkBlock(blocks.StructBlock): + title = blocks.CharBlock() + link = blocks.URLBlock() + + block = blocks.ListBlock(LinkBlock()) + content = block.get_searchable_content([ + { + 'title': "Wagtail", + 'link': 'http://www.wagtail.io', + }, + { + 'title': "Django", + 'link': 'http://www.djangoproject.com', + }, + ]) + + self.assertEqual(content, ["Wagtail", "Django"]) + class TestStreamBlock(unittest.TestCase): def test_initialisation(self): @@ -646,3 +697,32 @@ class TestStreamBlock(unittest.TestCase): block_value = block.value_from_datadict(post_data, {}, 'article') self.assertEqual(block_value[2].value, "heading 2") + + def test_searchable_content(self): + class ArticleBlock(blocks.StreamBlock): + heading = blocks.CharBlock() + paragraph = blocks.CharBlock() + + block = ArticleBlock() + value = block.to_python([ + { + 'type': 'heading', + 'value': "My title", + }, + { + 'type': 'paragraph', + 'value': 'My first paragraph', + }, + { + 'type': 'paragraph', + 'value': 'My second paragraph', + }, + ]) + + content = block.get_searchable_content(value) + + self.assertEqual(content, [ + "My title", + "My first paragraph", + "My second paragraph", + ]) diff --git a/wagtail/wagtailsearch/index.py b/wagtail/wagtailsearch/index.py index bbe9dbf6a..fa2f317ce 100644 --- a/wagtail/wagtailsearch/index.py +++ b/wagtail/wagtailsearch/index.py @@ -116,7 +116,10 @@ class BaseField(object): def get_value(self, obj): try: field = self.get_field(obj.__class__) - return field._get_val_from_obj(obj) + value = field._get_val_from_obj(obj) + if hasattr(field, 'get_searchable_content'): + value = field.get_searchable_content(value) + return value except models.fields.FieldDoesNotExist: value = getattr(obj, self.field_name, None) if hasattr(value, '__call__'):