Repository: django-cms/django-cms

Issue:
db_table error in Django 1.7 on sqlite
sqlite backend in Dajngo 1.7 implements some migrations by applying them to a temporary model with a patched db_table name and then renaming the new table (https://github.com/django/django/blob/stable/1.7.x/django/db/backends/sqlite3/schema.py#L124-L154)
`PlacheolderField` defines a `contribute_to_class` method (https://github.com/divio/django-cms/blob/develop/cms/models/fields.py#L70-L78) with some custom logic  (but that's also true for any `ForeignKey`).
Being part of the database schema `contribute_to_class` is called during migrations process::

```
    failures = test_runner.run_tests(test_labels)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/test/runner.py", line 147, in run_tests
    old_config = self.setup_databases()
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/test/runner.py", line 109, in setup_databases
    return setup_databases(self.verbosity, self.interactive, **kwargs)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/test/runner.py", line 299, in setup_databases
    serialize=connection.settings_dict.get("TEST", {}).get("SERIALIZE", True),
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/backends/creation.py", line 377, in create_test_db
    test_flush=True,
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/core/management/__init__.py", line 115, in call_command
    return klass.execute(*args, **defaults)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/core/management/base.py", line 338, in execute
    output = self.handle(*args, **options)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/core/management/commands/migrate.py", line 161, in handle
    executor.migrate(targets, plan, fake=options.get("fake", False))
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/migrations/executor.py", line 68, in migrate
    self.apply_migration(migration, fake=fake)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/migrations/executor.py", line 102, in apply_migration
    migration.apply(project_state, schema_editor)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/migrations/migration.py", line 108, in apply
    operation.database_forwards(self.app_label, schema_editor, project_state, new_state)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/migrations/operations/fields.py", line 37, in database_forwards
    field,
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/backends/sqlite3/schema.py", line 176, in add_field
    self._remake_table(model, create_fields=[field])
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/backends/sqlite3/schema.py", line 133, in _remake_table
    temp_model = type(model._meta.object_name, model.__bases__, body)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/models/base.py", line 168, in __new__
    new_class.add_to_class(obj_name, obj)
File "/var/local/venvs/cms-3.1-py3/lib/python3.4/site-packages/django/db/models/base.py", line 297, in add_to_class
    value.contribute_to_class(cls, name)
File "/srv/repo/django-cms-3.0/cms/models/fields.py", line 76, in contribute_to_class
    super(PlaceholderField, self).contribute_to_class(cls, name)
```

If the migration for the model definining a `PlaceholderField` instance which calls `_remake_table` is the last one in the series, the patched db_table is preserved and leaks in the `PlaceholderField` accessor for that model, resulting in silent errors when trying to access the accessor itself (in https://github.com/divio/django-cms/blob/develop/cms/models/placeholdermodel.py#L158 and https://github.com/divio/django-cms/blob/develop/cms/models/placeholdermodel.py#L180)

From my investigation there is nothing wrong in the CMS code, but a workaround has to be applied in our codebase while trying to reproduce and submit the issue to Django.



Discussion:
@mkoistinen @ojii would you recheck my findings here?
@timgraham could you review here and check if this makes sense?

Unfortunately, it's difficult for me to tell what is going on from a quick glance at that code. 

Ok, nevermind, I found a proper fix for this: default model for an extended `ForeignKey` should be provided as string, otherwise the fake model from migration is passed


Resolving patch:
diff --git a/cms/models/fields.py b/cms/models/fields.py
--- a/cms/models/fields.py
+++ b/cms/models/fields.py
@@ -20,7 +20,7 @@ def __init__(self, slotname, default_width=None, actions=PlaceholderNoAction, **
             del(kwargs['to'])
         kwargs.update({'null': True})  # always allow Null
         kwargs.update({'editable': False}) # never allow edits in admin
-        super(PlaceholderField, self).__init__(Placeholder, **kwargs)
+        super(PlaceholderField, self).__init__('cms.Placeholder', **kwargs)
 
     def deconstruct(self):
         name, path, args, kwargs = super(PlaceholderField, self).deconstruct()


Test patch:
diff --git a/cms/tests/placeholder.py b/cms/tests/placeholder.py
--- a/cms/tests/placeholder.py
+++ b/cms/tests/placeholder.py
@@ -39,6 +39,7 @@
     MultilingualExample1,
     TwoPlaceholderExample,
 )
+from cms.test_utils.project.sampleapp.models import Category
 from cms.test_utils.testcases import CMSTestCase
 from cms.test_utils.util.context_managers import (SettingsOverride, UserLoginContext)
 from cms.test_utils.util.mock import AttributeObject
@@ -306,6 +307,20 @@ def test_placeholder_scanning_nested_super(self):
     def test_placeholder_field_no_related_name(self):
         self.assertRaises(ValueError, PlaceholderField, 'placeholder', related_name='+')
 
+    def test_placeholder_field_db_table(self):
+        """
+        Test for leaking Django 1.7 Model._meta.db_table monkeypatching
+        on sqlite See #3891
+        This test for a side-effect of the above which prevents placeholder
+        fields to return the 
+        """
+        example = Category.objects.create(
+            name='category',
+            parent=None
+        )
+        self.assertEqual(example.description._get_attached_fields()[0].model, Category)
+        self.assertEqual(len(example.description._get_attached_fields()), 1)
+
     def test_placeholder_field_valid_slotname(self):
         self.assertRaises(ImproperlyConfigured, PlaceholderField, 10)