Merge branch 'master' into fix-edit-handlers-test

Conflicts:
	wagtail/wagtailembeds/tests.py
This commit is contained in:
Tom Talbot 2014-07-03 11:44:45 +01:00
commit d7f1e317f0
100 changed files with 896 additions and 361 deletions

View file

@ -1,10 +1,11 @@
# Python releases to test
language: python
# Test matrix
python:
- 2.7
# Django releases
- 2.7
- 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

View file

@ -4,6 +4,10 @@ Changelog
0.4 (xx.xx.20xx)
~~~~~~~~~~~~~~~~
* ElasticUtils/pyelasticsearch swapped for elasticsearch-py
* Python 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,22 @@ 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
* 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)
~~~~~~~~~~~~~~~~~~

View file

@ -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 <https://www.varnish-cache.org/>`_-friendly if you need it
* Excellent test coverage
* A simple `form builder <http://docs.wagtail.io/en/latest/form_builder.html>`_
* Optional `static site generation <http://docs.wagtail.io/en/latest/static_site_generation.html>`_
* Excellent `test coverage <https://coveralls.io/r/torchbox/wagtail?branch=master>`_
Find out more at `wagtail.io <http://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 <https://github.com/torchbox/wagtaildemo/>`_; see the `README <https://github.com/torchbox/wagtaildemo/blob/master/README.md>`_ for installation instructions.
* See the `Getting Started <http://wagtail.readthedocs.org/en/latest/gettingstarted.html#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 <https://github.com/spapas>`_ has written a `tutorial <http://spapas.github.io/2014/02/13/wagtail-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 <http://wagtail.readthedocs.org/>`_. and always being updated.
Available at `wagtail.readthedocs.org <http://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.3 and 3.4.
Django 1.7 support is in progress pending further release candidate testing.
Wagtail's dependencies are summarised at `requirements.io <https://requires.io/github/torchbox/wagtail/requirements>`_.
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 <https://twitter.com/WagtailCMS/status/432166799464210432/photo/1>`_. 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! <https://github.com/torchbox/wagtail/issues?labels=Help+develop+me%21>`_ label.
Send us a useful pull request and we'll post you a `t-shirt <https://twitter.com/WagtailCMS/status/432166799464210432/photo/1>`_.

View file

@ -4,26 +4,24 @@ 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 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.
@ -34,18 +32,17 @@ There are four basic types of panels:
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`.
``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 others 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 would be intuitive to find the start and end date on the same "row".
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 e.g 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 e.g: a field to switch between one layout/style and another.
* ``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:
@ -70,7 +67,7 @@ Let's look at an example of a panel definition:
FieldRowPanel([
FieldPanel('start_date', classname="col3"),
FieldPanel('end_date', classname="col3"),
]),
]),
ImageChooserPanel('splash_image'),
DocumentChooserPanel('free_download'),
PageChooserPanel('related_page'),
@ -134,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'),
# ...
@ -240,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),
# ...
@ -387,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
-----------
@ -401,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)
@ -414,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):
@ -435,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
@ -461,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")
@ -489,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( \
@ -505,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 ``<li>`` 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 ``<li>`` 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):
@ -531,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 = [
@ -559,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('<link rel="stylesheet" href="' \
@ -579,7 +574,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
from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes
def whitelister_element_rules():
@ -632,4 +627,3 @@ Custom Choosers
Tests
-----

View file

@ -0,0 +1,102 @@
Frontend cache purging
======================
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.
Setting it up
~~~~~~~~~~~~~
Firstly, add ``"wagtail.contrib.wagtailfrontendcache"`` to your INSTALLED_APPS:
.. code-block:: python
INSTALLED_APPS = [
...
"wagtail.contrib.wagtailfrontendcache"
]
The ``wagtailfrontendcache`` module provides a set of signal handlers which will automatically purge the cache whenever a page is published or deleted. You should register these somewhere at the top of your ``urls.py`` file:
.. code-block:: python
# urls.py
from wagtail.contrib.wagtailfrontendcache.signal_handlers import register_signal_handlers
register_signal_handlers()
You then need to set the ``WAGTAILFRONTENDCACHE_LOCATION`` setting to the URL of your Varnish/Squid cache server. This must be a direct connection to the server and cannot go through another proxy. By default, this is set to ``http://127.0.0.1:8000`` which is very likely incorrect.
Finally, make sure you have configured your frontend cache to accept PURGE requests:
- `Varnish <https://www.varnish-cache.org/docs/3.0/tutorial/purging.html>`_
- `Squid <http://wiki.squid-cache.org/SquidFaq/OperatingSquid#How_can_I_purge_an_object_from_my_cache.3F>`_
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)

View file

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

View file

@ -4,56 +4,49 @@ Management commands
publish_scheduled_pages
-----------------------
**./manage.py publish_scheduled_pages**
This command publishes/unpublishes pages that have had these actions scheduled by an editor.
It is recommended to run this command once an hour.
: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
-------
**./manage.py fixtree**
:code:`./manage.py fixtree`
This command scans for errors in your database and attempts to fix any issues it finds.
move_pages
----------
**./manage.py move_pages from to**
:code:`manage.py move_pages from to`
This mass moves a bunch of pages from one section of the tree to another.
This command moves a selection of pages from one section of the tree to another.
Options:
- **from**
This is 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.
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.
This is the **id** of the page to move pages to.
update_index
------------
**./manage.py update_index**
:code:`./manage.py update_index`
This command rebuilds the search index from scratch. It is only required when using ElasticSearch.
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 (eg, import)
- Whenever any changes have been made to models or search configuration
While this command is running, the search may not return any results so avoid running this command at peak 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
----------------------
**./manage.py 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 sometimes you may want to clean out old search queries.
This command cleans out all search query logs that are more than one week old.
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.

View file

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

View file

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

View file

@ -85,6 +85,7 @@ if not settings.configured:
'wagtail.wagtailredirects',
'wagtail.wagtailforms',
'wagtail.contrib.wagtailstyleguide',
'wagtail.contrib.wagtailsitemaps',
'wagtail.tests',
],

View file

@ -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,11 @@ setup(
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'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,
)

47
tox.ini
View file

