Repository: django-cms/django-cms

Issue:
api call create_page can create broken and unchangeable pages
When using the API, I can create a page, that uses a broken template (like the default page.html delivered with the current HEAD of Aldryn installer - see  https://github.com/nephila/aldryn-installer/issues/31)
which is broken as it uses a broken template, but also cannot be changed anymore to use another template.

To reproduce:
- do a "default" install with the aldry installer
- create a page with the api create_page method with the minimum input:

from cms.api import create_page, add_plugin
page = create_page('yeah', 'page.html', 'en')

This results in an error, and at first I thought the page wasn't created, which is fine - the system doesn't have to accept broken input.

But then I saw that the page actually was created - also acceptable behaviour, it saves the page and is forgiving about the fact that it will not work properly without a further fix.

The problem then is, when trying to change the template to something not broken via the Webinterface(edit page, change template in advanced settings, save), I get an error and the change doesn't happen.

Interesting thing is, when doing the change via code, it works:

from cms.models.pagemodel import Page
def reset_broken_template():
    for page in Page.objects.all():
        print page.get_page_title(language='en')
        print page.template
        if page.template == 'page.html':
            page.template = 'INHERIT'
            page.save()



Discussion:
Detailed error messages:
- when creating a page using a broken template:

> > > from cms.api import create_page, add_plugin
> > > page = create_page('yeah2', 'page.html', 'en')
> > > Traceback (most recent call last):
> > >   File "<console>", line 1, in <module>
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/api.py", line 196, in create_page
> > >     page.save()
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/models/pagemodel.py", line 377, in save
> > >     super(Page, self).save(*_kwargs)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/mptt/models.py", line 696, in save
> > >     super(MPTTModel, self).save(_args, *_kwargs)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/django/db/models/base.py", line 546, in save
> > >     force_update=force_update, update_fields=update_fields)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/models/pagemodel.py", line 394, in save_base
> > >     ret = super(Page, self).save_base(_args, *_kwargs)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/django/db/models/base.py", line 664, in save_base
> > >     update_fields=update_fields, raw=raw, using=using)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/django/dispatch/dispatcher.py", line 170, in send
> > >     response = receiver(signal=self, sender=sender, *_named)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/signals.py", line 190, in update_placeholders
> > >     instance.rescan_placeholders()
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/models/pagemodel.py", line 1111, in rescan_placeholders
> > >     placeholders = get_placeholders(self.get_template())
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/utils/plugins.py", line 128, in get_placeholders
> > >     placeholders = _scan_placeholders(compiled_template.nodelist)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/utils/plugins.py", line 95, in _scan_placeholders
> > >     placeholders += _extend_nodelist(node)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/utils/plugins.py", line 64, in _extend_nodelist
> > >     _extend_blocks(extend_node, blocks)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/cms/utils/plugins.py", line 26, in _extend_blocks
> > >     parent = extend_node.get_parent(None)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/django/template/loader_tags.py", line 99, in get_parent
> > >     return get_template(parent)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/django/template/loader.py", line 146, in get_template
> > >     template, origin = find_template(template_name)
> > >   File "/home/henning/data/extra/home-data/.virtualenvs/aldryn-drupal-import/local/lib/python2.7/site-packages/django/template/loader.py", line 139, in find_template
> > >     raise TemplateDoesNotExist(name)
> > > TemplateDoesNotExist: base.html

Actually, here it can be nice to tell the user what has been done - that the page has been created anyway, but will not be useably.
Or - a design decision to be made - do not really create the page as it is assumed to be broken.

Wait.

While repeating all the steps again for reproduction, I see some more details:
- the page is basically created, but it doesn't have a title/name and nothing, so it's not a full and valid page
- it's not the changing of the template that produces an error in the webinterface, but only trying to change the name. So it would work to first change the template to something useable and then change name etc.
- but the latter is probably not useful, as there seem to be not all originally intended data to be created via the API. So the reall proper solution seems to be, that the API call catched the error and cleanly removes the total page and everything that has been put into the database already and reports to the user that it refuses to create a page with a broken template.


