Repository: wagtail/wagtail

Issue:
wagtailcore.Page tries to migrate before contenttypes
I'm getting this error:

`RuntimeError: Error creating new content types. Please make sure contenttypes is migrated before trying to migrate apps individually.`

**Steps to reproduce:**
create a clean wagtail Project
add this models.py to the home app: https://gist.github.com/bloodywing/175ddf5d0b6d10f9da2cddb2bf2bbd98
run: manage.py migrate

The problem is in the class LinkFields with the ForeignKey to wagtailcore.Page
This is the same LinkFields class which is shown in the wagtail docs.
Tested with: 1.4.2 and master

Workaround:
Comment out PageChooserPanel in the models
run migrations
and put PageChooserPanel back in.



Discussion:
I ran into this by trying to `manage.py migrate` a Wagtail project from scratch after upgrading to Wagtail 1.4.2.

The issue seems to come from the following change in 56c5a78aa7b02fbbb1c782203f790e0939c7fb05:

``` diff
-        # Sadly, there is no way of checking the form class returned from
-        # cls.get_edit_handler().get_form_class(cls), as these calls can hit
-        # the DB in order to fetch content types.
+
+        edit_handler = cls.get_edit_handler()
+        if not issubclass(edit_handler.get_form_class(cls), WagtailAdminPageForm):
+            errors.append(checks.Error(
+                "{cls}.get_edit_handler().get_form_class({cls}) does not extend WagtailAdminPageForm".format(
+                    cls=cls.__name__),
+                hint="Ensure that the EditHandler for {cls} creates a subclass of WagtailAdminPageForm".format(
+                    cls=cls.__name__),
+                obj=cls,
+                id='wagtailcore.E003'))
```

The error that goes away if I revert the above change:

```
Traceback (most recent call last):
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/backends/utils.py", line 64, in execute
    return self.cursor.execute(sql, params)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/backends/sqlite3/base.py", line 318, in execute
    return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such table: django_content_type

The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/contrib/contenttypes/models.py", line 67, in get_for_model
    ct = self.get(app_label=opts.app_label, model=opts.model_name)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/models/manager.py", line 127, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/models/query.py", line 328, in get
    num = len(clone)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/models/query.py", line 144, in __len__
    self._fetch_all()
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/models/query.py", line 965, in _fetch_all
    self._result_cache = list(self.iterator())
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/models/query.py", line 238, in iterator
    results = compiler.execute_sql()
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/models/sql/compiler.py", line 840, in execute_sql
    cursor.execute(sql, params)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/backends/utils.py", line 79, in execute
    return super(CursorDebugWrapper, self).execute(sql, params)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/backends/utils.py", line 64, in execute
    return self.cursor.execute(sql, params)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/utils.py", line 98, in __exit__
    six.reraise(dj_exc_type, dj_exc_value, traceback)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/utils/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/backends/utils.py", line 64, in execute
    return self.cursor.execute(sql, params)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/db/backends/sqlite3/base.py", line 318, in execute
    return Database.Cursor.execute(self, query, params)
django.db.utils.OperationalError: no such table: django_content_type

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/core/management/__init__.py", line 354, in execute_from_command_line
    utility.execute()
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/core/management/__init__.py", line 346, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/core/management/base.py", line 394, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/core/management/base.py", line 444, in execute
    self.check()
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/core/management/base.py", line 482, in check
    include_deployment_checks=include_deployment_checks,
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/core/checks/registry.py", line 72, in run_checks
    new_errors = check(app_configs=app_configs)
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/core/checks/model_checks.py", line 28, in check_all_models
    errors.extend(model.check(**kwargs))
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailcore/models.py", line 572, in check
    if not issubclass(edit_handler.get_form_class(cls), WagtailAdminPageForm):
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailadmin/edit_handlers.py", line 294, in get_form_class
    formsets=cls.required_formsets(),
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailadmin/edit_handlers.py", line 240, in required_formsets
    formsets.update(handler_class.required_formsets())
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailadmin/edit_handlers.py", line 240, in required_formsets
    formsets.update(handler_class.required_formsets())
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailadmin/edit_handlers.py", line 603, in required_formsets
    'widgets': child_edit_handler_class.widget_overrides(),
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailadmin/edit_handlers.py", line 216, in widget_overrides
    widgets.update(handler_class.widget_overrides())
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailadmin/edit_handlers.py", line 216, in widget_overrides
    widgets.update(handler_class.widget_overrides())
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailadmin/edit_handlers.py", line 516, in widget_overrides
    content_type=cls.target_content_type(), can_choose_root=cls.can_choose_root)}
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/wagtail/wagtailadmin/edit_handlers.py", line 543, in target_content_type
    cls._target_content_type = [ContentType.objects.get_for_model(target_model)]
  File "/home/alexei/.virtualenvs/test/lib/python3.4/site-packages/django/contrib/contenttypes/models.py", line 80, in get_for_model
    "Error creating new content types. Please make sure contenttypes "
RuntimeError: Error creating new content types. Please make sure contenttypes is migrated before trying to migrate apps individually.
```


Resolving patch:
diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py
--- a/wagtail/wagtailadmin/edit_handlers.py
+++ b/wagtail/wagtailadmin/edit_handlers.py
@@ -508,41 +508,40 @@ def render_as_field(self):
 class BasePageChooserPanel(BaseChooserPanel):
     object_type_name = "page"
 
-    _target_content_type = None
-
     @classmethod
     def widget_overrides(cls):
         return {cls.field_name: widgets.AdminPageChooser(
-            content_type=cls.target_content_type(), can_choose_root=cls.can_choose_root)}
-
-    @classmethod
-    def target_content_type(cls):
-        if cls._target_content_type is None:
-            if cls.page_type:
-                target_models = []
-
-                for page_type in cls.page_type:
-                    try:
-                        target_models.append(resolve_model_string(page_type))
-                    except LookupError:
-                        raise ImproperlyConfigured(
-                            "{0}.page_type must be of the form 'app_label.model_name', given {1!r}".format(
-                                cls.__name__, page_type
-                            )
+            target_models=cls.target_models(),
+            can_choose_root=cls.can_choose_root)}
+
+    @cached_classmethod
+    def target_models(cls):
+        if cls.page_type:
+            target_models = []
+
+            for page_type in cls.page_type:
+                try:
+                    target_models.append(resolve_model_string(page_type))
+                except LookupError:
+                    raise ImproperlyConfigured(
+                        "{0}.page_type must be of the form 'app_label.model_name', given {1!r}".format(
+                            cls.__name__, page_type
                         )
-                    except ValueError:
-                        raise ImproperlyConfigured(
-                            "{0}.page_type refers to model {1!r} that has not been installed".format(
-                                cls.__name__, page_type
-                            )
+                    )
+                except ValueError:
+                    raise ImproperlyConfigured(
+                        "{0}.page_type refers to model {1!r} that has not been installed".format(
+                            cls.__name__, page_type
                         )
+                    )
 
-                cls._target_content_type = list(ContentType.objects.get_for_models(*target_models).values())
-            else:
-                target_model = cls.model._meta.get_field(cls.field_name).rel.to
-                cls._target_content_type = [ContentType.objects.get_for_model(target_model)]
+            return target_models
+        else:
+            return [cls.model._meta.get_field(cls.field_name).rel.to]
 
-        return cls._target_content_type
+    @cached_classmethod
+    def target_content_type(cls):
+        return list(ContentType.objects.get_for_models(*cls.target_models()).values())
 
 
 class PageChooserPanel(object):
diff --git a/wagtail/wagtailadmin/widgets.py b/wagtail/wagtailadmin/widgets.py
--- a/wagtail/wagtailadmin/widgets.py
+++ b/wagtail/wagtailadmin/widgets.py
@@ -130,24 +130,38 @@ class AdminPageChooser(AdminChooser):
     choose_another_text = _('Choose another page')
     link_to_chosen_text = _('Edit this page')
 
-    def __init__(self, content_type=None, can_choose_root=False, **kwargs):
+    def __init__(self, target_models=None, content_type=None, can_choose_root=False, **kwargs):
         super(AdminPageChooser, self).__init__(**kwargs)
-        self._content_type = content_type
+
+        self.target_models = list(target_models or [Page])
+
+        if content_type is not None:
+            if target_models is not None:
+                raise ValueError("Can not set both target_models and content_type")
+            if isinstance(content_type, ContentType):
+                self.target_models = [content_type.model_class()]
+            else:
+                self.target_models = [ct.model_class() for ct in content_type]
+
         self.can_choose_root = can_choose_root
 
     @cached_property
     def target_content_types(self):
-        target_content_types = self._content_type or ContentType.objects.get_for_model(Page)
-        # Make sure target_content_types is a list or tuple
-        if not isinstance(target_content_types, (list, tuple)):
-            target_content_types = [target_content_types]
-        return target_content_types
+        return list(ContentType.objects.get_for_models(*self.target_models).values())
+
+    def _get_lowest_common_page_class(self):
+        """
+        Return a Page class that is an ancestor for all Page classes in
+        ``target_models``, and is also a concrete Page class itself.
+        """
+        if len(self.target_models) == 1:
+            # Shortcut for a single page type
+            return self.target_models[0]
+        else:
+            return Page
 
     def render_html(self, name, value, attrs):
-        if len(self.target_content_types) == 1:
-            model_class = self.target_content_types[0].model_class()
-        else:
-            model_class = Page
+        model_class = self._get_lowest_common_page_class()
 
         instance, value = self.get_instance_and_id(model_class, value)
 
@@ -166,22 +180,18 @@ def render_js_init(self, id_, name, value):
             page = value
         else:
             # Value is an ID look up object
-            if len(self.target_content_types) == 1:
-                model_class = self.target_content_types[0].model_class()
-            else:
-                model_class = Page
-
+            model_class = self._get_lowest_common_page_class()
             page = self.get_instance(model_class, value)
 
         parent = page.get_parent() if page else None
 
-        return "createPageChooser({id}, {content_type}, {parent}, {can_choose_root});".format(
+        return "createPageChooser({id}, {model_names}, {parent}, {can_choose_root});".format(
             id=json.dumps(id_),
-            content_type=json.dumps([
+            model_names=json.dumps([
                 '{app}.{model}'.format(
-                    app=content_type.app_label,
-                    model=content_type.model)
-                for content_type in self.target_content_types
+                    app=model._meta.app_label,
+                    model=model._meta.model_name)
+                for model in self.target_models
             ]),
             parent=json.dumps(parent.id if parent else None),
             can_choose_root=('true' if self.can_choose_root else 'false')


Test patch:
diff --git a/wagtail/wagtailadmin/tests/test_edit_handlers.py b/wagtail/wagtailadmin/tests/test_edit_handlers.py
--- a/wagtail/wagtailadmin/tests/test_edit_handlers.py
+++ b/wagtail/wagtailadmin/tests/test_edit_handlers.py
@@ -536,6 +536,29 @@ def test_autodetect_page_type(self):
 
         self.assertIn(expected_js, result)
 
+    def test_target_models(self):
+        result = PageChooserPanel(
+            'barbecue',
+            'wagtailcore.site'
+        ).bind_to_model(PageChooserModel).target_models()
+        self.assertEqual(result, [Site])
+
+    def test_target_models_malformed_type(self):
+        result = PageChooserPanel(
+            'barbecue',
+            'snowman'
+        ).bind_to_model(PageChooserModel)
+        self.assertRaises(ImproperlyConfigured,
+                          result.target_models)
+
+    def test_target_models_nonexistent_type(self):
+        result = PageChooserPanel(
+            'barbecue',
+            'snowman.lorry'
+        ).bind_to_model(PageChooserModel)
+        self.assertRaises(ImproperlyConfigured,
+                          result.target_models)
+
     def test_target_content_type(self):
         result = PageChooserPanel(
             'barbecue',
diff --git a/wagtail/wagtailadmin/tests/test_widgets.py b/wagtail/wagtailadmin/tests/test_widgets.py
--- a/wagtail/wagtailadmin/tests/test_widgets.py
+++ b/wagtail/wagtailadmin/tests/test_widgets.py
@@ -51,12 +51,26 @@ def test_render_js_init_with_value(self):
     # def test_render_html_init_with_content_type omitted as HTML does not
     # change when selecting a content type
 
+    def test_render_js_init_with_target_model(self):
+        widget = widgets.AdminPageChooser(target_models=[SimplePage])
+
+        js_init = widget.render_js_init('test-id', 'test', None)
+        self.assertEqual(js_init, "createPageChooser(\"test-id\", [\"tests.simplepage\"], null, false);")
+
+    def test_render_js_init_with_multiple_target_models(self):
+        target_models = [SimplePage, EventPage]
+        widget = widgets.AdminPageChooser(target_models=target_models)
+
+        js_init = widget.render_js_init('test-id', 'test', None)
+        self.assertEqual(
+            js_init, "createPageChooser(\"test-id\", [\"tests.simplepage\", \"tests.eventpage\"], null, false);"
+        )
+
     def test_render_js_init_with_content_type(self):
         content_type = ContentType.objects.get_for_model(SimplePage)
         widget = widgets.AdminPageChooser(content_type=content_type)
 
-        js_init = widget.render_js_init('test-id', 'test', None)
-        self.assertEqual(js_init, "createPageChooser(\"test-id\", [\"tests.simplepage\"], null, false);")
+        self.assertEqual(widget.target_models, [SimplePage])
 
     def test_render_js_init_with_multiple_content_types(self):
         content_types = [
@@ -66,10 +80,7 @@ def test_render_js_init_with_multiple_content_types(self):
         ]
         widget = widgets.AdminPageChooser(content_type=content_types)
 
-        js_init = widget.render_js_init('test-id', 'test', None)
-        self.assertEqual(
-            js_init, "createPageChooser(\"test-id\", [\"tests.simplepage\", \"tests.eventpage\"], null, false);"
-        )
+        self.assertEqual(widget.target_models, [SimplePage, EventPage])
 
     def test_render_js_init_with_can_choose_root(self):
         widget = widgets.AdminPageChooser(can_choose_root=True)