Merge branch 'master' into unpublish_signal

Conflicts:
	wagtail/wagtailadmin/views/pages.py
This commit is contained in:
Karl Hobley 2014-07-22 09:49:23 +01:00
commit c8f1f82914
50 changed files with 9213 additions and 1431 deletions

View file

@ -3,7 +3,10 @@ Changelog
0.5 (xx.xx.20xx)
~~~~~~~~~~~~~~~~
* Added RoutablePage model to allow embedding Django-style URL routing within a page
* Explorer nav now rendered separately and fetched with AJAX when needed
* Added decorator syntax for hooks
* Replaced lxml dependency with html5lib, to simplify installation
0.4.1 (14.07.2014)
~~~~~~~~~~~~~~~~~~

View file

@ -197,13 +197,13 @@ In addition to the model fields provided, ``Page`` has many properties and metho
.. automodule:: wagtail.wagtailcore.models
.. autoclass:: Page
.. method:: specific
.. autoattribute:: specific
Return this page in its most specific subclassed form.
.. autoattribute:: specific_class
.. automethod:: url
.. autoattribute:: url
.. automethod:: full_url
.. autoattribute:: full_url
.. automethod:: relative_url
@ -217,7 +217,7 @@ In addition to the model fields provided, ``Page`` has many properties and metho
.. automethod:: get_template
.. automethod:: preview_modes
.. autoattribute:: preview_modes
.. automethod:: serve_preview
@ -381,6 +381,8 @@ Examples:
.. automethod:: public
See: :ref:`private_pages`
.. note::
This doesn't filter out unpublished pages. If you want to only have published public pages, use ``.live().public()``
@ -394,6 +396,8 @@ Examples:
.. automethod:: search
See: :ref:`wagtailsearch_for_python_developers`
Example:
.. code-block:: python

View file

@ -33,6 +33,9 @@ sys.path.insert(0, os.path.abspath('..'))
# be configured
os.environ['DJANGO_SETTINGS_MODULE'] = 'wagtail.tests.settings'
# Use SQLite3 database engine so it doesn't attempt to use psycopg2 on RTD
os.environ['DATABASE_ENGINE'] = 'django.db.backends.sqlite3'
# -- General configuration ------------------------------------------------

View file

@ -3,6 +3,8 @@
Frontend cache purging
======================
.. versionadded:: 0.4
Many websites use a frontend cache such as Varnish, Squid or Cloudflare to gain extra performance. The downside of using a frontend cache though is that they don't respond well to updating content and will often keep an old version of a page cached after it has been updated.
This document describes how to configure Wagtail to purge old versions of pages from a frontend cache whenever a page gets updated.

View file

@ -16,6 +16,7 @@ It supports Django 1.6.2+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4. Django 1.7 suppo
search/index
form_builder
model_recipes
routable_page
advanced_topics
deploying
performance

View file

@ -3,11 +3,13 @@ Performance
Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation.
Editor interface
~~~~~~~~~~~~~~~~
We have tried to minimise external dependencies for a working installation of Wagtail, in order to make it as simple as possible to get going. However, a number of default settings can be configured for better performance:
Cache
-----
@ -25,16 +27,19 @@ We recommend `Redis <http://redis.io/>`_ as a fast, persistent cache. Install Re
Without a persistent cache, Wagtail will recreate all compressable assets at each server start, e.g. when any files change under ```./manage.py runserver```.
Search
------
Wagtail has strong support for `Elasticsearch <http://www.elasticsearch.org/>`_ - both in the editor interface and for users of your site - but can fall back to a database search if Elasticsearch isn't present. Elasticsearch is faster and more powerful than the Django ORM for text search, so we recommend installing it or using a hosted service like `Searchly <http://www.searchly.com/>`_.
Database
--------
Wagtail is tested on SQLite, and should work on other Django-supported database backends, but we recommend PostgreSQL for production use.
Public users
~~~~~~~~~~~~
@ -42,3 +47,7 @@ Caching proxy
-------------
To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both `Varnish <http://www.varnish-cache.org/>`_ and `Squid <http://www.squid-cache.org/>`_ have been tested in production. Hosted proxies like `Cloudflare <https://www.cloudflare.com/>`_ should also work well.
.. versionadded:: 0.4
Wagtail supports automatic cache invalidation for Varnish/Squid. See :ref:`frontend_cache_purging` for more information.

View file

@ -3,10 +3,13 @@
Private pages
=============
.. versionadded:: 0.4
Users with publish permission on a page can set it to be private by clicking the 'Privacy' control in the top right corner of the page explorer or editing interface, and setting a password. Users visiting this page, or any of its subpages, will be prompted to enter a password before they can view the page.
Private pages work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default "password required" form is only a bare-bones HTML page, and site implementers may wish to replace this with a page customised to their site design.
Setting up a global "password required" page
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -52,6 +55,7 @@ A basic template suitable for use as ``PASSWORD_REQUIRED_TEMPLATE`` might look l
</body>
</html>
Setting a "password required" page for a specific page type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -7,8 +7,15 @@ Wagtail 0.5 release notes - IN DEVELOPMENT
:depth: 1
Whats new
=========
What's new
==========
RoutablePage
~~~~~~~~~~~~
A ``RoutablePage`` model has been added to allow embedding Django-style URL routing within a page.
:ref:`routable_page`
Minor features
@ -27,6 +34,16 @@ Core
MenuItem('Kittens!', '/kittens/', classnames='icon icon-folder-inverse', order=1000)
)
* The lxml library (used for whitelisting and rewriting of rich text fields) has been replaced with the pure-python html5lib library, to simplify installation.
Admin
-----
* Explorer nav now rendered separately and fetched with AJAX when needed.
This improves the general performance of the admin interface for large sites.
Bug fixes
~~~~~~~~~