Resolving patch:
diff --git a/cms/api.py b/cms/api.py
--- a/cms/api.py
+++ b/cms/api.py
@@ -7,6 +7,7 @@
 calling these methods!
 """
 import datetime
+from cms.constants import TEMPLATE_INHERITANCE_MAGIC
 
 from django.contrib.sites.models import Site
 from django.core.exceptions import FieldError
@@ -31,6 +32,7 @@
 from cms.utils.conf import get_cms_setting
 from cms.utils.i18n import get_language_list
 from cms.utils.permissions import _thread_locals
+from django.template.loader import get_template
 from menus.menu_pool import menu_pool
 
 
@@ -134,7 +136,9 @@ def create_page(title, template, language, menu_title=None, slug=None,
         _thread_locals.user = None
 
     # validate template
-    assert template in [tpl[0] for tpl in get_cms_setting('TEMPLATES')]
+    if not template == TEMPLATE_INHERITANCE_MAGIC:
+        assert template in [tpl[0] for tpl in get_cms_setting('TEMPLATES')]
+        get_template(template)
 
     # validate site
     if not site:


Test patch:
diff --git a/cms/tests/api.py b/cms/tests/api.py
--- a/cms/tests/api.py
+++ b/cms/tests/api.py
@@ -4,9 +4,11 @@
 from cms.api import _generate_valid_slug, create_page, _verify_plugin_type, assign_user_to_page
 from cms.apphook_pool import apphook_pool
 from cms.compat import get_user_model
+from cms.constants import TEMPLATE_INHERITANCE_MAGIC
 from cms.models.pagemodel import Page
 from cms.plugin_base import CMSPluginBase
 from django.core.exceptions import FieldError
+from django.template import TemplateDoesNotExist
 from djangocms_text_ckeditor.cms_plugins import TextPlugin
 from djangocms_text_ckeditor.models import Text
 from cms.test_utils.util.context_managers import SettingsOverride
@@ -70,6 +72,14 @@ def test_invalid_apphook_type(self):
         self.assertRaises(TypeError, create_page, apphook=1,
                           **self._get_default_create_page_arguments())
 
+    def test_invalid_template(self):
+        kwargs = self._get_default_create_page_arguments()
+        kwargs['template'] = "not_valid.htm"
+        with SettingsOverride(CMS_TEMPLATES=(("not_valid.htm", "notvalid"),)):
+            self.assertRaises(TemplateDoesNotExist, create_page, **kwargs)
+            kwargs['template'] = TEMPLATE_INHERITANCE_MAGIC
+        create_page(**kwargs)
+
     def test_apphook_by_class(self):
         if APP_MODULE in sys.modules:
             del sys.modules[APP_MODULE]
@@ -144,7 +154,7 @@ class InvalidPlugin(CMSPluginBase):
     def test_assign_user_to_page_nothing(self):
         page = create_page(**self._get_default_create_page_arguments())
         user = get_user_model().objects.create_user(username='user', email='user@django-cms.org',
-                                   password='user')
+                                                    password='user')
         user.is_staff = True
         request = AttributeObject(user=user)
         self.assertFalse(page.has_change_permission(request))
@@ -152,8 +162,8 @@ def test_assign_user_to_page_nothing(self):
     def test_assign_user_to_page_single(self):
         page = create_page(**self._get_default_create_page_arguments())
         user = get_user_model().objects.create_user(username='user', email='user@django-cms.org',
-                                   password='user')
-        user.is_staff=True
+                                                    password='user')
+        user.is_staff = True
         user.save()
         request = AttributeObject(user=user)
         assign_user_to_page(page, user, can_change=True)
@@ -169,8 +179,8 @@ def test_assign_user_to_page_single(self):
     def test_assign_user_to_page_all(self):
         page = create_page(**self._get_default_create_page_arguments())
         user = get_user_model().objects.create_user(username='user', email='user@django-cms.org',
-                                   password='user')
-        user.is_staff=True
+                                                    password='user')
+        user.is_staff = True
         user.save()
         request = AttributeObject(user=user)
         assign_user_to_page(page, user, grant_all=True)