Merge pull request #1 from torchbox/master

merge upstream
This commit is contained in:
Robert Clark 2014-05-21 16:15:00 -04:00
commit 54ebf42b51
30 changed files with 273 additions and 164 deletions

View file

@ -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
@ -24,6 +25,7 @@ Changelog
* Fix: Page slugs are now validated on page edit
* Fix: Filter objects are cached to avoid a database hit every time an {% image %} tag is compiled
* Fix: Moving or changing a site root page no longer causes URLs for subpages to change to 'None'
* Fix: Eliminated raw SQL queries from wagtailcore / wagtailadmin, to ensure cross-database compatibility
0.2 (11.03.2014)
~~~~~~~~~~~~~~~~

View file

@ -25,6 +25,7 @@ Contributors
* Miguel Vieira
* Ben Emery
* David Smith
* Ben Margolis
Translators
===========

View file

@ -102,12 +102,17 @@ Required dependencies
=====================
- `pip <https://github.com/pypa/pip>`_
- `libjpeg <http://ijg.org/>`_
- `libxml2 <http://xmlsoft.org/>`_
- `libxslt <http://xmlsoft.org/XSLT/>`_
- `zlib <http://www.zlib.net/>`_
Optional dependencies
=====================
- `PostgreSQL`_
- `Elasticsearch`_
- `Redis`_
Installation
============
@ -137,6 +142,7 @@ with a regular Django project.
.. _the Wagtail codebase: https://github.com/torchbox/wagtail
.. _PostgreSQL: http://www.postgresql.org
.. _Elasticsearch: http://www.elasticsearch.org
.. _Redis: http://redis.io/
_`Remove the demo app`
~~~~~~~~~~~~~~~~~~~~~~

View file

@ -13,6 +13,7 @@ It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support
wagtail_search
deploying
performance
static_site_generation
contributing
support
roadmap

View file

@ -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

View file

@ -1,5 +1,5 @@
# Production-configured Wagtail installation
# (secure services/account for full production use).
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# Tested on Debian 7.0.
# Tom Dyson and Neal Todd
@ -42,6 +42,7 @@ aptitude -y install openjdk-7-jre-headless
curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb
dpkg -i elasticsearch-1.0.0.deb
rm elasticsearch-1.0.0.deb
perl -pi -e"s/# network.host: 192.168.0.1/network.host: 127.0.0.1/" /etc/elasticsearch/elasticsearch.yml
update-rc.d elasticsearch defaults 95 10
service elasticsearch start

View file

@ -1,5 +1,5 @@
# Production-configured Wagtail installation
# (secure services/account for full production use).
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# Tested on Ubuntu 13.04 and 13.10.
# Tom Dyson and Neal Todd
@ -40,6 +40,7 @@ aptitude -y install openjdk-7-jre-headless
curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb
dpkg -i elasticsearch-1.0.0.deb
rm elasticsearch-1.0.0.deb
perl -pi -e"s/# network.host: 192.168.0.1/network.host: 127.0.0.1/" /etc/elasticsearch/elasticsearch.yml
update-rc.d elasticsearch defaults 95 10
service elasticsearch start

View file

@ -3,7 +3,6 @@ dj16=
Django>=1.6,<1.7
pyelasticsearch==0.6.1
elasticutils==0.8.2
unittest2
[tox]
envlist =

View file

View file

View file

@ -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]

View file

@ -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"),

View file

@ -1,9 +1,20 @@
from django.contrib.auth.models import User
# We need to make sure that we're using the same unittest library that Django uses internally
# Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors
try:
# Firstly, try to import unittest from Django
from django.utils import unittest
except ImportError:
# Django doesn't include unittest
# We must be running on Django 1.7+ which doesn't support Python 2.6 so
# the standard unittest library should be unittest2
import unittest
def login(client):
# Create a user
User.objects.create_superuser(username='test', email='test@email.com', password='password')
# Login
client.login(username='test', password='password')
client.login(username='test', password='password')

View file