@ -1,15 +1,17 @@
[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,
py33-dj16-postgres,
py34-dj16-postgres
# mysql not currently supported
# (wagtail.wagtailimages.tests.TestImageEditView currently fails with a
@ -17,6 +19,11 @@ envlist =
# py26-dj16-mysql
# py27-dj16-mysql
# South fails with sqlite on python3, because it tries to use DryRunMigrator which uses iteritems
# py33-dj16-sqlite,
# py34-dj16-sqlite
[testenv]
commands=./runtests.py
@ -24,7 +31,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 +55,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 +74,33 @@ deps =
setenv =
DATABASE_ENGINE=django.db.backends.mysql
DATABASE_USER=wagtail
[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

View file

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

View file

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

View file

@ -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()
})

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% spaceless %}
{% for url in urlset %}
<url>
<loc>{{ url.location }}</loc>
{% if url.lastmod %}<lastmod>{{ url.lastmod|date:"Y-m-d" }}</lastmod>{% endif %}
{% if url.changefreq %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %}
{% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %}
</url>
{% endfor %}
{% endspaceless %}
</urlset>

View file

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

View file

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

View file

@ -1,16 +1,10 @@
from django import forms
from django.db import models
from django.shortcuts import render
from django.utils.translation import ugettext as _
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailcore.fields import RichTextField
CHOICES = (

View file

@ -1,14 +1,10 @@
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls import url
from django.core import urlresolvers
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailimages import urls
from . import views

View file

@ -1,6 +1,9 @@
from django.db import models
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.encoding import python_2_unicode_compatible
from modelcluster.fields import ParentalKey
from wagtail.wagtailcore.models import Page, Orderable
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel, InlinePanel, PageChooserPanel
@ -259,6 +262,7 @@ FormPage.content_panels = [
# Snippets
@python_2_unicode_compatible
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
@ -268,7 +272,7 @@ class Advert(models.Model):
FieldPanel('text'),
]
def __unicode__(self):
def __str__(self):
return self.text
@ -281,18 +285,20 @@ register_snippet(Advert)
# to ensure specific [in]correct register ordering
# AlphaSnippet is registered during TestSnippetOrdering
@python_2_unicode_compatible
class AlphaSnippet(models.Model):
text = models.CharField(max_length=255)
def __unicode__(self):
def __str__(self):
return self.text
# ZuluSnippet is registered during TestSnippetOrdering
@python_2_unicode_compatible
class ZuluSnippet(models.Model):
text = models.CharField(max_length=255)
def __unicode__(self):
def __str__(self):
return self.text

View file

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

View file

@ -1,7 +1,4 @@
from django.test import TestCase
from django.contrib.auth.models import User
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

View file

@ -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():

View file

@ -1,6 +1,7 @@
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 +10,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
@ -245,7 +243,7 @@ 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
]
@ -483,7 +481,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:

View file

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

View file

@ -1,3 +1,5 @@
from six import text_type
from django.utils.text import slugify
from django.utils.html import format_html
@ -7,7 +9,7 @@ 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):

View file

@ -247,7 +247,10 @@ function InlinePanel(opts) {
}
self.initChildControls(fixPrefix(opts.emptyChildFormPrefix));
if (opts.canOrder) {
$(fixPrefix('#id_' + opts.emptyChildFormPrefix + '-ORDER')).val(formCount);
/* NB form hidden inputs use 0-based index and only increment formCount *after* this function is run.
Therefore formcount and order are currently equal and order must be incremented
to ensure it's *greater* than previous item */
$(fixPrefix('#id_' + opts.emptyChildFormPrefix + '-ORDER')).val(formCount + 1);
}
self.updateMoveButtonDisabledStates();

View file

