diff --git a/.travis.yml b/.travis.yml
index 8764f2859..326c22d01 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,12 @@
-# Python releases to test
language: python
+# Test matrix
python:
- - 2.7
-# Django releases
+ - 2.7
+ - 3.2
+ - 3.4
env:
- - DJANGO_VERSION=Django==1.6.2
+ - DJANGO_VERSION=Django==1.6.5
+ #- DJANGO_VERSION=Django==1.7.0
# Services
services:
- redis-server
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 8ebe93959..8f391a29a 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -4,6 +4,10 @@ Changelog
0.4 (xx.xx.20xx)
~~~~~~~~~~~~~~~~
* ElasticUtils/pyelasticsearch swapped for elasticsearch-py
+ * Python 3.2, 3.3 and 3.4 support
+ * Added scheduled publishing
+ * Added frontend cache invalidator
+ * Added sitemap generator
* Added notification preferences
* Added 'original' as a resizing rule supported by the 'image' tag
* Hallo.js updated to version 1.0.4
@@ -15,16 +19,23 @@ Changelog
* Added styleguide (mainly for wagtail developers)
* Aesthetic improvements to preview experience
* 'image' tag now accepts extra keyword arguments to be output as attributes on the img tag
+ * Login screen redirects to dashboard if user is already logged in
+ * Renamed some template tag libraries
+ * Any extra arguments given to serve are now passed through to get_context and get_template
* Added an 'attrs' property to image rendition objects to output src, width, height and alt attributes all in one go
* Added 'construct_whitelister_element_rules' hook for customising the HTML whitelist used when saving rich text fields
* Added 'in_menu' and 'not_in_menu' methods to PageQuerySet
* Added 'get_next_siblings' and 'get_prev_siblings' to Page
* Added init_new_page signal
+ * Added page_published signal
+ * Added copy method to Page to allow copying of pages
* Fix: Animated GIFs are now coalesced before resizing
* Fix: Wand backend clones images before modifying them
* Fix: Admin breadcrumb now positioned correctly on mobile
* Fix: Page chooser breadcrumb now updates the chooser modal instead of linking to Explorer
* Fix: Embeds - Fixed crash when no HTML field is sent back from the embed provider
+ * Fix: Multiple sites with same hostname but different ports are now allowed
+ * Fix: No longer possible to create multiple sites with is_default_site = True
0.3.1 (03.06.2014)
~~~~~~~~~~~~~~~~~~
diff --git a/README.rst b/README.rst
index 0e68b8c6b..f0dc14abf 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
:target: https://travis-ci.org/torchbox/wagtail
.. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master&zxcv1
- :target: https://coveralls.io/r/torchbox/wagtail?branch=master
+ :target: https://coveralls.io/r/torchbox/wagtail?branch=master
.. image:: https://pypip.in/v/wagtail/badge.png?zxcv
:target: https://crate.io/packages/wagtail/
@@ -24,7 +24,9 @@ Wagtail is a Django content management system built originally for the `Royal Co
* Support for tree-based content organisation
* Optional preview->submit->approve workflow
* Fast out of the box. `Varnish `_-friendly if you need it
-* Excellent test coverage
+* A simple `form builder `_
+* Optional `static site generation `_
+* Excellent `test coverage `_
Find out more at `wagtail.io `_.
@@ -35,16 +37,25 @@ Getting started
* To get you up and running quickly, we've provided a demonstration site with all the configuration in place, at `github.com/torchbox/wagtaildemo `_; see the `README `_ for installation instructions.
* See the `Getting Started `_ docs for installation (with the demo app) on a fresh Debian/Ubuntu box with production-ready dependencies, on OS X and on a Vagrant box.
* `Serafeim Papastefanos `_ has written a `tutorial `_ with all the steps to build a simple Wagtail site from scratch.
+* We've also provided a skeletal django-template to get started on a blank site: https://github.com/torchbox/wagtail-template
Documentation
~~~~~~~~~~~~~
-Available at `wagtail.readthedocs.org `_. and always being updated.
+Available at `wagtail.readthedocs.org `_ and always being updated.
Compatibility
~~~~~~~~~~~~~
-Wagtail supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
+Wagtail supports Django 1.6.2+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4.
+
+Django 1.7 support is in progress pending further release candidate testing.
+
+Wagtail's dependencies are summarised at `requirements.io `_.
Contributing
~~~~~~~~~~~~
-If you're a Python or Django developer, fork the repo and get stuck in! Send us a useful pull request and we'll post you a `t-shirt `_. Our immediate priorities are better docs, more tests, internationalisation and localisation.
+If you're a Python or Django developer, fork the repo and get stuck in!
+
+We suggest you start by checking the `Help develop me! `_ label.
+
+Send us a useful pull request and we'll post you a `t-shirt `_.
diff --git a/docs/editing_api.rst b/docs/editing_api.rst
index 33620fbca..5113eb3b8 100644
--- a/docs/editing_api.rst
+++ b/docs/editing_api.rst
@@ -1,28 +1,26 @@
-Editing API
+Defining models with the Editing API
===========
.. note::
This documentation is currently being written.
-
Wagtail provides a highly-customizable editing interface consisting of several components:
- * **Fields** — built-in content types to augment the basic types provided by Django.
+ * **Fields** — built-in content types to augment the basic types provided by Django
* **Panels** — the basic editing blocks for fields, groups of fields, and related object clusters
* **Choosers** — interfaces for finding related objects in a ForeignKey relationship
-Configuring your models to use these components will shape the Wagtail editor to your needs. Wagtail also provides an API for injecting custom CSS and Javascript for further customization, including extending the hallo.js rich text editor.
+Configuring your models to use these components will shape the Wagtail editor to your needs. Wagtail also provides an API for injecting custom CSS and JavaScript for further customization, including extending the hallo.js rich text editor.
There is also an Edit Handler API for creating your own Wagtail editor components.
-
Defining Panels
~~~~~~~~~~~~~~~
-A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types, you just need to add a panel for each field you want to show in the Wagtail page editor, in the order you want them to appear.
+A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types; implementors just need to add a panel for each field they want to show in the Wagtail page editor, in the order they want them to appear.
-There are three types of panels:
+There are four basic types of panels:
``FieldPanel( field_name, classname=None )``
This is the panel used for basic Django field types. ``field_name`` is the name of the class property used in your model definition. ``classname`` is a string of optional CSS classes given to the panel which are used in formatting and scripted interactivity. By default, panels are formatted as inset fields. The CSS class ``full`` can be used to format the panel so it covers the full width of the Wagtail page editor. The CSS class ``title`` can be used to mark a field as the source for auto-generated slug strings.
@@ -30,10 +28,21 @@ There are three types of panels:
``MultiFieldPanel( children, heading="", classname=None )``
This panel condenses several ``FieldPanel`` s or choosers, from a list or tuple, under a single ``heading`` string.
- ``InlinePanel( base_model, relation_name, panels=None, label='', help_text='' )``
+ ``InlinePanel( base_model, relation_name, panels=None, classname=None, label='', help_text='' )``
This panel allows for the creation of a "cluster" of related objects over a join to a separate model, such as a list of related links or slides to an image carousel. This is a very powerful, but tricky feature which will take some space to cover, so we'll skip over it for now. For a full explanation on the usage of ``InlinePanel``, see :ref:`inline_panels`.
-Wagtail provides a tabbed interface to help organize panels. ``content_panels`` is the main tab, used for the meat of your model content. The other, ``promote_panels``, is suggested for organizing metadata about the content, such as SEO information and other machine-readable information. Since you're writing the panel definitions, you can organize them however you want.
+ ``FieldRowPanel( children, classname=None)``
+ This panel is purely aesthetic. It creates a columnar layout in the editing interface, where each of the child Panels appears alongside each other rather than below. Use of FieldRowPanel particularly helps reduce the "snow-blindness" effect of seeing so many fields on the page, for complex models. It also improves the perceived association between fields of a similar nature. For example if you created a model representing an "Event" which had a starting date and ending date, it may be intuitive to find the start and end date on the same "row".
+
+ FieldRowPanel should be used in combination with ``col*`` classnames added to each of the child Panels of the FieldRowPanel. The Wagtail editing interface is layed out using a grid system, in which the maximum width of the editor is 12 columns wide. Classes ``col1``-``col12`` can be applied to each child of a FieldRowPanel. The class ``col3`` will ensure that field appears 3 columns wide or a quarter the width. ``col4`` would cause the field to be 4 columns wide, or a third the width.
+
+ **(In addition to these four, there are also Chooser Panels, detailed below.)**
+
+Wagtail provides a tabbed interface to help organize panels. Three such tabs are provided:
+
+* ``content_panels`` is the main tab, used for the bulk of your model's fields.
+* ``promote_panels`` is suggested for organizing fields regarding the promotion of the page around the site and the Internet. For example, a field to dictate whether the page should show in site-wide menus, descriptive text that should appear in site search results, SEO-friendly titles, OpenGraph meta tag content and other machine-readable information.
+* ``settings_panels`` is essentially for non-copy fields. By default it contains the page's scheduled publishing fields. Other suggested fields could include a field to switch between one layout/style and another.
Let's look at an example of a panel definition:
@@ -55,7 +64,10 @@ Let's look at an example of a panel definition:
ExamplePage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('body', classname="full"),
- FieldPanel('date'),
+ FieldRowPanel([
+ FieldPanel('start_date', classname="col3"),
+ FieldPanel('end_date', classname="col3"),
+ ]),
ImageChooserPanel('splash_image'),
DocumentChooserPanel('free_download'),
PageChooserPanel('related_page'),
@@ -119,7 +131,7 @@ One of the features of Wagtail is a unified image library, which you can access
on_delete=models.SET_NULL,
related_name='+'
)
-
+
BookPage.content_panels = [
ImageChooserPanel('cover'),
# ...
@@ -225,7 +237,7 @@ Snippets are vanilla Django models you create yourself without a Wagtail-provide
on_delete=models.SET_NULL,
related_name='+'
)
-
+
BookPage.content_panels = [
SnippetChooserPanel('advert', Advert),
# ...
@@ -254,6 +266,12 @@ Titles
Use ``classname="title"`` to make Page's built-in title field stand out with more vertical padding.
+Col*
+------
+
+Fields within a ``FieldRowPanel`` can have their width dictated in terms of the number of columns it should span. The ``FieldRowPanel`` is always considered to be 12 columns wide regardless of browser size or the nesting of ``FieldRowPanel`` in any other type of panel. Specify a number of columns thus: ``col3``, ``col4``, ``col6`` etc (up to 12). The resulting width with be *relative* to the full width of the ``FieldRowPanel``.
+
+
Required Fields
---------------
@@ -366,11 +384,9 @@ hallo.js plugin names are prefixed with the ``"IKS."`` namespace, but the ``name
For information on developing custom hallo.js plugins, see the project's page: https://github.com/bergie/hallo
-
Edit Handler API
~~~~~~~~~~~~~~~~
-
Admin Hooks
-----------
@@ -380,7 +396,7 @@ Registering functions with a Wagtail hook follows the following pattern:
.. code-block:: python
- from wagtail.wagtailadmin import hooks
+ from wagtail.wagtailcore import hooks
hooks.register('hook', function)
@@ -393,7 +409,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
.. code-block:: python
- from wagtail.wagtailadmin import hooks
+ from wagtail.wagtailcore import hooks
class UserbarPuppyLinkItem(object):
def render(self, request):
@@ -414,7 +430,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.utils.safestring import mark_safe
- from wagtail.wagtailadmin import hooks
+ from wagtail.wagtailcore import hooks
class WelcomePanel(object):
order = 50
@@ -440,7 +456,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.http import HttpResponse
- from wagtail.wagtailadmin import hooks
+ from wagtail.wagtailcore import hooks
def do_after_page_create(request, page):
return HttpResponse("Congrats on making content!", content_type="text/plain")
@@ -468,7 +484,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.http import HttpResponse
from django.conf.urls import url
- from wagtail.wagtailadmin import hooks
+ from wagtail.wagtailcore import hooks
def admin_view( request ):
return HttpResponse( \
@@ -484,13 +500,13 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
.. _construct_main_menu:
``construct_main_menu``
- Add, remove, or alter ``MenuItem`` objects from the Wagtail admin menu. The callable passed to this hook must take a ``request`` object and a list of ``menu_items``; it must return a list of menu items. New items can be constructed from the ``MenuItem`` class by passing in: a ``label`` which will be the text in the menu item, the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up), CSS class ``name`` applied to the wrapping ``
`` of the menu item as ``"menu-{name}"``, CSS ``classnames`` which are used to give the link an icon, and an ``order`` integer which determine's the item's place in the menu.
+ Add, remove, or alter ``MenuItem`` objects from the Wagtail admin menu. The callable passed to this hook must take a ``request`` object and a list of ``menu_items``; it must return a list of menu items. New items can be constructed from the ``MenuItem`` class by passing in: a ``label`` which will be the text in the menu item, the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up), CSS class ``name`` applied to the wrapping ``
`` of the menu item as ``"menu-{name}"``, CSS ``classnames`` which are used to give the link an icon, and an ``order`` integer which determine's the item's place in the menu.
.. code-block:: python
from django.core.urlresolvers import reverse
- from wagtail.wagtailadmin import hooks
+ from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
def construct_main_menu(request, menu_items):
@@ -510,7 +526,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.utils.html import format_html, format_html_join
from django.conf import settings
- from wagtail.wagtailadmin import hooks
+ from wagtail.wagtailcore import hooks
def editor_js():
js_files = [
@@ -538,7 +554,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.utils.html import format_html
from django.conf import settings
- from wagtail.wagtailadmin import hooks
+ from wagtail.wagtailcore import hooks
def editor_css():
return format_html('`_
+ - `Squid `_
+
+
+Advanced useage
+~~~~~~~~~~~~~~~
+
+Purging more than one URL per page
+----------------------------------
+
+By default, Wagtail will only purge one URL per page. If your page has more than one URL to be purged, you will need to override the ``get_cached_paths`` method on your page type.
+
+.. code-block:: python
+
+ class BlogIndexPage(Page):
+ def get_blog_items(self):
+ # This returns a Django paginator of blog items in this section
+ return Paginator(self.get_children().live().type(BlogPage), 10)
+
+ def get_cached_paths(self):
+ # Yield the main URL
+ yield '/'
+
+ # Yield one URL per page in the paginator to make sure all pages are purged
+ for page_number in range(1, self.get_blog_items().num_pages):
+ yield '/?page=' + str(page_number)
+
+
+Purging index pages
+-------------------
+
+Another problem is pages that list other pages (such as a blog index) will not be purged when a blog entry gets added, changed or deleted. You may want to purge the blog index page so the updates are added into the listing quickly.
+
+This can be solved by using the ``purge_page_from_cache`` utility function which can be found in the ``wagtail.contrib.wagtailfrontendcache.utils`` module.
+
+Let's take the the above BlogIndexPage as an example. We need to register a signal handler to run when one of the BlogPages get updated/deleted. This signal handler should call the ``purge_page_from_cache`` function on all BlogIndexPages that contain the BlogPage being updated/deleted.
+
+
+.. code-block:: python
+
+ # models.py
+ from django.db.models.signals import pre_delete
+
+ from wagtail.wagtailcore.signals import page_published
+ from wagtail.contrib.wagtailfrontendcache.utils import purge_page_from_cache
+
+
+ ...
+
+
+ def blog_page_changed(blog_page):
+ # Find all the live BlogIndexPages that contain this blog_page
+ for blog_index in BlogIndexPage.objects.live():
+ if blog_page in blog_index.get_blog_items().object_list:
+ # Purge this blog index
+ purge_page_from_cache(blog_index)
+
+
+ @register(page_published, sender=BlogPage):
+ def blog_published_handler(instance):
+ blog_page_changed(instance)
+
+
+ @register(pre_delete, sender=BlogPage)
+ def blog_deleted_handler(instance):
+ blog_page_changed(instance)
diff --git a/docs/index.rst b/docs/index.rst
index a35560f82..ae2300556 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,7 +3,7 @@ Welcome to Wagtail's documentation
Wagtail is a modern, flexible CMS, built on Django.
-It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
+It supports Django 1.6.2+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4. Django 1.7 support is in progress pending further release candidate testing.
.. toctree::
:maxdepth: 3
@@ -20,6 +20,7 @@ It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support
deploying
performance
static_site_generation
+ management_commands
contributing
support
roadmap
diff --git a/docs/management_commands.rst b/docs/management_commands.rst
new file mode 100644
index 000000000..3f77fc3ef
--- /dev/null
+++ b/docs/management_commands.rst
@@ -0,0 +1,52 @@
+Management commands
+===================
+
+publish_scheduled_pages
+-----------------------
+
+:code:`./manage.py publish_scheduled_pages`
+
+This command publishes or unpublishes pages that have had these actions scheduled by an editor. It is recommended to run this command once an hour.
+
+fixtree
+-------
+
+:code:`./manage.py fixtree`
+
+This command scans for errors in your database and attempts to fix any issues it finds.
+
+move_pages
+----------
+
+:code:`manage.py move_pages from to`
+
+This command moves a selection of pages from one section of the tree to another.
+
+Options:
+
+ - **from**
+ This is the **id** of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children.
+
+ - **to**
+ This is the **id** of the page to move pages to.
+
+update_index
+------------
+
+:code:`./manage.py update_index`
+
+This command rebuilds the search index from scratch. It is only required when using Elasticsearch.
+
+It is recommended to run this command once a week and at the following times:
+
+ - whenever any pages have been created through a script (after an import, for example)
+ - whenever any changes have been made to models or search configuration
+
+The search may not return any results while this command is running, so avoid running it at peak times.
+
+search_garbage_collect
+----------------------
+
+:code:`./manage.py search_garbage_collect`
+
+Wagtail keeps a log of search queries that are popular on your website. On high traffic websites, this log may get big and you may want to clean out old search queries. This command cleans out all search query logs that are more than one week old.
diff --git a/docs/sitemap_generation.rst b/docs/sitemap_generation.rst
new file mode 100644
index 000000000..9c6d50e48
--- /dev/null
+++ b/docs/sitemap_generation.rst
@@ -0,0 +1,58 @@
+Sitemap generation
+==================
+
+This document describes how to create XML sitemaps for your Wagtail website using the ``wagtail.contrib.wagtailsitemaps`` module.
+
+
+Basic configuration
+~~~~~~~~~~~~~~~~~~~
+
+You firstly need to add ``"wagtail.contrib.wagtailsitemaps"`` to INSTALLED_APPS in your Django settings file:
+
+ .. code-block:: python
+
+ INSTALLED_APPS = [
+ ...
+
+ "wagtail.contrib.wagtailsitemaps",
+ ]
+
+
+Then, in urls.py, you need to add a link to the ``wagtail.contrib.wagtailsitemaps.views.sitemap`` view which generates the sitemap:
+
+.. code-block:: python
+
+ from wagtail.contrib.wagtailsitemaps.views import sitemap
+
+ urlpatterns = patterns('',
+ ...
+
+ url('^sitemap\.xml$', sitemap),
+ )
+
+
+You should now be able to browse to "/sitemap.xml" and see the sitemap working. By default, all published pages in your website will be added to the site map.
+
+
+Customising
+~~~~~~~~~~~
+
+URLs
+----
+
+The Page class defines a ``get_sitemap_urls`` method which you can override to customise sitemaps per page instance. This method must return a list of dictionaries, one dictionary per URL entry in the sitemap. You can exclude pages from the sitemap by returning an empty list.
+
+Each dictionary can contain the following:
+
+ - **location** (required) - This is the full URL path to add into the sitemap.
+ - **lastmod** - A python date or datetime set to when the page was last modified.
+ - **changefreq**
+ - **priority**
+
+You can add more but you will need to override the ``wagtailsitemaps/sitemap.xml`` template in order for them to be displayed in the sitemap.
+
+
+Cache
+-----
+
+By default, sitemaps are cached for 100 minutes. You can change this by setting ``WAGTAILSITEMAPS_CACHE_TIMEOUT`` in your Django settings to the number of seconds you would like the cache to last for.
diff --git a/docs/static_site_generation.rst b/docs/static_site_generation.rst
index a9379cd67..9feb59c36 100644
--- a/docs/static_site_generation.rst
+++ b/docs/static_site_generation.rst
@@ -25,8 +25,19 @@ Then add ``django_medusa`` and ``wagtail.contrib.wagtailmedusa`` to ``INSTALLED_
]
+Rendering
+~~~~~~~~~
+
+To render a site, 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. This feature inherits django-medusa's ability to render your static site to Amazon S3 or Google App Engine; see the `medusa docs `_ for configuration details.
+
+To test, open the 'medusa_output' folder in a terminal and run ``python -m SimpleHTTPServer``.
+
+
+Advanced topics
+~~~~~~~~~~~~~~~
+
Replacing GET parameters with custom routing
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+--------------------------------------------
Pages which require GET parameters (e.g. for pagination) don't generate suitable filenames for generated HTML files so they need to be changed to use custom routing instead.
@@ -51,7 +62,7 @@ For example, let's say we have a Blog Index which uses pagination. We can overri
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.
@@ -72,12 +83,4 @@ For example, the BlogIndex above would need to yield one URL for each page of re
yield path
-Rendering
-~~~~~~~~~
-
-To render a site, 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. This feature inherits django-medusa's ability to render your static site to Amazon S3 or Google App Engine; see the `medusa docs `_ for configuration details.
-
-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/requirements-dev.txt b/requirements-dev.txt
index 4a9ff5c5e..b69c5d4e0 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,4 +1,4 @@
# For coverage and PEP8 linting
coverage==3.7.1
-flake8==2.1.0
+flake8==2.2.1
mock==1.0.1
diff --git a/runtests.py b/runtests.py
index 6902f7475..1c532cbe1 100755
--- a/runtests.py
+++ b/runtests.py
@@ -43,6 +43,7 @@ if not settings.configured:
STATIC_ROOT=STATIC_ROOT,
MEDIA_ROOT=MEDIA_ROOT,
USE_TZ=True,
+ TIME_ZONE='UTC',
STATICFILES_FINDERS=(
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
@@ -85,6 +86,7 @@ if not settings.configured:
'wagtail.wagtailredirects',
'wagtail.wagtailforms',
'wagtail.contrib.wagtailstyleguide',
+ 'wagtail.contrib.wagtailsitemaps',
'wagtail.tests',
],
diff --git a/setup.py b/setup.py
index 7b26817cd..6e0bc6949 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,8 @@
#!/usr/bin/env python
+import sys
+
+
try:
from setuptools import setup, find_packages
except ImportError:
@@ -16,6 +19,32 @@ except ImportError:
pass
+PY3 = sys.version_info[0] == 3
+
+
+install_requires = [
+ "Django>=1.6.2,<1.7",
+ "South>=0.8.4",
+ "django-compressor>=1.3",
+ "django-libsass>=0.1",
+ "django-modelcluster>=0.1",
+ "django-taggit==0.11.2",
+ "django-treebeard==2.0",
+ "Pillow>=2.3.0",
+ "beautifulsoup4>=4.3.2",
+ "lxml>=3.3.0",
+ "Unidecode>=0.04.14",
+ "six==1.7.3",
+ 'requests==2.3.0',
+]
+
+
+if not PY3:
+ install_requires += [
+ "unicodecsv>=0.9.4"
+ ]
+
+
setup(
name='wagtail',
version='0.3.1',
@@ -37,23 +66,13 @@ setup(
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
'Framework :: Django',
'Topic :: Internet :: WWW/HTTP :: Site Management',
],
- install_requires=[
- "Django>=1.6.2,<1.7",
- "South>=0.8.4",
- "django-compressor>=1.3",
- "django-libsass>=0.1",
- "django-modelcluster>=0.1",
- "django-taggit==0.11.2",
- "django-treebeard==2.0",
- "Pillow>=2.3.0",
- "beautifulsoup4>=4.3.2",
- "lxml>=3.3.0",
- 'unicodecsv>=0.9.4',
- 'Unidecode>=0.04.14',
- "BeautifulSoup==3.2.1", # django-compressor gets confused if we have lxml but not BS3 installed
- ],
+ install_requires=install_requires,
zip_safe=False,
)
diff --git a/tox.ini b/tox.ini
index 11b8bb3a2..2b17c65ba 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,15 +1,18 @@
[deps]
dj16=
Django>=1.6,<1.7
- pyelasticsearch==0.6.1
- elasticutils==0.8.2
+ elasticsearch==1.1.0
+ mock==1.0.1
[tox]
envlist =
py26-dj16-postgres,
py26-dj16-sqlite,
py27-dj16-postgres,
- py27-dj16-sqlite
+ py27-dj16-sqlite,
+ py32-dj16-postgres,
+ py33-dj16-postgres,
+ py34-dj16-postgres
# mysql not currently supported
# (wagtail.wagtailimages.tests.TestImageEditView currently fails with a
@@ -17,6 +20,12 @@ envlist =
# py26-dj16-mysql
# py27-dj16-mysql
+# South fails with sqlite on python3, because it tries to use DryRunMigrator which uses iteritems
+# py32-dj16-sqlite,
+# py33-dj16-sqlite,
+# py34-dj16-sqlite
+
+
[testenv]
commands=./runtests.py
@@ -24,7 +33,7 @@ commands=./runtests.py
basepython=python2.6
deps =
{[deps]dj16}
- psycopg2==2.5.2
+ psycopg2==2.5.3
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
@@ -48,7 +57,7 @@ setenv =
basepython=python2.7
deps =
{[deps]dj16}
- psycopg2==2.5.2
+ psycopg2==2.5.3
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
@@ -67,3 +76,48 @@ deps =
setenv =
DATABASE_ENGINE=django.db.backends.mysql
DATABASE_USER=wagtail
+
+[testenv:py32-dj16-postgres]
+basepython=python3.2
+deps =
+ {[deps]dj16}
+ psycopg2==2.5.3
+setenv =
+ DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
+
+[testenv:py32-dj16-sqlite]
+basepython=python3.2
+deps =
+ {[deps]dj16}
+setenv =
+ DATABASE_ENGINE=django.db.backends.sqlite3
+
+[testenv:py33-dj16-postgres]
+basepython=python3.3
+deps =
+ {[deps]dj16}
+ psycopg2==2.5.3
+setenv =
+ DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
+
+[testenv:py33-dj16-sqlite]
+basepython=python3.3
+deps =
+ {[deps]dj16}
+setenv =
+ DATABASE_ENGINE=django.db.backends.sqlite3
+
+[testenv:py34-dj16-postgres]
+basepython=python3.4
+deps =
+ {[deps]dj16}
+ psycopg2==2.5.3
+setenv =
+ DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
+
+[testenv:py34-dj16-sqlite]
+basepython=python3.4
+deps =
+ {[deps]dj16}
+setenv =
+ DATABASE_ENGINE=django.db.backends.sqlite3
diff --git a/wagtail/contrib/wagtailfrontendcache/__init__.py b/wagtail/contrib/wagtailfrontendcache/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/contrib/wagtailfrontendcache/models.py b/wagtail/contrib/wagtailfrontendcache/models.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/contrib/wagtailfrontendcache/signal_handlers.py b/wagtail/contrib/wagtailfrontendcache/signal_handlers.py
new file mode 100644
index 000000000..0fc92d46e
--- /dev/null
+++ b/wagtail/contrib/wagtailfrontendcache/signal_handlers.py
@@ -0,0 +1,25 @@
+from django.db import models
+from django.db.models.signals import post_delete
+
+from wagtail.wagtailcore.models import Page
+from wagtail.wagtailcore.signals import page_published
+
+from wagtail.contrib.wagtailfrontendcache.utils import purge_page_from_cache
+
+
+def page_published_signal_handler(instance, **kwargs):
+ purge_page_from_cache(instance)
+
+
+def post_delete_signal_handler(instance, **kwargs):
+ purge_page_from_cache(instance)
+
+
+def register_signal_handlers():
+ # Get list of models that are page types
+ indexed_models = [model for model in models.get_models() if issubclass(model, Page)]
+
+ # Loop through list and register signal handlers for each one
+ for model in indexed_models:
+ page_published.connect(page_published_signal_handler, sender=model)
+ post_delete.connect(post_delete_signal_handler, sender=model)
diff --git a/wagtail/contrib/wagtailfrontendcache/utils.py b/wagtail/contrib/wagtailfrontendcache/utils.py
new file mode 100644
index 000000000..b95ed7e95
--- /dev/null
+++ b/wagtail/contrib/wagtailfrontendcache/utils.py
@@ -0,0 +1,35 @@
+import requests
+from requests.adapters import HTTPAdapter
+
+from django.conf import settings
+
+
+class CustomHTTPAdapter(HTTPAdapter):
+ """
+ Requests will always send requests to whatever server is in the netloc
+ part of the URL. This is a problem with purging the cache as this netloc
+ may point to a different server (such as an nginx instance running in
+ front of the cache).
+
+ This class allows us to send a purge request directly to the cache server
+ with the host header still set correctly. It does this by changing the "url"
+ parameter of get_connection to always point to the cache server. Requests
+ will then use this connection to purge the page.
+ """
+ def __init__(self, cache_url):
+ self.cache_url = cache_url
+ super(CustomHTTPAdapter, self).__init__()
+
+ def get_connection(self, url, proxies=None):
+ return super(CustomHTTPAdapter, self).get_connection(self.cache_url, proxies)
+
+
+def purge_page_from_cache(page):
+ # Get session
+ cache_server_url = getattr(settings, 'WAGTAILFRONTENDCACHE_LOCATION', 'http://127.0.0.1:8000/')
+ session = requests.Session()
+ session.mount('http://', CustomHTTPAdapter(cache_server_url))
+
+ # Purge paths from cache
+ for path in page.get_cached_paths():
+ session.request('PURGE', page.full_url + path[1:])
diff --git a/wagtail/contrib/wagtailsitemaps/__init__.py b/wagtail/contrib/wagtailsitemaps/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/contrib/wagtailsitemaps/models.py b/wagtail/contrib/wagtailsitemaps/models.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/contrib/wagtailsitemaps/sitemap_generator.py b/wagtail/contrib/wagtailsitemaps/sitemap_generator.py
new file mode 100644
index 000000000..eb08d81b7
--- /dev/null
+++ b/wagtail/contrib/wagtailsitemaps/sitemap_generator.py
@@ -0,0 +1,21 @@
+from django.template.loader import render_to_string
+
+
+class Sitemap(object):
+ template = 'wagtailsitemaps/sitemap.xml'
+
+ def __init__(self, site):
+ self.site = site
+
+ def get_pages(self):
+ return self.site.root_page.get_descendants(inclusive=True).live().order_by('path')
+
+ def get_urls(self):
+ for page in self.get_pages():
+ for url in page.get_sitemap_urls():
+ yield url
+
+ def render(self):
+ return render_to_string(self.template, {
+ 'urlset': self.get_urls()
+ })
diff --git a/wagtail/contrib/wagtailsitemaps/templates/wagtailsitemaps/sitemap.xml b/wagtail/contrib/wagtailsitemaps/templates/wagtailsitemaps/sitemap.xml
new file mode 100644
index 000000000..30ca3c024
--- /dev/null
+++ b/wagtail/contrib/wagtailsitemaps/templates/wagtailsitemaps/sitemap.xml
@@ -0,0 +1,13 @@
+
+
+{% spaceless %}
+{% for url in urlset %}
+
+ {{ url.location }}
+ {% if url.lastmod %}{{ url.lastmod|date:"Y-m-d" }}{% endif %}
+ {% if url.changefreq %}{{ url.changefreq }}{% endif %}
+ {% if url.priority %}{{ url.priority }}{% endif %}
+
+{% endfor %}
+{% endspaceless %}
+
diff --git a/wagtail/contrib/wagtailsitemaps/tests.py b/wagtail/contrib/wagtailsitemaps/tests.py
new file mode 100644
index 000000000..d2d612fa2
--- /dev/null
+++ b/wagtail/contrib/wagtailsitemaps/tests.py
@@ -0,0 +1,83 @@
+from django.test import TestCase
+from django.core.cache import cache
+
+from wagtail.wagtailcore.models import Page, Site
+from wagtail.tests.models import SimplePage
+
+from .sitemap_generator import Sitemap
+
+
+class TestSitemapGenerator(TestCase):
+ def setUp(self):
+ self.home_page = Page.objects.get(id=2)
+
+ self.child_page = self.home_page.add_child(instance=SimplePage(
+ title="Hello world!",
+ slug='hello-world',
+ live=True,
+ ))
+
+ self.unpublished_child_page = self.home_page.add_child(instance=SimplePage(
+ title="Unpublished",
+ slug='unpublished',
+ live=False,
+ ))
+
+ self.site = Site.objects.get(is_default_site=True)
+
+ def test_get_pages(self):
+ sitemap = Sitemap(self.site)
+ pages = sitemap.get_pages()
+
+ self.assertIn(self.child_page.page_ptr, pages)
+ self.assertNotIn(self.unpublished_child_page.page_ptr, pages)
+
+ def test_get_urls(self):
+ sitemap = Sitemap(self.site)
+ urls = [url['location'] for url in sitemap.get_urls()]
+
+ self.assertIn('/', urls) # Homepage
+ self.assertIn('/hello-world/', urls) # Child page
+
+ def test_render(self):
+ sitemap = Sitemap(self.site)
+ xml = sitemap.render()
+
+ # Check that a URL has made it into the xml
+ self.assertIn('/hello-world/', xml)
+
+ # Make sure the unpublished page didn't make it into the xml
+ self.assertNotIn('/unpublished/', xml)
+
+
+class TestSitemapView(TestCase):
+ def test_sitemap_view(self):
+ response = self.client.get('/sitemap.xml')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailsitemaps/sitemap.xml')
+ self.assertEqual(response['Content-Type'], 'text/xml; charset=utf-8')
+
+ def test_sitemap_view_cache(self):
+ cache_key = 'wagtail-sitemap:%d' % Site.objects.get(is_default_site=True).id
+
+ # Check that the key is not in the cache
+ self.assertFalse(cache.has_key(cache_key))
+
+ # Hit the view
+ first_response = self.client.get('/sitemap.xml')
+
+ self.assertEqual(first_response.status_code, 200)
+ self.assertTemplateUsed(first_response, 'wagtailsitemaps/sitemap.xml')
+
+ # Check that the key is in the cache
+ self.assertTrue(cache.has_key(cache_key))
+
+ # Hit the view again. Should come from the cache this time
+ second_response = self.client.get('/sitemap.xml')
+
+ self.assertEqual(second_response.status_code, 200)
+ self.assertTemplateNotUsed(second_response, 'wagtailsitemaps/sitemap.xml') # Sitemap should not be re rendered
+
+ # Check that the content is the same
+ self.assertEqual(first_response.content, second_response.content)
diff --git a/wagtail/contrib/wagtailsitemaps/views.py b/wagtail/contrib/wagtailsitemaps/views.py
new file mode 100644
index 000000000..04f31fdae
--- /dev/null
+++ b/wagtail/contrib/wagtailsitemaps/views.py
@@ -0,0 +1,23 @@
+from django.http import HttpResponse
+from django.core.cache import cache
+from django.conf import settings
+
+from .sitemap_generator import Sitemap
+
+
+def sitemap(request):
+ cache_key = 'wagtail-sitemap:' + str(request.site.id)
+ sitemap_xml = cache.get(cache_key)
+
+ if not sitemap_xml:
+ # Rerender sitemap
+ sitemap = Sitemap(request.site)
+ sitemap_xml = sitemap.render()
+
+ cache.set(cache_key, sitemap_xml, getattr(settings, 'WAGTAILSITEMAPS_CACHE_TIMEOUT', 6000))
+
+ # Build response
+ response = HttpResponse(sitemap_xml)
+ response['Content-Type'] = "text/xml; charset=utf-8"
+
+ return response
diff --git a/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html b/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html
index 54cfd23a6..8506e9075 100644
--- a/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html
+++ b/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html
@@ -42,9 +42,10 @@
{% for event in events %}
diff --git a/wagtail/tests/templates/tests/simple_page.html b/wagtail/tests/templates/tests/simple_page.html
index 3413b3aa2..1f3593514 100644
--- a/wagtail/tests/templates/tests/simple_page.html
+++ b/wagtail/tests/templates/tests/simple_page.html
@@ -1,4 +1,5 @@
-{% load pageurl %}
+{% load wagtailcore_tags %}
+
diff --git a/wagtail/tests/urls.py b/wagtail/tests/urls.py
index d04c871a3..83e12adfb 100644
--- a/wagtail/tests/urls.py
+++ b/wagtail/tests/urls.py
@@ -4,6 +4,7 @@ from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls
+from wagtail.contrib.wagtailsitemaps.views import sitemap
# Signal handlers
from wagtail.wagtailsearch import register_signal_handlers as wagtailsearch_register_signal_handlers
@@ -15,6 +16,8 @@ urlpatterns = patterns('',
url(r'^search/', include(wagtailsearch_frontend_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
+ url(r'^sitemap\.xml$', sitemap),
+
# For anything not caught by a more specific rule above, hand over to
# Wagtail's serving mechanism
url(r'', include(wagtail_urls)),
diff --git a/wagtail/tests/utils.py b/wagtail/tests/utils.py
index 9a7e70f80..ef8434be0 100644
--- a/wagtail/tests/utils.py
+++ b/wagtail/tests/utils.py
@@ -1,7 +1,4 @@
-from django.test import TestCase
from django.contrib.auth import get_user_model
-from django.utils.six.moves.urllib.parse import urlparse, ParseResult
-from django.http import QueryDict
# 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
diff --git a/wagtail/tests/wagtail_hooks.py b/wagtail/tests/wagtail_hooks.py
index 5dccca666..13e3e46d2 100644
--- a/wagtail/tests/wagtail_hooks.py
+++ b/wagtail/tests/wagtail_hooks.py
@@ -1,4 +1,4 @@
-from wagtail.wagtailadmin import hooks
+from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes
def editor_css():
diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py
index a15440016..ba6edc4fa 100644
--- a/wagtail/wagtailadmin/edit_handlers.py
+++ b/wagtail/wagtailadmin/edit_handlers.py
@@ -1,6 +1,9 @@
+from __future__ import unicode_literals
+
import copy
-import re
-import datetime
+
+from six import string_types
+from six import text_type
from taggit.forms import TagWidget
from modelcluster.forms import ClusterForm, ClusterFormMetaclass
@@ -9,13 +12,10 @@ from django.template.loader import render_to_string
from django.template.defaultfilters import addslashes
from django.utils.safestring import mark_safe
from django import forms
-from django.db import models
from django.forms.models import fields_for_model
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured, ValidationError
+from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.core.urlresolvers import reverse
-from django.conf import settings
-from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from wagtail.wagtailcore.models import Page
@@ -72,7 +72,7 @@ class WagtailAdminModelFormMetaclass(ClusterFormMetaclass):
new_class = super(WagtailAdminModelFormMetaclass, cls).__new__(cls, name, bases, attrs)
return new_class
-WagtailAdminModelForm = WagtailAdminModelFormMetaclass('WagtailAdminModelForm', (ClusterForm,), {})
+WagtailAdminModelForm = WagtailAdminModelFormMetaclass(str('WagtailAdminModelForm'), (ClusterForm,), {})
# Now, any model forms built off WagtailAdminModelForm instead of ModelForm should pick up
# the nice form fields defined in FORM_FIELD_OVERRIDES.
@@ -108,7 +108,7 @@ def get_form_for_model(
# Give this new form class a reasonable name.
class_name = model.__name__ + str('Form')
form_class_attrs = {
- 'Meta': type('Meta', (object,), attrs)
+ 'Meta': type(str('Meta'), (object,), attrs)
}
return WagtailAdminModelFormMetaclass(class_name, (WagtailAdminModelForm,), form_class_attrs)
@@ -140,6 +140,10 @@ def extract_panel_definitions_from_model_class(model, exclude=None):
return panels
+def set_page_edit_handler(page_class, handlers):
+ page_class.handlers = handlers
+
+
class EditHandler(object):
"""
Abstract class providing sensible default behaviours for objects implementing
@@ -183,19 +187,21 @@ class EditHandler(object):
heading = ""
help_text = ""
- def object_classnames(self):
+ def classes(self):
"""
- Additional classnames to add to the
when rendering this
- within an ObjectList
+ Additional CSS classnames to add to whatever kind of object this is at output.
+ Subclasses of EditHandler should override this, invoking super(B, self).classes() to
+ append more classes specific to the situation.
"""
- return ""
- def field_classnames(self):
- """
- Additional classnames to add to the
when rendering this within a
-
- """
- return ""
+ classes = []
+
+ try:
+ classes.append(self.classname)
+ except AttributeError:
+ pass
+
+ return classes
def field_type(self):
"""
@@ -239,12 +245,12 @@ class EditHandler(object):
"""
rendered_fields = self.rendered_fields()
missing_fields_html = [
- unicode(self.form[field_name])
+ text_type(self.form[field_name])
for field_name in self.form.fields
if field_name not in rendered_fields
]
- return mark_safe(u''.join(missing_fields_html))
+ return mark_safe(''.join(missing_fields_html))
def render_form_content(self):
"""
@@ -261,12 +267,6 @@ class BaseCompositeEditHandler(EditHandler):
"""
_widget_overrides = None
- def object_classnames(self):
- try:
- return "multi-field " + self.classname
- except (AttributeError, TypeError):
- return "multi-field"
-
@classmethod
def widget_overrides(cls):
if cls._widget_overrides is None:
@@ -304,7 +304,7 @@ class BaseCompositeEditHandler(EditHandler):
}))
def render_js(self):
- return mark_safe(u'\n'.join([handler.render_js() for handler in self.children]))
+ return mark_safe('\n'.join([handler.render_js() for handler in self.children]))
def rendered_fields(self):
result = []
@@ -319,26 +319,41 @@ class BaseTabbedInterface(BaseCompositeEditHandler):
def TabbedInterface(children):
- return type('_TabbedInterface', (BaseTabbedInterface,), {'children': children})
+ return type(str('_TabbedInterface'), (BaseTabbedInterface,), {'children': children})
class BaseObjectList(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/object_list.html"
-def ObjectList(children, heading=""):
- return type('_ObjectList', (BaseObjectList,), {
+def ObjectList(children, heading="", classname=""):
+ return type(str('_ObjectList'), (BaseObjectList,), {
'children': children,
'heading': heading,
+ 'classname': classname
})
+class BaseFieldRowPanel(BaseCompositeEditHandler):
+ template = "wagtailadmin/edit_handlers/field_row_panel.html"
+
+def FieldRowPanel(children, classname=""):
+ return type(str('_FieldRowPanel'), (BaseFieldRowPanel,), {
+ 'children': children,
+ 'classname': classname,
+ })
+
class BaseMultiFieldPanel(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/multi_field_panel.html"
+ def classes(self):
+ classes = super(BaseMultiFieldPanel, self).classes()
+ classes.append("multi-field")
+
+ return classes
-def MultiFieldPanel(children, heading="", classname=None):
- return type('_MultiFieldPanel', (BaseMultiFieldPanel,), {
+def MultiFieldPanel(children, heading="", classname=""):
+ return type(str('_MultiFieldPanel'), (BaseMultiFieldPanel,), {
'children': children,
'heading': heading,
'classname': classname,
@@ -353,25 +368,23 @@ class BaseFieldPanel(EditHandler):
self.heading = self.bound_field.label
self.help_text = self.bound_field.help_text
- def object_classnames(self):
- try:
- return "single-field " + self.classname
- except (AttributeError, TypeError):
- return "single-field"
+ def classes(self):
+ classes = super(BaseFieldPanel, self).classes();
+
+ if self.bound_field.field.required:
+ classes.append("required")
+ if self.bound_field.errors:
+ classes.append("error")
+
+ classes.append(self.field_type())
+ classes.append("single-field")
+
+ return classes
def field_type(self):
return camelcase_to_underscore(self.bound_field.field.__class__.__name__)
- def field_classnames(self):
- classname = self.field_type()
- if self.bound_field.field.required:
- classname += " required"
- if self.bound_field.errors:
- classname += " error"
-
- return classname
-
- object_template = "wagtailadmin/edit_handlers/field_panel_object.html"
+ object_template = "wagtailadmin/edit_handlers/single_field_panel.html"
def render_as_object(self):
return mark_safe(render_to_string(self.object_template, {
@@ -401,8 +414,8 @@ class BaseFieldPanel(EditHandler):
return [self.field_name]
-def FieldPanel(field_name, classname=None):
- return type('_FieldPanel', (BaseFieldPanel,), {
+def FieldPanel(field_name, classname=""):
+ return type(str('_FieldPanel'), (BaseFieldPanel,), {
'field_name': field_name,
'classname': classname,
})
@@ -414,7 +427,7 @@ class BaseRichTextFieldPanel(BaseFieldPanel):
def RichTextFieldPanel(field_name):
- return type('_RichTextFieldPanel', (BaseRichTextFieldPanel,), {
+ return type(str('_RichTextFieldPanel'), (BaseRichTextFieldPanel,), {
'field_name': field_name,
})
@@ -470,7 +483,7 @@ class BasePageChooserPanel(BaseChooserPanel):
def target_content_type(cls):
if cls._target_content_type is None:
if cls.page_type:
- if isinstance(cls.page_type, basestring):
+ if isinstance(cls.page_type, string_types):
# translate the passed model name into an actual model class
from django.db.models import get_model
try:
@@ -505,7 +518,7 @@ class BasePageChooserPanel(BaseChooserPanel):
def PageChooserPanel(field_name, page_type=None):
- return type('_PageChooserPanel', (BasePageChooserPanel,), {
+ return type(str('_PageChooserPanel'), (BasePageChooserPanel,), {
'field_name': field_name,
'page_type': page_type,
})
@@ -588,7 +601,7 @@ class BaseInlinePanel(EditHandler):
def InlinePanel(base_model, relation_name, panels=None, label='', help_text=''):
rel = getattr(base_model, relation_name).related
- return type('_InlinePanel', (BaseInlinePanel,), {
+ return type(str('_InlinePanel'), (BaseInlinePanel,), {
'relation_name': relation_name,
'related': rel,
'panels': panels,
@@ -597,10 +610,23 @@ def InlinePanel(base_model, relation_name, panels=None, label='', help_text=''):
})
+# This allows users to include the publishing panel in their own per-model override
+# without having to write these fields out by hand, potentially losing 'classname'
+# and therefore the associated styling of the publishing panel
+def PublishingPanel():
+ return MultiFieldPanel([
+ FieldRowPanel([
+ FieldPanel('go_live_at'),
+ FieldPanel('expire_at'),
+ ], classname="label-above"),
+ ], ugettext_lazy('Scheduled publishing'), classname="publishing")
+
+
# Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some.
Page.content_panels = [
FieldPanel('title', classname="full title"),
]
+
Page.promote_panels = [
MultiFieldPanel([
FieldPanel('slug'),
@@ -609,3 +635,7 @@ Page.promote_panels = [
FieldPanel('search_description'),
], ugettext_lazy('Common page configuration')),
]
+
+Page.settings_panels = [
+ PublishingPanel()
+]
diff --git a/wagtail/wagtailadmin/hooks.py b/wagtail/wagtailadmin/hooks.py
index d299c3346..989c43bd8 100644
--- a/wagtail/wagtailadmin/hooks.py
+++ b/wagtail/wagtailadmin/hooks.py
@@ -1,38 +1,11 @@
-from django.conf import settings
-try:
- from importlib import import_module
-except ImportError:
- # for Python 2.6, fall back on django.utils.importlib (deprecated as of Django 1.7)
- from django.utils.importlib import import_module
+# The 'hooks' module is now part of wagtailcore.
+# Imports are provided here for backwards compatibility
-_hooks = {}
+import warnings
-# TODO: support 'register' as a decorator:
-# @hooks.register('construct_main_menu')
-# def construct_main_menu(menu_items):
-# ...
+warnings.warn(
+ "The wagtail.wagtailadmin.hooks module has been moved. "
+ "Use wagtail.wagtailcore.hooks instead.", DeprecationWarning)
-def register(hook_name, fn):
- if hook_name not in _hooks:
- _hooks[hook_name] = []
- _hooks[hook_name].append(fn)
-
-_searched_for_hooks = False
-
-
-def search_for_hooks():
- global _searched_for_hooks
- if not _searched_for_hooks:
- for app_module in settings.INSTALLED_APPS:
- try:
- import_module('%s.wagtail_hooks' % app_module)
- except ImportError:
- continue
-
- _searched_for_hooks = True
-
-
-def get_hooks(hook_name):
- search_for_hooks()
- return _hooks.get(hook_name, [])
+from wagtail.wagtailcore.hooks import register, get_hooks
diff --git a/wagtail/wagtailadmin/menu.py b/wagtail/wagtailadmin/menu.py
index b99e3b0ee..74e3c406c 100644
--- a/wagtail/wagtailadmin/menu.py
+++ b/wagtail/wagtailadmin/menu.py
@@ -1,3 +1,7 @@
+from __future__ import unicode_literals
+
+from six import text_type
+
from django.utils.text import slugify
from django.utils.html import format_html
@@ -7,10 +11,10 @@ class MenuItem(object):
self.label = label
self.url = url
self.classnames = classnames
- self.name = (name or slugify(unicode(label)))
+ self.name = (name or slugify(text_type(label)))
self.order = order
def render_html(self):
return format_html(
- u"""
- {% for error in field.errors %}
- {{ error|escape }}
- {% endfor %}
-
- {% endif %}
-
-
+
+ {% include "wagtailadmin/shared/field.html" %}
\ No newline at end of file
diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/shared/header.html b/wagtail/wagtailadmin/templates/wagtailadmin/shared/header.html
index 59340d2d8..be391989d 100644
--- a/wagtail/wagtailadmin/templates/wagtailadmin/shared/header.html
+++ b/wagtail/wagtailadmin/templates/wagtailadmin/shared/header.html
@@ -8,7 +8,7 @@