Merge branch 'master' into feature/styleguide

This commit is contained in:
Dave Cranwell 2014-06-17 09:36:21 +01:00
commit b638cc01e9
62 changed files with 4525 additions and 3040 deletions

View file

@ -12,7 +12,7 @@ services:
# Package installation
install:
- python setup.py install
- pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand
- pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand embedly
- pip install coveralls
# Pre-test configuration
before_script:

View file

@ -2,7 +2,15 @@ Changelog
=========
0.4 (xx.xx.20xx)
~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~
* Added 'original' as a resizing rule supported by the 'image' tag
* Hallo.js updated to version 1.0.4
* Snippets are now ordered alphabetically
* Removed the "More" section from the admin menu
* Added pagination to page listings in admin
* Fix: Animated GIFs are now coalesced before resizing
* Fix: Wand backend clones images before modifying them
* Fix: Admin breadcrumb now positioned correctly on mobile
0.3.1 (03.06.2014)
~~~~~~~~~~~~~~~~~~

View file

@ -94,7 +94,7 @@ The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, set
The syntax for the tag is thus::
{% image [image] [method]-[dimension(s)] %}
{% image [image] [resize-rule] %}
For example:
@ -108,16 +108,20 @@ For example:
<!-- or a square thumbnail: -->
{% image self.photo fill-80x80 %}
In the above syntax ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[method]`` defines which resizing algorithm to use and ``[dimension(s)]`` provides height and/or width values (as ``[width|height]`` or ``[width]x[height]``) to refine that algorithm.
In the above syntax ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[resize-rule]`` defines how the image is to be resized when inserted into the page; various resizing methods are supported, to cater for different usage cases (e.g. lead images that span the whole width of the page, or thumbnails to be cropped to a fixed size).
Note that a space separates ``[image]`` and ``[method]``, but not ``[method]`` and ``[dimensions]``: a hyphen between ``[method]`` and ``[dimensions]`` is mandatory. Multiple dimensions must be separated by an ``x``.
Note that a space separates ``[image]`` and ``[resize-rule]``, but the resize rule must not contain spaces.
The available ``method`` s are:
The available resizing methods are:
.. glossary::
``max``
(takes two dimensions)
.. code-block:: django
{% image self.photo max-1000x500 %}
Fit **within** the given dimensions.
The longest edge will be reduced to the equivalent dimension size defined. e.g A portrait image of width 1000, height 2000, treated with the ``max`` dimensions ``1000x500`` (landscape) would result in the image shrunk so the *height* was 500 pixels and the width 250.
@ -125,6 +129,10 @@ The available ``method`` s are:
``min``
(takes two dimensions)
.. code-block:: django
{% image self.photo min-500x200 %}
**Cover** the given dimensions.
This may result in an image slightly **larger** than the dimensions you specify. e.g A square image of width 2000, height 2000, treated with the ``min`` dimensions ``500x200`` (landscape) would have it's height and width changed to 500, i.e matching the width required, but greater than the height.
@ -132,27 +140,45 @@ The available ``method`` s are:
``width``
(takes one dimension)
.. code-block:: django
{% image self.photo width-640 %}
Reduces the width of the image to the dimension specified.
``height``
(takes one dimension)
.. code-block:: django
{% image self.photo height-480 %}
Resize the height of the image to the dimension specified..
``fill``
(takes two dimensions)
.. code-block:: django
{% image self.photo fill-200x200 %}
Resize and **crop** to fill the **exact** dimensions.
This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have its height reduced to 200, then its width (ordinarily 400) cropped to 200.
**The crop always aligns on the centre of the image.**
.. Note::
Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions.
``original``
(takes no dimensions)
.. code-block:: django
{% image self.photo original %}
Leaves the image at its original size - no resizing is performed.
.. Note::
Wagtail does not make the "original" version of an image explicitly available. To request it, you could rely on the lack of upscaling by requesting an image larger than its maximum dimensions. e.g to insert an image whose dimensions are unknown at its maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide.
Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions.
.. _image_tag_alt:
@ -190,6 +216,32 @@ Only fields using ``RichTextField`` need this applied in the template.
.. Note::
Note that the template tag loaded differs from the name of the filter.
Responsive Embeds
-----------------
Wagtail embeds and images are included at their full width, which may overflow the bounds of the content container you've defined in your templates. To make images and embeds responsive -- meaning they'll resize to fit their container -- include the following CSS.
.. code-block:: css
.rich-text img {
max-width: 100%;
height: auto;
}
.responsive-object {
position: relative;
}
.responsive-object iframe,
.responsive-object object,
.responsive-object embed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
Internal links (tag)
~~~~~~~~~~~~~~~~~~~~

View file

@ -211,7 +211,7 @@ You can explicitly link ``Page``-derived models together using the ``Page`` mode
Snippets
--------
Snippets are not subclasses, so you must include the model class directly. A chooser is provided which takes the field name snippet class.
Snippets are vanilla Django models you create yourself without a Wagtail-provided base class. So using them as a field in a page requires specifying your own ``appname.modelname``. A chooser, ``SnippetChooserPanel``, is provided which takes the field name and snippet class.
.. code-block:: python
@ -248,6 +248,12 @@ Full-Width Input
Use ``classname="full"`` to make a field (input element) stretch the full width of the Wagtail page editor. This will not work if the field is encapsulated in a ``MultiFieldPanel``, which places its child fields into a formset.
Titles
------
Use ``classname="title"`` to make Page's built-in title field stand out with more vertical padding.
Required Fields
---------------
@ -264,19 +270,11 @@ Without a panel definition, a default form field (without label) will be used to
.. _Django model field reference (editable): https://docs.djangoproject.com/en/dev/ref/models/fields/#editable
MultiFieldPanel
~~~~~~~~~~~~~~~
The ``MultiFieldPanel`` groups a list of child fields into a fieldset, which can also be collapsed into a heading bar to save space.
.. code-block:: python
BOOK_FIELD_COLLECTION = [
@ -294,8 +292,7 @@ MultiFieldPanel
# ...
]
By default, ``MultiFieldPanel`` s are expanded and not collapsible. Adding the classname ``collapsible`` will enable the collapse control. Adding both ``collapsible`` and ``collapsed`` to the classname parameter will load the editor page with the ``MultiFieldPanel`` collapsed under its heading.
.. _inline_panels:
@ -303,7 +300,55 @@ MultiFieldPanel
Inline Panels and Model Clusters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``django-modelcluster`` module allows for streamlined relation of extra models to a Wagtail page.
The ``django-modelcluster`` module allows for streamlined relation of extra models to a Wagtail page. For instance, you can create objects related through a ``ForeignKey`` relationship on the fly and save them to a draft revision of a ``Page`` object. Normally, your related objects "cluster" would need to be created beforehand (or asynchronously) before linking them to a Page.
Let's look at the example of adding related links to a ``Page``-derived model. We want to be able to add as many as we like, assign an order, and do all of this without leaving the page editing screen.
.. code-block:: python
from wagtail.wagtailcore.models import Orderable, Page
from modelcluster.fields import ParentalKey
# The abstract model for related links, complete with panels
class RelatedLink(models.Model):
title = models.CharField(max_length=255)
link_external = models.URLField("External link", blank=True)
panels = [
FieldPanel('title'),
FieldPanel('link_external'),
]
class Meta:
abstract = True
# The real model which combines the abstract model, an
# Orderable helper class, and what amounts to a ForeignKey link
# to the model we want to add related links to (BookPage)
class BookPageRelatedLinks(Orderable, RelatedLink):
page = ParentalKey('demo.BookPage', related_name='related_links')
class BookPage( Page ):
# ...
BookPage.content_panels = [
# ...
InlinePanel( BookPage, 'related_links', label="Related Links" ),
]
The ``RelatedLink`` class is a vanilla Django abstract model. The ``BookPageRelatedLinks`` model extends it with capability for being ordered in the Wagtail interface via the ``Orderable`` class as well as adding a ``page`` property which links the model to the ``BookPage`` model we're adding the related links objects to. Finally, in the panel definitions for ``BookPage``, we'll add an ``InlinePanel`` to provide an interface for it all. Let's look again at the parameters that ``InlinePanel`` accepts:
.. code-block:: python
InlinePanel( base_model, relation_name, panels=None, label='', help_text='' )
``base_model`` is the model you're extending with the cluster. The ``relation_name`` is the ``related_name`` label given to the cluster's ``ParentalKey`` relation. You can add the ``panels`` manually or make them part of the cluster model. Finally, ``label`` and ``help_text`` provide a heading and caption, respectively, for the Wagtail editor.
For another example of using model clusters, see :ref:`tagging`
For more on ``django-modelcluster``, visit `the django-modelcluster github project page`_ ).
.. _the django-modelcluster github page: https://github.com/torchbox/django-modelcluster
.. _extending_wysiwyg:
@ -311,12 +356,237 @@ The ``django-modelcluster`` module allows for streamlined relation of extra mode
Extending the WYSIWYG Editor (hallo.js)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adding hallo.js plugins:
https://github.com/torchbox/wagtail/commit/1ecc215759142e6cafdacb185bbfd3f8e9cd3185
To inject javascript into the Wagtail page editor, see the :ref:`insert_editor_js` hook. Once you have the hook in place and your hallo.js plugin loads into the Wagtail page editor, use the following Javascript to register the plugin with hallo.js.
.. code-block:: javascript
registerHalloPlugin(name, opts);
hallo.js plugin names are prefixed with the ``"IKS."`` namespace, but the ``name`` you pass into ``registerHalloPlugin()`` should be without the prefix. ``opts`` is an object passed into the plugin.
For information on developing custom hallo.js plugins, see the project's page: https://github.com/bergie/hallo
Edit Handler API
~~~~~~~~~~~~~~~~
Admin Hooks
-----------
On loading, Wagtail will search for any app with the file ``wagtail_hooks.py`` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a ``Page`` object is saved or when the main menu is constructed.
Registering functions with a Wagtail hook follows the following pattern:
.. code-block:: python
from wagtail.wagtailadmin import hooks
hooks.register('hook', function)
Where ``'hook'`` is one of the following hook strings and ``function`` is a function you've defined to handle the hook.
.. _construct_wagtail_edit_bird:
``construct_wagtail_edit_bird``
Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the ``request`` object and a list of menu objects, ``items``. The menu item objects must have a ``render`` method which can take a ``request`` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information.
.. code-block:: python
from wagtail.wagtailadmin import hooks
class UserbarPuppyLinkItem(object):
def render(self, request):
return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
+ 'target="_parent" class="action icon icon-wagtail">Puppies!</a></li>'
def add_puppy_link_item(request, items):
return items.append( UserbarPuppyLinkItem() )
hooks.register('construct_wagtail_edit_bird', add_puppy_link_item)
.. _construct_homepage_panels:
``construct_homepage_panels``
Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a ``request`` object and a list of ``panels``, objects which have a ``render()`` method returning a string. The objects also have an ``order`` property, an integer used for ordering the panels. The default panels use integers between ``100`` and ``300``.
.. code-block:: python
from django.utils.safestring import mark_safe
from wagtail.wagtailadmin import hooks
class WelcomePanel(object):
order = 50
def render(self):
return mark_safe("""
<section class="panel summary nice-padding">
<h3>No, but seriously -- welcome to the admin homepage.</h3>
</section>
""")
def add_another_welcome_panel(request, panels):
return panels.append( WelcomePanel() )
hooks.register('construct_homepage_panels', add_another_welcome_panel)
.. _after_create_page:
``after_create_page``
Do something with a ``Page`` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a ``request`` object and a ``page`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page's parent.
.. code-block:: python
from django.http import HttpResponse
from wagtail.wagtailadmin import hooks
def do_after_page_create(request, page):
return HttpResponse("Congrats on making content!", content_type="text/plain")
hooks.register('after_create_page', do_after_page_create)
.. _after_edit_page:
``after_edit_page``
Do something with a ``Page`` object after it has been updated. Uses the same behavior as ``after_create_page``.
.. _after_delete_page:
``after_delete_page``
Do something after a ``Page`` object is deleted. Uses the same behavior as ``after_create_page``.
.. _register_admin_urls:
``register_admin_urls``
Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see `url dispatcher`_.
.. _url dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/
.. code-block:: python
from django.http import HttpResponse
from django.conf.urls import url
from wagtail.wagtailadmin import hooks
def admin_view( request ):
return HttpResponse( \
"I have approximate knowledge of many things!", \
content_type="text/plain")
def urlconf_time():
return [
url(r'^how_did_you_almost_know_my_name/$', admin_view, name='frank' ),
]
hooks.register('register_admin_urls', urlconf_time)
.. _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.
.. code-block:: python
from django.core.urlresolvers import reverse
from wagtail.wagtailadmin import hooks
from wagtail.wagtailadmin.menu import MenuItem
def construct_main_menu(request, menu_items):
menu_items.append(
MenuItem( 'Frank', reverse('frank'), classnames='icon icon-folder-inverse', order=10000)
)
hooks.register('construct_main_menu', construct_main_menu)
.. _insert_editor_js:
``insert_editor_js``
Add additional Javascript files or code snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string.
.. code-block:: python
from django.utils.html import format_html, format_html_join
from django.conf import settings
from wagtail.wagtailadmin import hooks
def editor_js():
js_files = [
'demo/js/hallo-plugins/hallo-demo-plugin.js',
]
js_includes = format_html_join('\n', '<script src="{0}{1}"></script>',
((settings.STATIC_URL, filename) for filename in js_files)
)
return js_includes + format_html(
"""
<script>
registerHalloPlugin('demoeditor');
</script>
"""
)
hooks.register('insert_editor_js', editor_js)
.. _insert_editor_css:
``insert_editor_css``
Add additional CSS or SCSS files or snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string.
.. code-block:: python
from django.utils.html import format_html
from django.conf import settings
from wagtail.wagtailadmin import hooks
def editor_css():
return format_html('<link rel="stylesheet" href="' \
+ settings.STATIC_URL \
+ 'demo/css/vendor/font-awesome/css/font-awesome.min.css">')
hooks.register('insert_editor_css', editor_css)
Image Formats in the Rich Text Editor
-------------------------------------
On loading, Wagtail will search for any app with the file ``image_formats.py`` and execute the contents. This provides a way to customize the formatting options shown to the editor when inserting images in the ``RichTextField`` editor.
As an example, add a "thumbnail" format:
.. code-block:: python
# image_formats.py
from wagtail.wagtailimages.formats import Format, register_image_format
register_image_format(Format('thumbnail', 'Thumbnail', 'richtext-image thumbnail', 'max-120x120'))
To begin, import the the ``Format`` class, ``register_image_format`` function, and optionally ``unregister_image_format`` function. To register a new ``Format``, call the ``register_image_format`` with the ``Format`` object as the argument. The ``Format`` takes the following init arguments:
``name``
The unique key used to identify the format. To unregister this format, call ``unregister_image_format`` with this string as the only argument.
``label``
The label used in the chooser form when inserting the image into the ``RichTextField``.
``classnames``
The string to assign to the ``class`` attribute of the generated ``<img>`` tag.
``filter_spec``
The string specification to create the image rendition. For more, see the :ref:`image_tag`.
To unregister, call ``unregister_image_format`` with the string of the ``name`` of the ``Format`` as the only argument.
Content Index Pages (CRUD)
--------------------------
Custom Choosers
---------------
Tests
-----

View file

@ -65,7 +65,7 @@ Wagtail instance available as the basis for your new site:
- Install `Vagrant <http://www.vagrantup.com/>`_ 1.1+
- Clone the demonstration site, create the Vagrant box and initialise Wagtail::
git clone git@github.com:torchbox/wagtaildemo.git
git clone https://github.com/torchbox/wagtaildemo.git
cd wagtaildemo
vagrant up
vagrant ssh

View file

@ -118,6 +118,8 @@ Will return::
tauntaun kennel bed and breakfast
.. _tagging:
Tagging
-------

View file