@ -264,7 +264,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
left:0;
top:0;
width:2em;
line-height:2em;
line-height:1.85em;
height:100%;
text-align:center;
background-color:rgba(0,0,0,0.2);
@ -278,7 +278,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
&:before{
width:2em;
font-size:0.9rem;
line-height:2em;
line-height:1.65em;
}
}
@ -303,17 +303,8 @@ input[type=submit], input[type=reset], input[type=button], button{
}
}
button.icon{
&:before,
&:after{
line-height:0;
}
}
.multiple{
@include transition(max-height 10s ease);
padding:0;
max-height:10000px;
max-width:1024px - 50px;
overflow:hidden;
@ -476,7 +467,7 @@ li.focused > .help{
&.field-small{
.input{
&:before, &:after{
font-size:1.5em;
font-size:1.3rem; /* REMs are necessary here because IE doesn't treat generated content correctly */
top:0.3em;
}
&:before{
@ -778,11 +769,12 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
}
&.bicolor{
padding-left:3.5em;
padding-left:3.7em;
&:before{
width:2.2em;
line-height:2.45em;
width:2em;
line-height:2.2em;
font-size:1.1rem;
}
}

View file

@ -269,7 +269,6 @@
.multiple{
padding:0;
max-height:0px;
}
}

View file

@ -70,7 +70,7 @@
{% blocktrans with last_mod=page.get_latest_revision.created_at %}Last modified: {{ last_mod }}{% endblocktrans %}
{% if page.get_latest_revision.user %}
{% blocktrans with modified_by=page.get_latest_revision.user.get_full_name|default:page.get_latest_revision.user.username %}by {{ modified_by }}{% endblocktrans %}
{% if request.user.email %}
{% if page.get_latest_revision.user.email %}
<span class="avatar small icon icon-user"><img src="{% gravatar_url page.get_latest_revision.user.email 25 %}" /></span>
{% endif %}
{% endif %}

View file

@ -8,9 +8,11 @@
### <img src="{% gravatar_url sometemplatevariable %}">
### just make sure to update the "default" image path below
import urllib
import hashlib
from six import b
from six.moves.urllib.parse import urlencode
from django import template
register = template.Library()
@ -30,8 +32,8 @@ class GravatarUrlNode(template.Node):
default = "blank"
size = int(self.size) * 2 # requested at retina size by default and scaled down at point of use with css
gravatar_url = "//www.gravatar.com/avatar/" + hashlib.md5(email.lower()).hexdigest() + "?"
gravatar_url += urllib.urlencode({'s': str(size), 'd': default})
gravatar_url = "//www.gravatar.com/avatar/" + hashlib.md5(b(email.lower())).hexdigest() + "?"
gravatar_url += urlencode({'s': str(size), 'd': default})
return gravatar_url

View file

@ -2,9 +2,9 @@ from django import template
from django.core import urlresolvers
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import get_navigation_menu_items, UserPagePermissionsProxy
from wagtail.wagtailcore.utils import camelcase_to_underscore

View file

@ -4,7 +4,7 @@ from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import mail
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailusers.models import UserProfile

View file

@ -7,9 +7,10 @@ from django.core import mail
from django.core.paginator import Paginator
from django.utils import timezone
from wagtail.tests.models import SimplePage, EventPage, StandardIndex, StandardChild, BusinessIndex, BusinessChild, BusinessSubIndex
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.tests.models import SimplePage, EventPage, StandardIndex, BusinessIndex, BusinessChild, BusinessSubIndex
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailcore.signals import page_published
from wagtail.wagtailusers.models import UserProfile
@ -189,9 +190,9 @@ class TestPageCreation(TestCase, WagtailTestUtils):
# Find the page and check the scheduled times
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEquals(page.go_live_at.date(), go_live_at.date())
self.assertEquals(page.expire_at.date(), expire_at.date())
self.assertEquals(page.expired, False)
self.assertEqual(page.go_live_at.date(), go_live_at.date())
self.assertEqual(page.expire_at.date(), expire_at.date())
self.assertEqual(page.expired, False)
self.assertTrue(page.status_string, "draft")
# No revisions with approved_go_live_at
@ -228,6 +229,15 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
def test_create_simplepage_post_publish(self):
# Connect a mock signal handler to page_published signal
signal_fired = [False]
signal_page = [None]
def page_published_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_published.connect(page_published_handler)
# Post
post_data = {
'title': "New page!",
'content': "Some content",
@ -245,6 +255,11 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertIsInstance(page, SimplePage)
self.assertTrue(page.live)
# Check that the page_published signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], page)
self.assertEqual(signal_page[0], signal_page[0].specific)
def test_create_simplepage_post_publish_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
@ -263,9 +278,9 @@ class TestPageCreation(TestCase, WagtailTestUtils):
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEquals(page.go_live_at.date(), go_live_at.date())
self.assertEquals(page.expire_at.date(), expire_at.date())
self.assertEquals(page.expired, False)
self.assertEqual(page.go_live_at.date(), go_live_at.date())
self.assertEqual(page.expire_at.date(), expire_at.date())
self.assertEqual(page.expired, False)
# A revision with approved_go_live_at should exist now
self.assertTrue(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists())
@ -470,6 +485,14 @@ class TestPageEdit(TestCase, WagtailTestUtils):
self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
def test_page_edit_post_publish(self):
# Connect a mock signal handler to page_published signal
signal_fired = [False]
signal_page = [None]
def page_published_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_published.connect(page_published_handler)
# Tests publish from edit page
post_data = {
'title': "I've been edited!",
@ -486,6 +509,11 @@ class TestPageEdit(TestCase, WagtailTestUtils):
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertEqual(child_page_new.title, post_data['title'])
# Check that the page_published signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], child_page_new)
self.assertEqual(signal_page[0], signal_page[0].specific)
# The page shouldn't have "has_unpublished_changes" flag set
self.assertFalse(child_page_new.has_unpublished_changes)
@ -855,6 +883,14 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
"""
This posts to the approve moderation view and checks that the page was approved
"""
# Connect a mock signal handler to page_published signal
signal_fired = [False]
signal_page = [None]
def page_published_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_published.connect(page_published_handler)
# Post
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
@ -866,6 +902,11 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
# Page must be live
self.assertTrue(Page.objects.get(id=self.page.id).live)
# Check that the page_published signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], self.page)
self.assertEqual(signal_page[0], signal_page[0].specific)
def test_approve_moderation_view_bad_revision_id(self):
"""
This tests that the approve moderation view handles invalid revision ids correctly

View file

@ -1,6 +1,5 @@
from django.test import TestCase
from wagtail.tests.models import SimplePage, EventPage
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.tasks import send_email_task
from django.core.urlresolvers import reverse

View file

@ -1,9 +1,8 @@
from django.conf.urls import url
from django.conf import settings
from wagtail.wagtailadmin.forms import LoginForm, PasswordResetForm
from wagtail.wagtailadmin.forms import PasswordResetForm
from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
urlpatterns = [

View file

@ -1,4 +1,3 @@
from django.core.urlresolvers import reverse
from django.template import RequestContext
from django.template.loader import render_to_string

View file

@ -4,9 +4,9 @@ from django.conf import settings
from django.template import RequestContext
from django.template.loader import render_to_string
from wagtail.wagtailadmin import hooks
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision, UserPagePermissionsProxy
from wagtail.wagtaildocs.models import Document

View file

@ -11,9 +11,11 @@ from django.views.decorators.vary import vary_on_headers
from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailadmin import tasks, hooks, signals
from wagtail.wagtailadmin import tasks, signals
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailcore.signals import page_published
@permission_required('wagtailadmin.access_admin')
@ -201,6 +203,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
)
if is_publishing:
page_published.send(sender=page_class, instance=page)
messages.success(request, _("Page '{0}' published.").format(page.title))
elif is_submitting:
messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title))
@ -324,6 +327,7 @@ def edit(request, page_id):
)
if is_publishing:
page_published.send(sender=page.__class__, instance=page)
messages.success(request, _("Page '{0}' published.").format(page.title))
elif is_submitting:
messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title))
@ -343,7 +347,7 @@ def edit(request, page_id):
edit_handler = edit_handler_class(instance=page, form=form)
errors_debug = (
repr(edit_handler.form.errors)
+ repr([(name, formset.errors) for (name, formset) in edit_handler.form.formsets.iteritems() if formset.errors])
+ repr([(name, formset.errors) for (name, formset) in edit_handler.form.formsets.items() if formset.errors])
)
else:
form = form_class(instance=page)
@ -696,6 +700,7 @@ def approve_moderation(request, revision_id):
if request.POST:
revision.publish()
page_published.send(sender=revision.page.__class__, instance=revision.page.specific)
messages.success(request, _("Page '{0}' published.").format(revision.page.title))
tasks.send_notification.delay(revision.id, 'approved', request.user.id)

View file

@ -2,7 +2,7 @@ from django.shortcuts import render
from django.contrib.auth.decorators import permission_required
from wagtail.wagtailadmin.userbar import EditPageItem, AddPageItem, ApproveModerationEditPageItem, RejectModerationEditPageItem
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision

View file

@ -1,4 +1,5 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
# A setting that can be used in foreign key declarations
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')

View file

@ -0,0 +1,38 @@
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
_hooks = {}
# TODO: support 'register' as a decorator:
# @hooks.register('construct_main_menu')
# def construct_main_menu(menu_items):
# ...
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, [])

View file

@ -1,4 +1,5 @@
import datetime
from __future__ import print_function
import json
from optparse import make_option
@ -31,7 +32,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
dryrun = False
if options['dryrun']:
print "Will do a dry run."
print("Will do a dry run.")
dryrun = True
# 1. get all expired pages with live = True
@ -41,17 +42,17 @@ class Command(BaseCommand):
)
if dryrun:
if expired_pages:
print "Expired pages to be deactivated:"
print "Expiry datetime\t\tSlug\t\tName"
print "---------------\t\t----\t\t----"
print("Expired pages to be deactivated:")
print("Expiry datetime\t\tSlug\t\tName")
print("---------------\t\t----\t\t----")
for ep in expired_pages:
print "{0}\t{1}\t{2}".format(
print("{0}\t{1}\t{2}".format(
ep.expire_at.strftime("%Y-%m-%d %H:%M"),
ep.slug,
ep.title
)
))
else:
print "No expired pages to be deactivated found."
print("No expired pages to be deactivated found.")
else:
expired_pages.update(expired=True, live=False)
@ -62,22 +63,22 @@ class Command(BaseCommand):
) if revision_date_expired(r)
]
if dryrun:
print "---------------------------------"
print("---------------------------------")
if expired_revs:
print "Expired revisions to be dropped from moderation queue:"
print "Expiry datetime\t\tSlug\t\tName"
print "---------------\t\t----\t\t----"
print("Expired revisions to be dropped from moderation queue:")
print("Expiry datetime\t\tSlug\t\tName")
print("---------------\t\t----\t\t----")
for er in expired_revs:
rev_data = json.loads(er.content_json)
print "{0}\t{1}\t{2}".format(
print("{0}\t{1}\t{2}".format(
dateparse.parse_datetime(
rev_data.get('expire_at')
).strftime("%Y-%m-%d %H:%M"),
rev_data.get('slug'),
rev_data.get('title')
)
))
else:
print "No expired revision to be dropped from moderation."
print("No expired revision to be dropped from moderation.")
else:
for er in expired_revs:
er.submitted_for_moderation = False
@ -88,20 +89,20 @@ class Command(BaseCommand):
approved_go_live_at__lt=timezone.now()
)
if dryrun:
print "---------------------------------"
print("---------------------------------")
if revs_for_publishing:
print "Revisions to be published:"
print "Go live datetime\t\tSlug\t\tName"
print "---------------\t\t\t----\t\t----"
print("Revisions to be published:")
print("Go live datetime\t\tSlug\t\tName")
print("---------------\t\t\t----\t\t----")
for rp in revs_for_publishing:
rev_data = json.loads(rp.content_json)
print "{0}\t\t{1}\t{2}".format(
print("{0}\t\t{1}\t{2}".format(
rp.approved_go_live_at.strftime("%Y-%m-%d %H:%M"),
rev_data.get('slug'),
rev_data.get('title')
)
))
else:
print "No pages to go live."
print("No pages to go live.")
else:
for rp in revs_for_publishing:
# just run publish for the revision -- since the approved go

View file

@ -1,7 +1,10 @@
from StringIO import StringIO
from urlparse import urlparse
import warnings
import six
from six import string_types
from six import StringIO
from six.moves.urllib.parse import urlparse
from modelcluster.models import ClusterableModel
from django.db import models, connection, transaction
@ -15,10 +18,10 @@ from django.contrib.auth.models import Group
from django.conf import settings
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.utils.functional import cached_property
from django.utils.encoding import python_2_unicode_compatible
from treebeard.mp_tree import MP_Node
@ -33,6 +36,7 @@ class SiteManager(models.Manager):
return self.get(hostname=hostname, port=port)
@python_2_unicode_compatible
class Site(models.Model):
hostname = models.CharField(max_length=255, db_index=True)
port = models.IntegerField(default=80, help_text=_("Set this to something other than 80 if you need a specific port number to appear in URLs (e.g. development on port 8000). Does not affect request handling (so port forwarding still works)."))
@ -45,7 +49,7 @@ class Site(models.Model):
def natural_key(self):
return (self.hostname, self.port)
def __unicode__(self):
def __str__(self):
return self.hostname + ("" if self.port == 80 else (":%d" % self.port)) + (" [default]" if self.is_default_site else "")
@staticmethod
@ -255,9 +259,8 @@ class PageBase(models.base.ModelBase):
PAGE_MODEL_CLASSES.append(cls)
class Page(MP_Node, ClusterableModel, Indexed):
__metaclass__ = PageBase
@python_2_unicode_compatible
class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, Indexed)):
title = models.CharField(max_length=255, help_text=_("The page title as you'd like it to be seen by the public"))
slug = models.SlugField(help_text=_("The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/"))
# TODO: enforce uniqueness on slug field per parent (will have to be done at the Django
@ -300,7 +303,7 @@ class Page(MP_Node, ClusterableModel, Indexed):
# created as
self.content_type = ContentType.objects.get_for_model(self)
def __unicode__(self):
def __str__(self):
return self.title
is_abstract = True # don't offer Page in the list of page types a superuser can create
@ -536,7 +539,7 @@ class Page(MP_Node, ClusterableModel, Indexed):
else:
res = []
for page_type in cls.subpage_types:
if isinstance(page_type, basestring):
if isinstance(page_type, string_types):
try:
app_label, model_name = page_type.split(".")
except ValueError:
@ -686,12 +689,28 @@ class Page(MP_Node, ClusterableModel, Indexed):
"""
return self.serve(self.dummy_request())
def get_cached_paths(self):
"""
This returns a list of paths to invalidate in a frontend cache
"""
return ['/']
def get_sitemap_urls(self):
latest_revision = self.get_latest_revision()
return [
{
'location': self.url,
'lastmod': latest_revision.created_at if latest_revision else None
}
]
def get_static_site_paths(self):
"""
This is a generator of URL paths to feed into a static site generator
Override this if you would like to create static versions of subpages
"""
# Yield paths for this page
# Yield path for this page
yield '/'
# Yield paths for child pages
@ -770,6 +789,7 @@ class SubmittedRevisionsManager(models.Manager):
return super(SubmittedRevisionsManager, self).get_queryset().filter(submitted_for_moderation=True)
@python_2_unicode_compatible
class PageRevision(models.Model):
page = models.ForeignKey('Page', related_name='revisions')
submitted_for_moderation = models.BooleanField(default=False)
@ -827,7 +847,7 @@ class PageRevision(models.Model):
self.submitted_for_moderation = False
page.revisions.update(submitted_for_moderation=False)
def __unicode__(self):
def __str__(self):
return '"' + unicode(self.page) + '" at ' + unicode(self.created_at)

View file

@ -13,7 +13,7 @@ from wagtail.wagtaildocs.models import Document
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.formats import get_image_format
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
# Define a set of 'embed handlers' and 'link handlers'. These handle the translation
@ -166,8 +166,8 @@ class DbWhitelister(Whitelister):
def clean(cls, html):
if not cls.has_loaded_custom_whitelist_rules:
for fn in hooks.get_hooks('construct_whitelister_element_rules'):
cls.element_rules = dict(
cls.element_rules.items() + fn().items())
cls.element_rules = cls.element_rules.copy()
cls.element_rules.update(fn())
cls.has_loaded_custom_whitelist_rules = True
return super(DbWhitelister, cls).clean(html)

View file

@ -0,0 +1,4 @@
from django.dispatch import Signal
page_published = Signal(providing_args=['instance'])

View file

@ -1,14 +1,13 @@
from StringIO import StringIO
from datetime import timedelta
from django.test import TestCase, Client
from django.http import HttpRequest, Http404
from six import StringIO
from django.test import TestCase
from django.core import management
from django.contrib.auth.models import User
from django.utils import timezone
from wagtail.wagtailcore.models import Page, PageRevision, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.tests.models import SimplePage
class TestFixTreeCommand(TestCase):

View file

@ -1,11 +1,7 @@
from StringIO import StringIO
from django.test import TestCase, Client
from django.http import HttpRequest, Http404
from django.core import management
from django.contrib.auth.models import User
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.wagtailcore.models import Page, Site
from wagtail.tests.models import EventPage, EventIndex, SimplePage

View file

@ -1,12 +1,8 @@
from StringIO import StringIO
from django.test import TestCase, Client
from django.http import HttpRequest, Http404
from django.core import management
from django.test import TestCase
from django.contrib.auth.models import User
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
from wagtail.wagtailcore.models import Page, UserPagePermissionsProxy
from wagtail.tests.models import EventPage
class TestPagePermission(TestCase):

View file

@ -1,12 +1,7 @@
from StringIO import StringIO
from django.test import TestCase
from django.test import TestCase, Client
from django.http import HttpRequest, Http404
from django.core import management
from django.contrib.auth.models import User
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
from wagtail.wagtailcore.models import Page
from wagtail.tests.models import EventPage
class TestPageQuerySet(TestCase):

View file

@ -1,4 +1,4 @@
from bs4 import BeautifulSoup, NavigableString
from bs4 import BeautifulSoup
from django.test import TestCase
from wagtail.wagtailcore.whitelist import (

View file

@ -2,8 +2,9 @@
A generic HTML whitelisting engine, designed to accommodate subclassing to override
specific rules.
"""
from six.moves.urllib.parse import urlparse
from bs4 import BeautifulSoup, NavigableString, Tag
from urlparse import urlparse
ALLOWED_URL_SCHEMES = ['', 'http', 'https', 'ftp', 'mailto', 'tel']
@ -28,7 +29,7 @@ def attribute_rule(allowed_attrs):
* if the lookup returns a truthy value, keep the attribute; if falsy, drop it
"""
def fn(tag):
for attr, val in tag.attrs.items():
for attr, val in list(tag.attrs.items()):
rule = allowed_attrs.get(attr)
if rule:
if callable(rule):
@ -82,7 +83,7 @@ class Whitelister(object):
attributes"""
doc = BeautifulSoup(html, 'lxml')
cls.clean_node(doc, doc)
return unicode(doc)
return doc.decode()
@classmethod
def clean_node(cls, doc, node):

View file

@ -9,10 +9,12 @@ from django.dispatch import Signal
from django.core.urlresolvers import reverse
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from wagtail.wagtailadmin.taggable import TagSearchable
@python_2_unicode_compatible
class Document(models.Model, TagSearchable):
title = models.CharField(max_length=255, verbose_name=_('Title'))
file = models.FileField(upload_to='documents' , verbose_name=_('File'))
@ -30,7 +32,7 @@ class Document(models.Model, TagSearchable):
},
}
def __unicode__(self):
def __str__(self):
return self.title
@property

View file

@ -1,10 +1,14 @@
from six import b
from django.test import TestCase
from wagtail.wagtaildocs import models
from wagtail.tests.utils import WagtailTestUtils
from django.contrib.auth.models import User, Group, Permission
from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile
from wagtail.wagtaildocs import models
from wagtail.tests.utils import WagtailTestUtils
# TODO: Test serve view
@ -112,7 +116,7 @@ class TestDocumentAddView(TestCase, WagtailTestUtils):
def test_post(self):
# Build a fake file
fake_file = ContentFile("A boring example document")
fake_file = ContentFile(b("A boring example document"))
fake_file.name = 'test.txt'
# Submit
@ -134,7 +138,7 @@ class TestDocumentEditView(TestCase, WagtailTestUtils):
self.login()
# Build a fake file
fake_file = ContentFile("A boring example document")
fake_file = ContentFile(b("A boring example document"))
fake_file.name = 'test.txt'
# Create a document to edit
@ -147,7 +151,7 @@ class TestDocumentEditView(TestCase, WagtailTestUtils):
def test_post(self):
# Build a fake file
fake_file = ContentFile("A boring example document")
fake_file = ContentFile(b("A boring example document"))
fake_file.name = 'test.txt'
# Submit title change
@ -272,7 +276,7 @@ class TestDocumentChooserUploadView(TestCase, WagtailTestUtils):
def test_post(self):
# Build a fake file
fake_file = ContentFile("A boring example document")
fake_file = ContentFile(b("A boring example document"))
fake_file.name = 'test.txt'
# Submit

View file

@ -4,7 +4,7 @@ from django.core import urlresolvers
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtaildocs import admin_urls

View file

@ -1,4 +1,6 @@
import sys
from datetime import datetime
import json
try:
from importlib import import_module
@ -6,13 +8,20 @@ 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
# Needs to be imported like this to allow @patch to work in tests
from six.moves.urllib import request as urllib_request
from six.moves.urllib.request import Request
from six.moves.urllib.error import URLError
from six.moves.urllib.parse import urlencode
from django.conf import settings
from datetime import datetime
from django.utils import six
from wagtail.wagtailembeds.oembed_providers import get_oembed_provider
from wagtail.wagtailembeds.models import Embed
import urllib2, urllib
import json
class EmbedNotFoundException(Exception): pass
@ -99,11 +108,11 @@ def oembed(url, max_width=None):
params['maxwidth'] = max_width
# Perform request
request = urllib2.Request(provider + '?' + urllib.urlencode(params))
request = Request(provider + '?' + urlencode(params))
request.add_header('User-agent', 'Mozilla/5.0')
try:
r = urllib2.urlopen(request)
except urllib2.URLError:
r = urllib_request.urlopen(request)
except URLError:
raise EmbedNotFoundException
oembed = json.loads(r.read())

View file

@ -1,4 +1,5 @@
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
EMBED_TYPES = (
@ -9,6 +10,7 @@ EMBED_TYPES = (
)
@python_2_unicode_compatible
class Embed(models.Model):
url = models.URLField()
max_width = models.SmallIntegerField(null=True, blank=True)
@ -25,5 +27,5 @@ class Embed(models.Model):
class Meta:
unique_together = ('url', 'max_width')
def __unicode__(self):
def __str__(self):
return self.url

View file

@ -1,5 +1,8 @@
import six.moves.urllib.request
from six.moves.urllib.error import URLError
from mock import patch
import urllib2
import warnings
try:
import embedly
@ -217,12 +220,12 @@ class TestOembed(TestCase):
self.assertRaises(EmbedNotFoundException, wagtail_oembed, "foo")
def test_oembed_invalid_request(self):
config = {'side_effect': urllib2.URLError('foo')}
with patch.object(urllib2, 'urlopen', **config) as urlopen:
config = {'side_effect': URLError('foo')}
with patch.object(six.moves.urllib.request, 'urlopen', **config) as urlopen:
self.assertRaises(EmbedNotFoundException, wagtail_oembed,
"http://www.youtube.com/watch/")
@patch('urllib2.urlopen')
@patch('six.moves.urllib.request.urlopen')
@patch('json.loads')
def test_oembed_photo_request(self, loads, urlopen) :
urlopen.return_value = self.dummy_response
@ -233,7 +236,7 @@ class TestOembed(TestCase):
self.assertEqual(result['html'], '<img src="http://www.example.com" />')
loads.assert_called_with("foo")
@patch('urllib2.urlopen')
@patch('six.moves.urllib.request.urlopen')
@patch('json.loads')
def test_oembed_return_values(self, loads, urlopen):
urlopen.return_value = self.dummy_response
@ -268,7 +271,7 @@ class TestEmbedFilter(TestCase):
return "foo"
self.dummy_response = DummyResponse()
@patch('urllib2.urlopen')
@patch('six.moves.urllib.request.urlopen')
@patch('json.loads')
def test_valid_embed(self, loads, urlopen):
urlopen.return_value = self.dummy_response
@ -277,7 +280,7 @@ class TestEmbedFilter(TestCase):
result = embed_filter('http://www.youtube.com/watch/')
self.assertEqual(result, '<img src="http://www.example.com" />')
@patch('urllib2.urlopen')
@patch('six.moves.urllib.request.urlopen')
@patch('json.loads')
def test_render_filter(self, loads, urlopen):
urlopen.return_value = self.dummy_response
@ -288,7 +291,7 @@ class TestEmbedFilter(TestCase):
result = temp.render(context)
self.assertEqual(result, '<img src="http://www.example.com" />')
@patch('urllib2.urlopen')
@patch('six.moves.urllib.request.urlopen')
@patch('json.loads')
def test_render_filter_nonexistent_type(self, loads, urlopen):
urlopen.return_value = self.dummy_response

View file

@ -1,5 +1,4 @@
from django.forms.util import ErrorList
from django.conf import settings
from django.utils.translation import ugettext as _
from wagtail.wagtailadmin.modal_workflow import render_modal_workflow

View file

@ -3,7 +3,7 @@ from django.conf.urls import include, url
from django.core import urlresolvers
from django.utils.html import format_html
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailembeds import urls

View file

@ -1,11 +1,15 @@
import json
import re
from six import text_type
from unidecode import unidecode
from django.db import models
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
from django.utils.text import slugify
from unidecode import unidecode
import json
import re
from django.utils.encoding import python_2_unicode_compatible
from wagtail.wagtailcore.models import Page, Orderable, UserPagePermissionsProxy, get_page_types
from wagtail.wagtailadmin.edit_handlers import FieldPanel
@ -32,6 +36,7 @@ FORM_FIELD_CHOICES = (
HTML_EXTENSION_RE = re.compile(r"(.*)\.html")
@python_2_unicode_compatible
class FormSubmission(models.Model):
"""Data for a Form submission."""
@ -43,7 +48,7 @@ class FormSubmission(models.Model):
def get_data(self):
return json.loads(self.form_data)
def __unicode__(self):
def __str__(self):
return self.form_data
@ -73,7 +78,7 @@ class AbstractFormField(Orderable):
# unidecode will return an ascii string while slugify wants a
# unicode string on the other hand, slugify returns a safe-string
# which will be converted to a normal str
return str(slugify(unicode(unidecode(self.label))))
return str(slugify(text_type(unidecode(self.label))))
panels = [
FieldPanel('label'),

View file

@ -258,5 +258,5 @@ class TestFormsSubmissions(TestCase):
# Check response
self.assertEqual(response.status_code, 200)
data_line = response.content.split("\n")[1]
data_line = response.content.decode().split("\n")[1]
self.assertTrue('new@example.com' in data_line)

View file

@ -1,5 +1,11 @@
import datetime
import unicodecsv
try:
import unicodecsv as csv
using_unicodecsv = True
except ImportError:
import csv
using_unicodecsv = False
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import PermissionDenied
@ -65,7 +71,11 @@ def list_submissions(request, page_id):
# return a CSV instead
response = HttpResponse(content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = 'attachment;filename=export.csv'
writer = unicodecsv.writer(response, encoding='utf-8')
if using_unicodecsv:
writer = csv.writer(response, encoding='utf-8')
else:
writer = csv.writer(response)
header_row = ['Submission date'] + [label for name, label in data_fields]

View file

@ -3,7 +3,7 @@ from django.conf import settings
from django.conf.urls import include, url
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailforms import urls

View file

@ -8,11 +8,15 @@ 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
from django.utils import six
import sys
from django.conf import settings
from base import InvalidImageBackendError
from django.utils import six
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
class InvalidImageBackendError(ImproperlyConfigured):
pass
# Pinched from django 1.7 source code.
# TODO: Replace this with "from django.utils.module_loading import import_string"

View file

@ -1,9 +1,4 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
class InvalidImageBackendError(ImproperlyConfigured):
pass
class BaseImageBackend(object):

View file

@ -1,7 +1,8 @@
import StringIO
import os.path
import re
from six import BytesIO
from taggit.managers import TaggableManager
from django.core.files import File
@ -13,6 +14,7 @@ from django.utils.safestring import mark_safe
from django.utils.html import escape, format_html_join
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from unidecode import unidecode
@ -21,6 +23,7 @@ from wagtail.wagtailimages.backends import get_image_backend
from .utils import validate_image_format
@python_2_unicode_compatible
class AbstractImage(models.Model, TagSearchable):
title = models.CharField(max_length=255, verbose_name=_('Title') )
@ -54,7 +57,7 @@ class AbstractImage(models.Model, TagSearchable):
},
}
def __unicode__(self):
def __str__(self):
return self.title
def get_rendition(self, filter):
@ -208,7 +211,7 @@ class Filter(models.Model):
image = method(image, self.method_arg)
output = StringIO.StringIO()
output = BytesIO()
backend.save_image(image, output, file_format)
# and then close the input file

View file

@ -1,4 +1,5 @@
from mock import MagicMock
from django.utils import six
from django.test import TestCase
from django import template
@ -18,11 +19,11 @@ from wagtail.wagtailimages.backends import get_image_backend
from wagtail.wagtailimages.backends.pillow import PillowBackend
def get_test_image_file():
from StringIO import StringIO
from six import BytesIO
from PIL import Image
from django.core.files.images import ImageFile
f = StringIO()
f = BytesIO()
image = Image.new('RGB', (640, 480), 'white')
image.save(f, 'PNG')
return ImageFile(f, name='test.png')
@ -453,16 +454,14 @@ class TestFormat(TestCase):
self.image,
'test alt text'
)
self.assertRegexpMatches(
result,
six.assertRegex(self, result,
'<img data-embedtype="image" data-id="0" data-format="test name" data-alt="test alt text" class="test classnames" src="[^"]+" width="1" height="1" alt="test alt text">',
)
)
def test_image_to_html_no_classnames(self):
self.format.classnames = None
result = self.format.image_to_html(self.image, 'test alt text')
self.assertRegexpMatches(
result,
six.assertRegex(self, result,
'<img src="[^"]+" width="1" height="1" alt="test alt text">'
)
self.format.classnames = 'test classnames'

View file

@ -4,7 +4,7 @@ from django.core import urlresolvers
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailimages import urls

View file

@ -1,6 +1,6 @@
from django import http
import models
from wagtail.wagtailredirects import models
# Originally pinched from: https://github.com/django/django/blob/master/django/contrib/redirects/middleware.py

View file

@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel, PageChooserPanel
from urlparse import urlparse
from six.moves.urllib.parse import urlparse
class Redirect(models.Model):

View file

@ -8,7 +8,7 @@ from django.views.decorators.vary import vary_on_headers
from wagtail.wagtailadmin.edit_handlers import ObjectList
from wagtail.wagtailadmin.forms import SearchForm
import models
from wagtail.wagtailredirects import models
REDIRECT_EDIT_HANDLER = ObjectList(models.Redirect.content_panels)

View file

@ -2,7 +2,7 @@ from django.core import urlresolvers
from django.conf.urls import include, url
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailredirects import urls
from wagtail.wagtailadmin.menu import MenuItem

View file

@ -1,3 +1,3 @@
from indexed import Indexed
from signal_handlers import register_signal_handlers
from backends import get_search_backend
from wagtail.wagtailsearch.indexed import Indexed
from wagtail.wagtailsearch.signal_handlers import register_signal_handlers
from wagtail.wagtailsearch.backends import get_search_backend

View file

@ -8,10 +8,15 @@ 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
from django.utils import six
import sys
from django.utils import six
from django.conf import settings
from base import InvalidSearchBackendError
from django.core.exceptions import ImproperlyConfigured
class InvalidSearchBackendError(ImproperlyConfigured):
pass
# Pinched from django 1.7 source code.

View file

@ -1,13 +1,8 @@
from django.db import models
from django.core.exceptions import ImproperlyConfigured
from wagtail.wagtailsearch.indexed import Indexed
class InvalidSearchBackendError(ImproperlyConfigured):
pass
class BaseSearch(object):
def __init__(self, params):
pass

View file

@ -1,7 +1,6 @@
from __future__ import absolute_import
import json
import warnings
from django.db import models
@ -338,10 +337,11 @@ class ElasticSearch(BaseSearch):
indexed_fields = model.indexed_get_indexed_fields()
# Make field list
fields = dict({
fields = {
"pk": dict(type="string", index="not_analyzed", store="yes"),
"content_type": dict(type="string"),
}.items() + indexed_fields.items())
}
fields.update(indexed_fields)
# Put mapping
self.es.indices.put_mapping(index=self.es_index, doc_type=content_type, body={

View file

@ -1,7 +1,8 @@
from django import forms
from django.forms.models import inlineformset_factory
from django.utils.translation import ugettext_lazy as _
import models
from wagtail.wagtailsearch import models
class QueryForm(forms.Form):
@ -48,7 +49,7 @@ class EditorsPickFormSet(EditorsPickFormSetBase):
# Check there is at least one non-deleted form.
non_deleted_forms = self.total_form_count()
non_empty_forms = 0
for i in xrange(0, self.total_form_count()):
for i in range(0, self.total_form_count()):
form = self.forms[i]
if self.can_delete and self._should_delete_form(form):
non_deleted_forms -= 1

View file

@ -1,3 +1,5 @@
from six import string_types
from django.db import models
@ -35,21 +37,27 @@ class Indexed(object):
def indexed_get_indexed_fields(cls):
# Get indexed fields for this class as dictionary
indexed_fields = cls.indexed_fields
if isinstance(indexed_fields, tuple):
indexed_fields = list(indexed_fields)
if isinstance(indexed_fields, basestring):
indexed_fields = [indexed_fields]
if isinstance(indexed_fields, list):
indexed_fields = dict((field, dict(type="string")) for field in indexed_fields)
if not isinstance(indexed_fields, dict):
raise ValueError()
if isinstance(indexed_fields, dict):
# Make sure we have a copy to prevent us accidentally changing the configuration
indexed_fields = indexed_fields.copy()
else:
# Convert to dict
if isinstance(indexed_fields, tuple):
indexed_fields = list(indexed_fields)
if isinstance(indexed_fields, string_types):
indexed_fields = [indexed_fields]
if isinstance(indexed_fields, list):
indexed_fields = dict((field, dict(type="string")) for field in indexed_fields)
if not isinstance(indexed_fields, dict):
raise ValueError()
# Get indexed fields for parent class
parent = cls.indexed_get_parent(require_model=False)
if parent:
# Add parent fields into this list
parent_indexed_fields = parent.indexed_get_indexed_fields()
indexed_fields = dict(parent_indexed_fields.items() + indexed_fields.items())
parent_indexed_fields = parent.indexed_get_indexed_fields().copy()
parent_indexed_fields.update(indexed_fields)
indexed_fields = parent_indexed_fields
return indexed_fields
def indexed_get_document_id(self):

View file

@ -2,11 +2,13 @@ import datetime
from django.db import models
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from wagtail.wagtailsearch.indexed import Indexed
from wagtail.wagtailsearch.utils import normalise_query_string, MAX_QUERY_STRING_LENGTH
@python_2_unicode_compatible
class Query(models.Model):
query_string = models.CharField(max_length=MAX_QUERY_STRING_LENGTH, unique=True)
@ -23,7 +25,7 @@ class Query(models.Model):
daily_hits.hits = models.F('hits') + 1
daily_hits.save()
def __unicode__(self):
def __str__(self):
return self.query_string
@property

View file

@ -17,7 +17,7 @@
{% endif %}
</li>
<li class="char_field">
{% include "wagtailadmin/edit_handlers/field_panel_field.html" with field=form.description only %}
{% include "wagtailadmin/shared/field.html" with field=form.description only %}
</li>
</ul>

View file

@ -2,15 +2,16 @@
{% trans "Popular search terms" as pop_str %}
{% include "wagtailadmin/shared/header.html" with title=pop_str %}
<form class="query-search search-bar full-width" action="{% url 'wagtailsearch_queries_chooserresults' %}" method="GET" autocomplete="off">
<ul class="fields">
{% for field in searchform %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
<li class="submit"><input type="submit" value="{% trans 'Search' %}" /></li>
</ul>
</form>
<div class="nice-padding">
<form class="query-search search-bar full-width" action="{% url 'wagtailsearch_queries_chooserresults' %}" method="GET" autocomplete="off">
<ul class="fields">
{% for field in searchform %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% endfor %}
<li class="submit"><input type="submit" value="{% trans 'Search' %}" /></li>
</ul>
</form>
<div id="query-results">
{% include "wagtailsearch/queries/chooser/results.html" %}
</div>

View file

@ -1,8 +1,7 @@
{% load i18n %}
<table class="listing chooser">
<col />
<col width="40%"/>
<col />
<col width="50%"/>
<col width="50%"/>
<thead>
<tr>
<th class="title">{% trans "Terms" %}</th>
@ -20,7 +19,7 @@
</tr>
{% endfor %}
{% else %}
<tr><td colspan="3" class="no-results-message"><p>{% trans "No results found" %}</p></td></tr>
<tr><td colspan="100%" class="no-results-message"><p>{% trans "No results found" %}</p></td></tr>
{% endif %}
</tbody>
</table>

View file

@ -1,4 +1,4 @@
{% extends "wagtailadmin/edit_handlers/field_panel_field.html" %}
{% extends "wagtailadmin/shared/field.html" %}
{% block form_field %}
<div class="chooser searchterm-chooser">

View file

@ -1,12 +1,14 @@
from six import StringIO
from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
from django.core import management
from wagtail.tests.utils import unittest
from wagtail.wagtailsearch import models, get_search_backend
from wagtail.wagtailsearch.backends.db import DBSearch
from wagtail.wagtailsearch.backends import InvalidSearchBackendError
from StringIO import StringIO
# Register wagtailsearch signal handlers
@ -19,7 +21,7 @@ class BackendTests(object):
def setUp(self):
# Search WAGTAILSEARCH_BACKENDS for an entry that uses the given backend path
for (backend_name, backend_conf) in settings.WAGTAILSEARCH_BACKENDS.iteritems():
for backend_name, backend_conf in settings.WAGTAILSEARCH_BACKENDS.items():
if backend_conf['BACKEND'] == self.backend_path:
self.backend = get_search_backend(backend_name)
break

View file

@ -1,7 +1,7 @@
from django.test import TestCase
from django.core.urlresolvers import reverse
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailsearch import models

View file

@ -1,10 +1,10 @@
from StringIO import StringIO
from six import StringIO
from django.test import TestCase
from django.core import management
from wagtail.wagtailsearch import models
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailsearch.utils import normalise_query_string

View file

@ -1 +1 @@
from frontend import search
from wagtail.wagtailsearch.views.frontend import search

View file

@ -2,7 +2,7 @@ from django.core import urlresolvers
from django.conf.urls import include, url
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailsearch.urls import admin as admin_urls
from wagtail.wagtailadmin.menu import MenuItem

View file

@ -1,8 +1,7 @@
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.tests.utils import WagtailTestUtils
from wagtail.tests.models import Advert, AlphaSnippet, ZuluSnippet
from wagtail.wagtailsnippets.models import register_snippet, SNIPPET_MODELS

View file

@ -1,5 +1,7 @@
import json
from six import text_type
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import permission_required
@ -35,7 +37,7 @@ def chosen(request, content_type_app_name, content_type_model_name, id):
snippet_json = json.dumps({
'id': item.id,
'string': unicode(item),
'string': text_type(item),
})
return render_modal_workflow(

View file

@ -4,7 +4,7 @@ from django.core import urlresolvers
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailsnippets import urls

View file

@ -1,8 +1,10 @@
from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
@ -25,5 +27,5 @@ class UserProfile(models.Model):
def get_for_user(cls, user):
return cls.objects.get_or_create(user=user)[0]
def __unicode__(self):
def __str__(self):
return self.user.username

View file

@ -35,7 +35,7 @@
<td class="username">{{ user.username }}</td>
<td class="level">{% if user.is_superuser %}{% trans "Admin" %}{% endif %}</td>
<td class="status"><div class="status-tag {% if user.is_active %}primary{% endif %}">{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</div></td>
</li>
</tr>
{% endfor %}
</tbody>
</table>
</table>

View file

@ -2,7 +2,7 @@ from django.conf.urls import include, url
from django.core import urlresolvers
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailusers import urls