Repository: django-cms/django-cms

Issue:
Fix #3674
Fix for #3674 allowing deletion of pages attached to non existing templates

Unable to delete page after related template was removed
After I had removed template landing.html, I've tried to delete the related pages.
In result, I'm getting such ridiculous error:

```
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/twiket/local/lib/python2.7/site-packages/mptt/models.py", line 864, in delete
    super(MPTTModel, self).delete(*args, **kwargs)
  File "/home/twiket/local/lib/python2.7/site-packages/django/db/models/base.py", line 695, in delete
    collector.delete()
  File "/home/twiket/local/lib/python2.7/site-packages/django/db/models/deletion.py", line 255, in delete
    sender=model, instance=obj, using=self.using
  File "/home/twiket/local/lib/python2.7/site-packages/django/dispatch/dispatcher.py", line 185, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/home/twiket/local/lib/python2.7/site-packages/cms/signals/title.py", line 93, in pre_delete_title
    instance.page.save(no_signals=True)
  File "/home/twiket/local/lib/python2.7/site-packages/cms/models/pagemodel.py", line 455, in save
    self.save_base(**kwargs)
  File "/home/twiket/local/lib/python2.7/site-packages/cms/models/pagemodel.py", line 475, in save_base
    ret = super(Page, self).save_base(*args, **kwargs)
  File "/home/twiket/local/lib/python2.7/site-packages/django/db/models/base.py", line 582, in save_base
    update_fields=update_fields, raw=raw, using=using)
  File "/home/twiket/local/lib/python2.7/site-packages/django/dispatch/dispatcher.py", line 185, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/home/twiket/local/lib/python2.7/site-packages/cms/signals/page.py", line 24, in post_save_page
    instance.rescan_placeholders()
  File "/home/twiket/local/lib/python2.7/site-packages/cms/models/pagemodel.py", line 1218, in rescan_placeholders
    placeholders = get_placeholders(self.get_template())
  File "/home/twiket/local/lib/python2.7/site-packages/cms/utils/plugins.py", line 150, in get_placeholders
    compiled_template = get_template(template)
  File "/home/twiket/local/lib/python2.7/site-packages/django/template/loader.py", line 138, in get_template
    template, origin = find_template(template_name)
  File "/home/twiket/local/lib/python2.7/site-packages/django/template/loader.py", line 131, in find_template
    raise TemplateDoesNotExist(name)
TemplateDoesNotExist: landing.html
```

It must be possible to delete a page after template deletion!



Discussion:
@mkoistinen @ojii does disconnecting signals on page delete makes sense in your opinion?

I worry that taking this approach of disconnecting signals is going to set the wrong precedent. Is it possible to instead pass in a flag "force_delete=True" or something that can be carried into the logic and allow us to make the right decisions in the right contexts?

disconnecting signals feels wrong. agree with @mkoistinen this should be handled differently.

if there's no way around signal disconnect, at least wrap it in a contextmanager or try/finally

@mkoistinen @ojii I agree with you
This is just a tentative work to explore different solutions

Moving to 3.1 anyway

sounds good

@ojii @mkoistinen does it sound correct to you to ignore TemplateDoesNotExist at https://github.com/divio/django-cms/blob/develop/cms/signals/page.py#L24
In post save it's anyway too late to take action so swallowing the exception does not really change the ability to interrupt the wrong change



Resolving patch:
diff --git a/cms/signals/page.py b/cms/signals/page.py
--- a/cms/signals/page.py
+++ b/cms/signals/page.py
@@ -1,11 +1,14 @@
 # -*- coding: utf-8 -*-
+import warnings
+
+from django.core.exceptions import ObjectDoesNotExist
+from django.template import TemplateDoesNotExist
+
 from cms.cache.permissions import clear_permission_cache
 from cms.exceptions import NoHomeFound
+from cms.models import Page
 from cms.signals.apphook import apphook_post_delete_page_checker, apphook_post_page_checker
 from cms.signals.title import update_title, update_title_paths
-from django.core.exceptions import ObjectDoesNotExist
-
-from cms.models import Page
 from menus.menu_pool import menu_pool
 
 
@@ -21,7 +24,10 @@ def pre_save_page(instance, **kwargs):
 
 def post_save_page(instance, **kwargs):
     if not kwargs.get('raw'):
-        instance.rescan_placeholders()
+        try:
+            instance.rescan_placeholders()
+        except TemplateDoesNotExist as e:
+            warnings.warn('Exception occurred: %s template does not exists' % e)
         update_home(instance)
     if instance.old_page is None or instance.old_page.parent_id != instance.parent_id or instance.is_home != instance.old_page.is_home:
         for page in instance.get_descendants(include_self=True):


Test patch:
diff --git a/cms/tests/page.py b/cms/tests/page.py
--- a/cms/tests/page.py
+++ b/cms/tests/page.py
@@ -2,7 +2,6 @@
 from __future__ import with_statement
 import datetime
 import os.path
-from cms.utils.urlutils import admin_reverse
 
 from django.conf import settings
 from django.core.cache import cache
@@ -10,6 +9,7 @@
 from django.contrib import admin
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
+from django.db.models import signals
 from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from django.utils.timezone import now as tz_now, make_aware, get_current_timezone
 
@@ -21,6 +21,7 @@
 from cms.models import Page, Title
 from cms.models.placeholdermodel import Placeholder
 from cms.models.pluginmodel import CMSPlugin
+from cms.signals import pre_save_page, post_save_page
 from cms.sitemaps import CMSSitemap
 from cms.templatetags.cms_tags import get_placeholder_content
 from cms.test_utils.testcases import (CMSTestCase, URL_CMS_PAGE, URL_CMS_PAGE_ADD)
@@ -29,6 +30,7 @@
 from cms.utils.compat.dj import installed_apps
 from cms.utils.page_resolver import get_page_from_request, is_valid_url
 from cms.utils.page import is_valid_page_slug, get_available_slug
+from cms.utils.urlutils import admin_reverse
 
 from djangocms_link.cms_plugins import LinkPlugin
 from djangocms_text_ckeditor.cms_plugins import TextPlugin
@@ -112,6 +114,25 @@ def test_create_page_api(self):
         self.assertEqual(list(Title.objects.drafts().values_list('path', flat=True)), [u''])
         self.assertEqual(list(Title.objects.public().values_list('path', flat=True)), [u''])
 
+    def test_delete_page_no_template(self):
+        page_data = {
+            'title': 'root',
+            'slug': 'root',
+            'language': settings.LANGUAGES[0][0],
+            'template': 'nav_playground.html',
+
+        }
+        page = create_page(**page_data)
+        page.template = 'no_such_template.html'
+        signals.pre_save.disconnect(pre_save_page, sender=Page, dispatch_uid='cms_pre_save_page')
+        signals.post_save.disconnect(post_save_page, sender=Page, dispatch_uid='cms_post_save_page')
+        page.save(no_signals=True)
+        signals.pre_save.connect(pre_save_page, sender=Page, dispatch_uid='cms_pre_save_page')
+        signals.post_save.connect(post_save_page, sender=Page, dispatch_uid='cms_post_save_page')
+        page.delete()
+
+        self.assertEqual(Page.objects.count(), 0)
+
     def test_slug_collision(self):
         """
         Test a slug collision