88
docs/routable_page.rst Normal file
View file

@ -0,0 +1,88 @@
.. _routable_page:
====================================
Embedding URL configuration in Pages
====================================
.. versionadded:: 0.5
The ``RoutablePage`` class provides a convenient way for a page to respond on multiple sub-URLs with different views. For example, a blog section on a site might provide several different types of index page at URLs like ``/blog/2013/06/``, ``/blog/authors/bob/``, ``/blog/tagged/python/``, all served by the same ``BlogIndex`` page.
A ``RoutablePage`` exists within the page tree like any other page, but URL paths underneath it are checked against a list of patterns, using Django's urlconf scheme. If none of the patterns match, control is passed to subpages as usual (or failing that, a 404 error is thrown).
The basics
==========
To use ``RoutablePage``, you need to make your class inherit from :class:`wagtail.contrib.wagtailroutablepage.models.RoutablePage` and configure the ``subpage_urls`` attribute with your URL configuration.
Here's an example of an ``EventPage`` with three views:
.. code-block:: python
from django.conf.urls import url
from wagtail.contrib.wagtailroutablepage.models import RoutablePage
class EventPage(RoutablePage):
subpage_urls = (
url(r'^$', 'current_events', name='current_events'),
url(r'^past/$', 'past_events', name='past_events'),
url(r'^year/(\d+)/$', 'events_for_year', name='events_for_year'),
)
def current_events(self, request):
"""
View function for the current events page
"""
...
def past_events(self, request):
"""
View function for the current events page
"""
...
def events_for_year(self, request):
"""
View function for the events for year page
"""
...
The ``RoutablePage`` class
==========================
.. automodule:: wagtail.contrib.wagtailroutablepage.models
.. autoclass:: RoutablePage
.. autoattribute:: subpage_urls
Example:
.. code-block:: python
from django.conf.urls import url
subpage_urls = (
url(r'^$', 'serve', name='main'),
url(r'^archive/$', 'archive', name='archive'),
)
.. automethod:: resolve_subpage
Example:
.. code-block:: python
view, args, kwargs = page.resolve_subpage('/past/')
response = view(request, *args, **kwargs)
.. automethod:: reverse_subpage
Example:
.. code-block:: python
url = page.url + page.reverse_subpage('events_for_year', args=('2014', ))

View file

@ -31,6 +31,11 @@ All methods of ``PageQuerySet`` are supported by wagtailsearch:
Indexing extra fields
=====================
.. versionchanged:: 0.4
The ``indexed_fields`` configuration format was replaced with ``search_fields``
Fields need to be explicitly added to the search configuration in order for you to be able to search/filter on them.
You can add new fields to the search index by overriding the ``search_fields`` property and appending a list of extra ``SearchField``/``FilterField`` objects to it.

View file

@ -3,6 +3,8 @@
Sitemap generation
==================
.. versionadded:: 0.4
This document describes how to create XML sitemaps for your Wagtail website using the ``wagtail.contrib.wagtailsitemaps`` module.

View file

