Repository: wagtail/wagtail

Issue:
wagtailfrontendcache throws an error when a root page is created without a site
On a new install of wagtail w/ wagtailfrontendcache enabled, I go through the following steps:
1. Go to the admin
2. Delete the default "welcome" page from the database
3. Create a new root page

After I create the new root page, I get the following error:

```
[17/Apr/2015 20:02:28] ERROR [django.request:231] Internal Server Error: /admin/pages/new/pages/genericpage/1/
Traceback (most recent call last):
  File "/Users/jryding/.virtualenvs/cms/lib/python2.7/site-packages/django/core/handlers/base.py", line 111, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/jryding/.virtualenvs/cms/lib/python2.7/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/Users/jryding/.virtualenvs/cms/lib/python2.7/site-packages/wagtail/wagtailadmin/views/pages.py", line 211, in create
    revision.publish()
  File "/Users/jryding/.virtualenvs/cms/lib/python2.7/site-packages/wagtail/wagtailcore/models.py", line 1141, in publish
    page_published.send(sender=page.specific_class, instance=page.specific)
  File "/Users/jryding/.virtualenvs/cms/lib/python2.7/site-packages/django/dispatch/dispatcher.py", line 198, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/Users/jryding/.virtualenvs/cms/lib/python2.7/site-packages/wagtail/contrib/wagtailfrontendcache/signal_handlers.py", line 9, in page_published_signal_handler
    purge_page_from_cache(instance)
  File "/Users/jryding/.virtualenvs/cms/lib/python2.7/site-packages/wagtail/contrib/wagtailfrontendcache/utils.py", line 100, in purge_page_from_cache
    logger.info("[%s] Purging URL: %s", backend_name, page.full_url + path[1:])
TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'
```

Digging into the code, this error is the request of the following line:

```
logger.info("[%s] Purging URL: %s", backend_name, page.full_url + path[1:])
```

This line failes because `page.full_url` is set to `None` when it executes, which results in the line throwing an exception. The new page model is still saved, so I just need to refresh the admin in my browser to get back into a good working state.



Discussion:
The workaround for this issue is to register the new root page as the root of a new site in Wagtail. When the last root page is deleted, it looks like the Site record is also removed from the system. 

One caveat though is that it seems that this error will still occur when new root pages are created.

Hi @strife25,
Thank you for the report. You are hitting the bug described in #687.

Closing this as duplicate. Will work on issuing a warning as proposed in https://github.com/torchbox/wagtail/issues/687#issuecomment-60032801

I think this is different to #687, although the two are certainly related...

The way that the page/site model is designed makes it possible for a page to have no routable URL. This in itself is not a bug - although it's not something that you'd intentionally want to happen, so it's appropriate to give a warning as per #687.

Nevertheless, a page with no routable URL is still perfectly valid, and wagtailfrontendcache ought to be fixed so that it doesn't break in the presence of such pages.

Fair enough.

What is the desired behaviour in this case? Only add `page.full_url` to the purge string when it is not None? Or skip the purge all together?

I'm not really familiar with wagtailfrontendcache's behaviour, but it seems to me that it would be appropriate to simply skip the page. If a page is not accessible at any URL, then there's no way that it can exist in a front-end cache in the first place.


Resolving patch:
diff --git a/wagtail/contrib/wagtailfrontendcache/utils.py b/wagtail/contrib/wagtailfrontendcache/utils.py
--- a/wagtail/contrib/wagtailfrontendcache/utils.py
+++ b/wagtail/contrib/wagtailfrontendcache/utils.py
@@ -61,8 +61,12 @@ def purge_url_from_cache(url, backend_settings=None, backends=None):
 
 
 def purge_page_from_cache(page, backend_settings=None, backends=None):
+    page_url = page.full_url
+    if page_url is None:  # nothing to be done if the page has no routable URL
+        return
+
     for backend_name, backend in get_backends(backend_settings=backend_settings, backends=backends).items():
         # Purge cached paths from cache
         for path in page.specific.get_cached_paths():
-            logger.info("[%s] Purging URL: %s", backend_name, page.full_url + path[1:])
-            backend.purge(page.full_url + path[1:])
+            logger.info("[%s] Purging URL: %s", backend_name, page_url + path[1:])
+            backend.purge(page_url + path[1:])


Test patch:
diff --git a/wagtail/contrib/wagtailfrontendcache/tests.py b/wagtail/contrib/wagtailfrontendcache/tests.py
--- a/wagtail/contrib/wagtailfrontendcache/tests.py
+++ b/wagtail/contrib/wagtailfrontendcache/tests.py
@@ -1,8 +1,11 @@
 from django.test import TestCase
 from django.test.utils import override_settings
 
+from wagtail.wagtailcore.models import Page
+from wagtail.tests.testapp.models import EventIndex
+
 from wagtail.contrib.wagtailfrontendcache.utils import get_backends
-from wagtail.contrib.wagtailfrontendcache.backends import HTTPBackend, CloudflareBackend
+from wagtail.contrib.wagtailfrontendcache.backends import HTTPBackend, CloudflareBackend, BaseBackend
 
 
 class TestBackendConfiguration(TestCase):
@@ -76,3 +79,44 @@ def test_backwards_compatibility(self):
         self.assertEqual(set(backends.keys()), set(['default']))
         self.assertIsInstance(backends['default'], HTTPBackend)
         self.assertEqual(backends['default'].cache_location, 'http://localhost:8000')
+
+
+PURGED_URLS = []
+
+
+class MockBackend(BaseBackend):
+    def __init__(self, config):
+        pass
+
+    def purge(self, url):
+        PURGED_URLS.append(url)
+
+
+@override_settings(WAGTAILFRONTENDCACHE={
+    'varnish': {
+        'BACKEND': 'wagtail.contrib.wagtailfrontendcache.tests.MockBackend',
+    },
+})
+class TestCachePurging(TestCase):
+
+    fixtures = ['test.json']
+
+    def test_purge_on_publish(self):
+        PURGED_URLS[:] = []  # reset PURGED_URLS to the empty list
+        page = EventIndex.objects.get(url_path='/home/events/')
+        page.save_revision().publish()
+        self.assertEqual(PURGED_URLS, ['http://localhost/events/'])
+
+    def test_purge_on_unpublish(self):
+        PURGED_URLS[:] = []  # reset PURGED_URLS to the empty list
+        page = EventIndex.objects.get(url_path='/home/events/')
+        page.unpublish()
+        self.assertEqual(PURGED_URLS, ['http://localhost/events/'])
+
+    def test_purge_with_unroutable_page(self):
+        PURGED_URLS[:] = []  # reset PURGED_URLS to the empty list
+        root = Page.objects.get(url_path='/')
+        page = EventIndex(title='new top-level page', slug='new-top-level-page')
+        root.add_child(instance=page)
+        page.save_revision().publish()
+        self.assertEqual(PURGED_URLS, [])