From 1dad100695060435b597545cd61c3ee420afee9f Mon Sep 17 00:00:00 2001
From: Josh Schneier <josh.schneier@gmail.com>
Date: Thu, 31 Mar 2016 22:02:46 -0400
Subject: [PATCH] Add a check that the declared search_fields exist

---
 CHANGELOG.txt                                 |  1 +
 docs/releases/1.6.rst                         |  1 +
 wagtail/tests/search/models.py                |  2 +-
 wagtail/wagtailsearch/index.py                | 29 +++++++++++++++++++
 .../wagtailsearch/tests/test_indexed_class.py | 12 ++++++++
 5 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 14a71a6c79..22e7d51951 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -17,6 +17,7 @@ Changelog
  * Add new EmailBlock and IntegerBlock (Oktay Altay)
  * Added a new FloatBlock, DecimalBlock and a RegexBlock (Oktay Altay, Andy Babic)
  * Wagtail version number is now shown on the settings menu (Chris Rogers)
+ * Added a system check to validate that fields listed in `search_fields` are defined on the model (Josh Schneier)
  * Fix: Email templates and document uploader now support custom `STATICFILES_STORAGE` (Jonny Scholes)
  * Fix: Removed alignment options (deprecated in HTML and not rendered by Wagtail) from `TableBlock` context menu (Moritz Pfeiffer)
  * Fix: Fixed incorrect CSS path on ModelAdmin's "choose a parent page" view
diff --git a/docs/releases/1.6.rst b/docs/releases/1.6.rst
index 84e75d6467..2aa129a3ad 100644
--- a/docs/releases/1.6.rst
+++ b/docs/releases/1.6.rst
@@ -32,6 +32,7 @@ Minor features
  * Added new EmailBlock and IntegerBlock (Oktay Altay)
  * Added a new FloatBlock, DecimalBlock and a RegexBlock (Oktay Altay, Andy Babic)
  * Wagtail version number is now shown on the settings menu (Chris Rogers)
+ * Added a system check to validate that fields listed in ``search_fields`` are defined on the model (Josh Schneier)
 
 
 Bug fixes
diff --git a/wagtail/tests/search/models.py b/wagtail/tests/search/models.py
index f04e93dfe0..90ca4526a0 100644
--- a/wagtail/tests/search/models.py
+++ b/wagtail/tests/search/models.py
@@ -6,7 +6,7 @@ from taggit.managers import TaggableManager
 from wagtail.wagtailsearch import index
 
 
-class SearchTest(models.Model, index.Indexed):
+class SearchTest(index.Indexed, models.Model):
     title = models.CharField(max_length=255)
     content = models.TextField()
     live = models.BooleanField(default=False)
diff --git a/wagtail/wagtailsearch/index.py b/wagtail/wagtailsearch/index.py
index bc0ae3d170..98110f5665 100644
--- a/wagtail/wagtailsearch/index.py
+++ b/wagtail/wagtailsearch/index.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
 import logging
 
 from django.apps import apps
+from django.core import checks
 from django.db import models
 from django.db.models.fields import FieldDoesNotExist
 from django.db.models.fields.related import ForeignObjectRel, OneToOneRel, RelatedField
@@ -85,6 +86,34 @@ class Indexed(object):
         """
         return self
 
+    @classmethod
+    def _has_field(cls, name):
+        try:
+            cls._meta.get_field(name)
+            return True
+        except models.fields.FieldDoesNotExist:
+            return hasattr(cls, name)
+
+    @classmethod
+    def check(cls, **kwargs):
+        errors = super(Indexed, cls).check(**kwargs)
+        errors.extend(cls._check_search_fields(**kwargs))
+        return errors
+
+    @classmethod
+    def _check_search_fields(cls, **kwargs):
+        errors = []
+        for field in cls.get_search_fields():
+            message = "{model}.search_fields contains field '{name}' but it doesn't exist"
+            if not cls._has_field(field.field_name):
+                errors.append(
+                    checks.Warning(
+                        message.format(model=cls.__name__, name=field.field_name),
+                        obj=cls,
+                    )
+                )
+        return errors
+
     search_fields = SearchFieldsShouldBeAList([], name='search_fields on Indexed subclasses')
 
 
diff --git a/wagtail/wagtailsearch/tests/test_indexed_class.py b/wagtail/wagtailsearch/tests/test_indexed_class.py
index e7c9dd0684..5e518add7f 100644
--- a/wagtail/wagtailsearch/tests/test_indexed_class.py
+++ b/wagtail/wagtailsearch/tests/test_indexed_class.py
@@ -1,5 +1,6 @@
 from __future__ import absolute_import, unicode_literals
 
+from django.core import checks
 from django.test import TestCase
 
 from wagtail.tests.search import models
@@ -68,3 +69,14 @@ class TestSearchFields(TestCase):
         self.assertEqual(len(cls.get_search_fields()), 2)
         self.assertEqual(len(cls.get_searchable_search_fields()), 1)
         self.assertEqual(len(cls.get_filterable_search_fields()), 1)
+
+    def test_checking_search_fields(self):
+        models.SearchTest.search_fields += [index.SearchField('foo')]
+        expected_errors = [
+            checks.Warning(
+                "SearchTest.search_fields contains field 'foo' but it doesn't exist",
+                obj=models.SearchTest
+            )
+        ]
+        errors = models.SearchTest.check()
+        self.assertEqual(errors, expected_errors)