@ -92,26 +92,49 @@ Then in your own page templates, you can include your snippet template tag with:
{% endblock %}
Binding Pages to Snippets
-------------------------
An alternate strategy for including snippets might involve explicitly binding a specific page object to a specific snippet object. Lets add another snippet class to see how that might work:
In the above example, the list of adverts is a fixed list, displayed as part of the template independently of the page content. This might be what you want for a common panel in a sidebar, say - but in other scenarios you may wish to refer to a snippet within page content. This can be done by defining a foreign key to the snippet model within your page model, and adding a ``SnippetChooserPanel`` to the page's ``content_panels`` definitions. For example, if you wanted to be able to specify an advert to appear on ``BookPage``:
.. code-block:: python
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
# ...
class BookPage(Page):
advert = models.ForeignKey(
'demo.Advert',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
SnippetChooserPanel('advert', Advert),
# ...
]
The snippet could then be accessed within your template as ``self.advert``.
To attach multiple adverts to a page, the ``SnippetChooserPanel`` can be placed on an inline child object of ``BookPage``, rather than on ``BookPage`` itself. Here this child model is named ``BookPageAdvertPlacement`` (so called because there is one such object for each time that an advert is placed on a BookPage):
.. code-block:: python
from django.db import models
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
from wagtail.wagtailsnippets.models import register_snippet
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from modelcluster.fields import ParentalKey
...
class AdvertPlacement(models.Model):
page = ParentalKey('wagtailcore.Page', related_name='advert_placements')
class BookPageAdvertPlacement(Orderable, models.Model):
page = ParentalKey('demo.BookPage', related_name='advert_placements')
advert = models.ForeignKey('demo.Advert', related_name='+')
class Meta:
@ -119,25 +142,27 @@ An alternate strategy for including snippets might involve explicitly binding a
verbose_name_plural = "Advert Placements"
panels = [
PageChooserPanel('page'),
SnippetChooserPanel('advert', Advert),
]
def __unicode__(self):
return self.page.title + " -> " + self.advert.text
register_snippet(AdvertPlacement)
class BookPage(Page):
...
The class ``AdvertPlacement`` has two properties, ``page`` and ``advert``, which point to other models. Wagtail provides a ``PageChooserPanel`` and ``SnippetChooserPanel`` to let us make painless selection of those properties in the Wagtail admin. Note also the ``Meta`` class, which you can stock with the ``verbose_name`` and ``verbose_name_plural`` properties to override the snippet labels in the Wagtail admin. The text representation of the class has also gotten fancy, using both properties to construct a compound label showing the relationship it forms between a page and an Advert.
BookPage.content_panels = [
InlinePanel(BookPage, 'advert_placements', label="Adverts"),
# ...
]
With this snippet in place, we can use the reverse ``related_name`` lookup label ``advert_placements`` to iterate over any placements within our template files. In the template for a ``Page``-derived model, we could include the following:
These child objects are now accessible through the page's ``advert_placements`` property, and from there we can access the linked Advert snippet as ``advert``. In the template for ``BookPage``, we could include the following:
.. code-block:: django
{% if self.advert_placements %}
{% for advert_placement in self.advert_placements.all %}
<p><a href="{{ advert_placement.advert.url }}">{{ advert_placement.advert.text }}</a></p>
{% endfor %}
{% endif %}
{% for advert_placement in self.advert_placements.all %}
<p><a href="{{ advert_placement.advert.url }}">{{ advert_placement.advert.text }}</a></p>
{% endfor %}

View file

@ -1,3 +1,4 @@
#!/usr/bin/env bash
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# For a non-dummy email backend configure Django's EMAIL_BACKEND
@ -12,7 +13,7 @@ PROJECT_ROOT=/usr/local/django
echo "This script overwrites key files, and should only be run on a new box."
read -p "Type 'yes' to confirm: " CONFIRM
[ $CONFIRM== “yes” ] || exit
[ "$CONFIRM" == "yes" ] || exit
read -p "Enter a name for your project [$PROJECT]: " U_PROJECT
if [ ! -z "$U_PROJECT" ]; then
@ -35,7 +36,7 @@ SERVER_IP=`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1`
aptitude update
aptitude -y install git python-pip nginx postgresql redis-server
aptitude -y install postgresql-server-dev-all python-dev libxml2-dev libxslt-dev libjpeg62-dev
aptitude -y install postgresql-server-dev-all python-dev libjpeg62-dev
perl -pi -e "s/^(local\s+all\s+postgres\s+)peer$/\1trust/" /etc/postgresql/9.1/main/pg_hba.conf
service postgresql reload

View file

@ -1,3 +1,4 @@
#!/usr/bin/env bash
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# For a non-dummy email backend configure Django's EMAIL_BACKEND
@ -10,7 +11,7 @@ PROJECT_ROOT=/usr/local/django
echo "This script overwrites key files, and should only be run on a new box."
read -p "Type 'yes' to confirm: " CONFIRM
[ $CONFIRM== “yes” ] || exit
[ "$CONFIRM" == "yes" ] || exit
read -p "Enter a name for your project [$PROJECT]: " U_PROJECT
if [ ! -z "$U_PROJECT" ]; then
@ -33,7 +34,7 @@ SERVER_IP=`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1`
aptitude update
aptitude -y install git python-pip nginx postgresql redis-server
aptitude -y install postgresql-server-dev-all python-dev libxml2-dev libxslt-dev libjpeg62-dev
aptitude -y install postgresql-server-dev-all python-dev libjpeg62-dev
perl -pi -e "s/^(local\s+all\s+postgres\s+)peer$/\1trust/" /etc/postgresql/9.1/main/pg_hba.conf
service postgresql reload

View file