@ -115,7 +115,7 @@ img{
}
.nav-wrapper{
@include box-shadow(inset -2px 0px 10px 0px rgba(0, 0, 0, 0.5));
@include box-shadow(inset -5px 0px 5px -3px rgba(0, 0, 0, 0.3));
position:relative;
background: $color-grey-1;
margin-left: -100%;
@ -404,7 +404,7 @@ header{
width:1em;
display:none;
margin-right:0.4em;
font-size:1.5em;
font-size:1.3em;
}
}
@ -904,13 +904,8 @@ footer, .logo{
.wrapper{
max-width:$breakpoint-desktop-larger;
}
.nav-wrapper{
@include box-shadow(inset -6px 0px 4px 0px rgba(0, 0, 0, 0.2));
.inner{
background:$color-grey-1;
@include box-shadow(inset -6px 0px 4px 0px rgba(0, 0, 0, 0.2));
}
.nav-wrapper .inner{
background:$color-grey-1;
}
footer{

View file

@ -179,6 +179,7 @@ form{
left: $desktop-nice-padding;
margin-top: -1em;
top: 50%;
font-size:1.5em;
}
.full{

View file

@ -1,13 +0,0 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% blocktrans with page_type=content_type.model_class.get_verbose_name %}Where do you want to create a {{ page_type }}{% endblocktrans %}{% endblock %}
{% block content %}
{% trans "Where do you want to create this" as where_str %}
{% include "wagtailadmin/shared/header.html" with title=where_str subtitle=content_type.model_class.get_verbose_name icon="doc-empty-inverse" %}
<ul>
{% for page in parent_pages %}
<li><a href="{% url 'wagtailadmin_pages_create' content_type.app_label content_type.model page.id %}">{{ page.title }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -1,20 +0,0 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Create a new page" %}{% endblock %}
{% block bodyclass %}menu-explorer{% endblock %}
{% block content %}
{% trans "Create a new page" as create_str %}
{% include "wagtailadmin/shared/header.html" with title=create_str icon="doc-empty-inverse" %}
<div class="nice-padding">
<p>{% trans "Your new page will be saved in the <em>top level</em> of your website. You can move it after saving." %}</p>
{% if all_page_types %}
<ul class="listing">
{% for content_type in all_page_types %}
<li><a href="{% url 'wagtailadmin_pages_select_location' content_type.app_label content_type.model %}" class="icon icon-plus-inverse">{{ content_type.model_class.get_verbose_name }}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}

View file

@ -6,10 +6,6 @@
{{ menu_item.render_html }}
{% endfor %}
{% comment %}
{# TODO: make this work #}
<li><a href="{% url 'wagtailadmin_pages_select_type' %}" class="icon teal icon-plus-inverse">{% trans 'New page' %}</a></li>
{% endcomment %}
<li class="footer">
<div class="avatar icon icon-user"><a href="{% url 'wagtailadmin_account' %}" title="{% trans 'Account settings' %}">{% if request.user.email %}<img src="{% gravatar_url request.user.email %}" />{% endif %}</a></div>
<a href="{% url 'wagtailadmin_logout' %}">{% trans "Log out" %}</a>

View file

@ -1,7 +1,6 @@
from django.test import TestCase
import unittest2 as unittest
from wagtail.tests.models import SimplePage, EventPage
from wagtail.tests.utils import login
from wagtail.tests.utils import login, unittest
from wagtail.wagtailcore.models import Page
from django.core.urlresolvers import reverse
@ -37,32 +36,6 @@ class TestPageExplorer(TestCase):
self.assertTrue(response.context['pages'].filter(id=self.child_page.id).exists())
class TestPageSelectTypeLocation(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Login
login(self.client)
def test_select_type(self):
response = self.client.get(reverse('wagtailadmin_pages_select_type'))
self.assertEqual(response.status_code, 200)
@unittest.expectedFailure # For some reason, this returns a 302...
def test_select_location_testpage(self):
response = self.client.get(reverse('wagtailadmin_pages_select_location', args=('tests', 'eventpage')))
self.assertEqual(response.status_code, 200)
def test_select_location_nonexistanttype(self):
response = self.client.get(reverse('wagtailadmin_pages_select_location', args=('notanapp', 'notamodel')))
self.assertEqual(response.status_code, 404)
def test_select_location_nonpagetype(self):
response = self.client.get(reverse('wagtailadmin_pages_select_location', args=('wagtailimages', 'image')))
self.assertEqual(response.status_code, 404)
class TestPageCreation(TestCase):
def setUp(self):
# Find root page

View file

@ -49,8 +49,6 @@ urlpatterns += [
url(r'^pages/$', pages.index, name='wagtailadmin_explore_root'),
url(r'^pages/(\d+)/$', pages.index, name='wagtailadmin_explore'),
url(r'^pages/new/$', pages.select_type, name='wagtailadmin_pages_select_type'),
url(r'^pages/new/(\w+)/(\w+)/$', pages.select_location, name='wagtailadmin_pages_select_location'),
url(r'^pages/new/(\w+)/(\w+)/(\d+)/$', pages.create, name='wagtailadmin_pages_create'),
url(r'^pages/new/(\w+)/(\w+)/(\d+)/preview/$', pages.preview_on_create, name='wagtailadmin_pages_preview_on_create'),
url(r'^pages/usage/(\w+)/(\w+)/$', pages.content_type_use, name='wagtailadmin_pages_type_use'),

View file

@ -40,28 +40,6 @@ def index(request, parent_page_id=None):
})
@permission_required('wagtailadmin.access_admin')
def select_type(request):
# Get the list of page types that can be created within the pages that currently exist
existing_page_types = ContentType.objects.raw("""
SELECT DISTINCT content_type_id AS id FROM wagtailcore_page
""")
all_page_types = sorted(get_page_types(), key=lambda pagetype: pagetype.name.lower())
page_types = set()
for content_type in existing_page_types:
allowed_subpage_types = content_type.model_class().clean_subpage_types()
for subpage_type in allowed_subpage_types:
subpage_content_type = ContentType.objects.get_for_model(subpage_type)
page_types.add(subpage_content_type)
return render(request, 'wagtailadmin/pages/select_type.html', {
'page_types': page_types,
'all_page_types': all_page_types
})
@permission_required('wagtailadmin.access_admin')
def add_subpage(request, parent_page_id):
parent_page = get_object_or_404(Page, id=parent_page_id).specific
@ -78,38 +56,6 @@ def add_subpage(request, parent_page_id):
})
@permission_required('wagtailadmin.access_admin')
def select_location(request, content_type_app_name, content_type_model_name):
try:
content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
except ContentType.DoesNotExist:
raise Http404
page_class = content_type.model_class()
# page_class must be a Page type and not some other random model
if not issubclass(page_class, Page):
raise Http404
# find all the valid locations (parent pages) where a page of the chosen type can be added
parent_pages = page_class.allowed_parent_pages()
if len(parent_pages) == 0:
# user cannot create a page of this type anywhere - fail with an error
messages.error(request, _("Sorry, you do not have access to create a page of type <em>'{0}'</em>.").format(content_type.name))
return redirect('wagtailadmin_pages_select_type')
elif len(parent_pages) == 1:
# only one possible location - redirect them straight there
messages.warning(request, _("Pages of this type can only be created as children of <em>'{0}'</em>. This new page will be saved there.").format(parent_pages[0].title))
return redirect('wagtailadmin_pages_create', content_type_app_name, content_type_model_name, parent_pages[0].id)
else:
# prompt them to select a location
return render(request, 'wagtailadmin/pages/select_location.html', {
'content_type': content_type,
'page_class': page_class,
'parent_pages': parent_pages,
})
@permission_required('wagtailadmin.access_admin')
def content_type_use(request, content_type_app_name, content_type_model_name):
try:
@ -136,8 +82,6 @@ def content_type_use(request, content_type_app_name, content_type_model_name):
except EmptyPage:
pages = paginator.page(paginator.num_pages)
print page_class
return render(request, 'wagtailadmin/pages/content_type_use.html', {
'pages': pages,
'app_name': content_type_app_name,

View file

@ -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)
@ -638,17 +651,9 @@ def get_navigation_menu_items():
# or are at the top-level (this rule required so that an empty site out-of-the-box has a working menu)
navigable_content_type_ids = get_navigable_page_content_type_ids()
if navigable_content_type_ids:
pages = Page.objects.raw("""
SELECT * FROM wagtailcore_page
WHERE numchild > 0 OR content_type_id IN %s OR depth = 2
ORDER BY path
""", [tuple(navigable_content_type_ids)])
pages = Page.objects.filter(Q(content_type__in=navigable_content_type_ids)|Q(depth=2)|Q(numchild__gt=0)).order_by('path')
else:
pages = Page.objects.raw("""
SELECT * FROM wagtailcore_page
WHERE numchild > 0 OR depth = 2
ORDER BY path
""")
pages = Page.objects.filter(Q(depth=2)|Q(numchild__gt=0)).order_by('path')
# Turn this into a tree structure:
# tree_node = (page, children)

View file

@ -162,6 +162,47 @@ class TestServeView(TestCase):
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
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']

View file

@ -4,7 +4,7 @@ import os.path
from taggit.managers import TaggableManager
from django.core.files import File
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, ValidationError
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.db import models
from django.db.models.signals import pre_delete
from django.dispatch.dispatcher import receiver
@ -17,6 +17,8 @@ from unidecode import unidecode
from wagtail.wagtailadmin.taggable import TagSearchable
from wagtail.wagtailimages.backends import get_image_backend
from .utils import validate_image_format
class AbstractImage(models.Model, TagSearchable):
title = models.CharField(max_length=255, verbose_name=_('Title') )
@ -34,12 +36,7 @@ class AbstractImage(models.Model, TagSearchable):
filename = prefix[:-1] + dot + extension
return os.path.join(folder_name, filename)
def file_extension_validator(ffile):
extension = ffile.name.split(".")[-1].lower()
if extension not in ["gif", "jpg", "jpeg", "png"]:
raise ValidationError(_("Not a valid image format. Please use a gif, jpeg or png file instead."))
file = models.ImageField(verbose_name=_('File'), upload_to=get_upload_to, width_field='width', height_field='height', validators=[file_extension_validator])
file = models.ImageField(verbose_name=_('File'), upload_to=get_upload_to, width_field='width', height_field='height', validators=[validate_image_format])
width = models.IntegerField(editable=False)
height = models.IntegerField(editable=False)
created_at = models.DateTimeField(auto_now_add=True)

View file

@ -4,9 +4,7 @@ from django.contrib.auth.models import User, Group, Permission
from django.core.urlresolvers import reverse
from django.core.files.uploadedfile import SimpleUploadedFile
import unittest2 as unittest
from wagtail.tests.utils import login
from wagtail.tests.utils import login, unittest
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.templatetags import image_tags

View file

@ -0,0 +1,28 @@
import os
from PIL import Image
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
def validate_image_format(f):
# Check file extension
extension = os.path.splitext(f.name)[1].lower()[1:]
if extension == 'jpg':
extension = 'jpeg'
if extension not in ['gif', 'jpeg', 'png']:
raise ValidationError(_("Not a valid image. Please use a gif, jpeg or png file with the correct file extension."))
if not f.closed:
# Open image file
file_position = f.tell()
f.seek(0)
image = Image.open(f)
f.seek(file_position)
# Check that the internal format matches the extension
if image.format.upper() != extension.upper():
raise ValidationError(_("Not a valid %s image. Please use a gif, jpeg or png file with the correct file extension.") % (extension.upper()))

View file

@ -2,7 +2,7 @@ from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
from django.core import management
import unittest2 as unittest
from wagtail.tests.utils import unittest
from wagtail.wagtailsearch import models, get_search_backend
from wagtail.wagtailsearch.backends.db import DBSearch
from wagtail.wagtailsearch.backends import InvalidSearchBackendError

View file

@ -1,9 +1,8 @@
from django.test import TestCase
from django.core import management
from wagtail.wagtailsearch import models
from wagtail.tests.utils import login
from wagtail.tests.utils import login, unittest
from StringIO import StringIO
import unittest2 as unittest
class TestHitCounter(TestCase):

View file

@ -5,7 +5,7 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
import unittest2 as unittest
from wagtail.tests.utils import unittest
from django.test import TestCase