diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f9f60449ce..bcb58c15b1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -5,6 +5,7 @@ Changelog ~~~~~~~~~~~~~~~~ * Added toolbar to allow logged-in users to add and edit pages from the site front-end * Support for alternative image processing backends such as Wand, via the WAGTAILIMAGES_BACKENDS setting + * Added support for generating static sites using django-medusa * Added custom Query set for Pages with some handy methods for querying pages * Editor's guide documentation * Editor interface now outputs form media CSS / JS, to support custom widgets with assets diff --git a/docs/static_site_generation.rst b/docs/static_site_generation.rst new file mode 100644 index 0000000000..66bd23ff56 --- /dev/null +++ b/docs/static_site_generation.rst @@ -0,0 +1,83 @@ +Generating a static site +======================== + +This document describes how to render your Wagtail site into static HTML files using `django medusa`_ and the 'wagtail.contrib.wagtailmedusa' module. + + +Installing django-medusa +~~~~~~~~~~~~~~~~~~~~~~~~ + +Firstly, install django medusa from pip: + +.. code:: + + pip install django-medusa + + +Then add 'django_medusa' and 'wagtail.contrib.wagtailmedusa' to INSTALLED_APPS: + +.. code:: python + + INSTALLED_APPS = [ + ... + 'django_medusa', + 'wagtail.contrib.wagtailmedusa', + ] + + +Replacing GET parameters with custom routing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pages which require GET parameters (eg, pagination) don't generate suitable filenames for generated HTML files so they need to be changed to use custom routing instead. + +For example, lets say we have a Blog Index which uses pagination. We can override the 'route' method to make it respond on urls like '/page/1' and pass the page number through to the serve method: + +.. code:: python + + class BlogIndex(Page): + ... + + def serve(self, request, page=1): + ... + + def route(self, request, path_components): + if self.live and len(path_components) == 2 and path_components[0] == 'page': + try: + return self.serve(request, page=int(path_components[1])) + except (TypeError, ValueError): + pass + + return super(BlogIndex, self).route(request, path_components) + + +Rendering pages which use custom routing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For page types that override the route method, we need to let django medusa know which URLs it responds on. This is done by overriding the 'get_static_site_paths' method to make it yield one string per URL path. + +For example, the BlogIndex above would need to yield one URL for each page of results: + +.. code:: python + + def get_static_site_paths(self): + # Get page count + page_count = ... + + # Yield a path for each page + for page in range(page_count): + yield '/%d/' % (page + 1) + + # Yield from superclass + for path in super(BlogIndex, self).get_static_site_paths(): + yield path + + +Rendering +~~~~~~~~~ + +To render a site, just run ``./manage.py staticsitegen``. This will render the entire website and place the HTML in a folder called 'medusa_output'. The static and media folders need to be copied into this folder manually after the rendering is complete. + +To test, open the 'medusa_output' folder in a terminal and run ``python -m SimpleHTTPServer``. + + +.. _django medusa: https://github.com/mtigas/django-medusa diff --git a/wagtail/contrib/__init__.py b/wagtail/contrib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wagtail/contrib/wagtailmedusa/__init__.py b/wagtail/contrib/wagtailmedusa/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wagtail/contrib/wagtailmedusa/models.py b/wagtail/contrib/wagtailmedusa/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wagtail/contrib/wagtailmedusa/renderers.py b/wagtail/contrib/wagtailmedusa/renderers.py new file mode 100644 index 0000000000..950e13a146 --- /dev/null +++ b/wagtail/contrib/wagtailmedusa/renderers.py @@ -0,0 +1,24 @@ +from django_medusa.renderers import StaticSiteRenderer +from wagtail.wagtailcore.models import Site +from wagtail.wagtaildocs.models import Document + + +class PageRenderer(StaticSiteRenderer): + def get_paths(self): + # Get site + # TODO: Find way to get this to work with other sites + site = Site.objects.filter(is_default_site=True).first() + if site is None: + return [] + + # Return list of paths + return site.root_page.get_static_site_paths() + + +class DocumentRenderer(StaticSiteRenderer): + def get_paths(self): + # Return list of paths to documents + return (doc.url for doc in Document.objects.all()) + + +renderers = [PageRenderer, DocumentRenderer] diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index 9124cb253d..97858df4d9 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from modelcluster.fields import ParentalKey from wagtail.wagtailcore.models import Page, Orderable from wagtail.wagtailcore.fields import RichTextField @@ -187,11 +188,48 @@ class EventIndex(Page): intro = RichTextField(blank=True) ajax_template = 'tests/includes/event_listing.html' - def get_context(self, request): + def get_events(self): + return self.get_children().live().type(EventPage) + + def get_paginator(self): + return Paginator(self.get_events(), 4) + + def get_context(self, request, page=1): + # Pagination + paginator = self.get_paginator() + try: + events = paginator.page(page) + except PageNotAnInteger: + events = paginator.page(1) + except EmptyPage: + events = paginator.page(paginator.num_pages) + + # Update context context = super(EventIndex, self).get_context(request) - context['events'] = EventPage.objects.filter(live=True) + context['events'] = events return context + def route(self, request, path_components): + if self.live and len(path_components) == 1: + try: + return self.serve(request, page=int(path_components[0])) + except (TypeError, ValueError): + pass + + return super(EventIndex, self).route(request, path_components) + + def get_static_site_paths(self): + # Get page count + page_count = self.get_paginator().num_pages + + # Yield a path for each page + for page in range(page_count): + yield '/%d/' % (page + 1) + + # Yield from superclass + for path in super(EventIndex, self).get_static_site_paths(): + yield path + EventIndex.content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('intro', classname="full"), diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 462206e218..ffa11ee613 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -622,6 +622,19 @@ class Page(MP_Node, ClusterableModel, Indexed): """ return self.serve(self.dummy_request()) + def get_static_site_paths(self): + """ + This is a generator of URL paths to feed into a static site generator + Override this if you would like to create static versions of subpages + """ + # Yield paths for this page + yield '/' + + # Yield paths for child pages + for child in self.get_children().live(): + for path in child.specific.get_static_site_paths(): + yield '/' + child.slug + path + def get_ancestors(self, inclusive=False): return Page.objects.ancestor_of(self, inclusive) diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index 757221a297..db498650aa 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -162,6 +162,47 @@ class TestServeView(TestCase): self.assertContains(response, 'Christmas') +class TestStaticSitePaths(TestCase): + def setUp(self): + self.root_page = Page.objects.get(id=1) + + # For simple tests + self.home_page = self.root_page.add_child(instance=SimplePage(title="Homepage", slug="home")) + self.about_page = self.home_page.add_child(instance=SimplePage(title="About us", slug="about")) + self.contact_page = self.home_page.add_child(instance=SimplePage(title="Contact", slug="contact")) + + # For custom tests + self.event_index = self.root_page.add_child(instance=EventIndex(title="Events", slug="events")) + for i in range(20): + self.event_index.add_child(instance=EventPage(title="Event " + str(i), slug="event" + str(i))) + + def test_local_static_site_paths(self): + paths = list(self.about_page.get_static_site_paths()) + + self.assertEqual(paths, ['/']) + + def test_child_static_site_paths(self): + paths = list(self.home_page.get_static_site_paths()) + + self.assertEqual(paths, ['/', '/about/', '/contact/']) + + def test_custom_static_site_paths(self): + paths = list(self.event_index.get_static_site_paths()) + + # Event index path + expected_paths = ['/'] + + # One path for each page of results + expected_paths.extend(['/' + str(i + 1) + '/' for i in range(5)]) + + # One path for each event page + expected_paths.extend(['/event' + str(i) + '/' for i in range(20)]) + + paths.sort() + expected_paths.sort() + self.assertEqual(paths, expected_paths) + + class TestPageUrlTags(TestCase): fixtures = ['test.json']