@ -32,7 +32,7 @@ install_requires = [
"django-treebeard==2.0",
"Pillow>=2.3.0",
"beautifulsoup4>=4.3.2",
"lxml>=3.3.0",
"html5lib==0.999",
"Unidecode>=0.04.14",
"six==1.7.3",
'requests==2.3.0',

View file

@ -0,0 +1,74 @@
from six import string_types
from django.http import Http404
from django.core.urlresolvers import get_resolver
from django.core.exceptions import ImproperlyConfigured
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.url_routing import RouteResult
class RoutablePage(Page):
"""
This class extends Page by adding methods to allow urlconfs to be embedded inside pages
"""
#: Set this to a tuple of ``django.conf.urls.url`` objects.
subpage_urls = None
def reverse_subpage(self, name, args=None, kwargs=None):
"""
This method does the same job as Djangos' built in "urlresolvers.reverse()" function for subpage urlconfs.
"""
args = args or []
kwargs = kwargs or {}
if self.subpage_urls is None:
raise ImproperlyConfigured("You must set 'subpage_urls' on " + type(self).__name__)
resolver = get_resolver(self.subpage_urls)
return resolver.reverse(name, *args, **kwargs)
def resolve_subpage(self, path):
"""
This finds a view method/function from a URL path.
"""
if self.subpage_urls is None:
raise ImproperlyConfigured("You must set 'subpage_urls' on " + type(self).__name__)
resolver = get_resolver(self.subpage_urls)
view, args, kwargs = resolver.resolve(path)
# If view is a string, find it as an attribute of self
if isinstance(view, string_types):
view = getattr(self, view)
return view, args, kwargs
def route(self, request, path_components):
"""
This hooks the subpage urls into Wagtails routing.
"""
if self.live:
try:
path = '/'
if path_components:
path += '/'.join(path_components) + '/'
view, args, kwargs = self.resolve_subpage(path)
return RouteResult(self, args=(view, args, kwargs))
except Http404:
pass
return super(RoutablePage, self).route(request, path_components)
def serve(self, request, view, args, kwargs):
return view(request, *args, **kwargs)
def serve_preview(self, request, mode_name):
view, args, kwargs = self.resolve_subpage('/')
return view(*args, **kwargs)
is_abstract = True
class Meta:
abstract = True

View file

@ -0,0 +1,82 @@
from django.test import TestCase
from wagtail.wagtailcore.models import Page
from wagtail.tests.models import RoutablePageTest, routable_page_external_view
class TestRoutablePage(TestCase):
def setUp(self):
self.home_page = Page.objects.get(id=2)
self.routable_page = self.home_page.add_child(instance=RoutablePageTest(
title="Routable Page",
slug='routable-page',
live=True,
))
def test_resolve_main_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/')
self.assertEqual(view, self.routable_page.main)
self.assertEqual(args, ())
self.assertEqual(kwargs, {})
def test_resolve_archive_by_year_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/archive/year/2014/')
self.assertEqual(view, self.routable_page.archive_by_year)
self.assertEqual(args, ('2014', ))
self.assertEqual(kwargs, {})
def test_resolve_archive_by_author_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/archive/author/joe-bloggs/')
self.assertEqual(view, self.routable_page.archive_by_author)
self.assertEqual(args, ())
self.assertEqual(kwargs, {'author_slug': 'joe-bloggs'})
def test_resolve_external_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/external/joe-bloggs/')
self.assertEqual(view, routable_page_external_view)
self.assertEqual(args, ('joe-bloggs', ))
self.assertEqual(kwargs, {})
def test_reverse_main_view(self):
url = self.routable_page.reverse_subpage('main')
self.assertEqual(url, '')
def test_reverse_archive_by_year_view(self):
url = self.routable_page.reverse_subpage('archive_by_year', args=('2014', ))
self.assertEqual(url, 'archive/year/2014/')
def test_reverse_archive_by_author_view(self):
url = self.routable_page.reverse_subpage('archive_by_author', kwargs={'author_slug': 'joe-bloggs'})
self.assertEqual(url, 'archive/author/joe-bloggs/')
def test_reverse_external_view(self):
url = self.routable_page.reverse_subpage('external_view', args=('joe-bloggs', ))
self.assertEqual(url, 'external/joe-bloggs/')
def test_get_main_view(self):
response = self.client.get(self.routable_page.url)
self.assertContains(response, "MAIN VIEW")
def test_get_archive_by_year_view(self):
response = self.client.get(self.routable_page.url + 'archive/year/2014/')
self.assertContains(response, "ARCHIVE BY YEAR: 2014")
def test_get_archive_by_author_view(self):
response = self.client.get(self.routable_page.url + 'archive/author/joe-bloggs/')
self.assertContains(response, "ARCHIVE BY AUTHOR: joe-bloggs")
def test_get_external_view(self):
response = self.client.get(self.routable_page.url + 'external/joe-bloggs/')
self.assertContains(response, "EXTERNAL VIEW: joe-bloggs")

View file

@ -1,6 +1,6 @@
@import "../variables.scss";
@import "../mixins.scss";
@import "../grid.scss";
@import "wagtailadmin/scss/variables.scss";
@import "wagtailadmin/scss/mixins.scss";
@import "wagtailadmin/scss/grid.scss";
section{
border-top:1px solid $color-grey-3;
@ -50,6 +50,9 @@ section{
.color-green{
background-color:$color-green;
}
.color-blue{
background-color:$color-blue;
}
.color-grey-1{
background-color:$color-grey-1;
}

View file

@ -5,7 +5,7 @@
{% block extra_css %}
{% compress css %}
<link rel="stylesheet" href="{{ STATIC_URL }}wagtailadmin/scss/layouts/styleguide.scss" type="text/x-scss" />
<link rel="stylesheet" href="{{ STATIC_URL }}wagtailstyleguide/scss/styleguide.scss" type="text/x-scss" />
{% endcompress %}
{% endblock %}
@ -22,6 +22,7 @@
<ul class="unlist">
<li><a href="#palette">Colour palette</a></li>
<li><a href="#typography">Typography</a></li>
<li><a href="#help">Help text</a></li>
<li><a href="#listings">Listings</a></li>
<li><a href="#buttons">Buttons</a></li>
<li><a href="#dropdowns">Dropdown buttons</a></li>
@ -59,6 +60,7 @@
<li class="color-red">color-red</li>
<li class="color-orange">color-orange</li>
<li class="color-green">color-green</li>
<li class="color-blue">color-blue</li>
</ul>
</section>
@ -88,6 +90,26 @@
</section>
<section id="help">
<h2>Help text</h2>
<p>Help text is not to be confused with the messages that appear in a banner drop down from the top of the screen. Help text are permanent instructions, visible on every page view, that explain or warn about something.
<div class="help-block help-info">
<p>This is help text that might be just for information, explaining what happens next, or drawing the user's attention to something they're about to do</p>
<p>It could be multiple lines</p>
</div>
<p class="help-block help-warning">
A warning message might be output in cases where a user's action could have serious consequences
</p>
<div class="help-block help-critical">
A critical message would probably be rare, in cases where a particularly brittle or dangerously destructive action could be performed and needs to be warned about.
</div>
</section>
<section id="listings">
<h2>Listings</h2>
@ -416,6 +438,7 @@
<li class="icon icon-date">date</li>
<li class="icon icon-time">time</li>
<li class="icon icon-form">form</li>
<li class="icon icon-site">site</li>
</ul>
</section>

View file

@ -1,6 +1,8 @@
from django.db import models
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.encoding import python_2_unicode_compatible
from django.conf.urls import url
from django.http import HttpResponse
from modelcluster.fields import ParentalKey
@ -12,6 +14,7 @@ from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
from wagtail.wagtailsnippets.models import register_snippet
from wagtail.wagtailsearch import indexed
from wagtail.contrib.wagtailroutablepage.models import RoutablePage
EVENT_AUDIENCE_CHOICES = (
@ -383,9 +386,31 @@ class SearchTestOldConfig(models.Model, indexed.Indexed):
},
}
class SearchTestOldConfigList(models.Model, indexed.Indexed):
"""
This tests that the Indexed class can correctly handle models that
use the old "indexed_fields" configuration format using a list.
"""
indexed_fields = ['title', 'content']
def routable_page_external_view(request, arg):
return HttpResponse("EXTERNAL VIEW: " + arg)
class RoutablePageTest(RoutablePage):
subpage_urls = (
url(r'^$', 'main', name='main'),
url(r'^archive/year/(\d+)/$', 'archive_by_year', name='archive_by_year'),
url(r'^archive/author/(?P<author_slug>.+)/$', 'archive_by_author', name='archive_by_author'),
url(r'^external/(.+)/$', routable_page_external_view, name='external_view')
)
def archive_by_year(self, request, year):
return HttpResponse("ARCHIVE BY YEAR: " + str(year))
def archive_by_author(self, request, author_slug):
return HttpResponse("ARCHIVE BY AUTHOR: " + author_slug)
def main(self, request):
return HttpResponse("MAIN VIEW")

View file

@ -29,7 +29,6 @@ STATICFILES_FINDERS = (
)
USE_TZ = True
TIME_ZONE = 'UTC'
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'django.core.context_processors.request',
@ -72,6 +71,7 @@ INSTALLED_APPS = [
'wagtail.wagtailforms',
'wagtail.contrib.wagtailstyleguide',
'wagtail.contrib.wagtailsitemaps',
'wagtail.contrib.wagtailroutablepage',
'wagtail.tests',
]

View file

@ -12,7 +12,7 @@ $(function(){
$('body').addClass('ready');
// Enable toggle to open/close nav
$('#nav-toggle').click(function(){
$(document).on('click', '#nav-toggle', function(){
$('body').toggleClass('nav-open');
if(!$('body').hasClass('nav-open')){
$('body').addClass('nav-closed');
@ -21,12 +21,30 @@ $(function(){
}
});
// Enable swishy section navigation menu
$('.explorer').addClass('dl-menuwrapper').dlmenu({
animationClasses : {
classin : 'dl-animate-in-2',
classout : 'dl-animate-out-2'
// Dynamically load menu on request.
$(document).on('click', '.dl-trigger', function(){
var $this = $(this);
var $explorer = $('#explorer');
$this.addClass('icon-spinner');
if(!$explorer.children().length){
$explorer.load(window.explorer_menu_url, function() {
$this.removeClass('icon-spinner');
$explorer.addClass('dl-menuwrapper').dlmenu({
animationClasses : {
classin : 'dl-animate-in-2',
classout : 'dl-animate-out-2'
}
});
$explorer.dlmenu('openMenu');
});
}else{
$explorer.dlmenu('openMenu');
}
return false;
});
// Resize nav to fit height of window. This is an unimportant bell/whistle to make it look nice
@ -35,9 +53,6 @@ $(function(){
$('.nav-main').each(function(){
var thisHeight = $(this).height();
var footerHeight = $('.footer', $(this)).height();
// $(this).css({'height':thisHeight - footerHeight, 'overflow-y':'scroll'});
// $('> ul', $(this)).height(thisHeight)
});
};
fitNav();
@ -86,22 +101,6 @@ $(function(){
}
});
/* Bulk-selection */
$(document).on('click', 'thead .bulk', function(){
$(this).closest('table').find('tbody .bulk input').each(function(){
$(this).prop('checked', !$(this).prop('checked'));
});
});
$('#menu-search input').bind('focus', function(){
$('#menu-search').addClass('focussed');
}).bind('blur', function(){
$('#menu-search').removeClass('focussed');
});
$('#menu-search').bind('focus click', function(){
$(this).addClass('focussed');
});
/* Header search behaviour */
if(window.headerSearch){
var search_current_index = 0;

View file

@ -194,6 +194,13 @@ a.tag:hover{
}
}
/* general image style */
img{
max-width:100%;
height:auto;
border: 3px solid $color-grey-4;
}
/* make a block-level element inline */
.inline{

View file

@ -250,6 +250,9 @@
.icon-form:before{
content:"$";
}
.icon-site:before{
content:"@";
}
.icon.text-replace{
font-size:0em;

View file

@ -1,3 +1,10 @@
/*
Messages are specific to Django's "Messaging" system which adds messages into the session,
for display on the next page visited. These appear as an animated banner at the top of the page.
For inline help text, see typography.scss
*/
.messages{
position:relative;
z-index:5;

View file

@ -0,0 +1,110 @@
h1,h2,h3,h4,h5,h6{
font-weight:normal;
}
h1{
line-height:1.3em;
font-size:1.5em;
text-transform:uppercase;
color:$color-grey-1;
font-weight:600;
span{
text-transform:none;
font-weight:300;
}
.homepage &{
text-transform:none;
}
}
h2{
text-transform:uppercase;
font-size:1.3em;
font-family:Open Sans;
font-weight:600;
color:$color-grey-2;
.page-explorer &{
text-transform:none;
}
}
a{
outline:none;
color:$color-link;
text-decoration:none;
&:hover{
color:$color-link-hover;
}
}
code{
@include box-shadow(inset 0px 0px 4px 0px rgba(0, 0, 0, 0.2));
background-color:$color-fieldset-hover;
padding:2px 5px;
}
kbd{
@include border-radius(3px);
font-family:Open Sans, Arial, sans-serif;
border:1px solid $color-grey-2;
border-color:rgba(0,0,0,0.2);
padding:0.3em 0.5em;
}
/* Help text formatters */
.help-block{
padding:1em;
margin:1em 0;
p{
margin-top:0;
&:last-child{
margin-bottom:0;
}
}
}
.help-info, .help-warning, .help-critical{
@include border-radius(3px);
border:1px solid $color-grey-4;
padding-left:3.5em;
position:relative;
&:before{
font-family:wagtail;
position:absolute;
left:1em;
top:0.7em;
content:"?";
font-size:1.4em;
}
}
.help-info{
border-color:$color-blue;
&:before{
color:$color-blue;
}
}
.help-warning{
border-color:$color-orange;
&:before{
color:$color-orange;
content:"!";
}
}
.help-critical{
border-color:$color-red;
&:before{
color:$color-red;
content:"!";
}
}

View file

@ -4,6 +4,7 @@
@import "components/explorer.scss";
@import "components/icons.scss";
@import "components/typography.scss";
@import "components/tabs.scss";
@import "components/dropdowns.scss";
@import "components/modals.scss";
@ -35,67 +36,6 @@ body{
}
}
h1,h2,h3,h4,h5,h6{
font-weight:normal;
}
h1{
line-height:1.3em;
font-size:1.5em;
text-transform:uppercase;
color:$color-grey-1;
font-weight:600;
span{
text-transform:none;
font-weight:300;
}
.homepage &{
text-transform:none;
}
}
h2{
text-transform:uppercase;
font-size:1.3em;
font-family:Open Sans;
font-weight:600;
color:$color-grey-2;
.page-explorer &{
text-transform:none;
}
}
a{
outline:none;
color:$color-link;
text-decoration:none;
&:hover{
color:$color-link-hover;
}
}
code{
@include box-shadow(inset 0px 0px 4px 0px rgba(0, 0, 0, 0.2));
background-color:$color-fieldset-hover;
padding:2px 5px;
}
kbd{
@include border-radius(3px);
font-family:Open Sans, Arial, sans-serif;
border:1px solid $color-grey-2;
border-color:rgba(0,0,0,0.2);
padding:0.3em 0.5em;
}
img{
max-width:100%;
height:auto;
border: 3px solid $color-grey-4;
}
.browsermessage{
background-color:$color-red;
color:white;
@ -163,7 +103,6 @@ img{
}
.nav-main{
top: 43px;
bottom: 0px;
overflow: auto;
@ -226,6 +165,15 @@ img{
&:hover{
color:white;
}
/* only really used for spinners */
&:after{
font-size:1.5em;
margin:0;
position:absolute;
right:0.5em;
margin-top:0.15em;
}
}
.avatar{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -27,6 +27,7 @@ $breakpoint-desktop-larger: 100em; /* 1600px */
$color-teal: #43b1b0;
$color-teal-darker: darken($color-teal, 10%);
$color-teal-dark: #246060;
$color-blue: #71b2d4;
$color-red: #cd3238;
$color-orange:#e9b04d;
$color-green: #189370;

View file

@ -22,6 +22,9 @@
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/bootstrap-tab.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/jquery.dlmenu.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/core.js"></script>
<script>
window.explorer_menu_url = "{% url 'wagtailadmin_explorer_nav' %}";
</script>
{% endcompress %}
{% block extra_js %}{% endblock %}

View file

@ -8,11 +8,7 @@
{% main_nav %}
</div>
<nav class="explorer">
<ul class="dl-menu">
{% explorer_nav %}
</ul>
</nav>
<nav id="explorer" class="explorer"></nav>
</div>
<div class="content-wrapper">

View file

@ -1,13 +1,5 @@
{% load wagtailadmin_tags %}
{% for page, children in nodes %}
<li {% if children %}class="has-children"{% endif %}>
<a href="{% url 'wagtailadmin_explore' page.id %}" class="icon icon-folder-open-inverse">{{ page.title }}</a>
{% if children %}
<div class="children icon icon-arrow-right"></div>
<ul class="dl-submenu">
{% explorer_subnav children %}
</ul>
{% endif %}
</li>
{% endfor %}
<ul class="dl-menu">
{% include "wagtailadmin/shared/explorer_nav_child.html" %}
</ul>

View file

@ -0,0 +1,13 @@
{% load wagtailadmin_tags %}
{% for page, children in nodes %}
<li {% if children %}class="has-children"{% endif %}>
<a href="{% url 'wagtailadmin_explore' page.id %}" class="icon icon-folder-open-inverse">{{ page.title }}</a>
{% if children %}
<div class="children icon icon-arrow-right"></div>
<ul class="dl-submenu">
{% explorer_subnav children %}
</ul>
{% endif %}
</li>
{% endfor %}

View file

@ -21,7 +21,7 @@ def explorer_nav():
}
@register.inclusion_tag('wagtailadmin/shared/explorer_nav.html')
@register.inclusion_tag('wagtailadmin/shared/explorer_nav_child.html')
def explorer_subnav(nodes):
return {
'nodes': nodes
@ -31,7 +31,7 @@ def explorer_subnav(nodes):
@register.inclusion_tag('wagtailadmin/shared/main_nav.html', takes_context=True)
def main_nav(context):
menu_items = [
MenuItem(_('Explorer'), '#', classnames='icon icon-folder-open-inverse dl-trigger', order=100),
MenuItem(_('Explorer'), urlresolvers.reverse('wagtailadmin_explore_root'), classnames='icon icon-folder-open-inverse dl-trigger', order=100),
MenuItem(_('Search'), urlresolvers.reverse('wagtailadmin_pages_search'), classnames='icon icon-search', order=200),
]

View file

@ -14,6 +14,17 @@ from wagtail.wagtailcore.signals import page_published, page_unpublished
from wagtail.wagtailusers.models import UserProfile
def submittable_timestamp(timestamp):
"""
Helper function to translate a possibly-timezone-aware datetime into the format used in the
go_live_at / expire_at form fields - "YYYY-MM-DD hh:mm", with no timezone indicator.
This will be interpreted as being in the server's timezone (settings.TIME_ZONE), so we
need to pass it through timezone.localtime to ensure that the client and server are in
agreement about what the timestamp means.
"""
return str(timezone.localtime(timestamp)).split('.')[0]
class TestPageExplorer(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
@ -180,8 +191,8 @@ class TestPageCreation(TestCase, WagtailTestUtils):
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
@ -203,8 +214,8 @@ class TestPageCreation(TestCase, WagtailTestUtils):
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0],
'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0],
'go_live_at': submittable_timestamp(timezone.now() + timedelta(days=2)),
'expire_at': submittable_timestamp(timezone.now() + timedelta(days=1)),
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
@ -219,7 +230,7 @@ class TestPageCreation(TestCase, WagtailTestUtils):
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
'expire_at': submittable_timestamp(timezone.now() + timedelta(days=-1)),
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
@ -268,8 +279,8 @@ class TestPageCreation(TestCase, WagtailTestUtils):
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
@ -428,14 +439,16 @@ class TestPageEdit(TestCase, WagtailTestUtils):
self.assertTrue(child_page_new.has_unpublished_changes)
def test_edit_post_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
# put go_live_at and expire_at several days away from the current date, to avoid
# false matches in content_json__contains tests
go_live_at = timezone.now() + timedelta(days=10)
expire_at = timezone.now() + timedelta(days=20)
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -459,8 +472,8 @@ class TestPageEdit(TestCase, WagtailTestUtils):
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0],
'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0],
'go_live_at': submittable_timestamp(timezone.now() + timedelta(days=2)),
'expire_at': submittable_timestamp(timezone.now() + timedelta(days=1)),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -475,7 +488,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
'expire_at': submittable_timestamp(timezone.now() + timedelta(days=-1)),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -525,8 +538,8 @@ class TestPageEdit(TestCase, WagtailTestUtils):
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -550,8 +563,8 @@ class TestPageEdit(TestCase, WagtailTestUtils):
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)

View file

@ -43,3 +43,17 @@ class TestSendEmailTask(TestCase):
self.assertEqual(mail.outbox[0].subject, "Test subject")
self.assertEqual(mail.outbox[0].body, "Test content")
self.assertEqual(mail.outbox[0].to, ["nobody@email.com"])
class TestExplorerNavView(TestCase, WagtailTestUtils):
def setUp(self):
self.homepage = Page.objects.get(id=2).specific
self.login()
def test_explorer_nav_view(self):
response = self.client.get(reverse('wagtailadmin_explorer_nav'))
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html')
self.assertEqual(response.context['nodes'][0][0], self.homepage)

View file

@ -38,6 +38,8 @@ urlpatterns += [
url(r'^failwhale/$', home.error_test, name='wagtailadmin_error_test'),
url(r'^explorer-nav/$', pages.explorer_nav, name='wagtailadmin_explorer_nav'),
url(r'^pages/$', pages.index, name='wagtailadmin_explore_root'),
url(r'^pages/(\d+)/$', pages.index, name='wagtailadmin_explore'),

View file

@ -17,10 +17,17 @@ from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailadmin import tasks, signals
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailcore.models import Page, PageRevision, get_navigation_menu_items
from wagtail.wagtailcore.signals import page_published, page_unpublished
@permission_required('wagtailadmin.access_admin')
def explorer_nav(request):
return render(request, 'wagtailadmin/shared/explorer_nav.html', {
'nodes': get_navigation_menu_items(),
})
@permission_required('wagtailadmin.access_admin')
def index(request, parent_page_id=None):
if parent_page_id:

View file

@ -381,10 +381,11 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, indexed.Index
cursor.execute(update_statement,
[new_url_path, len(old_url_path) + 1, self.path + '%', self.id])
#: Return this page in its most specific subclassed form.
@cached_property
def specific(self):
"""
Return this page in its most specific subclassed form.
Return this page in its most specific subclassed form.
"""
# the ContentType.objects manager keeps a cache, so this should potentially
# avoid a database lookup over doing self.content_type. I think.
@ -395,11 +396,13 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, indexed.Index
else:
return content_type.get_object_for_this_type(id=self.id)
#: Return the class that this page would be if instantiated in its
#: most specific form
@cached_property
def specific_class(self):
"""
return the class that this page would be if instantiated in its
most specific form
Return the class that this page would be if instantiated in its
most specific form
"""
content_type = ContentType.objects.get_for_id(self.content_type_id)
return content_type.model_class()

View file

@ -157,9 +157,21 @@ LINK_HANDLERS = {
}
# Prepare a whitelisting engine with custom behaviour:
# rewrite any elements with a data-embedtype or data-linktype attribute
class DbWhitelister(Whitelister):
"""
A custom whitelisting engine to convert the HTML as returned by the rich text editor
into the pseudo-HTML format stored in the database (in which images, documents and other
linked objects are identified by ID rather than URL):
* implements a 'construct_whitelister_element_rules' hook so that other apps can modify
the whitelist ruleset (e.g. to permit additional HTML elements beyond those in the base
Whitelister module);
* replaces any element with a 'data-embedtype' attribute with an <embed> element, with
attributes supplied by the handler for that type as defined in EMBED_HANDLERS;
* rewrites the attributes of any <a> element with a 'data-linktype' attribute, as
determined by the handler for that type defined in LINK_HANDLERS, while keeping the
element content intact.
"""
has_loaded_custom_whitelist_rules = False
@classmethod

View file

@ -25,9 +25,9 @@ class TestDbWhitelister(TestCase):
self.assertHtmlEqual(expected, output_html)
def test_image_embed_is_rewritten(self):
input_html = '<p>OMG look at this picture of a kitten: <figure data-embedtype="image" data-id="5" data-format="image-with-caption" data-alt="A cute kitten" class="fancy-image"><img src="/media/images/kitten.jpg" width="320" height="200" alt="A cute kitten" /><figcaption>A kitten, yesterday.</figcaption></figure></p>'
input_html = '<p>OMG look at this picture of a kitten:</p><figure data-embedtype="image" data-id="5" data-format="image-with-caption" data-alt="A cute kitten" class="fancy-image"><img src="/media/images/kitten.jpg" width="320" height="200" alt="A cute kitten" /><figcaption>A kitten, yesterday.</figcaption></figure>'
output_html = DbWhitelister.clean(input_html)
expected = '<p>OMG look at this picture of a kitten: <embed embedtype="image" id="5" format="image-with-caption" alt="A cute kitten" /></p>'
expected = '<p>OMG look at this picture of a kitten:</p><embed embedtype="image" id="5" format="image-with-caption" alt="A cute kitten" />'
self.assertHtmlEqual(expected, output_html)
def test_media_embed_is_rewritten(self):

View file

@ -81,7 +81,7 @@ class Whitelister(object):
def clean(cls, html):
"""Clean up an HTML string to contain just the allowed elements /
attributes"""
doc = BeautifulSoup(html, 'lxml')
doc = BeautifulSoup(html, 'html5lib')
cls.clean_node(doc, doc)
return doc.decode()

View file

@ -5,4 +5,4 @@ warnings.warn(
"Use {% load wagtailembeds_tags %} instead.", DeprecationWarning)
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import register, embed, embedly
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import register, embed

View file

@ -19,12 +19,3 @@ def embed(url, max_width=None):
return ''
except:
return ''
@register.filter
def embedly(url, max_width=None):
warnings.warn(
"The 'embedly' filter has been renamed. "
"Use 'embed' instead.", DeprecationWarning)
return embed(url, max_width)

View file

@ -22,7 +22,7 @@ from wagtail.wagtailembeds.embeds import (
AccessDeniedEmbedlyException,
)
from wagtail.wagtailembeds.embeds import embedly as wagtail_embedly, oembed as wagtail_oembed
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import embed as embed_filter, embedly as embedly_filter
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import embed as embed_filter
class TestEmbeds(TestCase):