@ -224,10 +224,13 @@ Prerequisites are the Elasticsearch service itself and, via pip, the `elasticuti
.. code-block:: guess
pip install elasticutils pyelasticsearch
pip install elasticutils==0.8.2 pyelasticsearch
.. note::
The dependency on pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_.
ElasticUtils 0.9+ is not supported.
.. note::
The dependency on elasticutils and pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_.
The backend is configured in settings:

View file

@ -1,3 +1,4 @@
# For coverage and PEP8 linting
coverage==3.7.1
flake8==2.1.0
mock==1.0.1

View file

@ -102,7 +102,9 @@ if not settings.configured:
),
COMPRESS_ENABLED=False, # disable compression so that we can run tests on the content of the compress tag
WAGTAILSEARCH_BACKENDS=WAGTAILSEARCH_BACKENDS,
WAGTAIL_SITE_NAME='Test Site'
WAGTAIL_SITE_NAME='Test Site',
LOGIN_REDIRECT_URL='wagtailadmin_home',
LOGIN_URL='wagtailadmin_login',
)

View file

@ -255,18 +255,42 @@ FormPage.content_panels = [
]
# Snippets
# Snippets
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
def __unicode__(self):
return self.text
def __unicode__(self):
return self.text
register_snippet(Advert)
# AlphaSnippet and ZuluSnippet are for testing ordering of
# snippets when registering. They are named as such to ensure
# thier ordering is clear. They are registered during testing
# to ensure specific [in]correct register ordering
# AlphaSnippet is registered during TestSnippetOrdering
class AlphaSnippet(models.Model):
text = models.CharField(max_length=255)
def __unicode__(self):
return self.text
# ZuluSnippet is registered during TestSnippetOrdering
class ZuluSnippet(models.Model):
text = models.CharField(max_length=255)
def __unicode__(self):
return self.text

View file

@ -1,4 +1,7 @@
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
@ -12,11 +15,30 @@ except ImportError:
import unittest
def login(client):
# Create a user
user = User.objects.create_superuser(username='test', email='test@email.com', password='password')
class WagtailTestUtils(object):
def login(self):
# Create a user
user = User.objects.create_superuser(username='test', email='test@email.com', password='password')
# Login
client.login(username='test', password='password')
# Login
self.client.login(username='test', password='password')
return user
return user
# From: https://github.com/django/django/blob/255449c1ee61c14778658caae8c430fa4d76afd6/django/contrib/auth/tests/test_views.py#L70-L85
def assertURLEqual(self, url, expected, parse_qs=False):
"""
Given two URLs, make sure all their components (the ones given by
urlparse) are equal, only comparing components that are present in both
URLs.
If `parse_qs` is True, then the querystrings are parsed with QueryDict.
This is useful if you don't want the order of parameters to matter.
Otherwise, the query strings are compared as-is.
"""
fields = ParseResult._fields
for attr, x, y in zip(fields, urlparse(url), urlparse(expected)):
if parse_qs and attr == 'query':
x, y = QueryDict(x), QueryDict(y)
if x and y and x != y:
self.fail("%r != %r (%s doesn't match)" % (url, expected, attr))

View file

@ -16,9 +16,12 @@ function ModalWorkflow(opts) {
/* remove any previous modals before continuing (closing doesn't remove them from the dom) */
$('body > .modal').remove();
// set default contents of container
var container = $('<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">\n <div class="modal-dialog">\n <div class="modal-content">\n <button type="button" class="close icon text-replace icon-cross" data-dismiss="modal" aria-hidden="true">&times;</button>\n <div class="modal-body"></div>\n </div><!-- /.modal-content -->\n </div><!-- /.modal-dialog -->\n</div>');
// add container to body and hide it, so content can be added to it before display
$('body').append(container);
container.modal();
container.modal('hide');
self.body = container.find('.modal-body');
@ -49,15 +52,19 @@ function ModalWorkflow(opts) {
self.loadResponseText = function(responseText) {
var response = eval('(' + responseText + ')');
self.loadBody(response);
};
self.loadBody = function(body) {
if (body.html) {
self.body.html(body.html);
self.loadBody = function(response) {
if (response.html) {
// if the response is html
self.body.html(response.html);
container.modal('show');
}
if (body.onload) {
body.onload(self);
if (response.onload) {
// if the response is a function
response.onload(self);
}
};

View file

@ -0,0 +1,65 @@
/* ========================================================================
* Bootstrap: transition.js v3.1.1
* http://getbootstrap.com/javascript/#transitions
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function () { 'use strict';
(function (o_o) {
typeof define == 'function' && define.amd ? define(['jquery'], o_o) :
typeof exports == 'object' ? o_o(require('jquery')) : o_o(jQuery)
})(function ($) {
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
// ============================================================
function transitionEnd() {
var el = document.createElement('bootstrap')
var transEndEventNames = {
WebkitTransition : 'webkitTransitionEnd',
MozTransition : 'transitionend',
OTransition : 'oTransitionEnd otransitionend',
transition : 'transitionend'
}
for (var name in transEndEventNames) {
if (el.style[name] !== undefined) {
return { end: transEndEventNames[name] }
}
}
return false // explicit for ie8 ( ._.)
}
// http://blog.alexmaccaw.com/css-transitions
$.fn.emulateTransitionEnd = function (duration) {
var called = false
var $el = this
$(this).one('bsTransitionEnd', function () { called = true })
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
setTimeout(callback, duration)
return this
}
$(function () {
$.support.transition = transitionEnd()
if (!$.support.transition) return
$.event.special.bsTransitionEnd = {
bindType: $.support.transition.end,
delegateType: $.support.transition.end,
handle: function (e) {
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
}
}
})
})
}();

File diff suppressed because it is too large Load diff

View file

@ -650,6 +650,13 @@ ul.tagit li.tagit-choice-editable{
}
}
/* search-bars */
.search-bar{
.required label:after{
display:none;
}
}
/* Transitions */
fieldset, input, textarea, select{
@include transition(background-color 0.2s ease);

View file

@ -66,7 +66,7 @@ header{
/* necessary on mobile only to make way for hamburger menu */
&.nice-padding{
padding-left:4em;
padding-left:$desktop-nice-padding;
}
label{

View file

@ -1,5 +1,15 @@
$zindex-modal-background: 500;
.fade {
opacity: 0;
@include transition(opacity .15s linear);
&.in {
opacity: 1;
}
}
// Kill the scroll on the body
.modal-open {
overflow: hidden;
@ -70,7 +80,7 @@ $zindex-modal-background: 500;
background-color: black;
// Fade for backdrop
&.fade { opacity:0; }
&.in { opacity:0.7; }
&.in { opacity:0.5; }
}
.modal .close{

View file

@ -431,13 +431,13 @@ footer{
.breadcrumb{
@include unlist();
}
.breadcrumb{
@include clearfix();
overflow:hidden;
background:$color-teal-darker;
padding-top:1.4em;
font-size:0.85em;
li {
display: block;
float: left;
@ -469,7 +469,7 @@ footer{
content:"n";
padding-left:20px;
font-size:2em;
color:$color-teal;
color:$color-teal-darker;
line-height:0.9em;
}
}
@ -740,6 +740,19 @@ footer, .logo{
margin-left:50px;
}
.breadcrumb{
padding-top:0;
background:$color-teal-darker;
li {
a, span{
&:after{
color:$color-teal;
}
}
}
}
/* Z-indexes */
.nav-main{
li{

View file

@ -25,6 +25,7 @@ body{
.wrapper{
padding-left:$mobile-nice-padding;
padding-right:$mobile-nice-padding;
margin-left:0;
max-width:none;
}

View file

@ -6,11 +6,8 @@
margin-bottom:10em;
}
.page-editor header {
}
.page-editor .breadcrumb{
margin-top:-1.8em;
margin-top:-1.2em;
margin-bottom:2em;
}
.page-editor .modal .breadcrumb{
@ -336,6 +333,9 @@ footer .preview{
}
@media screen and (min-width: $breakpoint-mobile){
.page-editor .breadcrumb{
margin-top:-1.8em;
}
.object{
fieldset{
max-width:1024px;

View file

@ -13,6 +13,6 @@
{% block furniture %}
<div class="content-wrapper">
<h1>{% trans "Password change successful" %}</h1>
<p><a href="{% url 'django.contrib.auth.views.login' %}" class="button button-primary">{% trans "Login" %}</a></p>
<p><a href="{% url 'wagtailadmin_login' %}" class="button button-primary">{% trans "Login" %}</a></p>
</div>
{% endblock %}

View file

@ -18,6 +18,7 @@
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/jquery-ui-1.10.3.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/jquery.timepicker.min.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/jquery.autosize.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/bootstrap-transition.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/bootstrap-modal.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/bootstrap-tab.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/jquery.dlmenu.js"></script>

View file

@ -20,12 +20,12 @@
<td class="title" valign="top">
<h2><a href="{% url 'wagtailadmin_pages_edit' revision.page.id %}" title="{% trans 'Edit this page' %}">{{ revision.page.title }}</a></h2>
<ul class="actions">
<li><a href="{% url 'wagtailadmin_pages_edit' revision.page.id %}" class="button button-small" target="_blank">{% trans "Edit" %}</a></li>
<li><a href="{% url 'wagtailadmin_pages_edit' revision.page.id %}" class="button button-small">{% trans "Edit" %}</a></li>
{% if revision.page.has_unpublished_changes %}
<li><a href="{% url 'wagtailadmin_pages_view_draft' revision.page.id %}" class="button button-small" target="_blank">{% trans 'Draft' %}</a></li>
<li><a href="{% url 'wagtailadmin_pages_view_draft' revision.page.id %}" class="button button-small">{% trans 'Draft' %}</a></li>
{% endif %}
{% if revision.page.live %}
<li><a href="{{ revision.page.url }}" class="button button-small" target="_blank">{% trans 'Live' %}</a></li>
<li><a href="{{ revision.page.url }}" class="button button-small">{% trans 'Live' %}</a></li>
{% endif %}
</ul>
</td>

View file

@ -20,7 +20,7 @@
</div>
{% endif %}
<form action="{% url 'django.contrib.auth.views.login' %}" method="post" autocomplete="off">
<form action="{% url 'wagtailadmin_login' %}" method="post" autocomplete="off">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
<h1>{% trans "Sign in to Wagtail" %}</h1>

View file

@ -40,7 +40,7 @@
<li class="actions preview">
{% trans 'Preview' as preview_label %}
{% if display_modes|length > 1 %}
<div class="dropdown dropup button match-width">
<div class="dropdown dropup dropdown-button match-width">
{% include "wagtailadmin/pages/_preview_button_on_create.html" with label=preview_label icon=1 %}
<div class="dropdown-toggle icon icon-arrow-up"></div>
<ul role="menu">

View file

@ -1,5 +1,5 @@
{% load wagtailadmin_tags %}
<li class="{{ field.css_classes }} {{ field|fieldtype }} {{ li_classes }} {% if field.errors %}error{% endif %}">
<li class="{% if field.field.required %}required{% endif %} {{ field.css_classes }} {{ field|fieldtype }} {{ li_classes }} {% if field.errors %}error{% endif %}">
<div class="field">
{{ field.label_tag }}
<div class="field-content">

View file

@ -1,17 +1,17 @@
from django.test import TestCase
from wagtail.tests.utils import login, unittest
from wagtail.tests.utils import unittest, WagtailTestUtils
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import mail
class TestAuthentication(TestCase):
class TestAuthentication(TestCase, WagtailTestUtils):
"""
This tests that users can login and logout of the admin interface
"""
def setUp(self):
login(self.client)
self.login()
def test_login_view(self):
"""
@ -44,12 +44,12 @@ class TestAuthentication(TestCase):
# Check that the user was redirected to the dashboard
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_home'))
# Check that the user was logged in
self.assertTrue('_auth_user_id' in self.client.session)
self.assertEqual(self.client.session['_auth_user_id'], User.objects.get(username='test').id)
@unittest.expectedFailure # See: https://github.com/torchbox/wagtail/issues/25
def test_already_logged_in_redirect(self):
"""
This tests that a user who is already logged in is automatically
@ -61,27 +61,63 @@ class TestAuthentication(TestCase):
# Check that the user was redirected to the dashboard
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_home'))
def test_logout(self):
"""
This tests that the user can logout
"""
# Get logout page page
# Get logout page
response = self.client.get(reverse('wagtailadmin_logout'))
# Check that the user was redirected to the login page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_login'))
# Check that the user was logged out
self.assertFalse('_auth_user_id' in self.client.session)
def test_not_logged_in_redirect(self):
"""
This tests that a not logged in user is redirected to the
login page
"""
# Logout
self.client.logout()
class TestAccountSection(TestCase):
# Get dashboard
response = self.client.get(reverse('wagtailadmin_home'))
# Check that the user was redirected to the login page and that next was set correctly
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_login') + '?next=' + reverse('wagtailadmin_home'))
def test_not_logged_in_redirect_default_settings(self):
"""
This does the same as the above test but checks that it
redirects to the correct place when the user has not set
the LOGIN_URL setting correctly
"""
# Logout
self.client.logout()
# Get dashboard with default LOGIN_URL setting
with self.settings(LOGIN_URL='django.contrib.auth.views.login'):
response = self.client.get(reverse('wagtailadmin_home'))
# Check that the user was redirected to the login page and that next was set correctly
# Note: The user will be redirected to 'django.contrib.auth.views.login' but
# this must be the same URL as 'wagtailadmin_login'
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_login') + '?next=' + reverse('wagtailadmin_home'))
class TestAccountSection(TestCase, WagtailTestUtils):
"""
This tests that the accounts section is working
"""
def setUp(self):
login(self.client)
self.login()
def test_account_view(self):
"""
@ -117,8 +153,9 @@ class TestAccountSection(TestCase):
}
response = self.client.post(reverse('wagtailadmin_account_change_password'), post_data)
# Check that the user was redirected
# Check that the user was redirected to the account page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_account'))
# Check that the password was changed
self.assertTrue(User.objects.get(username='test').check_password('newpassword'))
@ -146,7 +183,7 @@ class TestAccountSection(TestCase):
self.assertTrue(User.objects.get(username='test').check_password('password'))
class TestPasswordReset(TestCase):
class TestPasswordReset(TestCase, WagtailTestUtils):
"""
This tests that the password reset is working
"""
@ -176,8 +213,9 @@ class TestPasswordReset(TestCase):
}
response = self.client.post(reverse('password_reset'), post_data)
# Check that the user was redirected
# Check that the user was redirected to the done page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('password_reset_done'))
# Check that a password reset email was sent to the user
self.assertEqual(len(mail.outbox), 1)
@ -267,8 +305,9 @@ class TestPasswordReset(TestCase):
}
response = self.client.post(reverse('password_reset_confirm', kwargs=self.url_kwargs), post_data)
# Check that the user was redirected
# Check that the user was redirected to the complete page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('password_reset_complete'))
# Check that the password was changed
self.assertTrue(User.objects.get(username='test').check_password('newpassword'))

View file

@ -0,0 +1,130 @@
from django.test import TestCase
from django.core.urlresolvers import reverse
from wagtail.wagtailcore.models import Page
from wagtail.tests.models import SimplePage
from wagtail.tests.utils import WagtailTestUtils
class TestChooserBrowse(TestCase, WagtailTestUtils):
def setUp(self):
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "foobarbaz"
self.child_page.slug = "foobarbaz"
self.root_page.add_child(instance=self.child_page)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailadmin_choose_page'), params)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
def test_search(self):
response = self.get({'q': "foobarbaz"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "There is one match")
self.assertContains(response, "foobarbaz")
def test_search_no_results(self):
response = self.get({'q': "quux"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "There are 0 matches")
def test_get_invalid(self):
response = self.get({'page_type': 'foo.bar'})
self.assertEqual(response.status_code, 404)
class TestChooserBrowseChild(TestCase, WagtailTestUtils):
def setUp(self):
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "foobarbaz"
self.child_page.slug = "foobarbaz"
self.root_page.add_child(instance=self.child_page)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailadmin_choose_page_child',
args=(self.root_page.id,)), params)
def get_invalid(self, params={}):
return self.client.get(reverse('wagtailadmin_choose_page_child',
args=(9999999,)), params)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
def test_search(self):
response = self.get({'q': "foobarbaz"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "There is one match")
self.assertContains(response, "foobarbaz")
def test_search_no_results(self):
response = self.get({'q': "quux"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "There are 0 matches")
def test_get_invalid(self):
self.assertEqual(self.get_invalid().status_code, 404)
class TestChooserExternalLink(TestCase, WagtailTestUtils):
def setUp(self):
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailadmin_choose_page_external_link'), params)
def post(self, post_data={}):
return self.client.post(reverse('wagtailadmin_choose_page_external_link'), post_data)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/chooser/external_link.html')
def test_get_with_param(self):
self.assertEqual(self.get({'prompt_for_link_text': 'foo'}).status_code, 200)
def test_create_link(self):
request = self.post({'url': 'http://www.example.com'})
self.assertContains(request, "'url': 'http://www.example.com/',")
self.assertContains(request, "'title': 'http://www.example.com/'")
class TestChooserEmailLink(TestCase, WagtailTestUtils):
def setUp(self):
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailadmin_choose_page_email_link'), params)
def post(self, post_data={}):
return self.client.post(reverse('wagtailadmin_choose_page_email_link'), post_data)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/chooser/email_link.html')
def test_get_with_param(self):
self.assertEqual(self.get({'prompt_for_link_text': 'foo'}).status_code, 200)
def test_create_link(self):
request = self.post({'email_address': 'example@example.com'})
self.assertContains(request, "'url': 'mailto:example@example.com',")
self.assertContains(request, "'title': 'example@example.com'")

View file

@ -1,13 +1,13 @@
from django.test import TestCase
from wagtail.tests.models import SimplePage, EventPage
from wagtail.tests.utils import login, unittest
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailcore.models import Page, PageRevision
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User, Permission
from django.core import mail
class TestPageExplorer(TestCase):
class TestPageExplorer(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
@ -19,7 +19,7 @@ class TestPageExplorer(TestCase):
self.root_page.add_child(instance=self.child_page)
# Login
login(self.client)
self.login()
def test_explore(self):
response = self.client.get(reverse('wagtailadmin_explore', args=(self.root_page.id, )))
@ -28,13 +28,13 @@ class TestPageExplorer(TestCase):
self.assertTrue(response.context['pages'].paginator.object_list.filter(id=self.child_page.id).exists())
class TestPageCreation(TestCase):
class TestPageCreation(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Login
self.user = login(self.client)
self.user = self.login()
def test_add_subpage(self):
response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(self.root_page.id, )))
@ -86,6 +86,7 @@ class TestPageCreation(TestCase):
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
@ -104,6 +105,7 @@ class TestPageCreation(TestCase):
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
@ -126,6 +128,7 @@ class TestPageCreation(TestCase):
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
@ -186,7 +189,7 @@ class TestPageCreation(TestCase):
self.assertContains(response, "New page!")
class TestPageEdit(TestCase):
class TestPageEdit(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
@ -206,7 +209,7 @@ class TestPageEdit(TestCase):
self.root_page.add_child(instance=self.event_page)
# Login
self.user = login(self.client)
self.user = self.login()
def test_page_edit(self):
# Tests that the edit page loads
@ -238,6 +241,7 @@ class TestPageEdit(TestCase):
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# The page should have "has_unpublished_changes" flag set
child_page_new = SimplePage.objects.get(id=self.child_page.id)
@ -255,6 +259,7 @@ class TestPageEdit(TestCase):
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page was edited
child_page_new = SimplePage.objects.get(id=self.child_page.id)
@ -278,6 +283,7 @@ class TestPageEdit(TestCase):
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# The page should have "has_unpublished_changes" flag set
child_page_new = SimplePage.objects.get(id=self.child_page.id)
@ -306,7 +312,7 @@ class TestPageEdit(TestCase):
self.assertContains(response, "I&#39;ve been edited!")
class TestPageDelete(TestCase):
class TestPageDelete(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
@ -318,7 +324,7 @@ class TestPageDelete(TestCase):
self.root_page.add_child(instance=self.child_page)
# Login
self.user = login(self.client)
self.user = self.login()
def test_page_delete(self):
response = self.client.get(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )))
@ -344,15 +350,16 @@ class TestPageDelete(TestCase):
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page is gone
self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0)
class TestPageSearch(TestCase):
class TestPageSearch(TestCase, WagtailTestUtils):
def setUp(self):
# Login
login(self.client)
self.login()
def get(self, params=None, **extra):
return self.client.get(reverse('wagtailadmin_pages_search'), params or {}, **extra)
@ -390,7 +397,7 @@ class TestPageSearch(TestCase):
self.assertTrue(any([r.slug == 'root' for r in results]))
class TestPageMove(TestCase):
class TestPageMove(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
@ -413,7 +420,7 @@ class TestPageMove(TestCase):
self.section_a.add_child(instance=self.test_page)
# Login
self.user = login(self.client)
self.user = self.login()
def test_page_move(self):
response = self.client.get(reverse('wagtailadmin_pages_move', args=(self.test_page.id, )))
@ -442,18 +449,18 @@ class TestPageMove(TestCase):
self.assertEqual(response.status_code, 200)
class TestPageUnpublish(TestCase):
class TestPageUnpublish(TestCase, WagtailTestUtils):
def setUp(self):
self.user = login(self.client)
self.user = self.login()
# Create a page to unpublish
root_page = Page.objects.get(id=2)
self.root_page = Page.objects.get(id=2)
self.page = SimplePage(
title="Hello world!",
slug='hello-world',
live=True,
)
root_page.add_child(instance=self.page)
self.root_page.add_child(instance=self.page)
def test_unpublish_view(self):
"""
@ -502,14 +509,15 @@ class TestPageUnpublish(TestCase):
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user was redirected
# Check that the user was redirected to the explore page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page was unpublished
self.assertFalse(SimplePage.objects.get(id=self.page.id).live)
class TestApproveRejectModeration(TestCase):
class TestApproveRejectModeration(TestCase, WagtailTestUtils):
def setUp(self):
self.submitter = User.objects.create_superuser(
username='submitter',
@ -517,7 +525,7 @@ class TestApproveRejectModeration(TestCase):
password='password',
)
self.user = login(self.client)
self.user = self.login()
# Create a page and submit it for moderation
root_page = Page.objects.get(id=2)
@ -540,8 +548,9 @@ class TestApproveRejectModeration(TestCase):
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user was redirected
# Check that the user was redirected to the dashboard
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_home'))
# Page must be live
self.assertTrue(Page.objects.get(id=self.page.id).live)
@ -591,8 +600,9 @@ class TestApproveRejectModeration(TestCase):
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user was redirected
# Check that the user was redirected to the dashboard
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailadmin_home'))
# Page must not be live
self.assertFalse(Page.objects.get(id=self.page.id).live)
@ -645,11 +655,11 @@ class TestApproveRejectModeration(TestCase):
self.assertContains(response, "Hello world!")
class TestContentTypeUse(TestCase):
class TestContentTypeUse(TestCase, WagtailTestUtils):
fixtures = ['test.json']
def setUp(self):
self.user = login(self.client)
self.user = self.login()
def test_content_type_use(self):
# Get use of event page

View file

@ -1,26 +1,26 @@
from django.test import TestCase
from wagtail.tests.models import SimplePage, EventPage
from wagtail.tests.utils import login, unittest
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.tasks import send_email_task
from django.core.urlresolvers import reverse
from django.core import mail
class TestHome(TestCase):
class TestHome(TestCase, WagtailTestUtils):
def setUp(self):
# Login
login(self.client)
self.login()
def test_status_code(self):
def test_simple(self):
response = self.client.get(reverse('wagtailadmin_home'))
self.assertEqual(response.status_code, 200)
class TestEditorHooks(TestCase):
class TestEditorHooks(TestCase, WagtailTestUtils):
def setUp(self):
self.homepage = Page.objects.get(id=2)
login(self.client)
self.login()
def test_editor_css_and_js_hooks_on_add(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.homepage.id)))

View file

@ -5,15 +5,8 @@ from wagtail.wagtailadmin.forms import LoginForm, PasswordResetForm
from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar
from wagtail.wagtailadmin import hooks
urlpatterns = [
url(
r'^login/$', 'django.contrib.auth.views.login', {
'template_name': 'wagtailadmin/login.html',
'authentication_form': LoginForm,
'extra_context': {'show_password_reset': getattr(settings, 'WAGTAIL_PASSWORD_MANAGEMENT_ENABLED', True)},
}, name='wagtailadmin_login'
),
urlpatterns = [
# Password reset
url(
r'^password_reset/$', 'django.contrib.auth.views.password_reset', {
@ -81,6 +74,7 @@ urlpatterns += [
url(r'^tag-autocomplete/$', tags.autocomplete, name='wagtailadmin_tag_autocomplete'),
url(r'^login/$', account.login, name='wagtailadmin_login'),
url(r'^account/$', account.account, name='wagtailadmin_account'),
url(r'^account/change_password/$', account.change_password, name='wagtailadmin_account_change_password'),
url(r'^logout/$', account.logout, name='wagtailadmin_logout'),
@ -90,6 +84,13 @@ urlpatterns += [
]
# This is here to make sure that 'django.contrib.auth.views.login' is reversed correctly
# It must be placed after 'wagtailadmin_login' to prevent this from being used
urlpatterns += [
url(r'^login/$', 'django.contrib.auth.views.login'),
]
# Import additional urlpatterns from any apps that define a register_admin_urls hook
for fn in hooks.get_hooks('register_admin_urls'):
urls = fn()

View file

@ -3,8 +3,13 @@ from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.forms import SetPasswordForm
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.views import logout as auth_logout
from django.contrib.auth.views import logout as auth_logout, login as auth_login
from django.utils.translation import ugettext as _
from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.cache import never_cache
from wagtail.wagtailadmin import forms
@permission_required('wagtailadmin.access_admin')
def account(request):
@ -37,6 +42,21 @@ def change_password(request):
})
@sensitive_post_parameters()
@never_cache
def login(request):
if request.user.is_authenticated():
return redirect('wagtailadmin_home')
else:
return auth_login(request,
template_name='wagtailadmin/login.html',
authentication_form=forms.LoginForm,
extra_context={
'show_password_reset': getattr(settings, 'WAGTAIL_PASSWORD_MANAGEMENT_ENABLED', True),
},
)
def logout(request):
response = auth_logout(request, next_page = 'wagtailadmin_login')

View file

@ -394,23 +394,23 @@ class Page(MP_Node, ClusterableModel, Indexed):
return revision.as_page_object()
def get_context(self, request):
def get_context(self, request, *args, **kwargs):
return {
'self': self,
'request': request,
}
def get_template(self, request):
def get_template(self, request, *args, **kwargs):
if request.is_ajax():
return self.ajax_template or self.template
else:
return self.template
def serve(self, request):
def serve(self, request, *args, **kwargs):
return TemplateResponse(
request,
self.get_template(request),
self.get_context(request)
self.get_template(request, *args, **kwargs),
self.get_context(request, *args, **kwargs)
)
def is_navigable(self):

View file

@ -1,860 +0,0 @@
from django.test import TestCase, Client
from django.http import HttpRequest, Http404
from django.core import management
from StringIO import StringIO
from django.contrib.auth.models import User
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
class TestRouting(TestCase):
fixtures = ['test.json']
def test_find_site_for_request(self):
default_site = Site.objects.get(is_default_site=True)
events_page = Page.objects.get(url_path='/home/events/')
events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
# requests without a Host: header should be directed to the default site
request = HttpRequest()
request.path = '/'
self.assertEqual(Site.find_for_request(request), default_site)
# requests with a known Host: header should be directed to the specific site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = 'events.example.com'
self.assertEqual(Site.find_for_request(request), events_site)
# requests with an unrecognised Host: header should be directed to the default site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = 'unknown.example.com'
self.assertEqual(Site.find_for_request(request), default_site)
def test_urls(self):
default_site = Site.objects.get(is_default_site=True)
homepage = Page.objects.get(url_path='/home/')
christmas_page = Page.objects.get(url_path='/home/events/christmas/')
# Basic installation only has one site configured, so page.url will return local URLs
self.assertEqual(homepage.full_url, 'http://localhost/')
self.assertEqual(homepage.url, '/')
self.assertEqual(homepage.relative_url(default_site), '/')
self.assertEqual(christmas_page.full_url, 'http://localhost/events/christmas/')
self.assertEqual(christmas_page.url, '/events/christmas/')
self.assertEqual(christmas_page.relative_url(default_site), '/events/christmas/')
def test_urls_with_multiple_sites(self):
events_page = Page.objects.get(url_path='/home/events/')
events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
default_site = Site.objects.get(is_default_site=True)
homepage = Page.objects.get(url_path='/home/')
christmas_page = Page.objects.get(url_path='/home/events/christmas/')
# with multiple sites, page.url will return full URLs to ensure that
# they work across sites
self.assertEqual(homepage.full_url, 'http://localhost/')
self.assertEqual(homepage.url, 'http://localhost/')
self.assertEqual(homepage.relative_url(default_site), '/')
self.assertEqual(homepage.relative_url(events_site), 'http://localhost/')
self.assertEqual(christmas_page.full_url, 'http://events.example.com/christmas/')
self.assertEqual(christmas_page.url, 'http://events.example.com/christmas/')
self.assertEqual(christmas_page.relative_url(default_site), 'http://events.example.com/christmas/')
self.assertEqual(christmas_page.relative_url(events_site), '/christmas/')
def test_request_routing(self):
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
request = HttpRequest()
request.path = '/events/christmas/'
response = homepage.route(request, ['events', 'christmas'])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context_data['self'], christmas_page)
used_template = response.resolve_template(response.template_name)
self.assertEqual(used_template.name, 'tests/event_page.html')
def test_route_to_unknown_page_returns_404(self):
homepage = Page.objects.get(url_path='/home/')
request = HttpRequest()
request.path = '/events/quinquagesima/'
with self.assertRaises(Http404):
homepage.route(request, ['events', 'quinquagesima'])
def test_route_to_unpublished_page_returns_404(self):
homepage = Page.objects.get(url_path='/home/')
request = HttpRequest()
request.path = '/events/tentative-unpublished-event/'
with self.assertRaises(Http404):
homepage.route(request, ['events', 'tentative-unpublished-event'])
class TestServeView(TestCase):
fixtures = ['test.json']
def setUp(self):
# Explicitly clear the cache of site root paths. Normally this would be kept
# in sync by the Site.save logic, but this is bypassed when the database is
# rolled back between tests using transactions.
from django.core.cache import cache
cache.delete('wagtail_site_root_paths')
def test_serve(self):
response = self.client.get('/events/christmas/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.templates[0].name, 'tests/event_page.html')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
self.assertEqual(response.context['self'], christmas_page)
self.assertContains(response, '<h1>Christmas</h1>')
self.assertContains(response, '<h2>Event</h2>')
def test_serve_unknown_page_returns_404(self):
response = self.client.get('/events/quinquagesima/')
self.assertEqual(response.status_code, 404)
def test_serve_unpublished_page_returns_404(self):
response = self.client.get('/events/tentative-unpublished-event/')
self.assertEqual(response.status_code, 404)
def test_serve_with_multiple_sites(self):
events_page = Page.objects.get(url_path='/home/events/')
Site.objects.create(hostname='events.example.com', root_page=events_page)
response = self.client.get('/christmas/', HTTP_HOST='events.example.com')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.templates[0].name, 'tests/event_page.html')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
self.assertEqual(response.context['self'], christmas_page)
self.assertContains(response, '<h1>Christmas</h1>')
self.assertContains(response, '<h2>Event</h2>')
# same request to the default host should return a 404
c = Client()
response = c.get('/christmas/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 404)
def test_serve_with_custom_context(self):
response = self.client.get('/events/')
self.assertEqual(response.status_code, 200)
# should render the whole page
self.assertContains(response, '<h1>Events</h1>')
# response should contain data from the custom 'events' context variable
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
def test_ajax_response(self):
response = self.client.get('/events/', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# should only render the content of includes/event_listing.html, not the whole page
self.assertNotContains(response, '<h1>Events</h1>')
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
class TestStaticSitePaths(TestCase):
def setUp(self):
self.root_page = Page.objects.get(id=1)
# For simple tests
self.home_page = self.root_page.add_child(instance=SimplePage(title="Homepage", slug="home"))
self.about_page = self.home_page.add_child(instance=SimplePage(title="About us", slug="about"))
self.contact_page = self.home_page.add_child(instance=SimplePage(title="Contact", slug="contact"))
# For custom tests
self.event_index = self.root_page.add_child(instance=EventIndex(title="Events", slug="events"))
for i in range(20):
self.event_index.add_child(instance=EventPage(title="Event " + str(i), slug="event" + str(i)))
def test_local_static_site_paths(self):
paths = list(self.about_page.get_static_site_paths())
self.assertEqual(paths, ['/'])
def test_child_static_site_paths(self):
paths = list(self.home_page.get_static_site_paths())
self.assertEqual(paths, ['/', '/about/', '/contact/'])
def test_custom_static_site_paths(self):
paths = list(self.event_index.get_static_site_paths())
# Event index path
expected_paths = ['/']
# One path for each page of results
expected_paths.extend(['/' + str(i + 1) + '/' for i in range(5)])
# One path for each event page
expected_paths.extend(['/event' + str(i) + '/' for i in range(20)])
paths.sort()
expected_paths.sort()
self.assertEqual(paths, expected_paths)
class TestPageUrlTags(TestCase):
fixtures = ['test.json']
def test_pageurl_tag(self):
response = self.client.get('/events/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
def test_slugurl_tag(self):
response = self.client.get('/events/christmas/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<a href="/events/">Back to events index</a>')
class TestPagePermission(TestCase):
fixtures = ['test.json']
def test_nonpublisher_page_permissions(self):
event_editor = User.objects.get(username='eventeditor')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
homepage_perms = homepage.permissions_for_user(event_editor)
christmas_page_perms = christmas_page.permissions_for_user(event_editor)
unpub_perms = unpublished_event_page.permissions_for_user(event_editor)
someone_elses_event_perms = someone_elses_event_page.permissions_for_user(event_editor)
self.assertFalse(homepage_perms.can_add_subpage())
self.assertTrue(christmas_page_perms.can_add_subpage())
self.assertTrue(unpub_perms.can_add_subpage())
self.assertTrue(someone_elses_event_perms.can_add_subpage())
self.assertFalse(homepage_perms.can_edit())
self.assertTrue(christmas_page_perms.can_edit())
self.assertTrue(unpub_perms.can_edit())
self.assertFalse(someone_elses_event_perms.can_edit()) # basic 'add' permission doesn't allow editing pages owned by someone else
self.assertFalse(homepage_perms.can_delete())
self.assertFalse(christmas_page_perms.can_delete()) # cannot delete because it is published
self.assertTrue(unpub_perms.can_delete())
self.assertFalse(someone_elses_event_perms.can_delete())
self.assertFalse(homepage_perms.can_publish())
self.assertFalse(christmas_page_perms.can_publish())
self.assertFalse(unpub_perms.can_publish())
self.assertFalse(homepage_perms.can_unpublish())
self.assertFalse(christmas_page_perms.can_unpublish())
self.assertFalse(unpub_perms.can_unpublish())
self.assertFalse(homepage_perms.can_publish_subpage())
self.assertFalse(christmas_page_perms.can_publish_subpage())
self.assertFalse(unpub_perms.can_publish_subpage())
self.assertFalse(homepage_perms.can_reorder_children())
self.assertFalse(christmas_page_perms.can_reorder_children())
self.assertFalse(unpub_perms.can_reorder_children())
self.assertFalse(homepage_perms.can_move())
self.assertFalse(christmas_page_perms.can_move()) # cannot move because this would involve unpublishing from its current location
self.assertTrue(unpub_perms.can_move())
self.assertFalse(someone_elses_event_perms.can_move())
self.assertFalse(christmas_page_perms.can_move_to(unpublished_event_page)) # cannot move because this would involve unpublishing from its current location
self.assertTrue(unpub_perms.can_move_to(christmas_page))
self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination
self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself
def test_publisher_page_permissions(self):
event_moderator = User.objects.get(username='eventmoderator')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
homepage_perms = homepage.permissions_for_user(event_moderator)
christmas_page_perms = christmas_page.permissions_for_user(event_moderator)
unpub_perms = unpublished_event_page.permissions_for_user(event_moderator)
self.assertFalse(homepage_perms.can_add_subpage())
self.assertTrue(christmas_page_perms.can_add_subpage())
self.assertTrue(unpub_perms.can_add_subpage())
self.assertFalse(homepage_perms.can_edit())
self.assertTrue(christmas_page_perms.can_edit())
self.assertTrue(unpub_perms.can_edit())
self.assertFalse(homepage_perms.can_delete())
self.assertTrue(christmas_page_perms.can_delete()) # cannot delete because it is published
self.assertTrue(unpub_perms.can_delete())
self.assertFalse(homepage_perms.can_publish())
self.assertTrue(christmas_page_perms.can_publish())
self.assertTrue(unpub_perms.can_publish())
self.assertFalse(homepage_perms.can_unpublish())
self.assertTrue(christmas_page_perms.can_unpublish())
self.assertFalse(unpub_perms.can_unpublish()) # cannot unpublish a page that isn't published
self.assertFalse(homepage_perms.can_publish_subpage())
self.assertTrue(christmas_page_perms.can_publish_subpage())
self.assertTrue(unpub_perms.can_publish_subpage())
self.assertFalse(homepage_perms.can_reorder_children())
self.assertTrue(christmas_page_perms.can_reorder_children())
self.assertTrue(unpub_perms.can_reorder_children())
self.assertFalse(homepage_perms.can_move())
self.assertTrue(christmas_page_perms.can_move())
self.assertTrue(unpub_perms.can_move())
self.assertTrue(christmas_page_perms.can_move_to(unpublished_event_page))
self.assertTrue(unpub_perms.can_move_to(christmas_page))
self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination
self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself
def test_inactive_user_has_no_permissions(self):
user = User.objects.get(username='inactiveuser')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
christmas_page_perms = christmas_page.permissions_for_user(user)
unpub_perms = unpublished_event_page.permissions_for_user(user)
self.assertFalse(unpub_perms.can_add_subpage())
self.assertFalse(unpub_perms.can_edit())
self.assertFalse(unpub_perms.can_delete())
self.assertFalse(unpub_perms.can_publish())
self.assertFalse(christmas_page_perms.can_unpublish())
self.assertFalse(unpub_perms.can_publish_subpage())
self.assertFalse(unpub_perms.can_reorder_children())
self.assertFalse(unpub_perms.can_move())
self.assertFalse(unpub_perms.can_move_to(christmas_page))
def test_superuser_has_full_permissions(self):
user = User.objects.get(username='superuser')
homepage = Page.objects.get(url_path='/home/')
root = Page.objects.get(url_path='/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
homepage_perms = homepage.permissions_for_user(user)
root_perms = root.permissions_for_user(user)
unpub_perms = unpublished_event_page.permissions_for_user(user)
self.assertTrue(homepage_perms.can_add_subpage())
self.assertTrue(root_perms.can_add_subpage())
self.assertTrue(homepage_perms.can_edit())
self.assertFalse(root_perms.can_edit()) # root is not a real editable page, even to superusers
self.assertTrue(homepage_perms.can_delete())
self.assertFalse(root_perms.can_delete())
self.assertTrue(homepage_perms.can_publish())
self.assertFalse(root_perms.can_publish())
self.assertTrue(homepage_perms.can_unpublish())
self.assertFalse(root_perms.can_unpublish())
self.assertFalse(unpub_perms.can_unpublish())
self.assertTrue(homepage_perms.can_publish_subpage())
self.assertTrue(root_perms.can_publish_subpage())
self.assertTrue(homepage_perms.can_reorder_children())
self.assertTrue(root_perms.can_reorder_children())
self.assertTrue(homepage_perms.can_move())
self.assertFalse(root_perms.can_move())
self.assertTrue(homepage_perms.can_move_to(root))
self.assertFalse(homepage_perms.can_move_to(unpublished_event_page))
def test_editable_pages_for_user_with_add_permission(self):
event_editor = User.objects.get(username='eventeditor')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(event_editor).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_user_with_edit_permission(self):
event_moderator = User.objects.get(username='eventmoderator')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(event_moderator).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_inactive_user(self):
user = User.objects.get(username='inactiveuser')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(user).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertFalse(editable_pages.filter(id=christmas_page.id).exists())
self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_superuser(self):
user = User.objects.get(username='superuser')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(user).editable_pages()
self.assertTrue(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
class TestPageQuerySet(TestCase):
fixtures = ['test.json']
def test_live(self):
pages = Page.objects.live()
# All pages must be live
for page in pages:
self.assertTrue(page.live)
# Check that the homepage is in the results
homepage = Page.objects.get(url_path='/home/')
self.assertTrue(pages.filter(id=homepage.id).exists())
def test_not_live(self):
pages = Page.objects.not_live()
# All pages must not be live
for page in pages:
self.assertFalse(page.live)
# Check that "someone elses event" is in the results
event = Page.objects.get(url_path='/home/events/someone-elses-event/')
self.assertTrue(pages.filter(id=event.id).exists())
def test_page(self):
homepage = Page.objects.get(url_path='/home/')
pages = Page.objects.page(homepage)
# Should only select the homepage
self.assertEqual(pages.count(), 1)
self.assertEqual(pages.first(), homepage)
def test_not_page(self):
homepage = Page.objects.get(url_path='/home/')
pages = Page.objects.not_page(homepage)
# Should select everything except for the homepage
self.assertEqual(pages.count(), Page.objects.all().count() - 1)
for page in pages:
self.assertNotEqual(page, homepage)
def test_descendant_of(self):
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.descendant_of(events_index)
# Check that all pages descend from events index
for page in pages:
self.assertTrue(page.get_ancestors().filter(id=events_index.id).exists())
def test_descendant_of_inclusive(self):
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.descendant_of(events_index, inclusive=True)
# Check that all pages descend from events index, includes event index
for page in pages:
self.assertTrue(page == events_index or page.get_ancestors().filter(id=events_index.id).exists())
# Check that event index was included
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_descendant_of(self):
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_descendant_of(events_index)
# Check that no pages descend from events_index
for page in pages:
self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists())
# As this is not inclusive, events index should be in the results
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_descendant_of_inclusive(self):
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_descendant_of(events_index, inclusive=True)
# Check that all pages descend from homepage but not events index
for page in pages:
self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists())
# As this is inclusive, events index should not be in the results
self.assertFalse(pages.filter(id=events_index.id).exists())
def test_child_of(self):
homepage = Page.objects.get(url_path='/home/')
pages = Page.objects.child_of(homepage)
# Check that all pages are children of homepage
for page in pages:
self.assertEqual(page.get_parent(), homepage)
def test_not_child_of(self):
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_child_of(events_index)
# Check that all pages are not children of events_index
for page in pages:
self.assertNotEqual(page.get_parent(), events_index)
def test_ancestor_of(self):
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.ancestor_of(events_index)
self.assertEqual(pages.count(), 2)
self.assertEqual(pages[0], root_page)
self.assertEqual(pages[1], homepage)
def test_ancestor_of_inclusive(self):
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.ancestor_of(events_index, inclusive=True)
self.assertEqual(pages.count(), 3)
self.assertEqual(pages[0], root_page)
self.assertEqual(pages[1], homepage)
self.assertEqual(pages[2], events_index)
def test_not_ancestor_of(self):
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_ancestor_of(events_index)
# Test that none of the ancestors are in pages
for page in pages:
self.assertNotEqual(page, root_page)
self.assertNotEqual(page, homepage)
# Test that events index is in pages
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_ancestor_of_inclusive(self):
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_ancestor_of(events_index, inclusive=True)
# Test that none of the ancestors or the events_index are in pages
for page in pages:
self.assertNotEqual(page, root_page)
self.assertNotEqual(page, homepage)
self.assertNotEqual(page, events_index)
def test_parent_of(self):
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.parent_of(events_index)
# Pages must only contain homepage
self.assertEqual(pages.count(), 1)
self.assertEqual(pages[0], homepage)
def test_not_parent_of(self):
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_parent_of(events_index)
# Pages must not contain homepage
for page in pages:
self.assertNotEqual(page, homepage)
# Test that events index is in pages
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_sibling_of(self):
events_index = Page.objects.get(url_path='/home/events/')
event = Page.objects.get(url_path='/home/events/christmas/')
pages = Page.objects.sibling_of(event)
# Check that all pages are children of events_index
for page in pages:
self.assertEqual(page.get_parent(), events_index)
# Check that the event is not included
self.assertFalse(pages.filter(id=event.id).exists())
def test_sibling_of_inclusive(self):
events_index = Page.objects.get(url_path='/home/events/')
event = Page.objects.get(url_path='/home/events/christmas/')
pages = Page.objects.sibling_of(event, inclusive=True)
# Check that all pages are children of events_index
for page in pages:
self.assertEqual(page.get_parent(), events_index)
# Check that the event is included
self.assertTrue(pages.filter(id=event.id).exists())
def test_not_sibling_of(self):
events_index = Page.objects.get(url_path='/home/events/')
event = Page.objects.get(url_path='/home/events/christmas/')
pages = Page.objects.not_sibling_of(event)
# Check that all pages are not children of events_index
for page in pages:
if page != event:
self.assertNotEqual(page.get_parent(), events_index)
# Check that the event is included
self.assertTrue(pages.filter(id=event.id).exists())
# Test that events index is in pages
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_sibling_of_inclusive(self):
events_index = Page.objects.get(url_path='/home/events/')
event = Page.objects.get(url_path='/home/events/christmas/')
pages = Page.objects.not_sibling_of(event, inclusive=True)
# Check that all pages are not children of events_index
for page in pages:
self.assertNotEqual(page.get_parent(), events_index)
# Check that the event is not included
self.assertFalse(pages.filter(id=event.id).exists())
# Test that events index is in pages
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_type(self):
pages = Page.objects.type(EventPage)
# Check that all objects are EventPages
for page in pages:
self.assertIsInstance(page.specific, EventPage)
# Check that "someone elses event" is in the results
event = Page.objects.get(url_path='/home/events/someone-elses-event/')
self.assertTrue(pages.filter(id=event.id).exists())
def test_not_type(self):
pages = Page.objects.not_type(EventPage)
# Check that no objects are EventPages
for page in pages:
self.assertNotIsInstance(page.specific, EventPage)
# Check that the homepage is in the results
homepage = Page.objects.get(url_path='/home/')
self.assertTrue(pages.filter(id=homepage.id).exists())
class TestMovePage(TestCase):
fixtures = ['test.json']
def test_move_page(self):
about_us_page = SimplePage.objects.get(url_path='/home/about-us/')
events_index = EventIndex.objects.get(url_path='/home/events/')
events_index.move(about_us_page, pos='last-child')
# re-fetch events index to confirm that db fields have been updated
events_index = EventIndex.objects.get(id=events_index.id)
self.assertEqual(events_index.url_path, '/home/about-us/events/')
self.assertEqual(events_index.depth, 4)
self.assertEqual(events_index.get_parent().id, about_us_page.id)
# children of events_index should also have been updated
christmas = events_index.get_children().get(slug='christmas')
self.assertEqual(christmas.depth, 5)
self.assertEqual(christmas.url_path, '/home/about-us/events/christmas/')
class TestIssue7(TestCase):
"""
This tests for an issue where if a site root page was moved, all the page
urls in that site would change to None.
The issue was caused by the 'wagtail_site_root_paths' cache variable not being
cleared when a site root page was moved. Which left all the child pages
thinking that they are no longer in the site and return None as their url.
Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
Discussion: https://github.com/torchbox/wagtail/issues/7
"""
fixtures = ['test.json']
def test_issue7(self):
# Get homepage, root page and site
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
default_site = Site.objects.get(is_default_site=True)
# Create a new homepage under current homepage
new_homepage = SimplePage(title="New Homepage", slug="new-homepage")
homepage.add_child(instance=new_homepage)
# Set new homepage as the site root page
default_site.root_page = new_homepage
default_site.save()
# Warm up the cache by getting the url
_ = homepage.url
# Move new homepage to root
new_homepage.move(root_page, pos='last-child')
# Get fresh instance of new_homepage
new_homepage = Page.objects.get(id=new_homepage.id)
# Check url
self.assertEqual(new_homepage.url, '/')
class TestIssue157(TestCase):
"""
This tests for an issue where if a site root pages slug was changed, all the page
urls in that site would change to None.
The issue was caused by the 'wagtail_site_root_paths' cache variable not being
cleared when a site root page was changed. Which left all the child pages
thinking that they are no longer in the site and return None as their url.
Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
Discussion: https://github.com/torchbox/wagtail/issues/157
"""
fixtures = ['test.json']
def test_issue157(self):
# Get homepage
homepage = Page.objects.get(url_path='/home/')
# Warm up the cache by getting the url
_ = homepage.url
# Change homepage title and slug
homepage.title = "New home"
homepage.slug = "new-home"
homepage.save()
# Get fresh instance of homepage
homepage = Page.objects.get(id=homepage.id)
# Check url
self.assertEqual(homepage.url, '/')
class TestFixTreeCommand(TestCase):
fixtures = ['test.json']
def run_command(self):
management.call_command('fixtree', interactive=False, stdout=StringIO())
def test_fixes_numchild(self):
# Get homepage and save old value
homepage = Page.objects.get(url_path='/home/')
old_numchild = homepage.numchild
# Break it
homepage.numchild = 12345
homepage.save()
# Check that its broken
self.assertEqual(Page.objects.get(url_path='/home/').numchild, 12345)
# Call command
self.run_command()
# Check if its fixed
self.assertEqual(Page.objects.get(url_path='/home/').numchild, old_numchild)
def test_fixes_depth(self):
# Get homepage and save old value
homepage = Page.objects.get(url_path='/home/')
old_depth = homepage.depth
# Break it
homepage.depth = 12345
homepage.save()
# Check that its broken
self.assertEqual(Page.objects.get(url_path='/home/').depth, 12345)
# Call command
self.run_command()
# Check if its fixed
self.assertEqual(Page.objects.get(url_path='/home/').depth, old_depth)
class TestMovePagesCommand(TestCase):
fixtures = ['test.json']
def run_command(self, from_, to):
management.call_command('move_pages', str(from_), str(to), interactive=False, stdout=StringIO())
def test_move_pages(self):
# Get pages
events_index = Page.objects.get(url_path='/home/events/')
about_us = Page.objects.get(url_path='/home/about-us/')
page_ids = events_index.get_children().values_list('id', flat=True)
# Move all events into "about us"
self.run_command(events_index.id, about_us.id)
# Check that all pages moved
for page_id in page_ids:
self.assertEqual(Page.objects.get(id=page_id).get_parent(), about_us)
class TestReplaceTextCommand(TestCase):
fixtures = ['test.json']
def run_command(self, from_text, to_text):
management.call_command('replace_text', from_text, to_text, interactive=False, stdout=StringIO())
def test_replace_text(self):
# Check that the christmas page is definitely about christmas
self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Christmas")
# Make it about easter
self.run_command("Christmas", "Easter")
# Check that its now about easter
self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter")

View file

View file

@ -0,0 +1,89 @@
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.tests.models import EventPage, EventIndex, SimplePage
class TestFixTreeCommand(TestCase):
fixtures = ['test.json']
def run_command(self):
management.call_command('fixtree', interactive=False, stdout=StringIO())
def test_fixes_numchild(self):
# Get homepage and save old value
homepage = Page.objects.get(url_path='/home/')
old_numchild = homepage.numchild
# Break it
homepage.numchild = 12345
homepage.save()
# Check that its broken
self.assertEqual(Page.objects.get(url_path='/home/').numchild, 12345)
# Call command
self.run_command()
# Check if its fixed
self.assertEqual(Page.objects.get(url_path='/home/').numchild, old_numchild)
def test_fixes_depth(self):
# Get homepage and save old value
homepage = Page.objects.get(url_path='/home/')
old_depth = homepage.depth
# Break it
homepage.depth = 12345
homepage.save()
# Check that its broken
self.assertEqual(Page.objects.get(url_path='/home/').depth, 12345)
# Call command
self.run_command()
# Check if its fixed
self.assertEqual(Page.objects.get(url_path='/home/').depth, old_depth)
class TestMovePagesCommand(TestCase):
fixtures = ['test.json']
def run_command(self, from_, to):
management.call_command('move_pages', str(from_), str(to), interactive=False, stdout=StringIO())
def test_move_pages(self):
# Get pages
events_index = Page.objects.get(url_path='/home/events/')
about_us = Page.objects.get(url_path='/home/about-us/')
page_ids = events_index.get_children().values_list('id', flat=True)
# Move all events into "about us"
self.run_command(events_index.id, about_us.id)
# Check that all pages moved
for page_id in page_ids:
self.assertEqual(Page.objects.get(id=page_id).get_parent(), about_us)
class TestReplaceTextCommand(TestCase):
fixtures = ['test.json']
def run_command(self, from_text, to_text):
management.call_command('replace_text', from_text, to_text, interactive=False, stdout=StringIO())
def test_replace_text(self):
# Check that the christmas page is definitely about christmas
self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Christmas")
# Make it about easter
self.run_command("Christmas", "Easter")
# Check that its now about easter
self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter")

View file

@ -0,0 +1,226 @@
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.tests.models import EventPage, EventIndex, SimplePage
class TestRouting(TestCase):
fixtures = ['test.json']
def test_find_site_for_request(self):
default_site = Site.objects.get(is_default_site=True)
events_page = Page.objects.get(url_path='/home/events/')
events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
# requests without a Host: header should be directed to the default site
request = HttpRequest()
request.path = '/'
self.assertEqual(Site.find_for_request(request), default_site)
# requests with a known Host: header should be directed to the specific site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = 'events.example.com'
self.assertEqual(Site.find_for_request(request), events_site)
# requests with an unrecognised Host: header should be directed to the default site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = 'unknown.example.com'
self.assertEqual(Site.find_for_request(request), default_site)
def test_urls(self):
default_site = Site.objects.get(is_default_site=True)
homepage = Page.objects.get(url_path='/home/')
christmas_page = Page.objects.get(url_path='/home/events/christmas/')
# Basic installation only has one site configured, so page.url will return local URLs
self.assertEqual(homepage.full_url, 'http://localhost/')
self.assertEqual(homepage.url, '/')
self.assertEqual(homepage.relative_url(default_site), '/')
self.assertEqual(christmas_page.full_url, 'http://localhost/events/christmas/')
self.assertEqual(christmas_page.url, '/events/christmas/')
self.assertEqual(christmas_page.relative_url(default_site), '/events/christmas/')
def test_urls_with_multiple_sites(self):
events_page = Page.objects.get(url_path='/home/events/')
events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
default_site = Site.objects.get(is_default_site=True)
homepage = Page.objects.get(url_path='/home/')
christmas_page = Page.objects.get(url_path='/home/events/christmas/')
# with multiple sites, page.url will return full URLs to ensure that
# they work across sites
self.assertEqual(homepage.full_url, 'http://localhost/')
self.assertEqual(homepage.url, 'http://localhost/')
self.assertEqual(homepage.relative_url(default_site), '/')
self.assertEqual(homepage.relative_url(events_site), 'http://localhost/')
self.assertEqual(christmas_page.full_url, 'http://events.example.com/christmas/')
self.assertEqual(christmas_page.url, 'http://events.example.com/christmas/')
self.assertEqual(christmas_page.relative_url(default_site), 'http://events.example.com/christmas/')
self.assertEqual(christmas_page.relative_url(events_site), '/christmas/')
def test_request_routing(self):
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
request = HttpRequest()
request.path = '/events/christmas/'
response = homepage.route(request, ['events', 'christmas'])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context_data['self'], christmas_page)
used_template = response.resolve_template(response.template_name)
self.assertEqual(used_template.name, 'tests/event_page.html')
def test_route_to_unknown_page_returns_404(self):
homepage = Page.objects.get(url_path='/home/')
request = HttpRequest()
request.path = '/events/quinquagesima/'
with self.assertRaises(Http404):
homepage.route(request, ['events', 'quinquagesima'])
def test_route_to_unpublished_page_returns_404(self):
homepage = Page.objects.get(url_path='/home/')
request = HttpRequest()
request.path = '/events/tentative-unpublished-event/'
with self.assertRaises(Http404):
homepage.route(request, ['events', 'tentative-unpublished-event'])
class TestServeView(TestCase):
fixtures = ['test.json']
def setUp(self):
# Explicitly clear the cache of site root paths. Normally this would be kept
# in sync by the Site.save logic, but this is bypassed when the database is
# rolled back between tests using transactions.
from django.core.cache import cache
cache.delete('wagtail_site_root_paths')
def test_serve(self):
response = self.client.get('/events/christmas/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.templates[0].name, 'tests/event_page.html')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
self.assertEqual(response.context['self'], christmas_page)
self.assertContains(response, '<h1>Christmas</h1>')
self.assertContains(response, '<h2>Event</h2>')
def test_serve_unknown_page_returns_404(self):
response = self.client.get('/events/quinquagesima/')
self.assertEqual(response.status_code, 404)
def test_serve_unpublished_page_returns_404(self):
response = self.client.get('/events/tentative-unpublished-event/')
self.assertEqual(response.status_code, 404)
def test_serve_with_multiple_sites(self):
events_page = Page.objects.get(url_path='/home/events/')
Site.objects.create(hostname='events.example.com', root_page=events_page)
response = self.client.get('/christmas/', HTTP_HOST='events.example.com')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.templates[0].name, 'tests/event_page.html')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
self.assertEqual(response.context['self'], christmas_page)
self.assertContains(response, '<h1>Christmas</h1>')
self.assertContains(response, '<h2>Event</h2>')
# same request to the default host should return a 404
c = Client()
response = c.get('/christmas/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 404)
def test_serve_with_custom_context(self):
response = self.client.get('/events/')
self.assertEqual(response.status_code, 200)
# should render the whole page
self.assertContains(response, '<h1>Events</h1>')
# response should contain data from the custom 'events' context variable
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
def test_ajax_response(self):
response = self.client.get('/events/', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# should only render the content of includes/event_listing.html, not the whole page
self.assertNotContains(response, '<h1>Events</h1>')
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
class TestStaticSitePaths(TestCase):
def setUp(self):
self.root_page = Page.objects.get(id=1)
# For simple tests
self.home_page = self.root_page.add_child(instance=SimplePage(title="Homepage", slug="home"))
self.about_page = self.home_page.add_child(instance=SimplePage(title="About us", slug="about"))
self.contact_page = self.home_page.add_child(instance=SimplePage(title="Contact", slug="contact"))
# For custom tests
self.event_index = self.root_page.add_child(instance=EventIndex(title="Events", slug="events"))
for i in range(20):
self.event_index.add_child(instance=EventPage(title="Event " + str(i), slug="event" + str(i)))
def test_local_static_site_paths(self):
paths = list(self.about_page.get_static_site_paths())
self.assertEqual(paths, ['/'])
def test_child_static_site_paths(self):
paths = list(self.home_page.get_static_site_paths())
self.assertEqual(paths, ['/', '/about/', '/contact/'])
def test_custom_static_site_paths(self):
paths = list(self.event_index.get_static_site_paths())
# Event index path
expected_paths = ['/']
# One path for each page of results
expected_paths.extend(['/' + str(i + 1) + '/' for i in range(5)])
# One path for each event page
expected_paths.extend(['/event' + str(i) + '/' for i in range(20)])
paths.sort()
expected_paths.sort()
self.assertEqual(paths, expected_paths)
class TestMovePage(TestCase):
fixtures = ['test.json']
def test_move_page(self):
about_us_page = SimplePage.objects.get(url_path='/home/about-us/')
events_index = EventIndex.objects.get(url_path='/home/events/')
events_index.move(about_us_page, pos='last-child')
# re-fetch events index to confirm that db fields have been updated
events_index = EventIndex.objects.get(id=events_index.id)
self.assertEqual(events_index.url_path, '/home/about-us/events/')
self.assertEqual(events_index.depth, 4)
self.assertEqual(events_index.get_parent().id, about_us_page.id)
# children of events_index should also have been updated
christmas = events_index.get_children().get(slug='christmas')
self.assertEqual(christmas.depth, 5)
self.assertEqual(christmas.url_path, '/home/about-us/events/christmas/')

View file

@ -0,0 +1,226 @@
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.tests.models import EventPage, EventIndex, SimplePage
class TestPagePermission(TestCase):
fixtures = ['test.json']
def test_nonpublisher_page_permissions(self):
event_editor = User.objects.get(username='eventeditor')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
homepage_perms = homepage.permissions_for_user(event_editor)
christmas_page_perms = christmas_page.permissions_for_user(event_editor)
unpub_perms = unpublished_event_page.permissions_for_user(event_editor)
someone_elses_event_perms = someone_elses_event_page.permissions_for_user(event_editor)
self.assertFalse(homepage_perms.can_add_subpage())
self.assertTrue(christmas_page_perms.can_add_subpage())
self.assertTrue(unpub_perms.can_add_subpage())
self.assertTrue(someone_elses_event_perms.can_add_subpage())
self.assertFalse(homepage_perms.can_edit())
self.assertTrue(christmas_page_perms.can_edit())
self.assertTrue(unpub_perms.can_edit())
self.assertFalse(someone_elses_event_perms.can_edit()) # basic 'add' permission doesn't allow editing pages owned by someone else
self.assertFalse(homepage_perms.can_delete())
self.assertFalse(christmas_page_perms.can_delete()) # cannot delete because it is published
self.assertTrue(unpub_perms.can_delete())
self.assertFalse(someone_elses_event_perms.can_delete())
self.assertFalse(homepage_perms.can_publish())
self.assertFalse(christmas_page_perms.can_publish())
self.assertFalse(unpub_perms.can_publish())
self.assertFalse(homepage_perms.can_unpublish())
self.assertFalse(christmas_page_perms.can_unpublish())
self.assertFalse(unpub_perms.can_unpublish())
self.assertFalse(homepage_perms.can_publish_subpage())
self.assertFalse(christmas_page_perms.can_publish_subpage())
self.assertFalse(unpub_perms.can_publish_subpage())
self.assertFalse(homepage_perms.can_reorder_children())
self.assertFalse(christmas_page_perms.can_reorder_children())
self.assertFalse(unpub_perms.can_reorder_children())
self.assertFalse(homepage_perms.can_move())
self.assertFalse(christmas_page_perms.can_move()) # cannot move because this would involve unpublishing from its current location
self.assertTrue(unpub_perms.can_move())
self.assertFalse(someone_elses_event_perms.can_move())
self.assertFalse(christmas_page_perms.can_move_to(unpublished_event_page)) # cannot move because this would involve unpublishing from its current location
self.assertTrue(unpub_perms.can_move_to(christmas_page))
self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination
self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself
def test_publisher_page_permissions(self):
event_moderator = User.objects.get(username='eventmoderator')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
homepage_perms = homepage.permissions_for_user(event_moderator)
christmas_page_perms = christmas_page.permissions_for_user(event_moderator)
unpub_perms = unpublished_event_page.permissions_for_user(event_moderator)
self.assertFalse(homepage_perms.can_add_subpage())
self.assertTrue(christmas_page_perms.can_add_subpage())
self.assertTrue(unpub_perms.can_add_subpage())
self.assertFalse(homepage_perms.can_edit())
self.assertTrue(christmas_page_perms.can_edit())
self.assertTrue(unpub_perms.can_edit())
self.assertFalse(homepage_perms.can_delete())
self.assertTrue(christmas_page_perms.can_delete()) # cannot delete because it is published
self.assertTrue(unpub_perms.can_delete())
self.assertFalse(homepage_perms.can_publish())
self.assertTrue(christmas_page_perms.can_publish())
self.assertTrue(unpub_perms.can_publish())
self.assertFalse(homepage_perms.can_unpublish())
self.assertTrue(christmas_page_perms.can_unpublish())
self.assertFalse(unpub_perms.can_unpublish()) # cannot unpublish a page that isn't published
self.assertFalse(homepage_perms.can_publish_subpage())
self.assertTrue(christmas_page_perms.can_publish_subpage())
self.assertTrue(unpub_perms.can_publish_subpage())
self.assertFalse(homepage_perms.can_reorder_children())
self.assertTrue(christmas_page_perms.can_reorder_children())
self.assertTrue(unpub_perms.can_reorder_children())
self.assertFalse(homepage_perms.can_move())
self.assertTrue(christmas_page_perms.can_move())
self.assertTrue(unpub_perms.can_move())
self.assertTrue(christmas_page_perms.can_move_to(unpublished_event_page))
self.assertTrue(unpub_perms.can_move_to(christmas_page))
self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination
self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself
def test_inactive_user_has_no_permissions(self):
user = User.objects.get(username='inactiveuser')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
christmas_page_perms = christmas_page.permissions_for_user(user)
unpub_perms = unpublished_event_page.permissions_for_user(user)
self.assertFalse(unpub_perms.can_add_subpage())
self.assertFalse(unpub_perms.can_edit())
self.assertFalse(unpub_perms.can_delete())
self.assertFalse(unpub_perms.can_publish())
self.assertFalse(christmas_page_perms.can_unpublish())
self.assertFalse(unpub_perms.can_publish_subpage())
self.assertFalse(unpub_perms.can_reorder_children())
self.assertFalse(unpub_perms.can_move())
self.assertFalse(unpub_perms.can_move_to(christmas_page))
def test_superuser_has_full_permissions(self):
user = User.objects.get(username='superuser')
homepage = Page.objects.get(url_path='/home/')
root = Page.objects.get(url_path='/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
homepage_perms = homepage.permissions_for_user(user)
root_perms = root.permissions_for_user(user)
unpub_perms = unpublished_event_page.permissions_for_user(user)
self.assertTrue(homepage_perms.can_add_subpage())
self.assertTrue(root_perms.can_add_subpage())
self.assertTrue(homepage_perms.can_edit())
self.assertFalse(root_perms.can_edit()) # root is not a real editable page, even to superusers
self.assertTrue(homepage_perms.can_delete())
self.assertFalse(root_perms.can_delete())
self.assertTrue(homepage_perms.can_publish())
self.assertFalse(root_perms.can_publish())
self.assertTrue(homepage_perms.can_unpublish())
self.assertFalse(root_perms.can_unpublish())
self.assertFalse(unpub_perms.can_unpublish())
self.assertTrue(homepage_perms.can_publish_subpage())
self.assertTrue(root_perms.can_publish_subpage())
self.assertTrue(homepage_perms.can_reorder_children())
self.assertTrue(root_perms.can_reorder_children())
self.assertTrue(homepage_perms.can_move())
self.assertFalse(root_perms.can_move())
self.assertTrue(homepage_perms.can_move_to(root))
self.assertFalse(homepage_perms.can_move_to(unpublished_event_page))
def test_editable_pages_for_user_with_add_permission(self):
event_editor = User.objects.get(username='eventeditor')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(event_editor).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_user_with_edit_permission(self):
event_moderator = User.objects.get(username='eventmoderator')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(event_moderator).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_inactive_user(self):
user = User.objects.get(username='inactiveuser')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(user).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertFalse(editable_pages.filter(id=christmas_page.id).exists())
self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_superuser(self):
user = User.objects.get(username='superuser')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(user).editable_pages()
self.assertTrue(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())

View file

@ -0,0 +1,256 @@
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.tests.models import EventPage, EventIndex, SimplePage
class TestPageQuerySet(TestCase):
fixtures = ['test.json']
def test_live(self):
pages = Page.objects.live()
# All pages must be live
for page in pages:
self.assertTrue(page.live)
# Check that the homepage is in the results
homepage = Page.objects.get(url_path='/home/')
self.assertTrue(pages.filter(id=homepage.id).exists())
def test_not_live(self):
pages = Page.objects.not_live()
# All pages must not be live
for page in pages:
self.assertFalse(page.live)
# Check that "someone elses event" is in the results
event = Page.objects.get(url_path='/home/events/someone-elses-event/')
self.assertTrue(pages.filter(id=event.id).exists())
def test_page(self):
homepage = Page.objects.get(url_path='/home/')
pages = Page.objects.page(homepage)
# Should only select the homepage
self.assertEqual(pages.count(), 1)
self.assertEqual(pages.first(), homepage)
def test_not_page(self):
homepage = Page.objects.get(url_path='/home/')
pages = Page.objects.not_page(homepage)
# Should select everything except for the homepage
self.assertEqual(pages.count(), Page.objects.all().count() - 1)
for page in pages:
self.assertNotEqual(page, homepage)
def test_descendant_of(self):
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.descendant_of(events_index)
# Check that all pages descend from events index
for page in pages:
self.assertTrue(page.get_ancestors().filter(id=events_index.id).exists())
def test_descendant_of_inclusive(self):
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.descendant_of(events_index, inclusive=True)
# Check that all pages descend from events index, includes event index
for page in pages:
self.assertTrue(page == events_index or page.get_ancestors().filter(id=events_index.id).exists())
# Check that event index was included
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_descendant_of(self):
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_descendant_of(events_index)
# Check that no pages descend from events_index
for page in pages:
self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists())
# As this is not inclusive, events index should be in the results
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_descendant_of_inclusive(self):
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_descendant_of(events_index, inclusive=True)
# Check that all pages descend from homepage but not events index
for page in pages:
self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists())
# As this is inclusive, events index should not be in the results
self.assertFalse(pages.filter(id=events_index.id).exists())
def test_child_of(self):
homepage = Page.objects.get(url_path='/home/')
pages = Page.objects.child_of(homepage)
# Check that all pages are children of homepage
for page in pages:
self.assertEqual(page.get_parent(), homepage)
def test_not_child_of(self):
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_child_of(events_index)
# Check that all pages are not children of events_index
for page in pages:
self.assertNotEqual(page.get_parent(), events_index)
def test_ancestor_of(self):
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.ancestor_of(events_index)
self.assertEqual(pages.count(), 2)
self.assertEqual(pages[0], root_page)
self.assertEqual(pages[1], homepage)
def test_ancestor_of_inclusive(self):
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.ancestor_of(events_index, inclusive=True)
self.assertEqual(pages.count(), 3)
self.assertEqual(pages[0], root_page)
self.assertEqual(pages[1], homepage)
self.assertEqual(pages[2], events_index)
def test_not_ancestor_of(self):
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_ancestor_of(events_index)
# Test that none of the ancestors are in pages
for page in pages:
self.assertNotEqual(page, root_page)
self.assertNotEqual(page, homepage)
# Test that events index is in pages
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_ancestor_of_inclusive(self):
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_ancestor_of(events_index, inclusive=True)
# Test that none of the ancestors or the events_index are in pages
for page in pages:
self.assertNotEqual(page, root_page)
self.assertNotEqual(page, homepage)
self.assertNotEqual(page, events_index)
def test_parent_of(self):
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.parent_of(events_index)
# Pages must only contain homepage
self.assertEqual(pages.count(), 1)
self.assertEqual(pages[0], homepage)
def test_not_parent_of(self):
homepage = Page.objects.get(url_path='/home/')
events_index = Page.objects.get(url_path='/home/events/')
pages = Page.objects.not_parent_of(events_index)
# Pages must not contain homepage
for page in pages:
self.assertNotEqual(page, homepage)
# Test that events index is in pages
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_sibling_of(self):
events_index = Page.objects.get(url_path='/home/events/')
event = Page.objects.get(url_path='/home/events/christmas/')
pages = Page.objects.sibling_of(event)
# Check that all pages are children of events_index
for page in pages:
self.assertEqual(page.get_parent(), events_index)
# Check that the event is not included
self.assertFalse(pages.filter(id=event.id).exists())
def test_sibling_of_inclusive(self):
events_index = Page.objects.get(url_path='/home/events/')
event = Page.objects.get(url_path='/home/events/christmas/')
pages = Page.objects.sibling_of(event, inclusive=True)
# Check that all pages are children of events_index
for page in pages:
self.assertEqual(page.get_parent(), events_index)
# Check that the event is included
self.assertTrue(pages.filter(id=event.id).exists())
def test_not_sibling_of(self):
events_index = Page.objects.get(url_path='/home/events/')
event = Page.objects.get(url_path='/home/events/christmas/')
pages = Page.objects.not_sibling_of(event)
# Check that all pages are not children of events_index
for page in pages:
if page != event:
self.assertNotEqual(page.get_parent(), events_index)
# Check that the event is included
self.assertTrue(pages.filter(id=event.id).exists())
# Test that events index is in pages
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_not_sibling_of_inclusive(self):
events_index = Page.objects.get(url_path='/home/events/')
event = Page.objects.get(url_path='/home/events/christmas/')
pages = Page.objects.not_sibling_of(event, inclusive=True)
# Check that all pages are not children of events_index
for page in pages:
self.assertNotEqual(page.get_parent(), events_index)
# Check that the event is not included
self.assertFalse(pages.filter(id=event.id).exists())
# Test that events index is in pages
self.assertTrue(pages.filter(id=events_index.id).exists())
def test_type(self):
pages = Page.objects.type(EventPage)
# Check that all objects are EventPages
for page in pages:
self.assertIsInstance(page.specific, EventPage)
# Check that "someone elses event" is in the results
event = Page.objects.get(url_path='/home/events/someone-elses-event/')
self.assertTrue(pages.filter(id=event.id).exists())
def test_not_type(self):
pages = Page.objects.not_type(EventPage)
# Check that no objects are EventPages
for page in pages:
self.assertNotIsInstance(page.specific, EventPage)
# Check that the homepage is in the results
homepage = Page.objects.get(url_path='/home/')
self.assertTrue(pages.filter(id=homepage.id).exists())

View file

@ -0,0 +1,136 @@
from bs4 import BeautifulSoup, NavigableString
from django.test import TestCase
from wagtail.wagtailcore.whitelist import (
check_url,
attribute_rule,
allow_without_attributes,
Whitelister
)
class TestCheckUrl(TestCase):
def test_allowed_url_schemes(self):
for url_scheme in ['', 'http', 'https', 'ftp', 'mailto', 'tel']:
url = url_scheme + "://www.example.com"
self.assertTrue(bool(check_url(url)))
def test_disallowed_url_scheme(self):
self.assertFalse(bool(check_url("invalid://url")))
class TestAttributeRule(TestCase):
def setUp(self):
self.soup = BeautifulSoup('<b foo="bar">baz</b>')
def test_no_rule_for_attr(self):
"""
Test that attribute_rule() drops attributes for
which no rule has been defined.
"""
tag = self.soup.b
fn = attribute_rule({'snowman': 'barbecue'})
fn(tag)
self.assertEqual(str(tag), '<b>baz</b>')
def test_rule_true_for_attr(self):
"""
Test that attribute_rule() does not change atrributes
when the corresponding rule returns True
"""
tag = self.soup.b
fn = attribute_rule({'foo': True})
fn(tag)
self.assertEqual(str(tag), '<b foo="bar">baz</b>')
def test_rule_false_for_attr(self):
"""
Test that attribute_rule() drops atrributes
when the corresponding rule returns False
"""
tag = self.soup.b
fn = attribute_rule({'foo': False})
fn(tag)
self.assertEqual(str(tag), '<b>baz</b>')
def test_callable_called_on_attr(self):
"""
Test that when the rule returns a callable,
attribute_rule() replaces the attribute with
the result of calling the callable on the attribute.
"""
tag = self.soup.b
fn = attribute_rule({'foo': len})
fn(tag)
self.assertEqual(str(tag), '<b foo="3">baz</b>')
def test_callable_returns_None(self):
"""
Test that when the rule returns a callable,
attribute_rule() replaces the attribute with
the result of calling the callable on the attribute.
"""
tag = self.soup.b
fn = attribute_rule({'foo': lambda x: None})
fn(tag)
self.assertEqual(str(tag), '<b>baz</b>')
def test_allow_without_attributes(self):
"""
Test that attribute_rule() with will drop all
attributes.
"""
soup = BeautifulSoup('<b foo="bar" baz="quux" snowman="barbecue"></b>')
tag = soup.b
allow_without_attributes(tag)
self.assertEqual(str(tag), '<b></b>')
class TestWhitelister(TestCase):
def test_clean_unknown_node(self):
"""
Unknown node should remove a node from the parent document
"""
soup = BeautifulSoup('<foo><bar>baz</bar>quux</foo>')
tag = soup.foo
Whitelister.clean_unknown_node('', soup.bar)
self.assertEqual(str(tag), '<foo>quux</foo>')
def test_clean_tag_node_cleans_nested_recognised_node(self):
"""
<b> tags are allowed without attributes. This remains true
when tags are nested.
"""
soup = BeautifulSoup('<b><b class="delete me">foo</b></b>')
tag = soup.b
Whitelister.clean_tag_node(tag, tag)
self.assertEqual(str(tag), '<b><b>foo</b></b>')
def test_clean_tag_node_disallows_nested_unrecognised_node(self):
"""
<foo> tags should be removed, even when nested.
"""
soup = BeautifulSoup('<b><foo>bar</foo></b>')
tag = soup.b
Whitelister.clean_tag_node(tag, tag)
self.assertEqual(str(tag), '<b>bar</b>')
def test_clean_string_node_does_nothing(self):
soup = BeautifulSoup('<b>bar</b>')
string = soup.b.string
Whitelister.clean_string_node(string, string)
self.assertEqual(str(string), 'bar')
def test_clean_node_does_not_change_navigable_strings(self):
soup = BeautifulSoup('<b>bar</b>')
string = soup.b.string
Whitelister.clean_node(string, string)
self.assertEqual(str(string), 'bar')
def test_clean(self):
"""
Whitelister.clean should remove disallowed tags and attributes from
a string
"""
string = '<b foo="bar">snowman <barbecue>Yorkshire</barbecue></b>'
cleaned_string = Whitelister.clean(string)
self.assertEqual(cleaned_string, '<b>snowman Yorkshire</b>')

View file

@ -0,0 +1,99 @@
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.tests.models import EventPage, EventIndex, SimplePage
class TestPageUrlTags(TestCase):
fixtures = ['test.json']
def test_pageurl_tag(self):
response = self.client.get('/events/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
def test_slugurl_tag(self):
response = self.client.get('/events/christmas/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<a href="/events/">Back to events index</a>')
class TestIssue7(TestCase):
"""
This tests for an issue where if a site root page was moved, all the page
urls in that site would change to None.
The issue was caused by the 'wagtail_site_root_paths' cache variable not being
cleared when a site root page was moved. Which left all the child pages
thinking that they are no longer in the site and return None as their url.
Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
Discussion: https://github.com/torchbox/wagtail/issues/7
"""
fixtures = ['test.json']
def test_issue7(self):
# Get homepage, root page and site
root_page = Page.objects.get(id=1)
homepage = Page.objects.get(url_path='/home/')
default_site = Site.objects.get(is_default_site=True)
# Create a new homepage under current homepage
new_homepage = SimplePage(title="New Homepage", slug="new-homepage")
homepage.add_child(instance=new_homepage)
# Set new homepage as the site root page
default_site.root_page = new_homepage
default_site.save()
# Warm up the cache by getting the url
_ = homepage.url
# Move new homepage to root
new_homepage.move(root_page, pos='last-child')
# Get fresh instance of new_homepage
new_homepage = Page.objects.get(id=new_homepage.id)
# Check url
self.assertEqual(new_homepage.url, '/')
class TestIssue157(TestCase):
"""
This tests for an issue where if a site root pages slug was changed, all the page
urls in that site would change to None.
The issue was caused by the 'wagtail_site_root_paths' cache variable not being
cleared when a site root page was changed. Which left all the child pages
thinking that they are no longer in the site and return None as their url.
Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
Discussion: https://github.com/torchbox/wagtail/issues/157
"""
fixtures = ['test.json']
def test_issue157(self):
# Get homepage
homepage = Page.objects.get(url_path='/home/')
# Warm up the cache by getting the url
_ = homepage.url
# Change homepage title and slug
homepage.title = "New home"
homepage.slug = "new-home"
homepage.save()
# Get fresh instance of homepage
homepage = Page.objects.get(id=homepage.id)
# Check url
self.assertEqual(homepage.url, '/')

View file

@ -89,7 +89,10 @@ class Whitelister(object):
cls.clean_string_node(doc, node)
elif isinstance(node, Tag):
cls.clean_tag_node(doc, node)
else:
# This branch is here in case node is a BeautifulSoup object that does
# not inherit from NavigableString or Tag. I can't find any examples
# of such a thing at the moment, so this branch is untested.
else: # pragma: no cover
cls.clean_unknown_node(doc, node)
@classmethod

View file

@ -1,6 +1,6 @@
from django.test import TestCase
from wagtail.wagtaildocs import models
from wagtail.tests.utils import login
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
@ -39,15 +39,17 @@ class TestDocumentPermissions(TestCase):
## ===== ADMIN VIEWS =====
class TestDocumentIndexView(TestCase):
class TestDocumentIndexView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtaildocs_index'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtaildocs/documents/index.html')
def test_search(self):
response = self.get({'q': "Hello"})
@ -67,20 +69,24 @@ class TestDocumentIndexView(TestCase):
self.assertEqual(response.status_code, 200)
class TestDocumentAddView(TestCase):
class TestDocumentAddView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtaildocs_add_document'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtaildocs/documents/add.html')
# TODO: Test posting
class TestDocumentEditView(TestCase):
class TestDocumentEditView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
# Create a document to edit
self.document = models.Document.objects.create(title="Test document")
@ -88,13 +94,17 @@ class TestDocumentEditView(TestCase):
def get(self, params={}):
return self.client.get(reverse('wagtaildocs_edit_document', args=(self.document.id,)), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtaildocs/documents/edit.html')
# TODO: Test posting
class TestDocumentDeleteView(TestCase):
class TestDocumentDeleteView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
# Create a document to delete
self.document = models.Document.objects.create(title="Test document")
@ -102,19 +112,26 @@ class TestDocumentDeleteView(TestCase):
def get(self, params={}):
return self.client.get(reverse('wagtaildocs_delete_document', args=(self.document.id,)), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtaildocs/documents/confirm_delete.html')
# TODO: Test posting
class TestDocumentChooserView(TestCase):
class TestDocumentChooserView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtaildocs_chooser'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtaildocs/chooser/chooser.html')
self.assertTemplateUsed(response, 'wagtaildocs/chooser/chooser.js')
def test_search(self):
response = self.get({'q': "Hello"})
@ -128,9 +145,9 @@ class TestDocumentChooserView(TestCase):
self.assertEqual(response.status_code, 200)
class TestDocumentChooserChosenView(TestCase):
class TestDocumentChooserChosenView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
# Create a document to choose
self.document = models.Document.objects.create(title="Test document")
@ -138,19 +155,28 @@ class TestDocumentChooserChosenView(TestCase):
def get(self, params={}):
return self.client.get(reverse('wagtaildocs_document_chosen', args=(self.document.id,)), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtaildocs/chooser/document_chosen.js')
# TODO: Test posting
class TestDocumentChooserUploadView(TestCase):
class TestDocumentChooserUploadView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtaildocs_chooser_upload'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtaildocs/chooser/chooser.html')
self.assertTemplateUsed(response, 'wagtaildocs/chooser/chooser.js')
# TODO: Test document upload with chooser
class TestDocumentFilenameProperties(TestCase):

View file

@ -16,7 +16,6 @@ import json
class EmbedNotFoundException(Exception): pass
class EmbedlyException(Exception): pass
class AccessDeniedEmbedlyException(EmbedlyException): pass
@ -52,7 +51,7 @@ def embedly(url, max_width=None, key=None):
key = settings.EMBEDLY_KEY
# Get embedly client
client = Embedly(key=settings.EMBEDLY_KEY)
client = Embedly(key=key)
# Call embedly
if max_width is not None:

View file

@ -1,7 +1,27 @@
from mock import patch
import urllib2
try:
import embedly
no_embedly = False
except ImportError:
no_embedly = True
from django.test import TestCase
from django.test.client import Client
from wagtail.tests.utils import login
from wagtail.tests.utils import WagtailTestUtils
from wagtail.tests.utils import unittest
from wagtail.wagtailembeds import get_embed
from wagtail.wagtailembeds.embeds import (
EmbedNotFoundException,
EmbedlyException,
AccessDeniedEmbedlyException,
)
from wagtail.wagtailembeds.embeds import embedly as wagtail_embedly
from wagtail.wagtailembeds.embeds import oembed as wagtail_oembed
class TestEmbeds(TestCase):
@ -63,13 +83,168 @@ class TestEmbeds(TestCase):
self.assertEqual(embed.width, None)
class TestChooser(TestCase):
class TestChooser(TestCase, WagtailTestUtils):
def setUp(self):
# login
login(self.client)
self.login()
def test_chooser(self):
r = self.client.get('/admin/embeds/chooser/')
self.assertEqual(r.status_code, 200)
# TODO: Test submitting
class TestEmbedly(TestCase):
@unittest.skipIf(no_embedly, "Embedly is not installed")
def test_embedly_oembed_called_with_correct_arguments(self):
with patch('embedly.Embedly.oembed') as oembed:
oembed.return_value = {'type': 'photo',
'url': 'http://www.example.com'}
wagtail_embedly('http://www.example.com', key='foo')
oembed.assert_called_with('http://www.example.com', better=False)
wagtail_embedly('http://www.example.com', max_width=100, key='foo')
oembed.assert_called_with('http://www.example.com', maxwidth=100, better=False)
@unittest.skipIf(no_embedly, "Embedly is not installed")
def test_embedly_401(self):
with patch('embedly.Embedly.oembed') as oembed:
oembed.return_value = {'type': 'photo',
'url': 'http://www.example.com',
'error': True,
'error_code': 401}
self.assertRaises(AccessDeniedEmbedlyException,
wagtail_embedly, 'http://www.example.com', key='foo')
@unittest.skipIf(no_embedly, "Embedly is not installed")
def test_embedly_403(self):
with patch('embedly.Embedly.oembed') as oembed:
oembed.return_value = {'type': 'photo',
'url': 'http://www.example.com',
'error': True,
'error_code': 403}
self.assertRaises(AccessDeniedEmbedlyException,
wagtail_embedly, 'http://www.example.com', key='foo')
@unittest.skipIf(no_embedly, "Embedly is not installed")
def test_embedly_404(self):
with patch('embedly.Embedly.oembed') as oembed:
oembed.return_value = {'type': 'photo',
'url': 'http://www.example.com',
'error': True,
'error_code': 404}
self.assertRaises(EmbedNotFoundException,
wagtail_embedly, 'http://www.example.com', key='foo')
@unittest.skipIf(no_embedly, "Embedly is not installed")
def test_embedly_other_error(self):
with patch('embedly.Embedly.oembed') as oembed:
oembed.return_value = {'type': 'photo',
'url': 'http://www.example.com',
'error': True,
'error_code': 999}
self.assertRaises(EmbedlyException, wagtail_embedly,
'http://www.example.com', key='foo')
@unittest.skipIf(no_embedly, "Embedly is not installed")
def test_embedly_html_conversion(self):
with patch('embedly.Embedly.oembed') as oembed:
oembed.return_value = {'type': 'photo',
'url': 'http://www.example.com'}
result = wagtail_embedly('http://www.example.com', key='foo')
self.assertEqual(result['html'], '<img src="http://www.example.com" />')
oembed.return_value = {'type': 'something else',
'html': '<foo>bar</foo>'}
result = wagtail_embedly('http://www.example.com', key='foo')
self.assertEqual(result['html'], '<foo>bar</foo>')
@unittest.skipIf(no_embedly, "Embedly is not installed")
def test_embedly_return_value(self):
with patch('embedly.Embedly.oembed') as oembed:
oembed.return_value = {'type': 'something else',
'html': '<foo>bar</foo>'}
result = wagtail_embedly('http://www.example.com', key='foo')
self.assertEqual(result, {
'title': '',
'author_name': '',
'provider_name': '',
'type': 'something else',
'thumbnail_url': None,
'width': None,
'height': None,
'html': '<foo>bar</foo>'})
oembed.return_value = {'type': 'something else',
'author_name': 'Alice',
'provider_name': 'Bob',
'title': 'foo',
'thumbnail_url': 'http://www.example.com',
'width': 100,
'height': 100,
'html': '<foo>bar</foo>'}
result = wagtail_embedly('http://www.example.com', key='foo')
self.assertEqual(result, {'type': 'something else',
'author_name': 'Alice',
'provider_name': 'Bob',
'title': 'foo',
'thumbnail_url': 'http://www.example.com',
'width': 100,
'height': 100,
'html': '<foo>bar</foo>'})
class TestOembed(TestCase):
def setUp(self):
class DummyResponse(object):
def read(self):
return "foo"
self.dummy_response = DummyResponse()
def test_oembed_invalid_provider(self):
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:
self.assertRaises(EmbedNotFoundException, wagtail_oembed,
"http://www.youtube.com/watch/")
@patch('urllib2.urlopen')
@patch('json.loads')
def test_oembed_photo_request(self, loads, urlopen) :
urlopen.return_value = self.dummy_response
loads.return_value = {'type': 'photo',
'url': 'http://www.example.com'}
result = wagtail_oembed("http://www.youtube.com/watch/")
self.assertEqual(result['type'], 'photo')
self.assertEqual(result['html'], '<img src="http://www.example.com" />')
loads.assert_called_with("foo")
@patch('urllib2.urlopen')
@patch('json.loads')
def test_oembed_return_values(self, loads, urlopen):
urlopen.return_value = self.dummy_response
loads.return_value = {
'type': 'something',
'url': 'http://www.example.com',
'title': 'test_title',
'author_name': 'test_author',
'provider_name': 'test_provider_name',
'thumbnail_url': 'test_thumbail_url',
'width': 'test_width',
'height': 'test_height',
'html': 'test_html'
}
result = wagtail_oembed("http://www.youtube.com/watch/")
self.assertEqual(result, {
'type': 'something',
'title': 'test_title',
'author_name': 'test_author',
'provider_name': 'test_provider_name',
'thumbnail_url': 'test_thumbail_url',
'width': 'test_width',
'height': 'test_height',
'html': 'test_html'
})

View file

@ -1,6 +1,7 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
class InvalidImageBackendError(ImproperlyConfigured):
pass
@ -8,23 +9,22 @@ class InvalidImageBackendError(ImproperlyConfigured):
class BaseImageBackend(object):
def __init__(self, params):
self.quality = getattr(settings, 'IMAGE_COMPRESSION_QUALITY', 85)
def open_image(self, input_file):
"""
Open an image and return the backend specific image object to pass
to other methods. The object return has to have a size attribute
Open an image and return the backend specific image object to pass
to other methods. The object return has to have a size attribute
which is a tuple with the width and height of the image and a format
attribute with the format of the image.
"""
raise NotImplementedError('subclasses of BaseImageBackend must provide an open_image() method')
def save_image(self, image, output):
"""
Save the image to the output
"""
raise NotImplementedError('subclasses of BaseImageBackend must provide a save_image() method')
def resize(self, image, size):
"""
resize image to the requested size, using highest quality settings
@ -32,11 +32,9 @@ class BaseImageBackend(object):
"""
raise NotImplementedError('subclasses of BaseImageBackend must provide an resize() method')
def crop_to_centre(self, image, size):
raise NotImplementedError('subclasses of BaseImageBackend must provide a crop_to_centre() method')
def resize_to_max(self, image, size):
"""
Resize image down to fit within the given dimensions, preserving aspect ratio.
@ -58,10 +56,8 @@ class BaseImageBackend(object):
final_size = (target_width, int(original_height * horz_scale))
else:
final_size = (int(original_width * vert_scale), target_height)
return self.resize(image, final_size)
return self.resize(image, final_size)
def resize_to_min(self, image, size):
"""
@ -87,7 +83,6 @@ class BaseImageBackend(object):
return self.resize(image, final_size)
def resize_to_width(self, image, target_width):
"""
Resize image down to the given width, preserving aspect ratio.
@ -104,7 +99,6 @@ class BaseImageBackend(object):
return self.resize(image, final_size)
def resize_to_height(self, image, target_height):
"""
Resize image down to the given height, preserving aspect ratio.
@ -121,7 +115,6 @@ class BaseImageBackend(object):
return self.resize(image, final_size)
def resize_to_fill(self, image, size):
"""
Resize down and crop image to fill the given dimensions. Most suitable for thumbnails.
@ -130,3 +123,8 @@ class BaseImageBackend(object):
"""
resized_image = self.resize_to_min(image, size)
return self.crop_to_centre(resized_image, size)
def no_operation(self, image, param):
"""Return the image unchanged"""
return image

View file

@ -1,6 +1,9 @@
from base import BaseImageBackend
from __future__ import absolute_import
from .base import BaseImageBackend
import PIL.Image
class PillowBackend(BaseImageBackend):
def __init__(self, params):
super(PillowBackend, self).__init__(params)
@ -32,4 +35,4 @@ class PillowBackend(BaseImageBackend):
top = (original_height - final_height) / 2
return image.crop(
(left, top, left + final_width, top + final_height)
)
)

View file

@ -1,8 +1,9 @@
from __future__ import absolute_import
from .base import BaseImageBackend
from wand.image import Image
from wand.api import library
class WandBackend(BaseImageBackend):
def __init__(self, params):
@ -10,6 +11,7 @@ class WandBackend(BaseImageBackend):
def open_image(self, input_file):
image = Image(file=input_file)
image.wand = library.MagickCoalesceImages(image.wand)
return image
def save_image(self, image, output, format):
@ -18,8 +20,9 @@ class WandBackend(BaseImageBackend):
image.save(file=output)
def resize(self, image, size):
image.resize(size[0], size[1])
return image
new_image = image.clone()
new_image.resize(size[0], size[1])
return new_image
def crop_to_centre(self, image, size):
(original_width, original_height) = image.size
@ -34,7 +37,9 @@ class WandBackend(BaseImageBackend):
left = (original_width - final_width) / 2
top = (original_height - final_height) / 2
image.crop(
new_image = image.clone()
new_image.crop(
left=left, top=top, right=left + final_width, bottom=top + final_height
)
return image
return new_image

View file

@ -1,5 +1,6 @@
import StringIO
import os.path
import re
from taggit.managers import TaggableManager
@ -150,6 +151,7 @@ class Filter(models.Model):
'width': 'resize_to_width',
'height': 'resize_to_height',
'fill': 'resize_to_fill',
'original': 'no_operation',
}
def __init__(self, *args, **kwargs):
@ -157,22 +159,34 @@ class Filter(models.Model):
self.method = None # will be populated when needed, by parsing the spec string
def _parse_spec_string(self):
# parse the spec string, which is formatted as (method)-(arg),
# and save the results to self.method_name and self.method_arg
try:
(method_name_simple, method_arg_string) = self.spec.split('-')
self.method_name = Filter.OPERATION_NAMES[method_name_simple]
# parse the spec string and save the results to
# self.method_name and self.method_arg. There are various possible
# formats to match against:
# 'original'
# 'width-200'
# 'max-320x200'
if method_name_simple in ('max', 'min', 'fill'):
# method_arg_string is in the form 640x480
(width, height) = [int(i) for i in method_arg_string.split('x')]
self.method_arg = (width, height)
else:
# method_arg_string is a single number
self.method_arg = int(method_arg_string)
if self.spec == 'original':
self.method_name = Filter.OPERATION_NAMES['original']
self.method_arg = None
return
except (ValueError, KeyError):
raise ValueError("Invalid image filter spec: %r" % self.spec)
match = re.match(r'(width|height)-(\d+)$', self.spec)
if match:
self.method_name = Filter.OPERATION_NAMES[match.group(1)]
self.method_arg = int(match.group(2))
return
match = re.match(r'(max|min|fill)-(\d+)x(\d+)$', self.spec)
if match:
self.method_name = Filter.OPERATION_NAMES[match.group(1)]
width = int(match.group(2))
height = int(match.group(3))
self.method_arg = (width, height)
return
# Spec is not one of our recognised patterns
raise ValueError("Invalid image filter spec: %r" % self.spec)
def process_image(self, input_file, backend_name='default'):
"""
@ -180,27 +194,25 @@ class Filter(models.Model):
generate an output image with this filter applied, returning it
as another django.core.files.File object
"""
backend = get_image_backend(backend_name)
if not self.method:
self._parse_spec_string()
# If file is closed, open it
input_file.open('rb')
image = backend.open_image(input_file)
file_format = image.format
method = getattr(backend, self.method_name)
image = method(image, self.method_arg)
output = StringIO.StringIO()
backend.save_image(image, output, file_format)
# and then close the input file
input_file.close()
# generate new filename derived from old one, inserting the filter spec string before the extension
input_filename_parts = os.path.basename(input_file.name).split('.')
@ -210,7 +222,6 @@ class Filter(models.Model):
output_filename = '.'.join(output_filename_parts)
output_file = File(output, name=output_filename)
return output_file

View file

@ -1,4 +1,6 @@
function(modal) {
var searchUrl = $('form.image-search', modal.body).attr('action');
function ajaxifyLinks (context) {
$('.listing a', context).click(function() {
modal.loadUrl(this.href);
@ -12,7 +14,6 @@ function(modal) {
});
}
var searchUrl = $('form.image-search', modal.body).attr('action');
function search() {
$.ajax({
url: searchUrl,
@ -24,8 +25,8 @@ function(modal) {
});
return false;
}
function setPage(page) {
function setPage(page) {
if($('#id_q').val().length){
dataObj = {q: $('#id_q').val(), p: page};
}else{

View file

@ -4,7 +4,7 @@ from django.contrib.auth.models import User, Group, Permission
from django.core.urlresolvers import reverse
from django.core.files.uploadedfile import SimpleUploadedFile
from wagtail.tests.utils import login, unittest
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.templatetags import image_tags
@ -84,10 +84,10 @@ class TestRenditions(TestCase):
# default backend should be pillow
backend = get_image_backend()
self.assertTrue(isinstance(backend, PillowBackend))
def test_minification(self):
rendition = self.image.get_rendition('width-400')
# Check size
self.assertEqual(rendition.width, 400)
self.assertEqual(rendition.height, 300)
@ -107,6 +107,13 @@ class TestRenditions(TestCase):
self.assertEqual(rendition.width, 160)
self.assertEqual(rendition.height, 120)
def test_resize_to_original(self):
rendition = self.image.get_rendition('original')
# Check size
self.assertEqual(rendition.width, 640)
self.assertEqual(rendition.height, 480)
def test_cache(self):
# Get two renditions with the same filter
first_rendition = self.image.get_rendition('width-400')
@ -114,7 +121,7 @@ class TestRenditions(TestCase):
# Check that they are the same object
self.assertEqual(first_rendition, second_rendition)
class TestRenditionsWand(TestCase):
def setUp(self):
@ -134,18 +141,18 @@ class TestRenditionsWand(TestCase):
def test_minification(self):
rendition = self.image.get_rendition('width-400')
# Check size
self.assertEqual(rendition.width, 400)
self.assertEqual(rendition.height, 300)
def test_resize_to_max(self):
rendition = self.image.get_rendition('max-100x100')
# Check size
self.assertEqual(rendition.width, 100)
self.assertEqual(rendition.height, 75)
def test_resize_to_min(self):
rendition = self.image.get_rendition('min-120x120')
@ -153,6 +160,13 @@ class TestRenditionsWand(TestCase):
self.assertEqual(rendition.width, 160)
self.assertEqual(rendition.height, 120)
def test_resize_to_original(self):
rendition = self.image.get_rendition('original')
# Check size
self.assertEqual(rendition.width, 640)
self.assertEqual(rendition.height, 480)
def test_cache(self):
# Get two renditions with the same filter
first_rendition = self.image.get_rendition('width-400')
@ -160,7 +174,7 @@ class TestRenditionsWand(TestCase):
# Check that they are the same object
self.assertEqual(first_rendition, second_rendition)
class TestImageTag(TestCase):
def setUp(self):
@ -187,15 +201,17 @@ class TestImageTag(TestCase):
## ===== ADMIN VIEWS =====
class TestImageIndexView(TestCase):
class TestImageIndexView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailimages_index'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/images/index.html')
def test_search(self):
response = self.get({'q': "Hello"})
@ -215,9 +231,9 @@ class TestImageIndexView(TestCase):
self.assertEqual(response.status_code, 200)
class TestImageAddView(TestCase):
class TestImageAddView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailimages_add_image'), params)
@ -225,8 +241,10 @@ class TestImageAddView(TestCase):
def post(self, post_data={}):
return self.client.post(reverse('wagtailimages_add_image'), post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/images/add.html')
def test_add(self):
response = self.post({
@ -236,6 +254,7 @@ class TestImageAddView(TestCase):
# Should redirect back to index
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailimages_index'))
# Check that the image was created
images = Image.objects.filter(title="Test image")
@ -247,9 +266,9 @@ class TestImageAddView(TestCase):
self.assertEqual(image.height, 480)
class TestImageEditView(TestCase):
class TestImageEditView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
# Create an image to edit
self.image = Image.objects.create(
@ -263,8 +282,10 @@ class TestImageEditView(TestCase):
def post(self, post_data={}):
return self.client.post(reverse('wagtailimages_edit_image', args=(self.image.id,)), post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/images/edit.html')
def test_edit(self):
response = self.post({
@ -273,15 +294,16 @@ class TestImageEditView(TestCase):
# Should redirect back to index
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailimages_index'))
# Check that the image was edited
image = Image.objects.get(id=self.image.id)
self.assertEqual(image.title, "Edited")
class TestImageDeleteView(TestCase):
class TestImageDeleteView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
# Create an image to edit
self.image = Image.objects.create(
@ -295,8 +317,10 @@ class TestImageDeleteView(TestCase):
def post(self, post_data={}):
return self.client.post(reverse('wagtailimages_delete_image', args=(self.image.id,)), post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/images/confirm_delete.html')
def test_delete(self):
response = self.post({
@ -305,21 +329,25 @@ class TestImageDeleteView(TestCase):
# Should redirect back to index
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailimages_index'))
# Check that the image was deleted
images = Image.objects.filter(title="Test image")
self.assertEqual(images.count(), 0)
class TestImageChooserView(TestCase):
class TestImageChooserView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailimages_chooser'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/chooser/chooser.html')
self.assertTemplateUsed(response, 'wagtailimages/chooser/chooser.js')
def test_search(self):
response = self.get({'q': "Hello"})
@ -333,9 +361,9 @@ class TestImageChooserView(TestCase):
self.assertEqual(response.status_code, 200)
class TestImageChooserChosenView(TestCase):
class TestImageChooserChosenView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
# Create an image to edit
self.image = Image.objects.create(
@ -346,16 +374,25 @@ class TestImageChooserChosenView(TestCase):
def get(self, params={}):
return self.client.get(reverse('wagtailimages_image_chosen', args=(self.image.id,)), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/chooser/image_chosen.js')
# TODO: Test posting
class TestImageChooserUploadView(TestCase):
class TestImageChooserUploadView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailimages_chooser_upload'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/chooser/chooser.html')
self.assertTemplateUsed(response, 'wagtailimages/chooser/chooser.js')
# TODO: Test uploading through chooser

View file

@ -1,7 +1,7 @@
from django.test import TestCase
from django.test.client import Client
from wagtail.wagtailredirects import models
from wagtail.tests.utils import login
from wagtail.tests.utils import WagtailTestUtils
from django.core.urlresolvers import reverse
@ -66,15 +66,17 @@ class TestRedirects(TestCase):
self.assertTrue(r.has_header('Location'))
class TestRedirectsIndexView(TestCase):
class TestRedirectsIndexView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailredirects_index'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailredirects/index.html')
def test_search(self):
response = self.get({'q': "Hello"})
@ -88,9 +90,9 @@ class TestRedirectsIndexView(TestCase):
self.assertEqual(response.status_code, 200)
class TestRedirectsAddView(TestCase):
class TestRedirectsAddView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailredirects_add_redirect'), params)
@ -98,8 +100,10 @@ class TestRedirectsAddView(TestCase):
def post(self, post_data={}):
return self.client.post(reverse('wagtailredirects_add_redirect'), post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailredirects/add.html')
def test_add(self):
response = self.post({
@ -110,6 +114,7 @@ class TestRedirectsAddView(TestCase):
# Should redirect back to index
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailredirects_index'))
# Check that the redirect was created
redirects = models.Redirect.objects.filter(old_path='/test')
@ -127,14 +132,14 @@ class TestRedirectsAddView(TestCase):
self.assertEqual(response.status_code, 200)
class TestRedirectsEditView(TestCase):
class TestRedirectsEditView(TestCase, WagtailTestUtils):
def setUp(self):
# Create a redirect to edit
self.redirect = models.Redirect(old_path='/test', redirect_link='http://www.test.com/')
self.redirect.save()
# Login
login(self.client)
self.login()
def get(self, params={}, redirect_id=None):
return self.client.get(reverse('wagtailredirects_edit_redirect', args=(redirect_id or self.redirect.id, )), params)
@ -142,8 +147,10 @@ class TestRedirectsEditView(TestCase):
def post(self, post_data={}, redirect_id=None):
return self.client.post(reverse('wagtailredirects_edit_redirect', args=(redirect_id or self.redirect.id, )), post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailredirects/edit.html')
def test_nonexistant_redirect(self):
self.assertEqual(self.get(redirect_id=100000).status_code, 404)
@ -157,6 +164,7 @@ class TestRedirectsEditView(TestCase):
# Should redirect back to index
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailredirects_index'))
# Check that the redirect was edited
redirects = models.Redirect.objects.filter(old_path='/test')
@ -173,14 +181,14 @@ class TestRedirectsEditView(TestCase):
# Should not redirect to index
self.assertEqual(response.status_code, 200)
class TestRedirectsDeleteView(TestCase):
class TestRedirectsDeleteView(TestCase, WagtailTestUtils):
def setUp(self):
# Create a redirect to edit
self.redirect = models.Redirect(old_path='/test', redirect_link='http://www.test.com/')
self.redirect.save()
# Login
login(self.client)
self.login()
def get(self, params={}, redirect_id=None):
return self.client.get(reverse('wagtailredirects_delete_redirect', args=(redirect_id or self.redirect.id, )), params)
@ -188,8 +196,10 @@ class TestRedirectsDeleteView(TestCase):
def post(self, post_data={}, redirect_id=None):
return self.client.post(reverse('wagtailredirects_delete_redirect', args=(redirect_id or self.redirect.id, )), post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailredirects/confirm_delete.html')
def test_nonexistant_redirect(self):
self.assertEqual(self.get(redirect_id=100000).status_code, 404)
@ -201,6 +211,7 @@ class TestRedirectsDeleteView(TestCase):
# Should redirect back to index
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailredirects_index'))
# Check that the redirect was deleted
redirects = models.Redirect.objects.filter(old_path='/test')

View file

@ -31,6 +31,8 @@ EditorsPickFormSetBase = inlineformset_factory(models.Query, models.EditorsPick,
class EditorsPickFormSet(EditorsPickFormSetBase):
minimum_forms = 1
minimum_forms_message = _("Please specify at least one recommendation for this search term.")
def add_fields(self, form, *args, **kwargs):
super(EditorsPickFormSet, self).add_fields(form, *args, **kwargs)
@ -40,3 +42,20 @@ class EditorsPickFormSet(EditorsPickFormSetBase):
# Remove query field
del form.fields['query']
def clean(self):
# Editors pick must have at least one recommended page to be valid
# 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()):
form = self.forms[i]
if self.can_delete and self._should_delete_form(form):
non_deleted_forms -= 1
if not (form.instance.id is None and not form.has_changed()):
non_empty_forms += 1
if (
non_deleted_forms < self.minimum_forms
or non_empty_forms < self.minimum_forms
):
raise forms.ValidationError(self.minimum_forms_message)

View file

@ -5,9 +5,10 @@ from indexed import Indexed
import datetime
import string
MAX_QUERY_STRING_LENGTH = 255
class Query(models.Model):
query_string = models.CharField(max_length=255, unique=True)
query_string = models.CharField(max_length=MAX_QUERY_STRING_LENGTH, unique=True)
def save(self, *args, **kwargs):
# Normalise query string
@ -48,6 +49,9 @@ class Query(models.Model):
@staticmethod
def normalise_query_string(query_string):
# Truncate query string
if len(query_string) > MAX_QUERY_STRING_LENGTH:
query_string = query_string[:MAX_QUERY_STRING_LENGTH]
# Convert query_string to lowercase
query_string = query_string.lower()

View file

@ -6,7 +6,7 @@
{% include "wagtailadmin/shared/header.html" with title=add_str icon="pick" %}
<div class="nice-padding">
{% blocktrans %}s
{% blocktrans %}
<p>Editors picks are a means of recommending specific pages that might not organically come high up in search results. E.g recommending your primary donation page to a user searching with a less common term like "<em>giving</em>".</p>
{% endblocktrans %}
{% blocktrans %}

View file

@ -1,5 +1,5 @@
from django.test import TestCase
from wagtail.tests.utils import login
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailsearch import models
@ -45,15 +45,17 @@ class TestEditorsPicks(TestCase):
self.assertEqual(models.Query.get("root page").editors_picks.last().description, "Last editors pick")
class TestEditorsPicksIndexView(TestCase):
class TestEditorsPicksIndexView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get('/admin/search/editorspicks/', params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/index.html')
def test_search(self):
response = self.get({'q': "Hello"})
@ -67,20 +69,24 @@ class TestEditorsPicksIndexView(TestCase):
self.assertEqual(response.status_code, 200)
class TestEditorsPicksAddView(TestCase):
class TestEditorsPicksAddView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get('/admin/search/editorspicks/add/', params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/add.html')
# TODO: Test posting
class TestEditorsPicksEditView(TestCase):
class TestEditorsPicksEditView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
# Create an editors pick to edit
self.query = models.Query.get("Hello")
@ -89,13 +95,17 @@ class TestEditorsPicksEditView(TestCase):
def get(self, params={}):
return self.client.get('/admin/search/editorspicks/' + str(self.query.id) + '/', params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/edit.html')
# TODO: Test posting
class TestEditorsPicksDeleteView(TestCase):
class TestEditorsPicksDeleteView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
# Create an editors pick to delete
self.query = models.Query.get("Hello")
@ -104,5 +114,9 @@ class TestEditorsPicksDeleteView(TestCase):
def get(self, params={}):
return self.client.get('/admin/search/editorspicks/' + str(self.query.id) + '/delete/', params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/confirm_delete.html')
# TODO: Test posting

View file

@ -5,8 +5,10 @@ class TestSearchView(TestCase):
def get(self, params={}):
return self.client.get('/search/', params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsearch/search_results.html')
def test_search(self):
response = self.get({'q': "Hello"})
@ -24,8 +26,10 @@ class TestSuggestionsView(TestCase):
def get(self, params={}):
return self.client.get('/search/suggest/', params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
# TODO: Check that a valid JSON document was returned
def test_search(self):
response = self.get({'q': "Hello"})

View file

@ -1,7 +1,7 @@
from django.test import TestCase
from django.core import management
from wagtail.wagtailsearch import models
from wagtail.tests.utils import login, unittest
from wagtail.tests.utils import unittest, WagtailTestUtils
from StringIO import StringIO
@ -53,6 +53,16 @@ class TestQueryStringNormalisation(TestCase):
for query in queries:
self.assertNotEqual(self.query, models.Query.get(query))
def test_truncation(self):
test_querystring = 'a' * 1000
result = models.Query.normalise_query_string(test_querystring)
self.assertEqual(len(result), 255)
def test_no_truncation(self):
test_querystring = 'a' * 10
result = models.Query.normalise_query_string(test_querystring)
self.assertEqual(len(result), 10)
class TestQueryPopularity(TestCase):
def test_query_popularity(self):
@ -139,15 +149,18 @@ class TestGarbageCollectCommand(TestCase):
# TODO: Test that this command is acctually doing its job
class TestQueryChooserView(TestCase):
class TestQueryChooserView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get('/admin/search/queries/chooser/', params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsearch/queries/chooser/chooser.html')
self.assertTemplateUsed(response, 'wagtailsearch/queries/chooser/chooser.js')
def test_search(self):
response = self.get({'q': "Hello"})

View file

@ -45,12 +45,12 @@ def index(request):
def save_editorspicks(query, new_query, editors_pick_formset):
# Set sort_order
for i, form in enumerate(editors_pick_formset.ordered_forms):
form.instance.sort_order = i
# Save
if editors_pick_formset.is_valid():
# Set sort_order
for i, form in enumerate(editors_pick_formset.ordered_forms):
form.instance.sort_order = i
editors_pick_formset.save()
# If query was changed, move all editors picks to the new query
@ -72,10 +72,14 @@ def add(request):
# Save editors picks
editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)
if save_editorspicks(query, query, editors_pick_formset):
messages.success(request, _("Editor's picks for '{0}' created.").format(query))
return redirect('wagtailsearch_editorspicks_index')
else:
if len(editors_pick_formset.non_form_errors()):
messages.error(request, " ".join(error for error in editors_pick_formset.non_form_errors())) # formset level error (e.g. no forms submitted)
else:
messages.error(request, _("Recommendations have not been created due to errors")) # specific errors will be displayed within form fields
else:
editors_pick_formset = forms.EditorsPickFormSet()
else:
@ -95,15 +99,22 @@ def edit(request, query_id):
if request.POST:
# Get query
query_form = forms.QueryForm(request.POST)
# and the recommendations
editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)
if query_form.is_valid():
new_query = models.Query.get(query_form['query_string'].value())
# Save editors picks
editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)
if save_editorspicks(query, new_query, editors_pick_formset):
messages.success(request, _("Editor's picks for '{0}' updated.").format(new_query))
return redirect('wagtailsearch_editorspicks_index')
else:
if len(editors_pick_formset.non_form_errors()):
messages.error(request, " ".join(error for error in editors_pick_formset.non_form_errors())) # formset level error (e.g. no forms submitted)
else:
messages.error(request, _("Recommendations have not been saved due to errors")) # specific errors will be displayed within form fields
else:
query_form = forms.QueryForm(initial=dict(query_string=query.query_string))
editors_pick_formset = forms.EditorsPickFormSet(instance=query)

View file

@ -19,3 +19,4 @@ def get_snippet_content_types():
def register_snippet(model):
if model not in SNIPPET_MODELS:
SNIPPET_MODELS.append(model)
SNIPPET_MODELS.sort(key=lambda x: x._meta.verbose_name)

View file

@ -2,45 +2,50 @@ from django.test import TestCase
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from wagtail.tests.utils import login, unittest
from wagtail.tests.models import Advert
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.tests.models import Advert, AlphaSnippet, ZuluSnippet
from wagtail.wagtailsnippets.models import register_snippet, SNIPPET_MODELS
from wagtail.wagtailsnippets.views.snippets import get_content_type_from_url_params, get_snippet_edit_handler
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
class TestSnippetIndexView(TestCase):
class TestSnippetIndexView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailsnippets_index'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsnippets/snippets/index.html')
def test_displays_snippet(self):
self.assertContains(self.get(), "Adverts")
class TestSnippetListView(TestCase):
class TestSnippetListView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailsnippets_list',
args=('tests', 'advert')),
params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsnippets/snippets/type_index.html')
def test_displays_add_button(self):
self.assertContains(self.get(), "Add advert")
class TestSnippetCreateView(TestCase):
class TestSnippetCreateView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailsnippets_create',
@ -52,8 +57,10 @@ class TestSnippetCreateView(TestCase):
args=('tests', 'advert')),
post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsnippets/snippets/create.html')
def test_create_invalid(self):
response = self.post(post_data={'foo': 'bar'})
@ -64,20 +71,21 @@ class TestSnippetCreateView(TestCase):
response = self.post(post_data={'text': 'test_advert',
'url': 'http://www.example.com/'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailsnippets_list', args=('tests', 'advert')))
snippets = Advert.objects.filter(text='test_advert')
self.assertEqual(snippets.count(), 1)
self.assertEqual(snippets.first().url, 'http://www.example.com/')
class TestSnippetEditView(TestCase):
class TestSnippetEditView(TestCase, WagtailTestUtils):
def setUp(self):
self.test_snippet = Advert()
self.test_snippet.text = 'test_advert'
self.test_snippet.url = 'http://www.example.com/'
self.test_snippet.save()
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailsnippets_edit',
@ -89,8 +97,10 @@ class TestSnippetEditView(TestCase):
args=('tests', 'advert', self.test_snippet.id)),
post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsnippets/snippets/edit.html')
def test_non_existant_model(self):
response = self.client.get(reverse('wagtailsnippets_edit',
@ -111,20 +121,21 @@ class TestSnippetEditView(TestCase):
response = self.post(post_data={'text': 'edited_test_advert',
'url': 'http://www.example.com/edited'})
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailsnippets_list', args=('tests', 'advert')))
snippets = Advert.objects.filter(text='edited_test_advert')
self.assertEqual(snippets.count(), 1)
self.assertEqual(snippets.first().url, 'http://www.example.com/edited')
class TestSnippetDelete(TestCase):
class TestSnippetDelete(TestCase, WagtailTestUtils):
def setUp(self):
self.test_snippet = Advert()
self.test_snippet.text = 'test_advert'
self.test_snippet.url = 'http://www.example.com/'
self.test_snippet.save()
login(self.client)
self.login()
def test_delete_get(self):
response = self.client.get(reverse('wagtailsnippets_delete', args=('tests', 'advert', self.test_snippet.id, )))
@ -136,6 +147,7 @@ class TestSnippetDelete(TestCase):
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailsnippets_list', args=('tests', 'advert')))
# Check that the page is gone
self.assertEqual(Advert.objects.filter(text='test_advert').count(), 0)
@ -168,3 +180,16 @@ class TestSnippetChooserPanel(TestCase):
def test_render_js(self):
self.assertTrue("createSnippetChooser(fixPrefix('id_text'), 'contenttypes/contenttype');"
in self.snippet_chooser_panel.render_js())
class TestSnippetOrdering(TestCase):
def setUp(self):
register_snippet(ZuluSnippet)
register_snippet(AlphaSnippet)
def test_snippets_ordering(self):
# Ensure AlphaSnippet is before ZuluSnippet
# Cannot check first and last position as other snippets
# may get registered elsewhere during test
self.assertLess(SNIPPET_MODELS.index(AlphaSnippet),
SNIPPET_MODELS.index(ZuluSnippet))

View file

@ -1,18 +1,20 @@
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from wagtail.tests.utils import login
from wagtail.tests.utils import WagtailTestUtils
class TestUserIndexView(TestCase):
class TestUserIndexView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailusers_index'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailusers/index.html')
def test_search(self):
response = self.get({'q': "Hello"})
@ -26,9 +28,9 @@ class TestUserIndexView(TestCase):
self.assertEqual(response.status_code, 200)
class TestUserCreateView(TestCase):
class TestUserCreateView(TestCase, WagtailTestUtils):
def setUp(self):
login(self.client)
self.login()
def get(self, params={}):
return self.client.get(reverse('wagtailusers_create'), params)
@ -36,8 +38,10 @@ class TestUserCreateView(TestCase):
def post(self, post_data={}):
return self.client.post(reverse('wagtailusers_create'), post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailusers/create.html')
def test_create(self):
response = self.post({
@ -51,6 +55,7 @@ class TestUserCreateView(TestCase):
# Should redirect back to index
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailusers_index'))
# Check that the user was created
users = User.objects.filter(username='testuser')
@ -58,13 +63,13 @@ class TestUserCreateView(TestCase):
self.assertEqual(users.first().email, 'test@user.com')
class TestUserEditView(TestCase):
class TestUserEditView(TestCase, WagtailTestUtils):
def setUp(self):
# Create a user to edit
self.test_user = User.objects.create_user(username='testuser', email='testuser@email.com', password='password')
# Login
login(self.client)
self.login()
def get(self, params={}, user_id=None):
return self.client.get(reverse('wagtailusers_edit', args=(user_id or self.test_user.id, )), params)
@ -72,8 +77,10 @@ class TestUserEditView(TestCase):
def post(self, post_data={}, user_id=None):
return self.client.post(reverse('wagtailusers_edit', args=(user_id or self.test_user.id, )), post_data)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_simple(self):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailusers/edit.html')
def test_nonexistant_redirect(self):
self.assertEqual(self.get(user_id=100000).status_code, 404)
@ -90,6 +97,7 @@ class TestUserEditView(TestCase):
# Should redirect back to index
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, reverse('wagtailusers_index'))
# Check that the user was edited
user = User.objects.get(id=self.test_user.id)