mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-04-17 21:41:01 +00:00
Merge branch 'master' into tests-changes
Conflicts: wagtail/wagtailembeds/tests.py wagtail/wagtailsnippets/tests.py
This commit is contained in:
commit
1d7cc2b3aa
49 changed files with 4063 additions and 2902 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-----
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ Will return::
|
|||
tauntaun kennel bed and breakfast
|
||||
|
||||
|
||||
.. _tagging:
|
||||
|
||||
Tagging
|
||||
-------
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# For coverage and PEP8 linting
|
||||
coverage==3.7.1
|
||||
flake8==2.1.0
|
||||
mock==1.0.1
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -93,17 +93,6 @@ $(function(){
|
|||
});
|
||||
});
|
||||
|
||||
$(".nav-main .more > a").bind('click keydown', function(){
|
||||
var currentAlt = $(this).data('altstate');
|
||||
var newAlt = $(this).html();
|
||||
|
||||
$(this).html(currentAlt);
|
||||
$(this).data('altstate', newAlt);
|
||||
$(this).toggleClass('icon-arrow-up icon-arrow-down');
|
||||
$(this).parent().find('ul').toggle('fast');
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#menu-search input').bind('focus', function(){
|
||||
$('#menu-search').addClass('focussed');
|
||||
}).bind('blur', function(){
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -648,6 +648,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);
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -226,19 +226,6 @@ img{
|
|||
}
|
||||
}
|
||||
|
||||
.more{
|
||||
border:0;
|
||||
|
||||
> a{
|
||||
&:before{
|
||||
margin-right:0.4em;
|
||||
}
|
||||
font-size:0.8em;
|
||||
padding:0.2em 1.2em;
|
||||
background-color:$color-grey-1-1;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar{
|
||||
display:none;
|
||||
}
|
||||
|
|
@ -312,10 +299,6 @@ img{
|
|||
}
|
||||
}
|
||||
|
||||
.js .nav-main .more ul{
|
||||
display:none;
|
||||
}
|
||||
|
||||
.explorer{
|
||||
position:absolute;
|
||||
margin-top:70px;
|
||||
|
|
@ -447,13 +430,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;
|
||||
|
|
@ -485,7 +468,7 @@ footer{
|
|||
content:"n";
|
||||
padding-left:20px;
|
||||
font-size:2em;
|
||||
color:$color-teal;
|
||||
color:$color-teal-darker;
|
||||
line-height:0.9em;
|
||||
}
|
||||
}
|
||||
|
|
@ -626,7 +609,7 @@ footer, .logo{
|
|||
padding-right:$desktop-nice-padding;
|
||||
}
|
||||
|
||||
body{
|
||||
.wrapper{
|
||||
margin-left:$menu-width;
|
||||
}
|
||||
|
||||
|
|
@ -645,7 +628,7 @@ footer, .logo{
|
|||
left:0;
|
||||
height:100%;
|
||||
width:$menu-width;
|
||||
margin-left: -$menu-width;
|
||||
margin-left: 0;
|
||||
|
||||
.inner{
|
||||
height:100%;
|
||||
|
|
@ -756,6 +739,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{
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ body{
|
|||
.wrapper{
|
||||
padding-left:$mobile-nice-padding;
|
||||
padding-right:$mobile-nice-padding;
|
||||
margin-left:0;
|
||||
max-width:none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@
|
|||
|
||||
.hallotoolbar{
|
||||
position:absolute;
|
||||
left:50px;
|
||||
left:$mobile-nice-padding;
|
||||
z-index:5;
|
||||
margin-top:4em;
|
||||
margin-left:0em;
|
||||
}
|
||||
.hallotoolbar.affixed{
|
||||
position:fixed;
|
||||
margin-left:140px;
|
||||
margin-top:0;
|
||||
}
|
||||
.hallotoolbar button{
|
||||
|
|
@ -148,18 +147,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $breakpoint-desktop-larger){
|
||||
/* .hallotoolbar{
|
||||
margin:0 auto;
|
||||
position:absolute;
|
||||
left:-$menu-width;
|
||||
right:0;
|
||||
z-index:5;
|
||||
margin-top:3em;
|
||||
@media screen and (min-width: $breakpoint-mobile){
|
||||
.hallotoolbar{
|
||||
left:$menu-width + $desktop-nice-padding;
|
||||
}
|
||||
.hallotoolbar.affixed{
|
||||
position:fixed;
|
||||
margin:0 auto;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -229,4 +229,24 @@
|
|||
<tr><td colspan="3" class="no-results-message"><p>{% trans "No pages have been created." %}{% if parent_page and parent_page_perms.can_add_subpage %} {% blocktrans %}Why not <a href="{{ add_page_url }}">add one</a>?{% endblocktrans %}{% endif %}</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
|
||||
{% if parent_page and pages and pages.paginator %}
|
||||
<div class="pagination">
|
||||
<p>{% blocktrans with page_number=pages.number num_pages=pages.paginator.num_pages%}
|
||||
Page {{ page_number }} of {{ num_pages }}.
|
||||
{% endblocktrans %}</p>
|
||||
<ul>
|
||||
<li class="prev">
|
||||
{% if pages.has_previous %}
|
||||
<a href="{% url 'wagtailadmin_explore' parent_page.id %}?p={{ pages.previous_page_number }}{% if ordering %}&ordering={{ ordering }}{% endif %}" class="icon icon-arrow-left">{% trans "Previous" %}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="next">
|
||||
{% if pages.has_next %}
|
||||
<a href="{% url 'wagtailadmin_explore' parent_page.id %}?p={{ pages.next_page_number }}{% if ordering %}&ordering={{ ordering }}{% endif %}" class="icon icon-arrow-right-after">{% trans 'Next' %}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -10,19 +10,5 @@
|
|||
<div class="avatar icon icon-user"><a href="{% url 'wagtailadmin_account' %}" title="{% trans 'Account settings' %}">{% if request.user.email %}<img src="{% gravatar_url request.user.email %}" />{% endif %}</a></div>
|
||||
<a href="{% url 'wagtailadmin_logout' %}">{% trans "Log out" %}</a>
|
||||
</li>
|
||||
{% if request.user.is_superuser %} {# for now, 'More' links will be superuser-only #}
|
||||
<li class="more">
|
||||
<a href="#" class="icon icon-arrow-down" data-altstate="{% trans 'Less' %}">{% trans 'More' %}</a>
|
||||
<ul>
|
||||
<li class="menu-redirects"><a href="{% url 'wagtailredirects_index' %}" class="icon icon-redirect">{% trans 'Redirects' %}</a></li>
|
||||
<li class="menu-editorspicks"><a href="{% url 'wagtailsearch_editorspicks_index' %}" class="icon icon-pick">{% trans 'Editors Picks' %}</a></li>
|
||||
{% get_wagtailadmin_tab_urls as wagtailadmin_tab_urls %}
|
||||
{% for name, title in wagtailadmin_tab_urls %}
|
||||
<li class="menu-{{ title|slugify }}"><a href="{% url name %}" class="icon icon-{{name}}">{{ title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -26,17 +26,6 @@ def explorer_subnav(nodes):
|
|||
}
|
||||
|
||||
|
||||
@register.assignment_tag
|
||||
def get_wagtailadmin_tab_urls():
|
||||
resolver = urlresolvers.get_resolver(None)
|
||||
return [
|
||||
(key, value[2].get("title", key))
|
||||
for key, value
|
||||
in resolver.reverse_dict.items()
|
||||
if isinstance(key, basestring) and key.startswith('wagtailadmin_tab_')
|
||||
]
|
||||
|
||||
|
||||
@register.inclusion_tag('wagtailadmin/shared/main_nav.html', takes_context=True)
|
||||
def main_nav(context):
|
||||
menu_items = [
|
||||
|
|
|
|||
121
wagtail/wagtailadmin/tests/test_page_chooser.py
Normal file
121
wagtail/wagtailadmin/tests/test_page_chooser.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
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 login
|
||||
|
||||
class TestChooserBrowse(TestCase):
|
||||
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)
|
||||
|
||||
login(self.client)
|
||||
|
||||
def get(self, params={}):
|
||||
return self.client.get(reverse('wagtailadmin_choose_page'), params)
|
||||
|
||||
def test_status_code(self):
|
||||
self.assertEqual(self.get().status_code, 200)
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
login(self.client)
|
||||
|
||||
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_status_code(self):
|
||||
self.assertEqual(self.get().status_code, 200)
|
||||
|
||||
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):
|
||||
def setUp(self):
|
||||
login(self.client)
|
||||
|
||||
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_status_code(self):
|
||||
self.assertEqual(self.get().status_code, 200)
|
||||
|
||||
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):
|
||||
def setUp(self):
|
||||
login(self.client)
|
||||
|
||||
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_status_code(self):
|
||||
self.assertEqual(self.get().status_code, 200)
|
||||
|
||||
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'")
|
||||
|
|
@ -25,7 +25,7 @@ class TestPageExplorer(TestCase, WagtailTestUtils):
|
|||
response = self.client.get(reverse('wagtailadmin_explore', args=(self.root_page.id, )))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.root_page, response.context['parent_page'])
|
||||
self.assertTrue(response.context['pages'].filter(id=self.child_page.id).exists())
|
||||
self.assertTrue(response.context['pages'].paginator.object_list.filter(id=self.child_page.id).exists())
|
||||
|
||||
|
||||
class TestPageCreation(TestCase, WagtailTestUtils):
|
||||
|
|
|
|||
|
|
@ -33,6 +33,17 @@ def index(request, parent_page_id=None):
|
|||
else:
|
||||
ordering = 'title'
|
||||
|
||||
# Pagination
|
||||
if ordering != 'ord':
|
||||
p = request.GET.get('p', 1)
|
||||
paginator = Paginator(pages, 50)
|
||||
try:
|
||||
pages = paginator.page(p)
|
||||
except PageNotAnInteger:
|
||||
pages = paginator.page(1)
|
||||
except EmptyPage:
|
||||
pages = paginator.page(paginator.num_pages)
|
||||
|
||||
return render(request, 'wagtailadmin/pages/index.html', {
|
||||
'parent_page': parent_page,
|
||||
'ordering': ordering,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
0
wagtail/wagtailcore/tests/__init__.py
Normal file
0
wagtail/wagtailcore/tests/__init__.py
Normal file
89
wagtail/wagtailcore/tests/test_management_commands.py
Normal file
89
wagtail/wagtailcore/tests/test_management_commands.py
Normal 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")
|
||||
226
wagtail/wagtailcore/tests/test_page_model.py
Normal file
226
wagtail/wagtailcore/tests/test_page_model.py
Normal 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/')
|
||||
226
wagtail/wagtailcore/tests/test_page_permissions.py
Normal file
226
wagtail/wagtailcore/tests/test_page_permissions.py
Normal 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())
|
||||
256
wagtail/wagtailcore/tests/test_page_queryset.py
Normal file
256
wagtail/wagtailcore/tests/test_page_queryset.py
Normal 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())
|
||||
136
wagtail/wagtailcore/tests/test_whitelist.py
Normal file
136
wagtail/wagtailcore/tests/test_whitelist.py
Normal 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>')
|
||||
99
wagtail/wagtailcore/tests/tests.py
Normal file
99
wagtail/wagtailcore/tests/tests.py
Normal 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, '/')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.wagtailembeds import get_embed
|
||||
|
||||
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):
|
||||
|
|
@ -73,3 +93,158 @@ class TestChooser(TestCase, WagtailTestUtils):
|
|||
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'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
from django.core import urlresolvers
|
||||
from django.conf.urls import include, url
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailredirects import urls
|
||||
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
|
||||
|
||||
def register_admin_urls():
|
||||
return [
|
||||
url(r'^redirects/', include(urls)),
|
||||
]
|
||||
hooks.register('register_admin_urls', register_admin_urls)
|
||||
|
||||
|
||||
def construct_main_menu(request, menu_items):
|
||||
# TEMPORARY: Only show if the user is a superuser
|
||||
if request.user.is_superuser:
|
||||
menu_items.append(
|
||||
MenuItem(_('Redirects'), urlresolvers.reverse('wagtailredirects_index'), classnames='icon icon-redirect', order=800)
|
||||
)
|
||||
hooks.register('construct_main_menu', construct_main_menu)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
from django.core import urlresolvers
|
||||
from django.conf.urls import include, url
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailsearch.urls import admin as admin_urls
|
||||
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
|
||||
|
||||
def register_admin_urls():
|
||||
return [
|
||||
url(r'^search/', include(admin_urls)),
|
||||
]
|
||||
hooks.register('register_admin_urls', register_admin_urls)
|
||||
|
||||
|
||||
def construct_main_menu(request, menu_items):
|
||||
# TEMPORARY: Only show if the user is a superuser
|
||||
if request.user.is_superuser:
|
||||
menu_items.append(
|
||||
MenuItem(_('Editors picks'), urlresolvers.reverse('wagtailsearch_editorspicks_index'), classnames='icon icon-pick', order=900)
|
||||
)
|
||||
hooks.register('construct_main_menu', construct_main_menu)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ from django.core.urlresolvers import reverse
|
|||
from django.contrib.auth.models import User
|
||||
|
||||
from wagtail.tests.utils import unittest, WagtailTestUtils
|
||||
from wagtail.tests.models import Advert
|
||||
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
|
||||
|
|
@ -179,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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue