This commit is contained in:
Robert Clark 2014-06-06 13:38:09 -04:00
commit 9b5d4f26f7
121 changed files with 7457 additions and 1542 deletions

View file

@ -13,7 +13,7 @@ services:
install:
- python setup.py install
- pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand
- pip install coveralls unittest2
- pip install coveralls
# Pre-test configuration
before_script:
- psql -c 'create database wagtaildemo;' -U postgres

View file

@ -1,13 +1,24 @@
Changelog
=========
0.3 (xx.xx.20xx)
0.4 (xx.xx.20xx)
~~~~~~~~~~~~~~~~~~
0.3.1 (03.06.2014)
~~~~~~~~~~~~~~~~~~
* Fix: When constructing dummy requests for pages with no routable URL, fall back on a hostname from ALLOWED_HOSTS and finally 'localhost', to avoid 'Invalid HTTP_HOST header' errors on preview when DEBUG=False.
* Fix: Ensure that url_path is populated when previewing a newly created page, to avoid unnecessarily taking the above fallback.
* Fix: Deleting an item from an InlinePanel, then generating a validation error on saving, no longer causes the deleted item to confusingly reappear with an error of its own.
0.3 (28.05.2014)
~~~~~~~~~~~~~~~~
* Added toolbar to allow logged-in users to add and edit pages from the site front-end
* Support for alternative image processing backends such as Wand, via the WAGTAILIMAGES_BACKENDS setting
* Added support for generating static sites using django-medusa
* Added custom Query set for Pages with some handy methods for querying pages
* Added 'wagtailforms' module for creating form pages on a site, and handling form submissions
* Editor's guide documentation
* Expanded developer documentation
* Editor interface now outputs form media CSS / JS, to support custom widgets with assets
* Migrations and user management now correctly handle custom AUTH_USER_MODEL settings
* Added 'slugurl' template tag to output the URL of a page with a given slug
@ -15,7 +26,7 @@ Changelog
* Added 'insert_editor_css' and 'insert_editor_js' hooks for passing in custom CSS / JS to the editor interface
* Made JPEG compression level configurable through the IMAGE_COMPRESSION_QUALITY setting, and increased default to 85
* Added document_served signal which gets fired when a document is downloaded
* Added translation for Portuguese Brazil
* Added translations for Portuguese Brazil and Traditional Chinese (Taiwan).
* Made compatible with Python 2.6
* 'richtext' template filter now wraps output in <div class="rich-text"></div>, to assist in styling
* Embeds now save author_name and provider_name if set by oEmbed provider
@ -26,6 +37,9 @@ Changelog
* Fix: Filter objects are cached to avoid a database hit every time an {% image %} tag is compiled
* Fix: Moving or changing a site root page no longer causes URLs for subpages to change to 'None'
* Fix: Eliminated raw SQL queries from wagtailcore / wagtailadmin, to ensure cross-database compatibility
* Fix: Snippets menu item is hidden for administrators if no snippet types are defined
* Fix: 'Upload' tab in image chooser now retains focus if submit action returns a form error.
* Fix: Search input now appears on image chooser after form validation error.
0.2 (11.03.2014)
~~~~~~~~~~~~~~~~

View file

@ -26,6 +26,8 @@ Contributors
* Ben Emery
* David Smith
* Ben Margolis
* Tom Talbot
* Jeffrey Hearn
Translators
===========
@ -33,7 +35,7 @@ Translators
* Basque: Unai Zalakain
* Bulgarian: Lyuboslav Petrov
* Catalan: David Llop
* Chinese: Lihan Li
* Chinese: Lihan Li, tulpar008, wwj718
* French: Sylvain Fankhauser
* Galician: fooflare
* German: Karl Sander, Johannes Spielmann
@ -43,3 +45,4 @@ Translators
* Portuguese Brazil: Gilson Filho
* Romanian: Dan Braghis
* Spanish: Unai Zalakain, fooflare
* Traditional Chinese (Taiwan): wdv4758h

View file

@ -1,10 +1,10 @@
.. image:: https://travis-ci.org/torchbox/wagtail.png?branch=master
:target: https://travis-ci.org/torchbox/wagtail
.. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master
.. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master&zxcv
:target: https://coveralls.io/r/torchbox/wagtail?branch=master
.. image:: https://pypip.in/v/wagtail/badge.png?asdf
.. image:: https://pypip.in/v/wagtail/badge.png?zxcv
:target: https://crate.io/packages/wagtail/
Wagtail CMS
@ -24,11 +24,9 @@ Wagtail is a Django content management system built originally for the `Royal Co
* Support for tree-based content organisation
* Optional preview->submit->approve workflow
* Fast out of the box. `Varnish <https://www.varnish-cache.org/>`_-friendly if you need it
* Tests! But not enough; we're working hard to improve this
* Excellent test coverage
It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
Find out more at `wagtail.io <http://wagtail.io/>`_. Documentation is at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_.
Find out more at `wagtail.io <http://wagtail.io/>`_.
Got a question? Ask it on our `Google Group <https://groups.google.com/forum/#!forum/wagtail>`_.
@ -38,6 +36,15 @@ Getting started
* See the `Getting Started <http://wagtail.readthedocs.org/en/latest/gettingstarted.html#getting-started>`_ docs for installation (with the demo app) on a fresh Debian/Ubuntu box with production-ready dependencies, on OS X and on a Vagrant box.
* `Serafeim Papastefanos <https://github.com/spapas>`_ has written a `tutorial <http://spapas.github.io/2014/02/13/wagtail-tutorial/>`_ with all the steps to build a simple Wagtail site from scratch.
Documentation
~~~~~~~~~~~~~
Available at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_. and always being updated.
Compatibility
~~~~~~~~~~~~~
Wagtail supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
Contributing
~~~~~~~~~~~~
If you're a Python or Django developer, fork the repo and get stuck in! Send us a useful pull request and we'll post you a `t-shirt <https://twitter.com/WagtailCMS/status/432166799464210432/photo/1>`_. Our immediate priorities are better docs, more tests, internationalisation and localisation.

17
docs/advanced_topics.rst Normal file
View file

@ -0,0 +1,17 @@
Advanced Topics
~~~~~~~~~~~~~~~~
.. note::
This documentation is currently being written.
replacing image processing backend
custom image processing tags?
wagtail user bar custom CSS option?
extending hallo editor plugins with editor_js()
injecting any JS into page edit with editor_js()
Custom content module (same level as docs or images)

View file

@ -1,6 +0,0 @@
Building your site
==================
Serafeim Papastefanos has written a comprehensive tutorial on creating a site from scratch in Wagtail; for the time being, this is our recommended resource:
`spapas.github.io/2014/02/13/wagtail-tutorial/ <http://spapas.github.io/2014/02/13/wagtail-tutorial/>`_

View file

@ -0,0 +1,280 @@
For Django developers
=====================
.. note::
This documentation is currently being written.
Wagtail requires a little careful setup to define the types of content that you want to present through your website. The basic unit of content in Wagtail is the ``Page``, and all of your page-level content will inherit basic webpage-related properties from it. But for the most part, you will be defining content yourself, through the construction of Django models using Wagtail's ``Page`` as a base.
Wagtail organizes content created from your models in a tree, which can have any structure and combination of model objects in it. Wagtail doesn't prescribe ways to organize and interrelate your content, but here we've sketched out some strategies for organizing your models.
The presentation of your content, the actual webpages, includes the normal use of the Django template system. We'll cover additional functionality that Wagtail provides at the template level later on.
But first, we'll take a look at the ``Page`` class and model definitions.
The Page Class
~~~~~~~~~~~~~~
``Page`` uses Django's model interface, so you can include any field type and field options that Django allows. Wagtail provides some fields and editing handlers that simplify data entry in the Wagtail admin interface, so you may want to keep those in mind when deciding what properties to add to your models in addition to those already provided by ``Page``.
Built-in Properties of the Page Class
-------------------------------------
Wagtail provides some properties in the ``Page`` class which are common to most webpages. Since you'll be subclassing ``Page``, you don't have to worry about implementing them.
Public Properties
`````````````````
``title`` (string, required)
Human-readable title for the content
``slug`` (string, required)
Machine-readable URL component for this piece of content. The name of the page as it will appear in URLs e.g ``http://domain.com/blog/[my-slug]/``
``seo_title`` (string)
Alternate SEO-crafted title which overrides the normal title for use in the ``<head>`` of a page
``search_description`` (string)
A SEO-crafted description of the content, used in both internal search indexing and for the meta description read by search engines
The ``Page`` class actually has alot more to it, but these are probably the only built-in properties you'll need to worry about when creating templates for your models.
Anatomy of a Wagtail Model
~~~~~~~~~~~~~~~~~~~~~~~~~~
So what does a Wagtail model definition look like? Here's a model representing a typical blog post:
.. code-block:: python
from django.db import models
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailimages.models import Image
class BlogPage(Page):
body = RichTextField()
date = models.DateField("Post date")
feed_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BlogPage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('date'),
FieldPanel('body', classname="full"),
]
BlogPage.promote_panels = [
FieldPanel('slug'),
FieldPanel('seo_title'),
FieldPanel('show_in_menus'),
FieldPanel('search_description'),
ImageChooserPanel('feed_image'),
]
To keep track of your ``Page``-derived models, it might be helpful to include "Page" as the last part of your class name. ``BlogPage`` defines three properties: ``body``, ``date``, and ``feed_image``. These are a mix of basic Django models (``DateField``), Wagtail fields (``RichTextField``), and a pointer to a Wagtail model (``Image``).
Next, the ``content_panels`` and ``promote_panels`` lists define the capabilities and layout of the Wagtail admin page edit interface. The lists are filled with "panels" and "choosers", which will provide a fine-grain interface for inputting the model's content. The ``ImageChooserPanel``, for instance, lets one browse the image library, upload new images, and input image metadata. The ``RichTextField`` is the basic field for creating web-ready website rich text, including text formatting and embedded media like images and video. The Wagtail admin offers other choices for fields, Panels, and Choosers, with the option of creating your own to precisely fit your content without workarounds or other compromises.
Your models may be even more complex, with methods overriding the built-in functionality of the ``Page`` to achieve webdev magic. Or, you can keep your models simple and let Wagtail's built-in functionality do the work.
Now that we have a basic idea of how our content is defined, lets look at relationships between pieces of content.
Introduction to Trees
~~~~~~~~~~~~~~~~~~~~~
If you're unfamiliar with trees as an abstract data type, you might want to `review the concepts involved. <http://en.wikipedia.org/wiki/Tree_(data_structure)>`_
As a web developer, though, you probably already have a good understanding of trees as filesystem directories or paths. Wagtail pages can create the same structure, as each page in the tree has its own URL path, like so::
/
people/
nien-nunb/
laura-roslin/
events/
captain-picard-day/
winter-wrap-up/
The Wagtail admin interface uses the tree to organize content for editing, letting you navigate up and down levels in the tree through its Explorer menu. This method of organization is a good place to start in thinking about your own Wagtail models.
Nodes and Leaves
----------------
It might be handy to think of the ``Page``-derived models you want to create as being one of two node types: parents and leaves. Wagtail isn't prescriptive in this approach, but it's a good place to start if you're not experienced in structuring your own content types.
Nodes
`````
Parent nodes on the Wagtail tree probably want to organize and display a browse-able index of their descendants. A blog, for instance, needs a way to show a list of individual posts.
A Parent node could provide its own function returning its descendant objects.
.. code-block:: python
class EventPageIndex(Page):
# ...
def events(self):
# Get list of live event pages that are descendants of this page
events = EventPage.objects.live().descendant_of(self)
# Filter events list to get ones that are either
# running now or start in the future
events = events.filter(date_from__gte=date.today())
# Order by date
events = events.order_by('date_from')
return events
This example makes sure to limit the returned objects to pieces of content which make sense, specifically ones which have been published through Wagtail's admin interface (``live()``) and are children of this node (``descendant_of(self)``). By setting a ``subpage_types`` class property in your model, you can specify which models are allowed to be set as children, but Wagtail will allow any ``Page``-derived model by default. Regardless, it's smart for a parent model to provide an index filtered to make sense.
Leaves
``````
Leaves are the pieces of content itself, a page which is consumable, and might just consist of a bunch of properties. A blog page leaf might have some body text and an image. A person page leaf might have a photo, a name, and an address.
It might be helpful for a leaf to provide a way to back up along the tree to a parent, such as in the case of breadcrumbs navigation. The tree might also be deep enough that a leaf's parent won't be included in general site navigation.
The model for the leaf could provide a function that traverses the tree in the opposite direction and returns an appropriate ancestor:
.. code-block:: python
class EventPage(Page):
# ...
def event_index(self):
# Find closest ancestor which is an event index
return self.get_ancestors().type(EventIndexPage).last()
If defined, ``subpage_types`` will also limit the parent models allowed to contain a leaf. If not, Wagtail will allow any combination of parents and leafs to be associated in the Wagtail tree. Like with index pages, it's a good idea to make sure that the index is actually of the expected model to contain the leaf.
Other Relationships
```````````````````
Your ``Page``-derived models might have other interrelationships which extend the basic Wagtail tree or depart from it entirely. You could provide functions to navigate between siblings, such as a "Next Post" link on a blog page (``post->post->post``). It might make sense for subtrees to interrelate, such as in a discussion forum (``forum->post->replies``) Skipping across the hierarchy might make sense, too, as all objects of a certain model class might interrelate regardless of their ancestors (``events = EventPage.objects.all``). It's largely up to the models to define their interrelations, the possibilities are really endless.
Anatomy of a Wagtail Request
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For going beyond the basics of model definition and interrelation, it might help to know how Wagtail handles requests and constructs responses. In short, it goes something like:
#. Django gets a request and routes through Wagtail's URL dispatcher definitions
#. Starting from the root content piece, Wagtail traverses the page tree, letting the model for each piece of content along the path decide how to ``route()`` the next step in the path.
#. A model class decides that routing is done and it's now time to ``serve()`` content.
#. ``serve()`` constructs a context using ``get_context()``
#. ``serve()`` finds a template to pass it to using ``get_template()``
#. A response object is returned by ``serve()`` and Django responds to the requester.
You can apply custom behavior to this process by overriding ``Page`` class methods such as ``route()`` and ``serve()`` in your own models. For examples, see :ref:`model_recipes`.
Page Properties and Methods Reference
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In addition to the model fields provided, ``Page`` has many properties and methods that you may wish to reference, use, or override in creating your own models. Those listed here are relatively straightforward to use, but consult the Wagtail source code for a full view of what's possible.
Properties:
* specific
* url
* full_url
* relative_url
* has_unpublished_changes
* status_string
* subpage_types
* indexed_fields
Methods:
* route
* serve
* get_context
* get_template
* is_navigable
* get_other_siblings
* get_ancestors
* get_descendants
* get_siblings
* search
* get_page_modes
* show_as_mode
Page Queryset Methods
~~~~~~~~~~~~~~~~~~~~~
The ``Page`` class uses a custom Django model manager which provides these methods for structuring queries on ``Page`` objects.
get_query_set()
return PageQuerySet(self.model).order_by('path')
live(self):
return self.get_query_set().live()
not_live(self):
return self.get_query_set().not_live()
page(self, other):
return self.get_query_set().page(other)
not_page(self, other):
return self.get_query_set().not_page(other)
descendant_of(self, other, inclusive=False):
return self.get_query_set().descendant_of(other, inclusive)
not_descendant_of(self, other, inclusive=False):
return self.get_query_set().not_descendant_of(other, inclusive)
child_of(self, other):
return self.get_query_set().child_of(other)
not_child_of(self, other):
return self.get_query_set().not_child_of(other)
ancestor_of(self, other, inclusive=False):
return self.get_query_set().ancestor_of(other, inclusive)
not_ancestor_of(self, other, inclusive=False):
return self.get_query_set().not_ancestor_of(other, inclusive)
parent_of(self, other):
return self.get_query_set().parent_of(other)
not_parent_of(self, other):
return self.get_query_set().not_parent_of(other)
sibling_of(self, other, inclusive=False):
return self.get_query_set().sibling_of(other, inclusive)
not_sibling_of(self, other, inclusive=False):
return self.get_query_set().not_sibling_of(other, inclusive)
type(self, model):
return self.get_query_set().type(model)
not_type(self, model):
return self.get_query_set().not_type(model)
Site
~~~~
Django's built-in admin interface provides the way to map a "site" (hostname or domain) to any node in the wagtail tree, using that node as the site's root.
Access this by going to ``/django-admin/`` and then "Home Wagtailcore Sites." To try out a development site, add a single site with the hostname ``localhost`` at port ``8000`` and map it to one of the pieces of content you have created.
Wagtail's developers plan to move the site settings into the Wagtail admin interface.

View file

@ -0,0 +1,255 @@
For Front End developers
========================
.. contents::
========================
Overview
========================
Wagtail uses Django's templating language. For developers new to Django, start with Django's own template documentation:
https://docs.djangoproject.com/en/dev/topics/templates/
Python programmers new to Django/Wagtail may prefer more technical documentation:
https://docs.djangoproject.com/en/dev/ref/templates/api/
You should be familiar with Django templating basics before continuing with this documentation.
==========================
Templates
==========================
Every type of page or "content type" in Wagtail is defined as a "model" in a file called ``models.py``. If your site has a blog, you might have a ``BlogPage`` model and another called ``BlogPageListing``. The names of the models are up to the Django developer.
For each page model in ``models.py``, Wagtail assumes an HTML template file exists of (almost) the same name. The Front End developer may need to create these templates themselves by refering to ``models.py`` to infer template names from the models defined therein.
To find a suitable template, Wagtail converts CamelCase names to underscore_case. So for a ``BlogPage``, a template ``blog_page.html`` will be expected. The name of the template file can be overridden per model if necessary.
Template files are assumed to exist here::
name_of_project/
name_of_app/
templates/
name_of_app/
blog_page.html
models.py
For more information, see the Django documentation for the `application directories template loader`_.
.. _application directories template loader: https://docs.djangoproject.com/en/dev/ref/templates/api/
Page content
~~~~~~~~~~~~
The data/content entered into each page is accessed/output through Django's ``{{ double-brace }}`` notation. Each field from the model must be accessed by prefixing ``self.``. e.g the page title ``{{ self.title }}`` or another field ``{{ self.author }}``.
Additionally ``request.`` is available and contains Django's request object.
==============
Static assets
==============
Static files e.g CSS, JS and images are typically stored here::
name_of_project/
name_of_app/
static/
name_of_app/
css/
js/
images/
models.py
(The names "css", "js" etc aren't important, only their position within the tree.)
Any file within the static folder should be inserted into your HTML using the ``{% static %}`` tag. More about it: :ref:`static_tag`.
User images
~~~~~~~~~~~
Images uploaded to Wagtail by its users (as opposed to a developer's static files, above) go into the image library and from there are added to pages via the :doc:`page editor interface </editor_manual/new_pages/inserting_images>`.
Unlike other CMS, adding images to a page does not involve choosing a "version" of the image to use. Wagtail has no predefined image "formats" or "sizes". Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template.
Images from the library must be requested using this syntax, but a developer's static images can be added via conventional means e.g ``img`` tags. Only images from the library can be manipulated on the fly.
Read more about the image manipulation syntax here :ref:`image_tag`.
========================
Template tags & filters
========================
In addition to Django's standard tags and filters, Wagtail provides some of its own, which can be ``load``-ed `as you would any other <https://docs.djangoproject.com/en/dev/topics/templates/#custom-tag-and-filter-libraries>`_
.. _image_tag:
Images (tag)
~~~~~~~~~~~~
The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, setting its ``src``, ``width``, ``height`` and ``alt``. See also :ref:`image_tag_alt`.
The syntax for the tag is thus::
{% image [image] [method]-[dimension(s)] %}
For example:
.. code-block:: django
{% load image %}
...
{% image self.photo width-400 %}
<!-- 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.
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``.
The available ``method`` s are:
.. glossary::
``max``
(takes two dimensions)
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.
``min``
(takes two dimensions)
**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.
``width``
(takes one dimension)
Reduces the width of the image to the dimension specified.
``height``
(takes one dimension)
Resize the height of the image to the dimension specified..
``fill``
(takes two dimensions)
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.
.. 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.
.. _image_tag_alt:
More control over the ``img`` tag
---------------------------------
In some cases greater control over the ``img`` tag is required, for example to add a custom ``class``. Rather than generating the ``img`` element for you, Wagtail can assign the relevant data to another object using Django's ``as`` syntax:
.. code-block:: django
{% load image %}
...
{% image self.photo width-400 as tmp_photo %}
<img src="{{ tmp_photo.src }}" width="{{ tmp_photo.width }}"
height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
.. _rich-text-filter:
Rich text (filter)
~~~~~~~~~~~~~~~~~~
This filter takes a chunk of HTML content and renders it as safe HTML in the page. Importantly it also expands internal shorthand references to embedded images and links made in the Wagtail editor into fully-baked HTML ready for display.
Only fields using ``RichTextField`` need this applied in the template.
.. code-block:: django
{% load rich_text %}
...
{{ self.body|richtext }}
.. Note::
Note that the template tag loaded differs from the name of the filter.
Internal links (tag)
~~~~~~~~~~~~~~~~~~~~
pageurl
--------
Takes a Page object and returns a relative URL (``/foo/bar/``) if within the same site as the current page, or absolute (``http://example.com/foo/bar/``) if not.
.. code-block:: django
{% load pageurl %}
...
<a href="{% pageurl self.blog_page %}">
slugurl
--------
Takes any ``slug`` as defined in a page's "Promote" tab and returns the URL for the matching Page. Like ``pageurl``, will try to provide a relative link if possible, but will default to an absolute link if on a different site. This is most useful when creating shared page furniture e.g top level navigation or site-wide links.
.. code-block:: django
{% load slugurl %}
...
<a href="{% slugurl self.your_slug %}">
.. _static_tag:
Static files (tag)
~~~~~~~~~~~~~~~~~~
Used to load anything from your static files directory. Use of this tag avoids rewriting all static paths if hosting arrangements change, as they might between local and a live environments.
.. code-block:: django
{% load static %}
...
<img src="{% static "name_of_app/myimage.jpg" %}" alt="My image"/>
Notice that the full path name is not required and the path snippet you enter only need begin with the parent app's directory name.
========================
Wagtail User Bar
========================
This tag provides a contextual flyout menu on the top-right of a page for logged-in users. The menu gives editors the ability to edit the current page or add another at the same level. Moderators are also given the ability to accept or reject a page previewed as part of content moderation.
.. code-block:: django
{% load wagtailuserbar %}
...
{% wagtailuserbar %}
By default the User Bar appears in the top right of the browser window, flush with the edge. If this conflicts with your design it can be moved with a css rule in your own CSS files e.g to move it down from the top:
.. code-block:: css
#wagtail-userbar{
top:200px
}

View file

@ -0,0 +1,11 @@
Building your site
==================
.. note::
This documentation is currently incomplete.
.. toctree::
:maxdepth: 3
djangodevelopers
frontenddevelopers

560
docs/editing_api.rst Normal file
View file

@ -0,0 +1,560 @@
Editing API
===========
.. note::
This documentation is currently being written.
Wagtail provides a highly-customizable editing interface consisting of several components:
* **Fields** — built-in content types to augment the basic types provided by Django.
* **Panels** — the basic editing blocks for fields, groups of fields, and related object clusters
* **Choosers** — interfaces for finding related objects in a ForeignKey relationship
Configuring your models to use these components will shape the Wagtail editor to your needs. Wagtail also provides an API for injecting custom CSS and Javascript for further customization, including extending the hallo.js rich text editor.
There is also an Edit Handler API for creating your own Wagtail editor components.
Defining Panels
~~~~~~~~~~~~~~~
A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types, you just need to add a panel for each field you want to show in the Wagtail page editor, in the order you want them to appear.
There are three types of panels:
``FieldPanel( field_name, classname=None )``
This is the panel used for basic Django field types. ``field_name`` is the name of the class property used in your model definition. ``classname`` is a string of optional CSS classes given to the panel which are used in formatting and scripted interactivity. By default, panels are formatted as inset fields. The CSS class ``full`` can be used to format the panel so it covers the full width of the Wagtail page editor. The CSS class ``title`` can be used to mark a field as the source for auto-generated slug strings.
``MultiFieldPanel( children, heading="", classname=None )``
This panel condenses several ``FieldPanel`` s or choosers, from a list or tuple, under a single ``heading`` string.
``InlinePanel( base_model, relation_name, panels=None, label='', help_text='' )``
This panel allows for the creation of a "cluster" of related objects over a join to a separate model, such as a list of related links or slides to an image carousel. This is a very powerful, but tricky feature which will take some space to cover, so we'll skip over it for now. For a full explanation on the usage of ``InlinePanel``, see :ref:`inline_panels`.
Wagtail provides a tabbed interface to help organize panels. ``content_panels`` is the main tab, used for the meat of your model content. The other, ``promote_panels``, is suggested for organizing metadata about the content, such as SEO information and other machine-readable information. Since you're writing the panel definitions, you can organize them however you want.
Let's look at an example of a panel definition:
.. code-block:: python
COMMON_PANELS = (
FieldPanel('slug'),
FieldPanel('seo_title'),
FieldPanel('show_in_menus'),
FieldPanel('search_description'),
)
...
class ExamplePage( Page ):
# field definitions omitted
...
ExamplePage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('body', classname="full"),
FieldPanel('date'),
ImageChooserPanel('splash_image'),
DocumentChooserPanel('free_download'),
PageChooserPanel('related_page'),
]
ExamplePage.promote_panels = [
MultiFieldPanel(COMMON_PANELS, "Common page configuration"),
]
After the ``Page``-derived class definition, just add lists of panel definitions to order and organize the Wagtail page editing interface for your model.
Built-in Fields and Choosers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Django's field types are automatically recognized and provided with an appropriate widget for input. Just define that field the normal Django way and pass the field name into ``FieldPanel()`` when defining your panels. Wagtail will take care of the rest.
Here are some Wagtail-specific types that you might include as fields in your models.
Rich Text (HTML)
----------------
Wagtail provides a general-purpose WYSIWYG editor for creating rich text content (HTML) and embedding media such as images, video, and documents. To include this in your models, use the ``RichTextField()`` function when defining a model field:
.. code-block:: python
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel
# ...
class BookPage(Page):
book_text = RichTextField()
BookPage.content_panels = [
FieldPanel('body', classname="full"),
# ...
]
``RichTextField`` inherits from Django's basic ``TextField`` field, so you can pass any field parameters into ``RichTextField`` as if using a normal Django field. This field does not need a special panel and can be defined with ``FieldPanel``.
However, template output from ``RichTextField`` is special and need to be filtered to preserve embedded content. See :ref:`rich-text-filter`.
If you're interested in extending the capabilities of the Wagtail WYSIWYG editor (hallo.js), See :ref:`extending_wysiwyg`.
Images
------
One of the features of Wagtail is a unified image library, which you can access in your models through the ``Image`` model and the ``ImageChooserPanel`` chooser. Here's how:
.. code-block:: python
from wagtail.wagtailimages.models import Image
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
# ...
class BookPage(Page):
cover = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
ImageChooserPanel('cover'),
# ...
]
Django's default behavior is to "cascade" deletions through a ForeignKey relationship, which is probably not what you want happening. This is why the ``null``, ``blank``, and ``on_delete`` parameters should be set to allow for an empty field. (See `Django model field reference (on_delete)`_ ). ``ImageChooserPanel`` takes only one argument: the name of the field.
.. _Django model field reference (on_delete): https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ForeignKey.on_delete
Displaying ``Image`` objects in a template requires the use of a template tag. See :ref:`image_tag`.
Documents
---------
For files in other formats, Wagtail provides a generic file store through the ``Document`` model:
.. code-block:: python
from wagtail.wagtaildocs.models import Document
from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
# ...
class BookPage(Page):
book_file = models.ForeignKey(
'wagtaildocs.Document',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
DocumentChooserPanel('book_file'),
# ...
]
As with images, Wagtail documents should also have the appropriate extra parameters to prevent cascade deletions across a ForeignKey relationship. ``DocumentChooserPanel`` takes only one argument: the name of the field.
Documents can be used directly in templates without tags or filters. Its properties are:
.. glossary::
``title``
The title of the document.
``url``
URL to the file.
``created_at``
The date and time the document was created (DateTime).
``filename``
The filename of the file.
``file_extension``
The extension of the file.
``tags``
A ``TaggableManager`` which keeps track of tags associated with the document (uses the ``django-taggit`` module).
Pages and Page-derived Models
-----------------------------
You can explicitly link ``Page``-derived models together using the ``Page`` model and ``PageChooserPanel``.
.. code-block:: python
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
# ...
class BookPage(Page):
publisher = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
)
BookPage.content_panels = [
PageChooserPanel('related_page', 'demo.PublisherPage'),
# ...
]
``PageChooserPanel`` takes two arguments: a field name and an optional page type. Specifying a page type (in the form of an ``"appname.modelname"`` string) will filter the chooser to display only pages of that type.
Snippets
--------
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
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
# ...
class BookPage(Page):
advert = models.ForeignKey(
'demo.Advert',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
SnippetChooserPanel('advert', Advert),
# ...
]
See :ref:`snippets` for more information.
Field Customization
~~~~~~~~~~~~~~~~~~~
By adding CSS classnames to your panel definitions or adding extra parameters to your field definitions, you can control much of how your fields will display in the Wagtail page editing interface. Wagtail's page editing interface takes much of its behavior from Django's admin, so you may find many options for customization covered there. (See `Django model field reference`_ ).
.. _Django model field reference:https://docs.djangoproject.com/en/dev/ref/models/fields/
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
---------------
To make input or chooser selection manditory for a field, add ``blank=False`` to its model definition. (See `Django model field reference (blank)`_ ).
.. _Django model field reference (blank): https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.Field.blank
Hiding Fields
-------------
Without a panel definition, a default form field (without label) will be used to represent your fields. If you intend to hide a field on the Wagtail page editor, define the field with ``editable=False`` (See `Django model field reference (editable)`_ ).
.. _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 = [
ImageChooserPanel('cover'),
DocumentChooserPanel('book_file'),
PageChooserPanel('publisher'),
]
BookPage.content_panels = [
MultiFieldPanel(
BOOK_FIELD_COLLECTION,
heading="Collection of Book Fields",
classname="collapsible collapsed"
),
# ...
]
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:
Inline Panels and Model Clusters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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:
Extending the WYSIWYG Editor (hallo.js)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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
~~~~~~~~~~~~~~~~
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)
Content Index Pages (CRUD)
--------------------------
Custom Choosers
---------------
Tests
-----

View file

@ -1,9 +1,10 @@
Using Wagtail: an Editor's guide
================================
This section of the documentation is written for the users of a Wagtail-powered site. That is, the content editors, moderators and administrators who will be running things on a day-to-day basis.
.. note::
Documentation currently incomplete and in draft status
**NOTE:** This section of the documentation is currently in draft status.
This section of the documentation is written for the users of a Wagtail-powered site. That is, the content editors, moderators and administrators who will be running things on a day-to-day basis.
.. toctree::
:maxdepth: 3

69
docs/form_builder.rst Normal file
View file

@ -0,0 +1,69 @@
Form builder
============
The `wagtailforms` module allows you to set up single-page forms, such as a 'Contact us' form, as pages of a Wagtail site. It provides a set of base models that site implementors can extend to create their own 'Form' page type with their own site-specific templates. Once a page type has been set up in this way, editors can build forms within the usual page editor, consisting of any number of fields. Form submissions are stored for later retrieval through a new 'Forms' section within the Wagtail admin interface; in addition, they can be optionally e-mailed to an address specified by the editor.
Usage
~~~~~
Add 'wagtail.wagtailforms' to your INSTALLED_APPS:
.. code:: python
INSTALLED_APPS = [
...
'wagtail.wagtailforms',
]
Within the models.py of one of your apps, create a model that extends wagtailforms.models.AbstractEmailForm:
.. code:: python
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
class FormField(AbstractFormField):
page = ParentalKey('FormPage', related_name='form_fields')
class FormPage(AbstractEmailForm):
intro = RichTextField(blank=True)
thank_you_text = RichTextField(blank=True)
FormPage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('intro', classname="full"),
InlinePanel(FormPage, 'form_fields', label="Form fields"),
FieldPanel('thank_you_text', classname="full"),
MultiFieldPanel([
FieldPanel('to_address', classname="full"),
FieldPanel('from_address', classname="full"),
FieldPanel('subject', classname="full"),
], "Email")
]
AbstractEmailForm defines the fields 'to_address', 'from_address' and 'subject', and expects form_fields to be defined. Any additional fields are treated as ordinary page content - note that FormPage is responsible for serving both the form page itself and the landing page after submission, so the model definition should include all necessary content fields for both of those views.
If you do not want your form page type to offer form-to-email functionality, you can inherit from AbstractForm instead of AbstractEmailForm, and omit the 'to_address', 'from_address' and 'subject' fields from the content_panels definition.
You now need to create two templates named form_page.html and form_page_landing.html (where 'form_page' is the underscore-formatted version of the class name). form_page.html differs from a standard Wagtail template in that it is passed a variable 'form', containing a Django form object, in addition to the usual 'self' variable. A very basic template for the form would thus be:
.. code:: html
{% load pageurl rich_text %}
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
{{ self.intro|richtext }}
<form action="{% pageurl self %}" method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit">
</form>
</body>
</html>
form_page_landing.html is a regular Wagtail template, displayed after the user makes a successful form submission.

View file

@ -4,7 +4,7 @@ Getting Started
On Ubuntu
~~~~~~~~~
If you have a fresh instance of Ubuntu 13.04 or 13.10, you can install Wagtail,
If you have a fresh instance of Ubuntu 13.04 or later, you can install Wagtail,
along with a demonstration site containing a set of standard templates and page
types, in one step. As the root user::
@ -166,4 +166,4 @@ Once you've experimented with the demo app and are ready to build your pages via
COMMIT;
EOF
rm -r demo media/images/* media/original_images/*
perl -pi -e"s/('demo',|WAGTAILSEARCH_RESULTS_TEMPLATE)/#\1/" $PROJECT/settingsbase.py
perl -pi -e"s/('demo',|WAGTAILSEARCH_RESULTS_TEMPLATE)/#\1/" $PROJECT/settings/base.py

View file

@ -9,8 +9,13 @@ It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support
:maxdepth: 3
gettingstarted
building_your_site
building_your_site/index
editing_api
snippets
wagtail_search
form_builder
model_recipes
advanced_topics
deploying
performance
static_site_generation

201
docs/model_recipes.rst Normal file
View file

@ -0,0 +1,201 @@
.. _model_recipes:
Model Recipes
=============
Overriding the serve() Method
-----------------------------
Wagtail defaults to serving ``Page``-derived models by passing ``self`` to a Django HTML template matching the model's name, but suppose you wanted to serve something other than HTML? You can override the ``serve()`` method provided by the ``Page`` class and handle the Django request and response more directly.
Consider this example from the Wagtail demo site's ``models.py``, which serves an ``EventPage`` object as an iCal file if the ``format`` variable is set in the request:
.. code-block:: python
class EventPage(Page):
...
def serve(self, request):
if "format" in request.GET:
if request.GET['format'] == 'ical':
# Export to ical format
response = HttpResponse(
export_event(self, 'ical'),
content_type='text/calendar',
)
response['Content-Disposition'] = 'attachment; filename=' + self.slug + '.ics'
return response
else:
# Unrecognised format error
message = 'Could not export event\n\nUnrecognised format: ' + request.GET['format']
return HttpResponse(message, content_type='text/plain')
else:
# Display event page as usual
return super(EventPage, self).serve(request)
``serve()`` takes a Django request object and returns a Django response object. Wagtail returns a ``TemplateResponse`` object with the template and context which it generates, which allows middleware to function as intended, so keep in mind that a simpler response object like a ``HttpResponse`` will not receive these benefits.
With this strategy, you could use Django or Python utilities to render your model in JSON or XML or any other format you'd like.
Adding Endpoints with Custom route() Methods
--------------------------------------------
Wagtail routes requests by iterating over the path components (separated with a forward slash ``/``), finding matching objects based on their slug, and delegating further routing to that object's model class. The Wagtail source is very instructive in figuring out what's happening. This is the default ``route()`` method of the ``Page`` class:
.. code-block:: python
class Page(...):
...
def route(self, request, path_components):
if path_components:
# request is for a child of this page
child_slug = path_components[0]
remaining_components = path_components[1:]
# find a matching child or 404
try:
subpage = self.get_children().get(slug=child_slug)
except Page.DoesNotExist:
raise Http404
# delegate further routing
return subpage.specific.route(request, remaining_components)
else:
# request is for this very page
if self.live:
# use the serve() method to render the request if the page is published
return self.serve(request)
else:
# the page matches the request, but isn't published, so 404
raise Http404
The contract is pretty simple. ``route()`` takes the current object (``self``), the ``request`` object, and a list of the remaining ``path_components`` from the request URL. It either continues delegating routing by calling ``route()`` again on one of its children in the Wagtail tree, or ends the routing process by serving something -- either normally through the ``self.serve()`` method or by raising a 404 error.
By overriding the ``route()`` method, we could create custom endpoints for each object in the Wagtail tree. One use case might be using an alternate template when encountering the ``print/`` endpoint in the path. Another might be a REST API which interacts with the current object. Just to see what's involved, lets make a simple model which prints out all of its child path components.
First, ``models.py``:
.. code-block:: python
from django.shortcuts import render
...
class Echoer(Page):
def route(self, request, path_components):
if path_components:
return render(request, self.template, {
'self': self,
'echo': ' '.join(path_components),
})
else:
if self.live:
return self.serve(request)
else:
raise Http404
Echoer.content_panels = [
FieldPanel('title', classname="full title"),
]
Echoer.promote_panels = [
MultiFieldPanel(COMMON_PANELS, "Common page configuration"),
]
This model, ``Echoer``, doesn't define any properties, but does subclass ``Page`` so objects will be able to have a custom title and slug. The template just has to display our ``{{ echo }}`` property. We're skipping the ``serve()`` method entirely, but you could include your render code there to stay consistent with Wagtail's conventions.
Now, once creating a new ``Echoer`` page in the Wagtail admin titled "Echo Base," requests such as::
http://127.0.0.1:8000/echo-base/tauntaun/kennel/bed/and/breakfast/
Will return::
tauntaun kennel bed and breakfast
.. _tagging:
Tagging
-------
Wagtail provides tagging capability through the combination of two django modules, ``taggit`` and ``modelcluster``. ``taggit`` provides a model for tags which is extended by ``modelcluster``, which in turn provides some magical database abstraction which makes drafts and revisions possible in Wagtail. It's a tricky recipe, but the net effect is a many-to-many relationship between your model and a tag class reserved for your model.
Using an example from the Wagtail demo site, here's what the tag model and the relationship field looks like in ``models.py``:
.. code-block:: python
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from taggit.models import Tag, TaggedItemBase
...
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey('demo.BlogPage', related_name='tagged_items')
...
class BlogPage(Page):
...
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
BlogPage.promote_panels = [
...
FieldPanel('tags'),
]
Wagtail's admin provides a nice interface for inputting tags into your content, with typeahead tag completion and friendly tag icons.
Now that we have the many-to-many tag relationship in place, we can fit in a way to render both sides of the relation. Here's more of the Wagtail demo site ``models.py``, where the index model for ``BlogPage`` is extended with logic for filtering the index by tag:
.. code-block:: python
class BlogIndexPage(Page):
...
def serve(self, request):
# Get blogs
blogs = self.blogs
# Filter by tag
tag = request.GET.get('tag')
if tag:
blogs = blogs.filter(tags__name=tag)
return render(request, self.template, {
'self': self,
'blogs': blogs,
})
Here, ``blogs.filter(tags__name=tag)`` invokes a reverse Django queryset filter on the ``BlogPageTag`` model to optionally limit the ``BlogPage`` objects sent to the template for rendering. Now, lets render both sides of the relation by showing the tags associated with an object and a way of showing all of the objects associated with each tag. This could be added to the ``blog_page.html`` template:
.. code-block:: django
{% for tag in self.tags.all %}
<a href="{% pageurl self.blog_index %}?tag={{ tag }}">{{ tag }}</a>
{% endfor %}
Iterating through ``self.tags.all`` will display each tag associated with ``self``, while the link(s) back to the index make use of the filter option added to the ``BlogIndexPage`` model. A Django query could also use the ``tagged_items`` related name field to get ``BlogPage`` objects associated with a tag.
This is just one possible way of creating a taxonomy for Wagtail objects. With all of the components for a taxonomy available through Wagtail, you should be able to fulfill even the most exotic taxonomic schemes.
Custom Page Contexts by Overriding get_context()
------------------------------------------------
Load Alternate Templates by Overriding get_template()
-----------------------------------------------------
Page Modes
----------
get_page_modes
show_as_mode

View file

@ -10,7 +10,7 @@ https://raw.github.com/torchbox/wagtail/master/CHANGELOG.txt
In summary:
* February 2013: Reduced dependencies, basic documentation, translations, tests
* February 2014: Reduced dependencies, basic documentation, translations, tests
What's next
~~~~~~~~~~~
@ -19,12 +19,10 @@ The `issue list <https://github.com/torchbox/wagtail/issues>`_ gives a detailed
* More and better tests (>80% `coverage <https://coveralls.io/r/torchbox/wagtail>`_)
* Better documentation: simple setup guides for all levels of user, a manual for editors and administrators, in-depth intstructions for Django developers.
* A form builder
* Move site section permissions out of Django admin
* Improved image handling: intelligent cropping, animated gif support
* Block-level editing UI (see `Sir Trevor <http://madebymany.github.io/sir-trevor-js/>`_)
* Site settings management
* Edit bird for logged-in visitors
* Support for an HTML content type
* Simple inline stats

143
docs/snippets.rst Normal file
View file

@ -0,0 +1,143 @@
.. _snippets:
Snippets
========
Snippets are pieces of content which do not necessitate a full webpage to render. They could be used for making secondary content, such as headers, footers, and sidebars, editable in the Wagtail admin. Snippets are models which do not inherit the ``Page`` class and are thus not organized into the Wagtail tree, but can still be made editable by assigning panels and identifying the model as a snippet with ``register_snippet()``.
Snippets are not search-able or order-able in the Wagtail admin, so decide carefully if the content type you would want to build into a snippet might be more suited to a page.
Snippet Models
--------------
Here's an example snippet from the Wagtail demo website:
.. code-block:: python
from django.db import models
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailsnippets.models import register_snippet
...
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
def __unicode__(self):
return self.text
register_snippet(Advert)
The ``Advert`` model uses the basic Django model class and defines two properties: text and url. The editing interface is very close to that provided for ``Page``-derived models, with fields assigned in the panels property. Snippets do not use multiple tabs of fields, nor do they provide the "save as draft" or "submit for moderation" features.
``register_snippet(Advert)`` tells Wagtail to treat the model as a snippet. The ``panels`` list defines the fields to show on the snippet editing page. It's also important to provide a string representation of the class through ``def __unicode__(self):`` so that the snippet objects make sense when listed in the Wagtail admin.
Including Snippets in Template Tags
-----------------------------------
The simplest way to make your snippets available to templates is with a template tag. This is mostly done with vanilla Django, so perhaps reviewing Django's documentation for `django custom template tags`_ will be more helpful. We'll go over the basics, though, and make note of any considerations to make for Wagtail.
First, add a new python file to a ``templatetags`` folder within your app. The demo website, for instance uses the path ``wagtaildemo/demo/templatetags/demo_tags.py``. We'll need to load some Django modules and our app's models and ready the ``register`` decorator:
.. _django custom template tags: https://docs.djangoproject.com/en/dev/howto/custom-template-tags/
.. code-block:: python
from django import template
from demo.models import *
register = template.Library()
...
# Advert snippets
@register.inclusion_tag('demo/tags/adverts.html', takes_context=True)
def adverts(context):
return {
'adverts': Advert.objects.all(),
'request': context['request'],
}
``@register.inclusion_tag()`` takes two variables: a template and a boolean on whether that template should be passed a request context. It's a good idea to include request contexts in your custom template tags, since some Wagtail-specific template tags like ``pageurl`` need the context to work properly. The template tag function could take arguments and filter the adverts to return a specific model, but for brevity we'll just use ``Advert.objects.all()``.
Here's what's in the template used by the template tag:
.. code-block:: django
{% for advert in adverts %}
<p>
<a href="{{ advert.url }}">
{{ advert.text }}
</a>
</p>
{% endfor %}
Then in your own page templates, you can include your snippet template tag with:
.. code-block:: django
{% block content %}
...
{% adverts %}
{% endblock %}
Binding Pages to Snippets
-------------------------
An alternate strategy for including snippets might involve explicitly binding a specific page object to a specific snippet object. Lets add another snippet class to see how that might work:
.. code-block:: python
from django.db import models
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
from wagtail.wagtailsnippets.models import register_snippet
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from modelcluster.fields import ParentalKey
...
class AdvertPlacement(models.Model):
page = ParentalKey('wagtailcore.Page', related_name='advert_placements')
advert = models.ForeignKey('demo.Advert', related_name='+')
class Meta:
verbose_name = "Advert Placement"
verbose_name_plural = "Advert Placements"
panels = [
PageChooserPanel('page'),
SnippetChooserPanel('advert', Advert),
]
def __unicode__(self):
return self.page.title + " -> " + self.advert.text
register_snippet(AdvertPlacement)
The class ``AdvertPlacement`` has two properties, ``page`` and ``advert``, which point to other models. Wagtail provides a ``PageChooserPanel`` and ``SnippetChooserPanel`` to let us make painless selection of those properties in the Wagtail admin. Note also the ``Meta`` class, which you can stock with the ``verbose_name`` and ``verbose_name_plural`` properties to override the snippet labels in the Wagtail admin. The text representation of the class has also gotten fancy, using both properties to construct a compound label showing the relationship it forms between a page and an Advert.
With this snippet in place, we can use the reverse ``related_name`` lookup label ``advert_placements`` to iterate over any placements within our template files. In the template for a ``Page``-derived model, we could include the following:
.. code-block:: django
{% if self.advert_placements %}
{% for advert_placement in self.advert_placements.all %}
<p><a href="{{ advert_placement.advert.url }}">{{ advert_placement.advert.text }}</a></p>
{% endfor %}
{% endif %}

View file

@ -1,20 +1,20 @@
Generating a static site
========================
This document describes how to render your Wagtail site into static HTML files using `django medusa`_ and the 'wagtail.contrib.wagtailmedusa' module.
This document describes how to render your Wagtail site into static HTML files on your local filesystem, Amazon S3 or Google App Engine, using `django medusa`_ and the ``wagtail.contrib.wagtailmedusa`` module.
Installing django-medusa
~~~~~~~~~~~~~~~~~~~~~~~~
Firstly, install django medusa from pip:
First, install django medusa from pip:
.. code::
pip install django-medusa
Then add 'django_medusa' and 'wagtail.contrib.wagtailmedusa' to INSTALLED_APPS:
Then add ``django_medusa`` and ``wagtail.contrib.wagtailmedusa`` to ``INSTALLED_APPS``:
.. code:: python
@ -28,9 +28,9 @@ Then add 'django_medusa' and 'wagtail.contrib.wagtailmedusa' to INSTALLED_APPS:
Replacing GET parameters with custom routing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pages which require GET parameters (eg, pagination) don't generate suitable filenames for generated HTML files so they need to be changed to use custom routing instead.
Pages which require GET parameters (e.g. for pagination) don't generate suitable filenames for generated HTML files so they need to be changed to use custom routing instead.
For example, lets say we have a Blog Index which uses pagination. We can override the 'route' method to make it respond on urls like '/page/1' and pass the page number through to the serve method:
For example, let's say we have a Blog Index which uses pagination. We can override the ``route`` method to make it respond on urls like '/page/1', and pass the page number through to the ``serve`` method:
.. code:: python
@ -53,7 +53,7 @@ For example, lets say we have a Blog Index which uses pagination. We can overrid
Rendering pages which use custom routing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For page types that override the route method, we need to let django medusa know which URLs it responds on. This is done by overriding the 'get_static_site_paths' method to make it yield one string per URL path.
For page types that override the ``route`` method, we need to let django medusa know which URLs it responds on. This is done by overriding the ``get_static_site_paths`` method to make it yield one string per URL path.
For example, the BlogIndex above would need to yield one URL for each page of results:
@ -75,7 +75,7 @@ For example, the BlogIndex above would need to yield one URL for each page of re
Rendering
~~~~~~~~~
To render a site, just run ``./manage.py staticsitegen``. This will render the entire website and place the HTML in a folder called 'medusa_output'. The static and media folders need to be copied into this folder manually after the rendering is complete.
To render a site, run ``./manage.py staticsitegen``. This will render the entire website and place the HTML in a folder called 'medusa_output'. The static and media folders need to be copied into this folder manually after the rendering is complete. This feature inherits django-medusa's ability to render your static site to Amazon S3 or Google App Engine; see the `medusa docs <https://github.com/mtigas/django-medusa/blob/master/README.markdown>`_ for configuration details.
To test, open the 'medusa_output' folder in a terminal and run ``python -m SimpleHTTPServer``.

View file

@ -1,15 +1,264 @@
Search
======
Wagtail can degrade to a database-backed text search, but we strongly recommend `Elasticsearch`_. If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly:
Wagtail provides a comprehensive and extensible search interface. In addition, it provides ways to promote search results through "Editor's Picks." Wagtail also collects simple statistics on queries made through the search interface.
Default Page Search
-------------------
Wagtail provides a default frontend search interface which indexes the ``title`` field common to all ``Page``-derived models. Let's take a look at all the components of the search interface.
The most basic search functionality just needs a search box which submits a request. Since this will be reused throughout the site, let's put it in ``mysite/includes/search_box.html`` and then use ``{% include ... %}`` to weave it into templates:
.. code-block:: django
<form action="{% url 'wagtailsearch_search' %}" method="get">
<input type="text" name="q"{% if query_string %} value="{{ query_string }}"{% endif %}>
<input type="submit" value="Search">
</form>
The form is submitted to the url of the ``wagtailsearch_search`` view, with the search terms variable ``q``. The view will use its own basic search results template.
Let's use our own template for the results, though. First, in your project's ``settings.py``, define a path to your template:
.. code-block:: python
WAGTAILSEARCH_RESULTS_TEMPLATE = 'mysite/search_results.html'
Next, let's look at the template itself:
.. code-block:: django
{% extends "mysite/base.html" %}
{% load pageurl %}
{% block title %}Search{% if search_results %} Results{% endif %}{% endblock %}
{% block search_box %}
{% include "mysite/includes/search_box.html" with query_string=query_string only %}
{% endblock %}
{% block content %}
<h2>Search Results{% if request.GET.q %} for {{ request.GET.q }}{% endif %}</h2>
<ul>
{% for result in search_results %}
<li>
<h4><a href="{% pageurl result.specific %}">{{ result.specific }}</a></h4>
{% if result.specific.search_description %}
{{ result.specific.search_description|safe }}
{% endif %}
</li>
{% empty %}
<li>No results found</li>
{% endfor %}
</ul>
{% endblock %}
The search view provides a context with a few useful variables.
``query_string``
The terms (string) used to make the search.
``search_results``
A collection of Page objects matching the query. The ``specific`` property of ``Page`` will give the most-specific subclassed model object for the Wagtail page. For instance, if an ``Event`` model derived from the basic Wagtail ``Page`` were included in the search results, you could use ``specific`` to access the custom properties of the ``Event`` model (``result.specific.date_of_event``).
``is_ajax``
Boolean. This returns Django's ``request.is_ajax()``.
``query``
A Wagtail ``Query`` object matching the terms. The ``Query`` model provides several class methods for viewing the statistics of all queries, but exposes only one property for single objects, ``query.hits``, which tracks the number of time the search string has been used over the lifetime of the site. ``Query`` also joins to the Editor's Picks functionality though ``query.editors_picks``. See :ref:`editors-picks`.
Editor's Picks
--------------
Editor's Picks are a way of explicitly linking relevant content to search terms, so results pages can contain curated content instead of being at the mercy of the search algorithm. In a template using the search results view, editor's picks can be accessed through the variable ``query.editors_picks``. To include editor's picks in your search results template, use the following properties.
``query.editors_picks.all``
This gathers all of the editor's picks objects relating to the current query, in order according to their sort order in the Wagtail admin. You can then iterate through them using a ``{% for ... %}`` loop. Each editor's pick object provides these properties:
``editors_pick.page``
The page object associated with the pick. Use ``{% pageurl editors_pick.page %}`` to generate a URL or provide other properties of the page object.
``editors_pick.description``
The description entered when choosing the pick, perhaps explaining why the page is relevant to the search terms.
Putting this all together, a block of your search results template displaying editor's Picks might look like this:
.. code-block:: django
{% with query.editors_picks.all as editors_picks %}
{% if editors_picks %}
<div class="well">
<h3>Editors picks</h3>
<ul>
{% for editors_pick in editors_picks %}
<li>
<h4>
<a href="{% pageurl editors_pick.page %}">
{{ editors_pick.page.title }}
</a>
</h4>
<p>{{ editors_pick.description|safe }}</p>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
Asynchronous Search with JSON and AJAX
--------------------------------------
Wagtail provides JSON search results when queries are made to the ``wagtailsearch_suggest`` view. To take advantage of it, we need a way to make that URL available to a static script. Instead of hard-coding it, let's set a global variable in our ``base.html``:
.. code-block:: django
<script>
var wagtailJSONSearchURL = "{% url 'wagtailsearch_suggest' %}";
</script>
Now add a simple interface for the search with a ``<input>`` element to gather search terms and a ``<div>`` to display the results:
.. code-block:: html
<div>
<h3>Search</h3>
<input id="json-search" type="text">
<div id="json-results"></div>
</div>
Finally, we'll use JQuery to make the asynchronous requests and handle the interactivity:
.. code-block:: guess
$(function() {
// cache the elements
var searchBox = $('#json-search'),
resultsBox = $('#json-results');
// when there's something in the input box, make the query
searchBox.on('input', function() {
if( searchBox.val() == ''){
resultsBox.html('');
return;
}
// make the request to the Wagtail JSON search view
$.ajax({
url: wagtailJSONSearchURL + "?q=" + searchBox.val(),
dataType: "json"
})
.done(function(data) {
console.log(data);
if( data == undefined ){
resultsBox.html('');
return;
}
// we're in business! let's format the results
var htmlOutput = '';
data.forEach(function(element, index, array){
htmlOutput += '<p><a href="' + element.url + '">' + element.title + '</a></p>';
});
// and display them
resultsBox.html(htmlOutput);
})
.error(function(data){
console.log(data);
});
});
});
Results are returned as a JSON object with this structure:
.. code-block:: guess
{
[
{
title: "Lumpy Space Princess",
url: "/oh-my-glob/"
},
{
title: "Lumpy Space",
url: "/no-smooth-posers/"
},
...
]
}
What if you wanted access to the rest of the results context or didn't feel like using JSON? Wagtail also provides a generalized AJAX interface where you can use your own template to serve results asynchronously.
The AJAX interface uses the same view as the normal HTML search, ``wagtailsearch_search``, but will serve different results if Django classifies the request as AJAX (``request.is_ajax()``). Another entry in your project settings will let you override the template used to serve this response:
.. code-block:: python
WAGTAILSEARCH_RESULTS_TEMPLATE_AJAX = 'myapp/includes/search_listing.html'
In this template, you'll have access to the same context variables provided to the HTML template. You could provide a template in JSON format with extra properties, such as ``query.hits`` and editor's picks, or render an HTML snippet that can go directly into your results ``<div>``. If you need more flexibility, such as multiple formats/templates based on differing requests, you can set up a custom search view.
.. _editors-picks:
Indexing Custom Fields & Custom Search Views
--------------------------------------------
This functionality is still under active development to provide a streamlined interface, but take a look at ``wagtail/wagtail/wagtailsearch/views/frontend.py`` if you are interested in coding custom search views.
Search Backends
---------------
Wagtail can degrade to a database-backed text search, but we strongly recommend `Elasticsearch`_.
.. _Elasticsearch: http://www.elasticsearch.org/
Default DB Backend
``````````````````
The default DB search backend uses Django's ``__icontains`` filter.
Elasticsearch Backend
`````````````````````
Prerequisites are the Elasticsearch service itself and, via pip, the `elasticutils`_ and `pyelasticsearch`_ packages:
.. code-block:: guess
pip install elasticutils pyelasticsearch
.. note::
The dependency on pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_.
The backend is configured in settings:
.. code-block:: python
WAGTAILSEARCH_BACKENDS = {
'default': {
'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch',
'URLS': ['http://localhost:9200'],
'INDEX': 'wagtail',
'TIMEOUT': 5,
'FORCE_NEW': False,
}
}
Other than ``BACKEND`` the keys are optional and default to the values shown. ``FORCE_NEW`` is used by elasticutils. In addition, any other keys are passed directly to the Elasticsearch constructor as case-sensitive keyword arguments (e.g. ``'max_retries': 1``).
If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly:
- Sign up for an account at `dashboard.searchly.com/users/sign\_up`_
- Use your Searchly dashboard to create a new index, e.g. 'wagtaildemo'
- Note the connection URL from your Searchly dashboard
- Update ``WAGTAILSEARCH_ES_URLS`` and ``WAGTAILSEARCH_ES_INDEX`` in
your local settings
- Configure ``URLS`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS``
- Run ``./manage.py update_index``
.. _Elasticsearch: http://www.elasticsearch.org/
.. _elasticutils: http://elasticutils.readthedocs.org
.. _pyelasticsearch: http://pyelasticsearch.readthedocs.org
.. _elasticsearch-py: http://elasticsearch-py.readthedocs.org
.. _Searchly: http://www.searchly.com/
.. _dashboard.searchly.com/users/sign\_up: https://dashboard.searchly.com/users/sign_up
.. _dashboard.searchly.com/users/sign\_up: https://dashboard.searchly.com/users/sign_up
Rolling Your Own
````````````````
Wagtail search backends implement the interface defined in ``wagtail/wagtail/wagtailsearch/backends/base.py``. At a minimum, the backend's ``search()`` method must return a collection of objects or ``model.objects.none()``. For a fully-featured search backend, examine the Elasticsearch backend code in ``elasticsearch.py``.

View file

@ -1,7 +1,3 @@
# Requirements essential for developing wagtail (not needed to run it)
unittest2==0.5.1
# For coverage and PEP8 linting
coverage==3.7.1
flake8==2.1.0

View file

@ -26,6 +26,8 @@ if not settings.configured:
if has_elasticsearch:
WAGTAILSEARCH_BACKENDS['elasticsearch'] = {
'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch',
'TIMEOUT': 10,
'max_retries': 1,
}
settings.configure(
@ -40,6 +42,7 @@ if not settings.configured:
STATIC_URL='/static/',
STATIC_ROOT=STATIC_ROOT,
MEDIA_ROOT=MEDIA_ROOT,
USE_TZ=True,
STATICFILES_FINDERS=(
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
@ -80,6 +83,7 @@ if not settings.configured:
'wagtail.wagtailembeds',
'wagtail.wagtailsearch',
'wagtail.wagtailredirects',
'wagtail.wagtailforms',
'wagtail.tests',
],

View file

@ -1,5 +1,7 @@
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# For a non-dummy email backend configure Django's EMAIL_BACKEND
# in settings/production.py post-installation.
# Tested on Debian 7.0.
# Tom Dyson and Neal Todd

View file

@ -1,5 +1,7 @@
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# For a non-dummy email backend configure Django's EMAIL_BACKEND
# in settings/production.py post-installation.
# Tested on Ubuntu 13.04 and 13.10.
# Tom Dyson and Neal Todd

View file

@ -18,7 +18,7 @@ except ImportError:
setup(
name='wagtail',
version='0.2',
version='0.3.1',
description='A Django content management system focused on flexibility and user experience',
author='Matthew Westcott',
author_email='matthew.westcott@torchbox.com',
@ -51,6 +51,7 @@ setup(
"Pillow>=2.3.0",
"beautifulsoup4>=4.3.2",
"lxml>=3.3.0",
'unicodecsv>=0.9.4',
'Unidecode>=0.04.14',
"BeautifulSoup==3.2.1", # django-compressor gets confused if we have lxml but not BS3 installed
],

View file

@ -23,7 +23,7 @@
"model": "wagtailcore.page",
"fields": {
"title": "Welcome to the Wagtail test site!",
"numchild": 1,
"numchild": 3,
"show_in_menus": false,
"live": true,
"depth": 2,
@ -39,7 +39,7 @@
"model": "wagtailcore.page",
"fields": {
"title": "Events",
"numchild": 2,
"numchild": 3,
"show_in_menus": true,
"live": true,
"depth": 3,
@ -62,7 +62,7 @@
"model": "wagtailcore.page",
"fields": {
"title": "Christmas",
"numchild": 1,
"numchild": 0,
"show_in_menus": true,
"live": true,
"depth": 4,
@ -90,7 +90,7 @@
"model": "wagtailcore.page",
"fields": {
"title": "Tentative Unpublished Event",
"numchild": 1,
"numchild": 0,
"show_in_menus": true,
"live": false,
"depth": 4,
@ -118,7 +118,7 @@
"model": "wagtailcore.page",
"fields": {
"title": "Someone Else's Event",
"numchild": 1,
"numchild": 0,
"show_in_menus": true,
"live": false,
"depth": 4,
@ -164,6 +164,57 @@
}
},
{
"pk": 8,
"model": "wagtailcore.page",
"fields": {
"title": "Contact us",
"numchild": 0,
"show_in_menus": true,
"live": true,
"depth": 3,
"content_type": ["tests", "formpage"],
"path": "000100010003",
"url_path": "/home/contact-us/",
"slug": "contact-us"
}
},
{
"pk": 8,
"model": "tests.formpage",
"fields": {
}
},
{
"pk": 1,
"model": "tests.formfield",
"fields": {
"sort_order": 1,
"label": "Your email",
"field_type": "email",
"required": true,
"choices": "",
"default_value": "",
"help_text": "",
"page": 8
}
},
{
"pk": 2,
"model": "tests.formfield",
"fields": {
"sort_order": 2,
"label": "Your message",
"field_type": "multiline",
"required": true,
"choices": "",
"default_value": "",
"help_text": "",
"page": 8
}
},
{
"pk": 1,
"model": "wagtailcore.site",
@ -201,6 +252,19 @@
]
}
},
{
"pk": 5,
"model": "auth.group",
"fields": {
"name": "Site-wide editors",
"permissions": [
["access_admin", "wagtailadmin", "admin"],
["add_image", "wagtailimages", "image"],
["change_image", "wagtailimages", "image"],
["delete_image", "wagtailimages", "image"]
]
}
},
{
"pk": 1,
"model": "wagtailcore.grouppagepermission",
@ -237,6 +301,15 @@
"permission_type": "publish"
}
},
{
"pk": 5,
"model": "wagtailcore.grouppagepermission",
"fields": {
"group": ["Site-wide editors"],
"page": 2,
"permission_type": "edit"
}
},
{
"pk": 1,
@ -308,5 +381,42 @@
"password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22",
"email": "inactiveuser@example.com"
}
},
{
"pk": 5,
"model": "auth.user",
"fields": {
"username": "siteeditor",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"groups": [
["Site-wide editors"]
],
"user_permissions": [],
"password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22",
"email": "siteeditor@example.com"
}
},
{
"pk": 1,
"model": "wagtailforms.formsubmission",
"fields": {
"form_data": "{\"your-email\": \"old@example.com\", \"your-message\": \"this is a really old message\"}",
"page": 8,
"submit_time": "2013-01-01T12:00:00.000Z"
}
},
{
"pk": 2,
"model": "wagtailforms.formsubmission",
"fields": {
"form_data": "{\"your-email\": \"new@example.com\", \"your-message\": \"this is a fairly new message\"}",
"page": 8,
"submit_time": "2014-01-01T12:00:00.000Z"
}
}
]

View file

@ -6,6 +6,8 @@ from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel, InlinePanel, PageChooserPanel
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
from wagtail.wagtailsnippets.models import register_snippet
EVENT_AUDIENCE_CHOICES = (
@ -234,3 +236,61 @@ EventIndex.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('intro', classname="full"),
]
class FormField(AbstractFormField):
page = ParentalKey('FormPage', related_name='form_fields')
class FormPage(AbstractEmailForm):
pass
FormPage.content_panels = [
FieldPanel('title', classname="full title"),
InlinePanel(FormPage, 'form_fields', label="Form fields"),
MultiFieldPanel([
FieldPanel('to_address', classname="full"),
FieldPanel('from_address', classname="full"),
FieldPanel('subject', classname="full"),
], "Email")
]
# Snippets
# Snippets
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
panels = [
FieldPanel('url'),
FieldPanel('text'),
]
def __unicode__(self):
return self.text
register_snippet(Advert)
# AlphaSnippet and ZuluSnippet are for testing ordering of
# snippets when registering. They are named as such to ensure
# thier ordering is clear. They are registered during testing
# to ensure specific [in]correct register ordering
# AlphaSnippet is registered during TestSnippetOrdering
class AlphaSnippet(models.Model):
text = models.CharField(max_length=255)
def __unicode__(self):
return self.text
# ZuluSnippet is registered during TestSnippetOrdering
class ZuluSnippet(models.Model):
text = models.CharField(max_length=255)
def __unicode__(self):
return self.text

View file

@ -0,0 +1,15 @@
{% load pageurl %}
<!DOCTYPE HTML>
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
<form action="{% pageurl self %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit">
</form>
</body>
</html>

View file

@ -0,0 +1,11 @@
{% load pageurl %}
<!DOCTYPE HTML>
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
<p>Thank you for your feedback.</p>
</body>
</html>

View file

@ -0,0 +1,11 @@
{% load pageurl %}
<!DOCTYPE HTML>
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
<h2>Simple page</h2>
</body>
</html>

View file

@ -14,7 +14,9 @@ except ImportError:
def login(client):
# Create a user
User.objects.create_superuser(username='test', email='test@email.com', password='password')
user = User.objects.create_superuser(username='test', email='test@email.com', password='password')
# Login
client.login(username='test', password='password')
return user

View file

@ -31,11 +31,11 @@ msgstr "搜索词"
#: .\forms.py:42
msgid "Enter your username"
msgstr ""
msgstr "请输入用户名"
#: .\forms.py:45
msgid "Enter password"
msgstr ""
msgstr "请输入密码"
#: .\forms.py:50
msgid "Enter your email address to reset your password"
@ -89,7 +89,7 @@ msgstr "登录Wagtail"
#: .\templates\wagtailadmin\login.html:42
msgid "Forgotten it?"
msgstr "忘记"
msgstr "忘记密码"
#: .\templates\wagtailadmin\account\account.html:4
msgid "Account"
@ -104,7 +104,7 @@ msgid ""
"Your avatar image is provided by Gravatar and is connected to your email "
"address. With a Gravatar account you can set an avatar for any number of "
"other email addresses you use."
msgstr "您的头像图片是由Gravatar提供的并且关联了您的电子邮件地址。一个Gravatar账号可以设置多个电子邮件地址的头像图片。"
msgstr "您的头像图片是由Gravatar提供的并且关联了您的电子邮箱。一个Gravatar账号可以设置多个电子邮箱的头像图片。"
#: .\templates\wagtailadmin\account\account.html:23
#: .\templates\wagtailadmin\account\change_password.html:4
@ -113,7 +113,7 @@ msgstr "修改密码"
#: .\templates\wagtailadmin\account\account.html:27
msgid "Change the password you use to log in."
msgstr "修改您用于登录密码。"
msgstr "修改登录密码。"
#: .\templates\wagtailadmin\account\change_password.html:16
msgid "Change Password"

View file

@ -0,0 +1,843 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Wagtail\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-03-14 23:02+0200\n"
"PO-Revision-Date: 2014-05-01 12:09+0000\n"
"Last-Translator: wdv4758h <wdv4758h@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_TW\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: .\edit_handlers.py:81 .\edit_handlers.py:130 .\edit_handlers.py:134
msgid "Please type a valid time"
msgstr "請輸入一個有效的時間"
#: .\edit_handlers.py:724
msgid "Common page configuration"
msgstr "一般頁面設定"
#: .\forms.py:18
msgid "Search term"
msgstr "搜尋關鍵字"
#: .\forms.py:42
msgid "Enter your username"
msgstr "請輸入您的帳號"
#: .\forms.py:45
msgid "Enter password"
msgstr "請輸入密碼"
#: .\forms.py:50
msgid "Enter your email address to reset your password"
msgstr "請輸入您的電子信箱來重新設定密碼"
#: .\forms.py:59
msgid "Please fill your email address."
msgstr "請輸入您的電子信箱"
#: .\forms.py:72
msgid ""
"Sorry, you cannot reset your password here as your user account is managed "
"by another server."
msgstr "對不起,您不能在此重新設定您的密碼,因為您的帳號是由其他伺服器所管理。"
#: .\forms.py:75
msgid "This email address is not recognised."
msgstr "找不到這個電子信箱。"
#: .\templates\wagtailadmin\base.html:7 .\templates\wagtailadmin\home.html:4
msgid "Dashboard"
msgstr "Dashboard"
#: .\templates\wagtailadmin\base.html:31
msgid "Menu"
msgstr "選單"
#: .\templates\wagtailadmin\home.html:22
#, python-format
msgid "Welcome to the %(site_name)s Wagtail CMS"
msgstr "歡迎進入 %(site_name)s 的 Wagtail 內容管理系統"
#: .\templates\wagtailadmin\home.html:33
msgid ""
"This is your dashboard on which helpful information about content you've "
"created will be displayed."
msgstr "這是您的 Dashboard會顯示對於已建立的內容有幫助的訊息。"
#: .\templates\wagtailadmin\login.html:4
#: .\templates\wagtailadmin\login.html:55
msgid "Sign in"
msgstr "登入"
#: .\templates\wagtailadmin\login.html:18
msgid "Your username and password didn't match. Please try again."
msgstr "您的帳號和密碼輸入錯誤,請再試一次。"
#: .\templates\wagtailadmin\login.html:26
msgid "Sign in to Wagtail"
msgstr "登入 Wagtail"
#: .\templates\wagtailadmin\login.html:42
msgid "Forgotten it?"
msgstr "忘記了嗎?"
#: .\templates\wagtailadmin\account\account.html:4
msgid "Account"
msgstr "帳號"
#: .\templates\wagtailadmin\account\account.html:11
msgid "Set gravatar"
msgstr "設定 gravatar"
#: .\templates\wagtailadmin\account\account.html:15
msgid ""
"Your avatar image is provided by Gravatar and is connected to your email "
"address. With a Gravatar account you can set an avatar for any number of "
"other email addresses you use."
msgstr "您的頭像是由 Gravatar 所提供,並且已經聯結你的電子信箱。一個 Gravatar 帳號可以設定多個電子信箱的頭像圖片。"
#: .\templates\wagtailadmin\account\account.html:23
#: .\templates\wagtailadmin\account\change_password.html:4
msgid "Change password"
msgstr "修改密碼"
#: .\templates\wagtailadmin\account\account.html:27
msgid "Change the password you use to log in."
msgstr "修改登入用的密碼。"
#: .\templates\wagtailadmin\account\change_password.html:16
msgid "Change Password"
msgstr "修改密碼"
#: .\templates\wagtailadmin\account\change_password.html:19
msgid ""
"Your password can't be changed here. Please contact a site administrator."
msgstr "您的密碼不能在這更改。請聯絡網站管理員。"
#: .\templates\wagtailadmin\account\password_reset\complete.html:4
#: .\templates\wagtailadmin\account\password_reset\confirm.html:42
#: .\templates\wagtailadmin\account\password_reset\done.html:4
#: .\templates\wagtailadmin\account\password_reset\form.html:37
msgid "Reset password"
msgstr "重新設定密碼"
#: .\templates\wagtailadmin\account\password_reset\complete.html:15
msgid "Password change successful"
msgstr "密碼修改成功"
#: .\templates\wagtailadmin\account\password_reset\complete.html:16
msgid "Login"
msgstr "登入"
#: .\templates\wagtailadmin\account\password_reset\confirm.html:4
#: .\templates\wagtailadmin\account\password_reset\confirm.html:26
msgid "Set your new password"
msgstr "設定您的新密碼"
#: .\templates\wagtailadmin\account\password_reset\confirm.html:19
msgid "The passwords do not match. Please try again."
msgstr "密碼不一致,請再試一次。"
#: .\templates\wagtailadmin\account\password_reset\done.html:15
msgid "Check your email"
msgstr "請檢查您的電子信箱"
#: .\templates\wagtailadmin\account\password_reset\done.html:16
msgid "A link to reset your password has been emailed to you."
msgstr "一個重新設定密碼連結已經寄到您的電子信箱了"
#: .\templates\wagtailadmin\account\password_reset\email.txt:2
msgid "Please follow the link below to reset your password"
msgstr "請點擊下面的連結來重新設定您的密碼"
#: .\templates\wagtailadmin\account\password_reset\email_subject.txt:2
msgid "Password reset"
msgstr "密碼已經重新設定"
#: .\templates\wagtailadmin\account\password_reset\form.html:27
msgid "Reset your password"
msgstr "重新設定您的密碼"
#: .\templates\wagtailadmin\chooser\_link_types.html:5
#: .\templates\wagtailadmin\chooser\_link_types.html:7
msgid "Internal link"
msgstr "內部連結"
#: .\templates\wagtailadmin\chooser\_link_types.html:11
#: .\templates\wagtailadmin\chooser\_link_types.html:13
msgid "External link"
msgstr "外部連結"
#: .\templates\wagtailadmin\chooser\_link_types.html:17
#: .\templates\wagtailadmin\chooser\_link_types.html:19
msgid "Email link"
msgstr "電子信箱連結"
#: .\templates\wagtailadmin\chooser\_search_form.html:7
#: .\templates\wagtailadmin\pages\search.html:3
#: .\templates\wagtailadmin\pages\search.html:16
#: .\templatetags\wagtailadmin_nav.py:44
msgid "Search"
msgstr "搜尋"
#: .\templates\wagtailadmin\chooser\_search_results.html:3
#: .\templatetags\wagtailadmin_nav.py:43
msgid "Explorer"
msgstr "瀏覽"
#: .\templates\wagtailadmin\chooser\_search_results.html:5
#: .\templates\wagtailadmin\pages\index.html:15
#: .\templates\wagtailadmin\pages\move_choose_destination.html:10
msgid "Home"
msgstr "首頁"
#: .\templates\wagtailadmin\chooser\_search_results.html:13
#, python-format
msgid ""
"\n"
" There is one match\n"
" "
msgid_plural ""
"\n"
" There are %(counter)s matches\n"
" "
msgstr[0] "\n 有一個符合"
msgstr[1] "\n 有 $(counter)s 個符合"
#: .\templates\wagtailadmin\chooser\browse.html:2
#: .\templates\wagtailadmin\chooser\search.html:2
#: .\templates\wagtailadmin\edit_handlers\page_chooser_panel.html:13
msgid "Choose a page"
msgstr "選擇一個頁面"
#: .\templates\wagtailadmin\chooser\email_link.html:2
msgid "Add an email link"
msgstr "新增一個電子信箱"
#: .\templates\wagtailadmin\chooser\email_link.html:14
#: .\templates\wagtailadmin\chooser\external_link.html:14
msgid "Insert link"
msgstr "插入一個連結"
#: .\templates\wagtailadmin\chooser\external_link.html:2
msgid "Add an external link"
msgstr "新增一個外部連結"
#: .\templates\wagtailadmin\edit_handlers\chooser_panel.html:20
msgid "Clear choice"
msgstr "清除選擇"
#: .\templates\wagtailadmin\edit_handlers\chooser_panel.html:22
msgid "Choose another item"
msgstr "選擇其他選項"
#: .\templates\wagtailadmin\edit_handlers\chooser_panel.html:27
msgid "Choose an item"
msgstr "選擇一個選項"
#: .\templates\wagtailadmin\edit_handlers\inline_panel_child.html:5
msgid "Move up"
msgstr "往上移動"
#: .\templates\wagtailadmin\edit_handlers\inline_panel_child.html:6
msgid "Move down"
msgstr "往下移動"
#: .\templates\wagtailadmin\edit_handlers\inline_panel_child.html:8
#: .\templates\wagtailadmin\pages\confirm_delete.html:7
#: .\templates\wagtailadmin\pages\edit.html:36
#: .\templates\wagtailadmin\pages\list.html:68
#: .\templates\wagtailadmin\pages\list.html:188
msgid "Delete"
msgstr "刪除"
#: .\templates\wagtailadmin\edit_handlers\page_chooser_panel.html:12
msgid "Choose another page"
msgstr "選擇另外一個頁面"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:5
msgid "Pages awaiting moderation"
msgstr "這些頁面正等待審核"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:13
#: .\templates\wagtailadmin\home\recent_edits.html:12
#: .\templates\wagtailadmin\pages\list.html:101
msgid "Title"
msgstr "標題"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:14
#: .\templates\wagtailadmin\pages\list.html:22
msgid "Parent"
msgstr "上一層"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:15
#: .\templates\wagtailadmin\pages\list.html:24
#: .\templates\wagtailadmin\pages\list.html:116
msgid "Type"
msgstr "類型"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:16
msgid "Edited"
msgstr "編輯"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:23
#: .\templates\wagtailadmin\home\recent_edits.html:21
#: .\templates\wagtailadmin\pages\list.html:167
#: .\templates\wagtailadmin\pages\list.html:176
msgid "Edit this page"
msgstr "編輯這個頁面"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:28
#: .\templates\wagtailadmin\pages\_moderator_userbar.html:12
msgid "Approve"
msgstr "通過"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:34
#: .\templates\wagtailadmin\pages\_moderator_userbar.html:17
msgid "Reject"
msgstr "拒絕"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:37
#: .\templates\wagtailadmin\home\recent_edits.html:23
#: .\templates\wagtailadmin\pages\_moderator_userbar.html:9
#: .\templates\wagtailadmin\pages\list.html:56
#: .\templates\wagtailadmin\pages\list.html:176
msgid "Edit"
msgstr "編輯"
#: .\templates\wagtailadmin\home\pages_for_moderation.html:38
#: .\templates\wagtailadmin\pages\create.html:24
#: .\templates\wagtailadmin\pages\edit.html:42
msgid "Preview"
msgstr "預覽"
#: .\templates\wagtailadmin\home\recent_edits.html:5
msgid "Your most recent edits"
msgstr "你最近的編輯"
#: .\templates\wagtailadmin\home\recent_edits.html:13
msgid "Date"
msgstr "日期"
#: .\templates\wagtailadmin\home\recent_edits.html:14
#: .\templates\wagtailadmin\pages\list.html:25
#: .\templates\wagtailadmin\pages\list.html:128
msgid "Status"
msgstr "狀態"
#: .\templates\wagtailadmin\home\recent_edits.html:25
#: .\templates\wagtailadmin\pages\list.html:59
#: .\templates\wagtailadmin\pages\list.html:179
msgid "View draft"
msgstr "觀看草稿"
#: .\templates\wagtailadmin\home\recent_edits.html:28
#: .\templates\wagtailadmin\pages\list.html:62
#: .\templates\wagtailadmin\pages\list.html:182
msgid "View live"
msgstr "觀看線上版"
#: .\templates\wagtailadmin\home\site_summary.html:3
msgid "Site summary"
msgstr "網站摘要"
#: .\templates\wagtailadmin\home\site_summary.html:6
#, python-format
msgid ""
"\n"
" <span>%(total_pages)s</span> Page\n"
" "
msgid_plural ""
"\n"
" <span>%(total_pages)s</span> Pages\n"
" "
msgstr[0] "\n <span>%(total_pages)s</span> 頁面\n "
msgstr[1] "\n <span>%(total_pages)s</span> 頁面\n "
#: .\templates\wagtailadmin\home\site_summary.html:13
#, python-format
msgid ""
"\n"
" <span>%(total_images)s</span> Image\n"
" "
msgid_plural ""
"\n"
" <span>%(total_images)s</span> Images\n"
" "
msgstr[0] "\n <span>%(total_images)s</span> 圖片\n "
msgstr[1] "\n <span>%(total_images)s</span> 圖片\n "
#: .\templates\wagtailadmin\home\site_summary.html:20
#, python-format
msgid ""
"\n"
" <span>%(total_docs)s</span> Document\n"
" "
msgid_plural ""
"\n"
" <span>%(total_docs)s</span> Documents\n"
" "
msgstr[0] "\n <span>%(total_docs)s</span> 文件\n "
msgstr[1] "\n <span>%(total_docs)s</span> 文件\n "
#: .\templates\wagtailadmin\notifications\approved.html:1
#, python-format
msgid "The page \"%(title)s\" has been approved"
msgstr "這個頁面 \"%(title)s\" 已經通過"
#: .\templates\wagtailadmin\notifications\approved.html:2
#, python-format
msgid "The page \"%(title)s\" has been approved."
msgstr "這個頁面 \"%(title)s\" 已經通過"
#: .\templates\wagtailadmin\notifications\approved.html:4
msgid "You can view the page here:"
msgstr "你可以在此觀看這個頁面"
#: .\templates\wagtailadmin\notifications\rejected.html:1
#, python-format
msgid "The page \"%(title)s\" has been rejected"
msgstr "這個頁面 \"%(title)s\" 已經被拒絕"
#: .\templates\wagtailadmin\notifications\rejected.html:2
#, python-format
msgid "The page \"%(title)s\" has been rejected."
msgstr "這個頁面 \"%(title)s\" 已經被拒絕"
#: .\templates\wagtailadmin\notifications\rejected.html:4
#: .\templates\wagtailadmin\notifications\submitted.html:5
msgid "You can edit the page here:"
msgstr "你可以在此編輯這個頁面:"
#: .\templates\wagtailadmin\notifications\submitted.html:1
#, python-format
msgid "The page \"%(page)s\" has been submitted for moderation"
msgstr "這個頁面 \"%(page)s\" 已經送審"
#: .\templates\wagtailadmin\notifications\submitted.html:2
#, python-format
msgid "The page \"%(page)s\" has been submitted for moderation."
msgstr "這個頁面 \"%(page)s\" 已經送審。"
#: .\templates\wagtailadmin\notifications\submitted.html:4
msgid "You can preview the page here:"
msgstr "你可以在此預覽這個頁面:"
#: .\templates\wagtailadmin\pages\_moderator_userbar.html:4
#, python-format
msgid ""
"\n"
" Previewing '%(title)s', submitted by %(submitted_by)s on %(submitted_on)s.\n"
" "
msgstr "\n 預覽 '%(title)s', %(submitted_by)s 在 %(submitted_on)s 送審。\n "
#: .\templates\wagtailadmin\pages\add_subpage.html:6
#, python-format
msgid "Create a page in %(title)s"
msgstr "以 %(title)s 為題建立一個頁面"
#: .\templates\wagtailadmin\pages\add_subpage.html:9
msgid "Create a page in"
msgstr "在這建立一個頁面"
#: .\templates\wagtailadmin\pages\add_subpage.html:13
msgid "Choose which type of page you'd like to create."
msgstr "選擇希望建立的頁面類型。"
#: .\templates\wagtailadmin\pages\add_subpage.html:26
#, python-format
msgid "Pages using %(page_type)s"
msgstr "%(page_type)s 類的頁面"
#: .\templates\wagtailadmin\pages\confirm_delete.html:3
#, python-format
msgid "Delete %(title)s"
msgstr "刪除 %(title)s"
#: .\templates\wagtailadmin\pages\confirm_delete.html:12
msgid "Are you sure you want to delete this page?"
msgstr "你確定要刪除這頁嗎?"
#: .\templates\wagtailadmin\pages\confirm_delete.html:14
#, python-format
msgid ""
"\n"
" This will also delete one more subpage.\n"
" "
msgid_plural ""
"\n"
" This will also delete %(descendant_count)s more subpages.\n"
" "
msgstr[0] "\n 這也會刪除一個子頁面。 "
msgstr[1] "\n 這也會刪除 %(descendant_count)s 個子頁面。 "
#: .\templates\wagtailadmin\pages\confirm_delete.html:22
msgid ""
"Alternatively you can unpublish the page. This removes the page from public "
"view and you can edit or publish it again later."
msgstr "你可以選擇取消發佈此頁面。此頁面將將無法從外部觀看,你可以編輯後再次發。"
#: .\templates\wagtailadmin\pages\confirm_delete.html:26
msgid "Delete it"
msgstr "刪除"
#: .\templates\wagtailadmin\pages\confirm_delete.html:26
msgid "Unpublish it"
msgstr "取消發佈"
#: .\templates\wagtailadmin\pages\confirm_move.html:3
#, python-format
msgid "Move %(title)s"
msgstr "移動 %(title)s"
#: .\templates\wagtailadmin\pages\confirm_move.html:6
#: .\templates\wagtailadmin\pages\list.html:65
#: .\templates\wagtailadmin\pages\list.html:185
msgid "Move"
msgstr "移動"
#: .\templates\wagtailadmin\pages\confirm_move.html:11
#, python-format
msgid "Are you sure you want to move this page into '%(title)s'?"
msgstr "你確定想要移動此頁面至 '%(title)s' 嗎?"
#: .\templates\wagtailadmin\pages\confirm_move.html:13
#, python-format
msgid ""
"Are you sure you want to move this page and all of its children into "
"'%(title)s'?"
msgstr "你確定要移動此頁面和其所有子頁面至 '%(title)s' 嗎?"
#: .\templates\wagtailadmin\pages\confirm_move.html:18
msgid "Yes, move this page"
msgstr "是的,移動此頁面"
#: .\templates\wagtailadmin\pages\confirm_unpublish.html:3
#, python-format
msgid "Unpublish %(title)s"
msgstr "取消發佈 %(title)s"
#: .\templates\wagtailadmin\pages\confirm_unpublish.html:6
#: .\templates\wagtailadmin\pages\edit.html:33
#: .\templates\wagtailadmin\pages\list.html:71
#: .\templates\wagtailadmin\pages\list.html:191
msgid "Unpublish"
msgstr "取消發佈"
#: .\templates\wagtailadmin\pages\confirm_unpublish.html:10
msgid "Are you sure you want to unpublish this page?"
msgstr "你確定想取消發佈此頁面嗎?"
#: .\templates\wagtailadmin\pages\confirm_unpublish.html:13
msgid "Yes, unpublish it"
msgstr "是的,取消發佈"
#: .\templates\wagtailadmin\pages\content_type_use.html:7
msgid "Pages using"
msgstr "頁面正在使用"
#: .\templates\wagtailadmin\pages\create.html:5
#, python-format
msgid "New %(page_type)s"
msgstr "新 %(page_type)s 分類"
#: .\templates\wagtailadmin\pages\create.html:9
msgid "New"
msgstr "新"
#: .\templates\wagtailadmin\pages\create.html:21
msgid "Save as draft"
msgstr "儲存為草稿"
#: .\templates\wagtailadmin\pages\create.html:26
#: .\templates\wagtailadmin\pages\edit.html:39
msgid "Publish"
msgstr "發佈"
#: .\templates\wagtailadmin\pages\create.html:28
#: .\templates\wagtailadmin\pages\edit.html:41
msgid "Submit for moderation"
msgstr "送審"
#: .\templates\wagtailadmin\pages\edit.html:5
#, python-format
msgid "Editing %(title)s"
msgstr "編輯 %(title)s"
#: .\templates\wagtailadmin\pages\edit.html:12
#, python-format
msgid "Editing <span>%(title)s</span>"
msgstr "編輯 <span>%(title)s</span>"
#: .\templates\wagtailadmin\pages\edit.html:15
msgid "Status:"
msgstr "狀態:"
#: .\templates\wagtailadmin\pages\edit.html:29
msgid "Save draft"
msgstr "儲存草稿"
#: .\templates\wagtailadmin\pages\edit.html:52
#, python-format
msgid "Last modified: %(last_mod)s"
msgstr "上一次編輯:%(last_mod)s"
#: .\templates\wagtailadmin\pages\edit.html:54
#, python-format
msgid "by %(modified_by)s"
msgstr "作者 %(modified_by)s"
#: .\templates\wagtailadmin\pages\index.html:4
#, python-format
msgid "Exploring %(title)s"
msgstr "瀏覽%(title)s"
#: .\templates\wagtailadmin\pages\list.html:53
#: .\templates\wagtailadmin\pages\list.html:194
msgid "Add child page"
msgstr "新增子頁面"
#: .\templates\wagtailadmin\pages\list.html:94
msgid "Disable ordering of child pages"
msgstr "禁止子頁面的排序"
#: .\templates\wagtailadmin\pages\list.html:94
#: .\templates\wagtailadmin\pages\list.html:96
msgid "Order"
msgstr "排序"
#: .\templates\wagtailadmin\pages\list.html:96
msgid "Enable ordering of child pages"
msgstr "開啟子頁面排序"
#: .\templates\wagtailadmin\pages\list.html:149
msgid "Drag"
msgstr "拖曳"
#: .\templates\wagtailadmin\pages\list.html:220
#: .\templates\wagtailadmin\pages\list.html:224
#, python-format
msgid "Explorer subpages of '%(title)s'"
msgstr "瀏覽 '%(title)s' 的子頁面"
#: .\templates\wagtailadmin\pages\list.html:220
#: .\templates\wagtailadmin\pages\list.html:224
#: .\templates\wagtailadmin\pages\list.html:228
msgid "Explore"
msgstr "瀏覽"
#: .\templates\wagtailadmin\pages\list.html:228
#, python-format
msgid "Explorer child pages of '%(title)s'"
msgstr "瀏覽 '%(title)s' 的子頁面"
#: .\templates\wagtailadmin\pages\list.html:230
#, python-format
msgid "Add a child page to '%(title)s'"
msgstr "新增子頁面至 '%(title)s'"
#: .\templates\wagtailadmin\pages\list.html:230
msgid "Add subpage"
msgstr "新增子頁面"
#: .\templates\wagtailadmin\pages\list.html:239
msgid "No pages have been created."
msgstr "沒有已儲存的頁面"
#: .\templates\wagtailadmin\pages\list.html:239
#, python-format
msgid "Why not <a href=\"%(add_page_url)s\">add one</a>?"
msgstr "為什麼不 <a href=\"%(add_page_url)s\"> 新增一個頁面呢</a>"
#: .\templates\wagtailadmin\pages\move_choose_destination.html:3
#, python-format
msgid "Select a new parent page for %(title)s"
msgstr "為 %(title)s 選擇一個新的母頁面"
#: .\templates\wagtailadmin\pages\move_choose_destination.html:7
#, python-format
msgid "Select a new parent page for <span>%(title)s</span>"
msgstr "為 <span>%(title)s</span> 選擇一個新的母頁面"
#: .\templates\wagtailadmin\pages\search_results.html:6
#, python-format
msgid ""
"\n"
" There is one match\n"
" "
msgid_plural ""
"\n"
" There are %(counter)s matches\n"
" "
msgstr[0] "\n 有一個符合"
msgstr[1] "\n 有 $(counter)s 個符合"
#: .\templates\wagtailadmin\pages\search_results.html:17
#, python-format
msgid ""
"\n"
" Page %(page_number)s of %(num_pages)s.\n"
" "
msgstr "\n 第 %(page_number)s / %(num_pages)s頁。\n "
#: .\templates\wagtailadmin\pages\search_results.html:24
#: .\templates\wagtailadmin\pages\search_results.html:26
#: .\templates\wagtailadmin\shared\pagination_nav.html:8
#: .\templates\wagtailadmin\shared\pagination_nav.html:10
#: .\templates\wagtailadmin\shared\pagination_nav.html:14
msgid "Previous"
msgstr "往前"
#: .\templates\wagtailadmin\pages\search_results.html:33
#: .\templates\wagtailadmin\pages\search_results.html:35
#: .\templates\wagtailadmin\shared\pagination_nav.html:21
#: .\templates\wagtailadmin\shared\pagination_nav.html:23
#: .\templates\wagtailadmin\shared\pagination_nav.html:25
msgid "Next"
msgstr "往後"
#: .\templates\wagtailadmin\pages\search_results.html:43
#, python-format
msgid "Sorry, no pages match <em>\"%(query_string)s\"</em>"
msgstr "對不起,沒有任何頁面符合 \"<em>%(query_string)s</em>\""
#: .\templates\wagtailadmin\pages\search_results.html:45
msgid "Enter a search term above"
msgstr "請輸入關鍵字"
#: .\templates\wagtailadmin\pages\select_location.html:3
#, python-format
msgid "Where do you want to create a %(page_type)s"
msgstr "你想在哪建立 %(page_type)s"
#: .\templates\wagtailadmin\pages\select_location.html:5
msgid "Where do you want to create this"
msgstr "你想在哪建立這個"
#: .\templates\wagtailadmin\pages\select_type.html:3
#: .\templates\wagtailadmin\pages\select_type.html:6
msgid "Create a new page"
msgstr "建立一個新頁面"
#: .\templates\wagtailadmin\pages\select_type.html:10
msgid ""
"Your new page will be saved in the <em>top level</em> of your website. You "
"can move it after saving."
msgstr "你的新頁面將會儲存到網站的 <em>最上層</em> 你可以在儲存後移動它。"
#: .\templates\wagtailadmin\shared\main_nav.html:15
msgid "Account settings"
msgstr "帳號設定"
#: .\templates\wagtailadmin\shared\main_nav.html:16
msgid "Log out"
msgstr "登出"
#: .\templates\wagtailadmin\shared\main_nav.html:20
msgid "More"
msgstr "更多"
#: .\templates\wagtailadmin\shared\main_nav.html:22
msgid "Redirects"
msgstr "重導向"
#: .\templates\wagtailadmin\shared\main_nav.html:23
msgid "Editors Picks"
msgstr "編者精選"
#: .\templates\wagtailadmin\shared\pagination_nav.html:3
#, python-format
msgid "Page %(page_num)s of %(total_pages)s."
msgstr "第 %(page_num)s 頁 共 %(total_pages)s 頁"
#: .\templatetags\wagtailadmin_nav.py:52
msgid "Images"
msgstr "圖片"
#: .\templatetags\wagtailadmin_nav.py:56
msgid "Documents"
msgstr "文件"
#: .\templatetags\wagtailadmin_nav.py:61
msgid "Snippets"
msgstr "片段"
#: .\templatetags\wagtailadmin_nav.py:66
msgid "Users"
msgstr "使用者"
#: .\views\account.py:26
msgid "Your password has been changed successfully!"
msgstr "您的密碼已經更改成功。"
#: .\views\pages.py:99
msgid "Sorry, you do not have access to create a page of type <em>'{0}'</em>."
msgstr "對不起,你沒有建立 <em>'{0}'</em> 類型頁面的權限。"
#: .\views\pages.py:103
msgid ""
"Pages of this type can only be created as children of <em>'{0}'</em>. This "
"new page will be saved there."
msgstr "這一類的頁面只能建立為 <em>'{0}'</em> 的子頁面。此頁面將會儲存在那邊。"
#: .\views\pages.py:166
msgid "This slug is already in use"
msgstr "這個地址已被使用"
#: .\views\pages.py:187 .\views\pages.py:254 .\views\pages.py:589
msgid "Page '{0}' published."
msgstr "第 '{0}' 頁已發佈。"
#: .\views\pages.py:189 .\views\pages.py:256
msgid "Page '{0}' submitted for moderation."
msgstr "第 '{0}' 頁已送審。"
#: .\views\pages.py:192
msgid "Page '{0}' created."
msgstr "第 '{0}' 頁已建立。"
#: .\views\pages.py:201
msgid "The page could not be created due to errors."
msgstr "這頁面因有錯誤而無法建立。"
#: .\views\pages.py:259
msgid "Page '{0}' updated."
msgstr "第 '{0}' 頁已更新"
#: .\views\pages.py:268
msgid "The page could not be saved due to validation errors"
msgstr "這頁面因有驗證錯誤而無法儲存。"
#: .\views\pages.py:280
msgid "This page is currently awaiting moderation"
msgstr "這頁正等待審核"
#: .\views\pages.py:298
msgid "Page '{0}' deleted."
msgstr "第 '{0}' 頁已刪除"
#: .\views\pages.py:428
msgid "Page '{0}' unpublished."
msgstr "第 '{0}' 頁已取消發佈"
#: .\views\pages.py:479
msgid "Page '{0}' moved."
msgstr "第 '{0}' 頁已移動。"
#: .\views\pages.py:584 .\views\pages.py:602 .\views\pages.py:621
msgid "The page '{0}' is not currently awaiting moderation."
msgstr "第 '{0}' 頁目前不需要等待審核。"
#: .\views\pages.py:608
msgid "Page '{0}' rejected for publication."
msgstr "第 '{0}' 頁已被拒絕發佈。"

View file

@ -23,4 +23,4 @@ def render_modal_workflow(request, html_template, js_template, template_vars={})
response_text = "{%s}" % ','.join(response_keyvars)
return HttpResponse(response_text, mimetype="text/javascript")
return HttpResponse(response_text, content_type="text/javascript")

View file

@ -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(){
@ -128,7 +117,7 @@ $(function(){
$(window.headerSearch.termInput).trigger('focus');
function search () {
var workingClasses = "working icon icon-spinner";
var workingClasses = "icon-spinner";
$(window.headerSearch.termInput).parent().addClass(workingClasses);
search_next_index++;

View file

@ -181,6 +181,17 @@ function InlinePanel(opts) {
self.updateMoveButtonDisabledStates();
});
}
/* Hide container on page load if it is marked as deleted. Remove the error
message so that it doesn't count towards the number of errors on the tab at the
top of the page. */
if ( $('#' + deleteInputId).val() === "1" ) {
$('#' + childId).hide(0, function() {
self.updateMoveButtonDisabledStates();
self.setHasContent();
});
$('#' + childId).find(".error-message").remove();
}
};
self.formsUl = $('#' + opts.formsetPrefix + '-FORMS');

View file

@ -177,4 +177,9 @@ a.tag:hover{
/* make a block-level element inline */
.inline{
display:inline;
}
/* utility class to allow things to be scrollable if their contents can't wrap more nicely */
.overflow{
overflow:auto;
}

View file

@ -21,22 +21,13 @@ legend{
@include visuallyhidden();
}
.fields li{
padding-top:0.5em;
padding-bottom:0.5em;
}
.field{
padding:0 0 0.6em 0;
}
label{
font-weight:bold;
color:$color-grey-1;
font-size:1.1em;
display:block;
padding:0 0 0.8em 0;
line-height:1em;
line-height:1.3em;
.checkbox &,
.radio &{
@ -47,10 +38,10 @@ label{
input, textarea, select, .richtext, .tagit{
@include border-radius(6px);
@include border-box();
font-family:Open Sans,Arial,sans-serif;
width:100%;
border:1px dashed $color-input-border;
padding:1.2em;
font-family:Open Sans,Arial,sans-serif;
border:1px solid $color-input-border;
padding:0.9em 1.2em;
background-color:$color-fieldset-hover;
-webkit-appearance: none;
color:$color-text-input;
@ -76,10 +67,44 @@ input, textarea, select, .richtext, .tagit{
}
}
input[type=radio],input[type=checkbox]{
/* select boxes */
.typed_choice_field .input{
position:relative;
select{
outline:none;
}
&:after{
@include border-radius(0 6px 6px 0);
z-index:0;
position:absolute;
right:1px;
top:1px;
height:95%;
width:1.5em;
font-family:wagtail;
content:"q";
border:1px solid $color-input-border;
border-width:0 0 0 1px;
text-align:center;
line-height:1.4em;
font-size:3em;
pointer-events:none;
color:$color-grey-3;
background-color:$color-fieldset-hover;
margin:0px 1px 0 0;
}
.ie &:after{
display:none;
}
}
/* radio and check boxes */
input[type=radio], input[type=checkbox]{
@include border-radius(0);
cursor:pointer;
float:left;
border:0;
}
@ -350,18 +375,15 @@ button.icon{
.help, .error-message{
font-size:0.85em;
font-weight:normal;
margin:0 0 0.5em 0;
margin:0.5em 0 0 0;
}
.error-message{
color:$color-red;
}
.help{
color:$color-grey-2;
}
/* permanently show checkbox/radio help as they have no focus state */
.boolean_field .help, .radio .help{
opacity:1;
}
fieldset:hover > .help,
.field.focused + .help,
.field:focus + .help,
@ -380,18 +402,77 @@ li.focused > .help{
font-size:13px;
}
.error-message{
margin:0;
color:$color-red;
clear:both;
}
.error input, .error textarea, .error select, .error .tagit{
border-color:$color-red;
background-color:$color-input-error-bg;
}
/* Layouts for particular kinds of of fields */
/* permanently show checkbox/radio help as they have no focus state */
.boolean_field .help, .radio .help{
opacity:1;
}
.iconfield {
position:relative;
input:not([type=radio]), input:not([type=checkbox]), input:not([type=submit]), input:not([type=button]){
padding-left:2.5em;
}
&:before, &:after{
font-family:wagtail;
position:absolute;
top:0.4em;
font-size:1.4em;
color:$color-grey-3;
}
&:before{
left:0.5em;
}
&:after{
right:0.5em;
}
/* special case for search spinners */
&.icon-spinner:after{
color:$color-teal;
opacity:0.8;
font-size:20px;
width:20px;
height:20px;
line-height:23px;
text-align:center;
top:0.3em;
}
}
.fields li{
padding-top:0.5em;
padding-bottom:1.2em;
}
.field-content .input li{
label{
width:auto;
float:none;
}
}
.input{
clear:both;
}
/* field sizing */
.field-small{
input, textarea, select, .richtext, .tagit{
@include border-radius(3px);
padding:0.4em 1em;
}
}
.field{
&.col1,
&.col2,
@ -453,7 +534,7 @@ ul.inline li:first-child, li.inline:first-child{
display:block;
float:left;
color:$color-grey-3;
line-height:0.85em;
line-height:1em;
font-size:2.5em;
margin-right:0.3em;
}
@ -495,7 +576,6 @@ ul.inline li:first-child, li.inline:first-child{
.unchosen, .chosen{
&:before{
content:"b";
margin-left:-0.1em; /* this glyphs appear to have left padding, counteracted here */
}
}
}
@ -568,112 +648,6 @@ ul.tagit li.tagit-choice-editable{
}
}
/* search bars (search integrated into header area) */
.search-bar{
margin-top:-2em;
padding-top:1em;
padding-bottom:1em;
margin-bottom:2em;
&.full-width{
@include nice-padding();
background-color:$color-header-bg;
border-bottom:1px solid $color-grey-4;
}
label{
display:none;
}
.fields{
position:relative;
clear:both;
.field input{
padding-left:3em;
&:focus{
background-color:white;
}
}
.field:before, .field:after{
font-family:wagtail;
position:absolute;
top:1em;
font-size:25px;
}
.field:before{
left:0.5em;
content:"f";
color:$color-grey-3;
}
.field:after{
color:$color-teal;
opacity:0.8;
font-size:20px;
width:20px;
height:20px;
line-height:23px;
text-align:center;
top:0.3em;
right:0.5em;
}
}
.submit{
display:none;
position:absolute;
right:0;
top:0;
input{
padding:1.55em 2em;
}
}
.taglist{
font-size:0.9em;
line-height:2.4em;
h3{
display:inline;
}
a{
white-space: nowrap
}
}
&.small{
margin:0;
padding:0;
.fields{
li{
padding:0;
}
.field{
padding:0;
}
.field input{
padding:0.4em 1.4em 0.4em 2em;
&:focus{
background-color:white;
}
}
.field:before{
font-size:1.1rem;
top:0.45em;
}
}
}
}
/* mozilla specific hack */
@-moz-document url-prefix() {
.search-bar .fields .field:after{
line-height:20px;
}
}
/* Transitions */
fieldset, input, textarea, select{
@include transition(background-color 0.2s ease);
@ -686,11 +660,22 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
}
@media screen and (min-width: $breakpoint-mobile){
.help{
opacity:1;
}
.fields{
max-width:800px;
label{
@include column(2);
padding-top:1.2em;
padding-left:0;
.model_multiple_choice_field &,
.boolean_field &,
.model_choice_field &,
.image_field &,
.file_field &{
padding-top:0;
}
.boolean_field &{
padding-bottom:0;
}
}
input[type=submit], input[type=reset], input[type=button], .button, button{
@ -706,4 +691,20 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
}
}
}
.help{
opacity:1;
}
.fields{
max-width:800px;
}
.field{
@include row();
}
.field-content{
@include column(10);
padding-right:0;
}
}

View file

@ -0,0 +1,156 @@
header{
padding-top:1em;
padding-bottom:1em;
background-color: $color-header-bg;
margin-bottom:2em;
color:white;
h1, h2{
margin:0;
color:white;
}
h1{
padding:0.2em 0;
&.icon:before{
width:1em;
display:none;
margin-right:0.4em;
font-size:1.5em;
}
}
.col{
float:left;
margin-right:2em;
}
.left{
float:left;
.hasform &:first-child{
padding-bottom:0.5em;
float:none;
}
}
.right{
text-align:right;
float:right;
}
/* For case where content below header should merge with it */
&.merged{
margin-bottom:0;
}
&.tab-merged, &.no-border{
border:0;
}
&.merged.no-border{
padding-bottom:0;
}
&.no-v-padding{
padding-top:0;
padding-bottom:0;
}
/*
&.hasform h1{
margin-top:0.2em;
}
*/
.button{
background-color:$color-teal-darker;
&:hover{
background-color:$color-teal-dark;
}
}
/* necessary on mobile only to make way for hamburger menu */
&.nice-padding{
padding-left:4em;
}
label{
@include visuallyhidden();
}
input[type=text], select{
border-width:0;
&:focus{
background-color:white;
}
}
.fields{
margin-top:-0.5em;
li{
padding-bottom:0;
}
.field{
padding:0;
}
}
.field-content{
width:auto;
padding:0;
}
}
/* mozilla specific hack */
@-moz-document url-prefix() {
.iconfield.icon-spinner:after{
line-height:20px;
}
}
.page-explorer header{
margin-bottom:0;
padding-bottom:0em;
}
@media screen and (min-width: $breakpoint-mobile){
header{
padding-top:1.5em;
padding-bottom:1.5em;
.left{
float:left;
margin-right:0;
&:first-child{
padding-bottom:0;
float:left;
}
}
.second{
clear:none;
.right, .left{
float:right;
}
}
h1.icon:before{
display:inline-block;
}
.col3{
@include column(3);
}
.col3.addbutton{
width:auto;
}
.col6{
@include column(6);
}
.col9{
@include column(9);
}
.breadcrumb{
margin-left:-($desktop-nice-padding);
margin-right:-($desktop-nice-padding);
}
}
}

View file

@ -231,14 +231,20 @@
.icon-collapse-up:before{
content:"6";
}
.icon-date:before{
content:"7";
}
.icon-success:before{
content:"9";
}
.icon-help:before{
content:"?";
}
.icon-warning:before{
content:"!";
}
.icon-success:before{
content:"9";
.icon-form:before{
content:"$";
}
.icon.text-replace{

View file

@ -11,6 +11,7 @@
@import "components/listing.scss";
@import "components/messages.scss";
@import "components/formatters.scss";
@import "components/header.scss";
@import "fonts.scss";
@ -225,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;
}
@ -311,10 +299,6 @@ img{
}
}
.js .nav-main .more ul{
display:none;
}
.explorer{
position:absolute;
margin-top:70px;
@ -385,88 +369,6 @@ body.explorer-open {
}
}
header{
padding-top:1em;
padding-bottom:1em;
background-color: $color-header-bg;
margin-bottom:2em;
color:white;
h1, h2{
margin:0;
color:white;
}
h1{
padding:0.2em 0;
&.icon:before{
width:1em;
display:none;
margin-right:0.4em;
font-size:1.3em;
}
}
.col{
float:left;
margin-right:2em;
}
.left{
float:left;
.hasform &:first-child{
padding-bottom:0.5em;
float:none;
}
}
.search-bar input{
@include border-radius(3px);
width:auto;
border-width:0;
}
.right{
text-align:right;
float:right;
}
/* For case where content below header should merge with it */
&.merged{
margin-bottom:0;
}
&.tab-merged, &.no-border{
border:0;
}
&.merged.no-border{
padding-bottom:0;
}
&.no-v-padding{
padding-top:0;
padding-bottom:0;
}
/*
&.hasform h1{
margin-top:0.2em;
}
*/
.button{
background-color:$color-teal-darker;
&:hover{
background-color:$color-teal-dark;
}
}
/* necessary on mobile only to make way for hamburger menu */
&.nice-padding{
padding-left:4em;
}
}
.page-explorer header{
margin-bottom:0;
padding-bottom:0em;
}
footer{
@include row();
@include border-radius(3px 3px 0 0);
@ -707,7 +609,7 @@ footer, .logo{
padding-right:$desktop-nice-padding;
}
body{
.wrapper{
margin-left:$menu-width;
}
@ -726,7 +628,7 @@ footer, .logo{
left:0;
height:100%;
width:$menu-width;
margin-left: -$menu-width;
margin-left: 0;
.inner{
height:100%;
@ -832,52 +734,6 @@ footer, .logo{
}
}
header{
padding-top:1.5em;
padding-bottom:1.5em;
&.nice-padding{
@include nice-padding();
}
.left{
float:left;
margin-right:0;
&:first-child{
padding-bottom:0;
float:left;
}
}
.second{
clear:none;
.right, .left{
float:right;
}
}
h1.icon:before{
display:inline-block;
}
.col3{
@include column(3);
}
.col3.addbutton{
width:auto;
}
.col6{
@include column(6);
}
.col9{
@include column(9);
}
.breadcrumb{
margin-left:-($desktop-nice-padding);
margin-right:-($desktop-nice-padding);
}
}
footer{
width:80%;
margin-left:50px;

View file

@ -67,6 +67,8 @@
<glyph unicode="&#63;" d="M253 492c65 0 120-22 167-67 46-45 70-100 72-165 0-65-22-121-68-167-45-47-100-71-165-73-65 0-121 22-167 68-47 45-71 100-72 165-1 65 21 121 67 167 46 47 101 71 166 72m-1-379c10 0 19 3 25 9 6 7 10 15 10 24 0 11-2 19-9 26-6 6-15 9-25 9 0 0-1 0-1 0-10 0-18-3-24-9-7-6-10-14-11-24 0-10 3-18 10-25 6-6 14-10 24-10 0 0 1 0 1 0m85 168c9 11 13 24 13 40 0 26-9 46-27 59-18 13-41 19-69 19-22 0-40-4-53-13-24-14-36-39-37-75 0 0 0-2 0-2 0 0 56 0 56 0 0 0 0 2 0 2 0 9 3 18 8 28 6 8 15 12 28 12 14 0 23-3 27-10 5-7 8-14 8-23 0-6-3-13-8-20-3-4-7-8-11-10 0 0-3-2-3-2-2-2-4-4-8-6-3-2-7-5-10-8-4-2-7-5-11-8-4-3-7-6-9-9-4-7-7-20-9-40 0 0 0-4 0-4 0 0 56 0 56 0 0 0 0 2 0 2 0 4 0 9 2 14 2 7 6 13 14 19 0 0 14 9 14 9 16 12 25 20 29 26"/>
<glyph unicode="&#33;" d="M499 65c4-6 4-12 0-18-3-5-8-8-15-8 0 0-457 0-457 0-6 0-11 3-14 8-4 6-4 12-1 18 0 0 228 400 228 400 3 6 8 9 16 9 7 0 12-3 15-9 0 0 228-400 228-400m-215 25c0 0 0 51 0 51 0 0-56 0-56 0 0 0 0-51 0-51 0 0 56 0 56 0m0 89c0 0 0 154 0 154 0 0-56 0-56 0 0 0 0-154 0-154 0 0 56 0 56 0"/>
<glyph unicode="&#57;" d="M256 512c-141 0-256-115-256-256 0-141 115-256 256-256 141 0 256 115 256 256 0 141-115 256-256 256z m-40-374l-117 118 45 45 72-73 154 154 45-45z"/>
<glyph unicode="&#52;" d="M256 309c8 0 15-1 22-4 7-3 13-6 18-11 5-5 9-11 11-17 3-7 5-14 5-21 0-7-2-14-5-21-2-6-6-12-11-17-5-5-11-8-18-11-7-3-14-4-22-4-8 0-15 1-22 4-7 3-13 6-18 11-5 5-9 11-11 17-3 7-5 14-5 21 0 7 2 14 5 21 2 6 6 12 11 17 5 5 11 8 18 11 7 3 14 4 22 4z m0-136c12 0 23 2 34 6 11 4 20 10 28 18 8 7 14 16 19 26 4 11 6 21 6 33 0 12-2 22-6 33-5 10-11 19-19 26-8 8-17 14-28 18-11 4-22 6-34 6-12 0-23-2-34-6-11-4-20-10-28-18-8-7-14-16-19-26-4-11-6-21-6-33 0-12 2-22 6-33 5-10 11-19 19-26 8-8 17-14 28-18 11-4 22-6 34-6z m0 194c18 0 35-2 51-6 16-4 31-10 45-17 14-6 26-14 37-22 11-9 21-17 29-25 7-8 13-16 17-23 5-8 7-14 7-18 0-4-2-10-7-18-4-7-10-15-17-23-8-8-18-16-29-25-11-8-23-16-37-22-14-7-29-13-45-17-16-4-33-6-51-6-18 0-35 2-51 6-17 4-31 10-45 17-14 6-26 14-37 22-11 9-21 17-29 25-7 8-13 16-17 23-5 8-7 14-7 18 0 4 2 10 7 18 4 7 10 15 17 23 8 8 18 16 29 25 11 8 23 16 37 22 14 7 28 13 45 17 16 4 33 6 51 6z"/>
<glyph unicode="&#83;" d="M435 186l0-117-358 0 0 117-68 0 0-151c0-19 15-35 34-35l426 0c19 0 34 16 34 35l0 151z m-185 12l-99 118c0 0-15 15 2 15 16 0 55 0 55 0 0 0 0 9 0 24 0 41 0 118 0 149 0 0-2 8 11 8 12 0 68 0 78 0 9 0 8-7 8-7 0-30 0-109 0-149 0-13 0-22 0-22 0 0 32 0 52 0 19 0 5-15 5-15 0 0-84-111-96-122-8-9-16 1-16 1z"/>
<glyph unicode="&#52;" d="M256 309c8 0 15-1 22-4 7-3 13-6 18-11 5-5 9-11 11-17 3-7 5-14 5-21 0-7-2-14-5-21-2-6-6-12-11-17-5-5-11-8-18-11-7-3-14-4-22-4-8 0-15 1-22 4-7 3-13 6-18 11-5 5-9 11-11 17-3 7-5 14-5 21 0 7 2 14 5 21 2 6 6 12 11 17 5 5 11 8 18 11 7 3 14 4 22 4z m0-136c12 0 23 2 34 6 11 4 20 10 28 18 8 7 14 16 19 26 4 11 6 21 6 33 0 12-2 22-6 33-5 10-11 19-19 26-8 8-17 14-28 18-11 4-22 6-34 6-12 0-23-2-34-6-11-4-20-10-28-18-8-7-14-16-19-26-4-11-6-21-6-33 0-12 2-22 6-33 5-10 11-19 19-26 8-8 17-14 28-18 11-4 22-6 34-6z m0 194c18 0 35-2 51-6 16-4 31-10 45-17 14-6 26-14 37-22 11-9 21-17 29-25 7-8 13-16 17-23 5-8 7-14 7-18 0-4-2-10-7-18-4-7-10-15-17-23-8-8-18-16-29-25-11-8-23-16-37-22-14-7-29-13-45-17-16-4-33-6-51-6-18 0-35 2-51 6-17 4-31 10-45 17-14 6-26 14-37 22-11 9-21 17-29 25-7 8-13 16-17 23-5 8-7 14-7 18 0 4 2 10 7 18 4 7 10 15 17 23 8 8 18 16 29 25 11 8 23 16 37 22 14 7 28 13 45 17 16 4 33 6 51 6z"/>
<glyph unicode="&#55;" d="M416 0l-320 0c-53 0-96 43-96 96l0 320c0 42 27 77 64 90l0-42c0-27 22-48 48-48 26 0 48 21 48 48l0 48 192 0 0-48c0-27 22-48 48-48 26 0 48 21 48 48l0 42c37-13 64-48 64-90l0-320c0-53-43-96-96-96z m32 352l-384 0 0-256c0-18 14-32 32-32l320 0c18 0 32 14 32 32z m-128-160l64 0 0-64-64 0z m0 96l64 0 0-64-64 0z m-96-96l64 0 0-64-64 0z m0 96l64 0 0-64-64 0z m-96-96l64 0 0-64-64 0z m0 96l64 0 0-64-64 0z m272 160c-9 0-16 7-16 16l0 48 32 0 0-48c0-9-7-16-16-16z m-288 0c-9 0-16 7-16 16l0 48 32 0 0-48c0-9-7-16-16-16z"/>
<glyph unicode="&#36;" d="M0 512l0-512 512 0 0 512z m157-431l-104 0 0 53 189 0 0-53z m274 269l-378 0 0 81 377 0 0-81z m-1-144l-377 0 0 80 377 0z"/>
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -82,7 +82,7 @@ form{
.field{
padding:0;
}
.field.icon:before{
.iconfield:before{
display:none;
}
@ -168,25 +168,29 @@ form{
font-size:4em;
}
.field.icon:before{
display:inline-block;
position: absolute;
color:$color-grey-4;
border: 2px solid $color-grey-4;
border-radius: 100%;
width: 1em;
padding: 0.3em;
left: $desktop-nice-padding;
margin-top: -1em;
top: 50%;
font-size:1.5em;
}
.full{
margin:0px (-$desktop-nice-padding);
input{
padding-left:($desktop-nice-padding + 50px);
.iconfield{
&:before{
display:inline-block;
position: absolute;
color:$color-grey-4;
border: 2px solid $color-grey-4;
border-radius: 100%;
width: 1em;
padding: 0.3em;
left: $desktop-nice-padding;
margin-top: -1.1rem;
top: 50%;
font-size:1.3rem;
}
input{
padding-left:($desktop-nice-padding + 50px);
}
}
}
}

View file

@ -148,7 +148,6 @@
display:block;
float:none;
.help{
display:none;
}
@ -343,6 +342,10 @@ footer .preview{
@include column(10);
padding-left:0;
padding-right:0;
fieldset{
width:100%;
}
}
.object-help{
@ -371,6 +374,12 @@ footer .preview{
.field{
padding:0;
}
.field-content{
display: block;
float: none;
width: auto;
padding: inherit;
}
}
.multiple{
@include column(10);
@ -381,5 +390,12 @@ footer .preview{
&.empty .add{
margin:0 0 0 -50px;
}
&.single-field label{
display: block;
float: none;
width: auto;
padding:auto;
}
}
}

View file

@ -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;
}*/
}
}

View file

@ -87,11 +87,13 @@ li, .home{
.action{
@include transition(background-color 0.2s ease, color 0.2s ease);
background-color:$color-teal;
color:white;
color:$color-teal;
&:before{
margin-right:0.4em;
vertical-align:middle;
font-size:1.7em;
color:white;
}
&:hover{

View file

@ -42,7 +42,7 @@ $color-grey-5: #fafafa;
$color-thead-bg: $color-grey-5;
$color-header-bg: $color-teal; // #ff6a58;
$color-fieldset-hover: $color-grey-5;
$color-input-border: $color-grey-3;
$color-input-border: $color-grey-4;
$color-input-focus: #f4fcfc;
$color-input-error-bg: #feedee;
$color-button: $color-teal;

View file

@ -85,3 +85,16 @@ def send_notification(page_revision_id, notification, excluded_user_id):
# Send email
send_mail(email_subject, email_content, from_email, email_addresses)
@task
def send_email_task(email_subject, email_content, email_addresses, from_email=None):
if not from_email:
if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'):
from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL
elif hasattr(settings, 'DEFAULT_FROM_EMAIL'):
from_email = settings.DEFAULT_FROM_EMAIL
else:
from_email = 'webmaster@localhost'
send_mail(email_subject, email_content, from_email, email_addresses)

View file

@ -1,6 +1,6 @@
modal.ajaxifyForm($('form.search-bar', modal.body));
modal.ajaxifyForm($('form.search-form', modal.body));
var searchUrl = $('form.search-bar', modal.body).attr('action');
var searchUrl = $('form.search-form', modal.body).attr('action');
function search() {
$.ajax({

View file

@ -1,18 +1,23 @@
<div class="field">
{% if field_type != "boolean_field" %}{{ field.label_tag }}{% endif %}
{% block form_field %}
{{ field }}
{% endblock %}
{% if field_type = "boolean_field" %}{{ field.label_tag }}{% endif %}
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
<span></span>
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error }}</span>
{% endfor %}
</p>
{% endif %}
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}

View file

@ -28,15 +28,19 @@
<ul class="fields">
<li class="full">
<div class="field icon icon-user">
<div class="field">
{{ form.username.label_tag }}
{{ form.username }}
<div class="input iconfield icon-user">
{{ form.username }}
</div>
</div>
</li>
<li class="full">
<div class="field icon icon-password">
<div class="field">
{{ form.password.label_tag }}
{{ form.password }}
<div class="input iconfield icon-password">
{{ form.password }}
</div>
</div>
{% if show_password_reset %}
<p class="help"><a href="{% url 'django.contrib.auth.views.password_reset' %}">{% trans "Forgotten it?" %}</a></p>

View file

@ -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 %}&amp;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 %}&amp;ordering={{ ordering }}{% endif %}" class="icon icon-arrow-right-after">{% trans 'Next' %}</a>
{% endif %}
</li>
</ul>
</div>
{% endif %}

View file

@ -1,21 +1,25 @@
{% load wagtailadmin_tags %}
<li class="{{ field.css_classes }} {{ field|fieldtype }} {% if field.errors %}error{% endif %}">
<li class="{{ field.css_classes }} {{ field|fieldtype }} {{ li_classes }} {% if field.errors %}error{% endif %}">
<div class="field">
{% if field|fieldtype != "boolean_field" %}{{ field.label_tag }}{% endif %}
{% block form_field %}
{{ field }}
{% endblock %}
{% if field|fieldtype = "boolean_field" %}{{ field.label_tag }}{% endif %}
</div>
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
<span></span>
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error|escape }}</span>
{% endfor %}
</p>
{% endif %}
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error|escape }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>
</li>

View file

@ -5,12 +5,12 @@
<h1 class="icon icon-{{ icon }}">{{ title }} <span>{{ subtitle }}</span></h1>
</div>
{% if search_url %}
<form class="col search-bar small" action="{% url search_url %}" method="get">
<form class="col search-form" action="{% url search_url %}" method="get">
<ul class="fields">
{% for field in search_form %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field input_classes="field-small iconfield icon-search" %}
{% endfor %}
<li class="submit icon icon-search"><input type="submit" value="Search" /></li>
<li class="submit visuallyhidden"><input type="submit" value="Search" class="button" /></li>
</ul>
</form>
{% endif %}

View file

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

View file

@ -1,4 +1,16 @@
{% load i18n %}
{% if not is_ajax %}
{% comment %}
HACK: This template expects to be passed a 'linkurl' parameter, containing a URL name
that can be reverse-resolved by the {% url %} tag with no further parameters.
Views that have parameters in their URL can work around this by passing a bogus
(but non-blank) URL name, which will return an empty string and produce a final URL
of the form "?q=123", implicitly preserving the current URL path.
Using the {% url ... as ... %} form of the tag ensures that this fails silently,
rather than throwing a NoReverseMatch exception.
{% endcomment %}
{% url linkurl as url_to_use %}
{% endif %}
<div class="pagination">
<p>{% blocktrans with page_num=items.number total_pages=items.paginator.num_pages %}Page {{ page_num }} of {{ total_pages }}.{% endblocktrans %}</p>
<ul>
@ -7,11 +19,9 @@
{% if is_ajax %}
<a href="#" data-page="{{ items.previous_page_number }}" class="icon icon-arrow-left">{% trans 'Previous' %}</a>
{% elif is_searching %}
<a href="{% url linkurl %}?q={{ query_string|urlencode }}&amp;p={{ items.previous_page_number }}" class="icon icon-arrow-left">{% trans 'Previous' %}</a>
{% elif is_ajax %}
<a href="{{ url_to_use }}?q={{ query_string|urlencode }}&amp;p={{ items.previous_page_number }}" class="icon icon-arrow-left">{% trans 'Previous' %}</a>
{% else %}
<a href="{% url linkurl %}?p={{ items.previous_page_number }}&ordering={{ ordering }}" class="icon icon-arrow-left">{% trans 'Previous' %}</a>
<a href="{{ url_to_use }}?p={{ items.previous_page_number }}&amp;ordering={{ ordering }}" class="icon icon-arrow-left">{% trans 'Previous' %}</a>
{% endif %}
{% endif %}
</li>
@ -20,9 +30,9 @@
{% if is_ajax %}
<a href="#" data-page="{{ items.next_page_number }}" class="icon icon-arrow-right-after">{% trans 'Next' %}</a>
{% elif is_searching %}
<a href="{% url linkurl %}?q={{ query_string|urlencode }}&amp;p={{ items.next_page_number }}" class="icon icon-arrow-right-after">{% trans 'Next' %}</a>
<a href="{{ url_to_use }}?q={{ query_string|urlencode }}&amp;p={{ items.next_page_number }}" class="icon icon-arrow-right-after">{% trans 'Next' %}</a>
{% else %}
<a href="{% url linkurl %}?p={{ items.next_page_number }}&ordering={{ ordering }}" class="icon icon-arrow-right-after">{% trans 'Next' %}</a>
<a href="{{ url_to_use }}?p={{ items.next_page_number }}&amp;ordering={{ ordering }}" class="icon icon-arrow-right-after">{% trans 'Next' %}</a>
{% endif %}
{% endif %}
</li>

View file

@ -1,9 +1,10 @@
<!doctype html>
{% load compress %}
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <!--<![endif]-->
<!--[if lt IE 7]> <html class="no-js ie lt-ie9 lt-ie8 lt-ie7" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie lt-ie9 lt-ie8" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie lt-ie9" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if IE 9]> <html class="no-js ie lt-ie10" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <![endif]-->
<!--[if gt IE 9]><!--> <html class="no-js" lang="{{ LANGUAGE_CODE|default:"en-gb" }}"> <!--<![endif]-->
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

View file

@ -8,6 +8,7 @@ from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailcore.models import get_navigation_menu_items, UserPagePermissionsProxy
from wagtail.wagtailcore.util import camelcase_to_underscore
register = template.Library()
@ -25,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 = [

View file

@ -1,301 +0,0 @@
from django.test import TestCase
from wagtail.tests.models import SimplePage, EventPage
from wagtail.tests.utils import login, unittest
from wagtail.wagtailcore.models import Page
from django.core.urlresolvers import reverse
class TestHome(TestCase):
def setUp(self):
# Login
login(self.client)
def test_status_code(self):
response = self.client.get(reverse('wagtailadmin_home'))
self.assertEqual(response.status_code, 200)
class TestPageExplorer(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.root_page.add_child(instance=self.child_page)
# Login
login(self.client)
def test_explore(self):
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())
class TestPageCreation(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Login
login(self.client)
def test_add_subpage(self):
response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(self.root_page.id, )))
self.assertEqual(response.status_code, 200)
def test_add_subpage_nonexistantparent(self):
response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(100000, )))
self.assertEqual(response.status_code, 404)
def test_create_simplepage(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)))
self.assertEqual(response.status_code, 200)
def test_create_simplepage_post(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEqual(page.title, post_data['title'])
self.assertIsInstance(page, SimplePage)
self.assertFalse(page.live)
def test_create_simplepage_post_publish(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEqual(page.title, post_data['title'])
self.assertIsInstance(page, SimplePage)
self.assertTrue(page.live)
def test_create_simplepage_post_existingslug(self):
# This tests the existing slug checking on page save
# Create a page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.root_page.add_child(instance=self.child_page)
# Attempt to create a new one with the same slug
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should not be redirected (as the save should fail)
self.assertEqual(response.status_code, 200)
def test_create_nonexistantparent(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', 100000)))
self.assertEqual(response.status_code, 404)
@unittest.expectedFailure # FIXME: Crashes!
def test_create_nonpagetype(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('wagtailimages', 'image', self.root_page.id)))
self.assertEqual(response.status_code, 404)
class TestPageEdit(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.child_page.live = True
self.root_page.add_child(instance=self.child_page)
self.child_page.save_revision()
# Add event page (to test edit handlers)
self.event_page = EventPage()
self.event_page.title = "Event page"
self.event_page.slug = "event-page"
self.root_page.add_child(instance=self.event_page)
# Login
login(self.client)
def test_edit_page(self):
# Tests that the edit page loads
response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.event_page.id, )))
self.assertEqual(response.status_code, 200)
def test_edit_post(self):
# Tests simple editing
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# The page should have "has_unpublished_changes" flag set
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertTrue(child_page_new.has_unpublished_changes)
def test_edit_post_publish(self):
# Tests publish from edit page
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Check that the page was edited
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertEqual(child_page_new.title, post_data['title'])
# The page shouldn't have "has_unpublished_changes" flag set
self.assertFalse(child_page_new.has_unpublished_changes)
class TestPageDelete(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.root_page.add_child(instance=self.child_page)
# Login
login(self.client)
def test_delete(self):
response = self.client.get(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )))
self.assertEqual(response.status_code, 200)
def test_delete_post(self):
post_data = {'hello': 'world'} # For some reason, this test doesn't work without a bit of POST data
response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Check that the page is gone
self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0)
class TestPageSearch(TestCase):
def setUp(self):
# Login
login(self.client)
def get(self, params={}):
return self.client.get(reverse('wagtailadmin_pages_search'), params)
def test_status_code(self):
self.assertEqual(self.get().status_code, 200)
def test_search(self):
response = self.get({'q': "Hello"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['query_string'], "Hello")
def test_pagination(self):
pages = ['0', '1', '-1', '9999', 'Not a page']
for page in pages:
response = self.get({'p': page})
self.assertEqual(response.status_code, 200)
def test_root_can_appear_in_search_results(self):
response = self.client.get('/admin/pages/search/?q=roo')
self.assertEqual(response.status_code, 200)
# 'pages' list in the response should contain root
results = response.context['pages']
self.assertTrue(any([r.slug == 'root' for r in results]))
class TestPageMove(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Create two sections
self.section_a = SimplePage()
self.section_a.title = "Section A"
self.section_a.slug = "section-a"
self.root_page.add_child(instance=self.section_a)
self.section_b = SimplePage()
self.section_b.title = "Section B"
self.section_b.slug = "section-b"
self.root_page.add_child(instance=self.section_b)
# Add test page into section A
self.test_page = SimplePage()
self.test_page.title = "Hello world!"
self.test_page.slug = "hello-world"
self.section_a.add_child(instance=self.test_page)
# Login
login(self.client)
def test_page_move(self):
response = self.client.get(reverse('wagtailadmin_pages_move', args=(self.test_page.id, )))
self.assertEqual(response.status_code, 200)
def test_page_move_confirm(self):
response = self.client.get(reverse('wagtailadmin_pages_move_confirm', args=(self.test_page.id, self.section_b.id)))
self.assertEqual(response.status_code, 200)
def test_page_set_page_position(self):
response = self.client.get(reverse('wagtailadmin_pages_set_page_position', args=(self.test_page.id, )))
self.assertEqual(response.status_code, 200)
class TestEditorHooks(TestCase):
def setUp(self):
self.homepage = Page.objects.get(id=2)
login(self.client)
def test_editor_css_and_js_hooks_on_add(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.homepage.id)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<link rel="stylesheet" href="/path/to/my/custom.css">')
self.assertContains(response, '<script src="/path/to/my/custom.js"></script>')
def test_editor_css_and_js_hooks_on_edit(self):
response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.homepage.id, )))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<link rel="stylesheet" href="/path/to/my/custom.css">')
self.assertContains(response, '<script src="/path/to/my/custom.js"></script>')

View file

View file

@ -0,0 +1,320 @@
from django.test import TestCase
from wagtail.tests.utils import login, unittest
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import mail
class TestAuthentication(TestCase):
"""
This tests that users can login and logout of the admin interface
"""
def setUp(self):
login(self.client)
def test_login_view(self):
"""
This tests that the login view responds with a login page
"""
# Logout so we can test the login view
self.client.logout()
# Get login page
response = self.client.get(reverse('wagtailadmin_login'))
# Check that the user recieved a login page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/login.html')
def test_login_view_post(self):
"""
This posts user credentials to the login view and checks that
the user was logged in successfully
"""
# Logout so we can test the login view
self.client.logout()
# Post credentials to the login page
post_data = {
'username': 'test',
'password': 'password',
}
response = self.client.post(reverse('wagtailadmin_login'), post_data)
# Check that the user was redirected to the dashboard
self.assertEqual(response.status_code, 302)
# Check that the user was logged in
self.assertTrue('_auth_user_id' in self.client.session)
self.assertEqual(self.client.session['_auth_user_id'], User.objects.get(username='test').id)
@unittest.expectedFailure # See: https://github.com/torchbox/wagtail/issues/25
def test_already_logged_in_redirect(self):
"""
This tests that a user who is already logged in is automatically
redirected to the admin dashboard if they try to access the login
page
"""
# Get login page
response = self.client.get(reverse('wagtailadmin_login'))
# Check that the user was redirected to the dashboard
self.assertEqual(response.status_code, 302)
def test_logout(self):
"""
This tests that the user can logout
"""
# Get logout page page
response = self.client.get(reverse('wagtailadmin_logout'))
# Check that the user was redirected to the login page
self.assertEqual(response.status_code, 302)
# Check that the user was logged out
self.assertFalse('_auth_user_id' in self.client.session)
class TestAccountSection(TestCase):
"""
This tests that the accounts section is working
"""
def setUp(self):
login(self.client)
def test_account_view(self):
"""
This tests that the login view responds with a login page
"""
# Get account page
response = self.client.get(reverse('wagtailadmin_account'))
# Check that the user recieved an account page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/account/account.html')
def test_change_password_view(self):
"""
This tests that the change password view responds with a change password page
"""
# Get change password page
response = self.client.get(reverse('wagtailadmin_account_change_password'))
# Check that the user recieved a change password page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/account/change_password.html')
def test_change_password_view_post(self):
"""
This posts a new password to the change password view and checks
that the users password was changed
"""
# Post new password to change password page
post_data = {
'new_password1': 'newpassword',
'new_password2': 'newpassword',
}
response = self.client.post(reverse('wagtailadmin_account_change_password'), post_data)
# Check that the user was redirected
self.assertEqual(response.status_code, 302)
# Check that the password was changed
self.assertTrue(User.objects.get(username='test').check_password('newpassword'))
def test_change_password_view_post_password_mismatch(self):
"""
This posts a two passwords that don't match to the password change
view and checks that a validation error was raised
"""
# Post new password to change password page
post_data = {
'new_password1': 'newpassword',
'new_password2': 'badpassword',
}
response = self.client.post(reverse('wagtailadmin_account_change_password'), post_data)
# Check that the user wasn't redirected
self.assertEqual(response.status_code, 200)
# Check that a validation error was raised
self.assertTrue('new_password2' in response.context['form'].errors.keys())
self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2'])
# Check that the password was not changed
self.assertTrue(User.objects.get(username='test').check_password('password'))
class TestPasswordReset(TestCase):
"""
This tests that the password reset is working
"""
def setUp(self):
# Create a user
User.objects.create_superuser(username='test', email='test@email.com', password='password')
def test_password_reset_view(self):
"""
This tests that the password reset view returns a password reset page
"""
# Get password reset page
response = self.client.get(reverse('password_reset'))
# Check that the user recieved a password reset page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/account/password_reset/form.html')
def test_password_reset_view_post(self):
"""
This posts an email address to the password reset view and
checks that a password reset email was sent
"""
# Post email address to password reset view
post_data = {
'email': 'test@email.com',
}
response = self.client.post(reverse('password_reset'), post_data)
# Check that the user was redirected
self.assertEqual(response.status_code, 302)
# Check that a password reset email was sent to the user
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['test@email.com'])
self.assertEqual(mail.outbox[0].subject, "Password reset")
def test_password_reset_view_post_unknown_email(self):
"""
This posts an unknown email address to the password reset view and
checks that the password reset form raises a validation error
"""
post_data = {
'email': 'unknown@email.com',
}
response = self.client.post(reverse('password_reset'), post_data)
# Check that the user wasn't redirected
self.assertEqual(response.status_code, 200)
# Check that a validation error was raised
self.assertTrue('__all__' in response.context['form'].errors.keys())
self.assertTrue("This email address is not recognised." in response.context['form'].errors['__all__'])
# Check that an email was not sent
self.assertEqual(len(mail.outbox), 0)
def test_password_reset_view_post_invalid_email(self):
"""
This posts an incalid email address to the password reset view and
checks that the password reset form raises a validation error
"""
post_data = {
'email': 'Hello world!',
}
response = self.client.post(reverse('password_reset'), post_data)
# Check that the user wasn't redirected
self.assertEqual(response.status_code, 200)
# Check that a validation error was raised
self.assertTrue('email' in response.context['form'].errors.keys())
self.assertTrue("Enter a valid email address." in response.context['form'].errors['email'])
# Check that an email was not sent
self.assertEqual(len(mail.outbox), 0)
def setup_password_reset_confirm_tests(self):
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
# Get user
self.user = User.objects.get(username='test')
# Generate a password reset token
self.password_reset_token = PasswordResetTokenGenerator().make_token(self.user)
# Generate a password reset uid
self.password_reset_uid = urlsafe_base64_encode(force_bytes(self.user.pk))
# Create url_args
self.url_kwargs = dict(uidb64=self.password_reset_uid, token=self.password_reset_token)
def test_password_reset_confirm_view(self):
"""
This tests that the password reset confirm view returns a password reset confirm page
"""
self.setup_password_reset_confirm_tests()
# Get password reset confirm page
response = self.client.get(reverse('password_reset_confirm', kwargs=self.url_kwargs))
# Check that the user recieved a password confirm done page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/account/password_reset/confirm.html')
def test_password_reset_confirm_view_post(self):
"""
This posts a new password to the password reset confirm view and checks
that the users password was changed
"""
self.setup_password_reset_confirm_tests()
# Post new password to change password page
post_data = {
'new_password1': 'newpassword',
'new_password2': 'newpassword',
}
response = self.client.post(reverse('password_reset_confirm', kwargs=self.url_kwargs), post_data)
# Check that the user was redirected
self.assertEqual(response.status_code, 302)
# Check that the password was changed
self.assertTrue(User.objects.get(username='test').check_password('newpassword'))
def test_password_reset_confirm_view_post_password_mismatch(self):
"""
This posts a two passwords that don't match to the password reset
confirm view and checks that a validation error was raised
"""
self.setup_password_reset_confirm_tests()
# Post new password to change password page
post_data = {
'new_password1': 'newpassword',
'new_password2': 'badpassword',
}
response = self.client.post(reverse('password_reset_confirm', kwargs=self.url_kwargs), post_data)
# Check that the user wasn't redirected
self.assertEqual(response.status_code, 200)
# Check that a validation error was raised
self.assertTrue('new_password2' in response.context['form'].errors.keys())
self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2'])
# Check that the password was not changed
self.assertTrue(User.objects.get(username='test').check_password('password'))
def test_password_reset_done_view(self):
"""
This tests that the password reset done view returns a password reset done page
"""
# Get password reset done page
response = self.client.get(reverse('password_reset_done'))
# Check that the user recieved a password reset done page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/account/password_reset/done.html')
def test_password_reset_complete_view(self):
"""
This tests that the password reset complete view returns a password reset complete page
"""
# Get password reset complete page
response = self.client.get(reverse('password_reset_complete'))
# Check that the user recieved a password reset complete page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/account/password_reset/complete.html')

View file

@ -0,0 +1,661 @@
from django.test import TestCase
from wagtail.tests.models import SimplePage, EventPage
from wagtail.tests.utils import login, unittest
from wagtail.wagtailcore.models import Page, PageRevision
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User, Permission
from django.core import mail
class TestPageExplorer(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.root_page.add_child(instance=self.child_page)
# Login
login(self.client)
def test_explore(self):
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'].paginator.object_list.filter(id=self.child_page.id).exists())
class TestPageCreation(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Login
self.user = login(self.client)
def test_add_subpage(self):
response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(self.root_page.id, )))
self.assertEqual(response.status_code, 200)
def test_add_subpage_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Get add subpage page
response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(self.root_page.id, )))
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
def test_add_subpage_nonexistantparent(self):
response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(100000, )))
self.assertEqual(response.status_code, 404)
def test_create_simplepage(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)))
self.assertEqual(response.status_code, 200)
def test_create_simplepage_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Get page
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id, )))
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
def test_create_simplepage_post(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEqual(page.title, post_data['title'])
self.assertIsInstance(page, SimplePage)
self.assertFalse(page.live)
def test_create_simplepage_post_publish(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEqual(page.title, post_data['title'])
self.assertIsInstance(page, SimplePage)
self.assertTrue(page.live)
def test_create_simplepage_post_submit(self):
# Create a moderator user for testing email
moderator = User.objects.create_superuser('moderator', 'moderator@email.com', 'password')
# Submit
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'action-submit': "Submit",
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEqual(page.title, post_data['title'])
self.assertIsInstance(page, SimplePage)
self.assertFalse(page.live)
# The latest revision for the page should now be in moderation
self.assertTrue(page.get_latest_revision().submitted_for_moderation)
# Check that the moderator got an email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['moderator@email.com'])
self.assertEqual(mail.outbox[0].subject, 'The page "New page!" has been submitted for moderation')
def test_create_simplepage_post_existingslug(self):
# This tests the existing slug checking on page save
# Create a page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.root_page.add_child(instance=self.child_page)
# Attempt to create a new one with the same slug
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should not be redirected (as the save should fail)
self.assertEqual(response.status_code, 200)
def test_create_nonexistantparent(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', 100000)))
self.assertEqual(response.status_code, 404)
@unittest.expectedFailure # FIXME: Crashes!
def test_create_nonpagetype(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('wagtailimages', 'image', self.root_page.id)))
self.assertEqual(response.status_code, 404)
def test_preview_on_create(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'action-submit': "Submit",
}
response = self.client.post(reverse('wagtailadmin_pages_preview_on_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Check the response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tests/simple_page.html')
self.assertContains(response, "New page!")
class TestPageEdit(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.child_page.live = True
self.root_page.add_child(instance=self.child_page)
self.child_page.save_revision()
# Add event page (to test edit handlers)
self.event_page = EventPage()
self.event_page.title = "Event page"
self.event_page.slug = "event-page"
self.root_page.add_child(instance=self.event_page)
# Login
self.user = login(self.client)
def test_page_edit(self):
# Tests that the edit page loads
response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.event_page.id, )))
self.assertEqual(response.status_code, 200)
def test_page_edit_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Get edit page
response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )))
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
def test_page_edit_post(self):
# Tests simple editing
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# The page should have "has_unpublished_changes" flag set
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertTrue(child_page_new.has_unpublished_changes)
def test_page_edit_post_publish(self):
# Tests publish from edit page
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Check that the page was edited
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertEqual(child_page_new.title, post_data['title'])
# The page shouldn't have "has_unpublished_changes" flag set
self.assertFalse(child_page_new.has_unpublished_changes)
def test_page_edit_post_submit(self):
# Create a moderator user for testing email
moderator = User.objects.create_superuser('moderator', 'moderator@email.com', 'password')
# Tests submitting from edit page
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-submit': "Submit",
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# The page should have "has_unpublished_changes" flag set
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertTrue(child_page_new.has_unpublished_changes)
# The latest revision for the page should now be in moderation
self.assertTrue(child_page_new.get_latest_revision().submitted_for_moderation)
# Check that the moderator got an email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['moderator@email.com'])
self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been submitted for moderation') # Note: should this be "I've been edited!"?
def test_preview_on_edit(self):
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-submit': "Submit",
}
response = self.client.post(reverse('wagtailadmin_pages_preview_on_edit', args=(self.child_page.id, )), post_data)
# Check the response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tests/simple_page.html')
self.assertContains(response, "I&#39;ve been edited!")
class TestPageDelete(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.root_page.add_child(instance=self.child_page)
# Login
self.user = login(self.client)
def test_page_delete(self):
response = self.client.get(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )))
self.assertEqual(response.status_code, 200)
def test_page_delete_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Get delete page
response = self.client.get(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )))
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
def test_page_delete_post(self):
post_data = {'hello': 'world'} # For some reason, this test doesn't work without a bit of POST data
response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Check that the page is gone
self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0)
class TestPageSearch(TestCase):
def setUp(self):
# Login
login(self.client)
def get(self, params=None, **extra):
return self.client.get(reverse('wagtailadmin_pages_search'), params or {}, **extra)
def test_view(self):
response = self.get()
self.assertTemplateUsed(response, 'wagtailadmin/pages/search.html')
self.assertEqual(response.status_code, 200)
def test_search(self):
response = self.get({'q': "Hello"})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/pages/search.html')
self.assertEqual(response.context['query_string'], "Hello")
def test_ajax(self):
response = self.get({'q': "Hello"}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertTemplateNotUsed(response, 'wagtailadmin/pages/search.html')
self.assertTemplateUsed(response, 'wagtailadmin/pages/search_results.html')
self.assertEqual(response.context['query_string'], "Hello")
def test_pagination(self):
pages = ['0', '1', '-1', '9999', 'Not a page']
for page in pages:
response = self.get({'q': "Hello", 'p': page})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/pages/search.html')
def test_root_can_appear_in_search_results(self):
response = self.get({'q': "roo"})
self.assertEqual(response.status_code, 200)
# 'pages' list in the response should contain root
results = response.context['pages']
self.assertTrue(any([r.slug == 'root' for r in results]))
class TestPageMove(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Create two sections
self.section_a = SimplePage()
self.section_a.title = "Section A"
self.section_a.slug = "section-a"
self.root_page.add_child(instance=self.section_a)
self.section_b = SimplePage()
self.section_b.title = "Section B"
self.section_b.slug = "section-b"
self.root_page.add_child(instance=self.section_b)
# Add test page into section A
self.test_page = SimplePage()
self.test_page.title = "Hello world!"
self.test_page.slug = "hello-world"
self.section_a.add_child(instance=self.test_page)
# Login
self.user = login(self.client)
def test_page_move(self):
response = self.client.get(reverse('wagtailadmin_pages_move', args=(self.test_page.id, )))
self.assertEqual(response.status_code, 200)
def test_page_move_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Get move page
response = self.client.get(reverse('wagtailadmin_pages_move', args=(self.test_page.id, )))
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
def test_page_move_confirm(self):
response = self.client.get(reverse('wagtailadmin_pages_move_confirm', args=(self.test_page.id, self.section_b.id)))
self.assertEqual(response.status_code, 200)
def test_page_set_page_position(self):
response = self.client.get(reverse('wagtailadmin_pages_set_page_position', args=(self.test_page.id, )))
self.assertEqual(response.status_code, 200)
class TestPageUnpublish(TestCase):
def setUp(self):
self.user = login(self.client)
# Create a page to unpublish
root_page = Page.objects.get(id=2)
self.page = SimplePage(
title="Hello world!",
slug='hello-world',
live=True,
)
root_page.add_child(instance=self.page)
def test_unpublish_view(self):
"""
This tests that the unpublish view responds with an unpublish confirm page
"""
# Get unpublish page
response = self.client.get(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, )))
# Check that the user recieved an unpublish confirm page
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/pages/confirm_unpublish.html')
def test_unpublish_view_invalid_page_id(self):
"""
This tests that the unpublish view returns an error if the page id is invalid
"""
# Get unpublish page
response = self.client.get(reverse('wagtailadmin_pages_unpublish', args=(12345, )))
# Check that the user recieved a 404 response
self.assertEqual(response.status_code, 404)
def test_unpublish_view_bad_permissions(self):
"""
This tests that the unpublish view doesn't allow users without unpublish permissions
"""
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Get unpublish page
response = self.client.get(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, )))
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
def test_unpublish_view_post(self):
"""
This posts to the unpublish view and checks that the page was unpublished
"""
# Post to the unpublish page
response = self.client.post(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user was redirected
self.assertEqual(response.status_code, 302)
# Check that the page was unpublished
self.assertFalse(SimplePage.objects.get(id=self.page.id).live)
class TestApproveRejectModeration(TestCase):
def setUp(self):
self.submitter = User.objects.create_superuser(
username='submitter',
email='submitter@email.com',
password='password',
)
self.user = login(self.client)
# Create a page and submit it for moderation
root_page = Page.objects.get(id=2)
self.page = SimplePage(
title="Hello world!",
slug='hello-world',
live=False,
)
root_page.add_child(instance=self.page)
self.page.save_revision(user=self.submitter, submitted_for_moderation=True)
self.revision = self.page.get_latest_revision()
def test_approve_moderation_view(self):
"""
This posts to the approve moderation view and checks that the page was approved
"""
# Post
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user was redirected
self.assertEqual(response.status_code, 302)
# Page must be live
self.assertTrue(Page.objects.get(id=self.page.id).live)
# Submitter must recieve an approved email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['submitter@email.com'])
self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been approved')
def test_approve_moderation_view_bad_revision_id(self):
"""
This tests that the approve moderation view handles invalid revision ids correctly
"""
# Post
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(12345, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user recieved a 404 response
self.assertEqual(response.status_code, 404)
def test_approve_moderation_view_bad_permissions(self):
"""
This tests that the approve moderation view doesn't allow users without moderation permissions
"""
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Post
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
def test_reject_moderation_view(self):
"""
This posts to the reject moderation view and checks that the page was rejected
"""
# Post
response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(self.revision.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user was redirected
self.assertEqual(response.status_code, 302)
# Page must not be live
self.assertFalse(Page.objects.get(id=self.page.id).live)
# Revision must no longer be submitted for moderation
self.assertFalse(PageRevision.objects.get(id=self.revision.id).submitted_for_moderation)
# Submitter must recieve a rejected email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ['submitter@email.com'])
self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been rejected')
def test_reject_moderation_view_bad_revision_id(self):
"""
This tests that the reject moderation view handles invalid revision ids correctly
"""
# Post
response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(12345, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user recieved a 404 response
self.assertEqual(response.status_code, 404)
def test_reject_moderation_view_bad_permissions(self):
"""
This tests that the reject moderation view doesn't allow users without moderation permissions
"""
# Remove privileges from user
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
)
self.user.save()
# Post
response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(self.revision.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
})
# Check that the user recieved a 403 response
self.assertEqual(response.status_code, 403)
def test_preview_for_moderation(self):
response = self.client.get(reverse('wagtailadmin_pages_preview_for_moderation', args=(self.revision.id, )))
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tests/simple_page.html')
self.assertContains(response, "Hello world!")
class TestContentTypeUse(TestCase):
fixtures = ['test.json']
def setUp(self):
self.user = login(self.client)
def test_content_type_use(self):
# Get use of event page
response = self.client.get(reverse('wagtailadmin_pages_type_use', args=('tests', 'eventpage')))
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailadmin/pages/content_type_use.html')
self.assertContains(response, "Christmas")

View file

@ -0,0 +1,46 @@
from django.test import TestCase
from wagtail.tests.models import SimplePage, EventPage
from wagtail.tests.utils import login, unittest
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.tasks import send_email_task
from django.core.urlresolvers import reverse
from django.core import mail
class TestHome(TestCase):
def setUp(self):
# Login
login(self.client)
def test_status_code(self):
response = self.client.get(reverse('wagtailadmin_home'))
self.assertEqual(response.status_code, 200)
class TestEditorHooks(TestCase):
def setUp(self):
self.homepage = Page.objects.get(id=2)
login(self.client)
def test_editor_css_and_js_hooks_on_add(self):
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.homepage.id)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<link rel="stylesheet" href="/path/to/my/custom.css">')
self.assertContains(response, '<script src="/path/to/my/custom.js"></script>')
def test_editor_css_and_js_hooks_on_edit(self):
response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.homepage.id, )))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<link rel="stylesheet" href="/path/to/my/custom.css">')
self.assertContains(response, '<script src="/path/to/my/custom.js"></script>')
class TestSendEmailTask(TestCase):
def test_send_email(self):
send_email_task("Test subject", "Test content", ["nobody@email.com"], "test@email.com")
# Check that the email was sent
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "Test subject")
self.assertEqual(mail.outbox[0].body, "Test content")
self.assertEqual(mail.outbox[0].to, ["nobody@email.com"])

View file

@ -5,7 +5,7 @@ from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.decorators import permission_required
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext as _
from django.views.decorators.vary import vary_on_headers
from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList
@ -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,
@ -349,6 +360,10 @@ def preview_on_create(request, content_type_app_name, content_type_model_name, p
if form.is_valid():
form.save(commit=False)
# ensure that our unsaved page instance has a suitable url set
parent_page = get_object_or_404(Page, id=parent_page_id).specific
page.set_url_path(parent_page)
# This view will generally be invoked as an AJAX request; as such, in the case of
# an error Django will return a plaintext response. This isn't what we want, since
# we will be writing the response back to an HTML page regardless of success or

View file

@ -16,4 +16,4 @@ def autocomplete(request):
response = json.dumps([tag.name for tag in tags])
return HttpResponse(response, mimetype='text/javascript')
return HttpResponse(response, content_type='text/javascript')

Binary file not shown.

View file

@ -0,0 +1,64 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Wagtail\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-03-10 12:24+0200\n"
"PO-Revision-Date: 2014-02-28 16:07+0000\n"
"Last-Translator: wdv4758h <wdv4758h@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_TW\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: .\models.py:36
msgid ""
"Set this to something other than 80 if you need a specific port number to "
"appear in URLs (e.g. development on port 8000). Does not affect request "
"handling (so port forwarding still works)."
msgstr ""
"如果你需要指定 port請選擇一個非 80 的 port number (例如: 開發中用 8000 port)。 不影響 request 處理"
" (port forwarding 仍然有效)"
#: .\models.py:38
msgid ""
"If true, this site will handle requests for all other hostnames that do not "
"have a site entry of their own"
msgstr ""
"如果這是 Ture 的話,這個網站將處理其他沒有自己網站的 hostname 的 request。"
#: .\models.py:163
msgid "The page title as you'd like it to be seen by the public"
msgstr "頁面標題 (你想讓外界看到的)"
#: .\models.py:164
msgid ""
"The name of the page as it will appear in URLs e.g http://domain.com/blog/"
"[my-slug]/"
msgstr "一個出現在 URL 的名字,例如 http://domain.com/blog/[my-slug]/"
#: .\models.py:173
msgid "Page title"
msgstr "頁面標題"
#: .\models.py:173
msgid ""
"Optional. 'Search Engine Friendly' title. This will appear at the top of the "
"browser window."
msgstr "(可選) '搜尋引擎友善' 標題。 這會顯示在瀏覽器的視窗最上方"
#: .\models.py:174
msgid ""
"Whether a link to this page will appear in automatically generated menus"
msgstr "是否在自動生成的 Menu 裡顯示一個連結到此頁面"
#: .\models.py:418
#, python-format
msgid "name '%s' (used in subpage_types list) is not defined."
msgstr "'%s' (用於子頁面類型列表) 沒有被建立。"

View file

@ -12,15 +12,15 @@ class Command(NoArgsCommand):
try:
page.specific
except ObjectDoesNotExist:
print "Page %d (%s) is missing a subclass record; deleting." % (page.id, page.title)
self.stdout.write("Page %d (%s) is missing a subclass record; deleting." % (page.id, page.title))
problems_found = True
page.delete()
(_, _, _, bad_depth, bad_numchild) = Page.find_problems()
if bad_depth:
print "Incorrect depth value found for pages: %r" % bad_depth
self.stdout.write("Incorrect depth value found for pages: %r" % bad_depth)
if bad_numchild:
print "Incorrect numchild value found for pages: %r" % bad_numchild
self.stdout.write("Incorrect numchild value found for pages: %r" % bad_numchild)
if bad_depth or bad_numchild:
Page.fix_tree(destructive=False)
@ -28,20 +28,20 @@ class Command(NoArgsCommand):
remaining_problems = Page.find_problems()
if any(remaining_problems):
print "Remaining problems (cannot fix automatically):"
self.stdout.write("Remaining problems (cannot fix automatically):")
(bad_alpha, bad_path, orphans, bad_depth, bad_numchild) = remaining_problems
if bad_alpha:
print "Invalid characters found in path for pages: %r" % bad_alpha
self.stdout.write("Invalid characters found in path for pages: %r" % bad_alpha)
if bad_path:
print "Invalid path length found for pages: %r" % bad_path
self.stdout.write("Invalid path length found for pages: %r" % bad_path)
if orphans:
print "Orphaned pages found: %r" % orphans
self.stdout.write("Orphaned pages found: %r" % orphans)
if bad_depth:
print "Incorrect depth value found for pages: %r" % bad_depth
self.stdout.write("Incorrect depth value found for pages: %r" % bad_depth)
if bad_numchild:
print "Incorrect numchild value found for pages: %r" % bad_numchild
self.stdout.write("Incorrect numchild value found for pages: %r" % bad_numchild)
elif problems_found:
print "All problems fixed."
self.stdout.write("All problems fixed.")
else:
print "No problems found."
self.stdout.write("No problems found.")

View file

@ -15,8 +15,8 @@ class Command(BaseCommand):
pages = from_page.get_children()
# Move the pages
print 'Moving ' + str(len(pages)) + ' pages from "' + from_page.title + '" to "' + to_page.title + '"'
self.stdout.write('Moving ' + str(len(pages)) + ' pages from "' + from_page.title + '" to "' + to_page.title + '"')
for page in pages:
page.move(to_page, pos='last-child')
print 'Done'
self.stdout.write('Done')

View file

@ -24,7 +24,7 @@ class Command(BaseCommand):
revision.save(update_fields=['content_json'])
for content_type in get_page_types():
print "scanning %s" % content_type.name
self.stdout.write("scanning %s" % content_type.name)
page_class = content_type.model_class()
try:

View file

@ -581,7 +581,12 @@ class Page(MP_Node, ClusterableModel, Indexed):
path = url_info.path
port = url_info.port or 80
else:
hostname = 'example.com'
# Cannot determine a URL to this page - cobble one together based on
# whatever we find in ALLOWED_HOSTS
try:
hostname = settings.ALLOWED_HOSTS[0]
except IndexError:
hostname = 'localhost'
path = '/'
port = 80
@ -799,6 +804,37 @@ class UserPagePermissionsProxy(object):
permission to perform specific tasks on the given page"""
return PagePermissionTester(self, page)
def editable_pages(self):
"""Return a queryset of the pages that this user has permission to edit"""
# Deal with the trivial cases first...
if not self.user.is_active:
return Page.objects.none()
if self.user.is_superuser:
return Page.objects.all()
# Translate each of the user's permission rules into a Q-expression
q_expressions = []
for perm in self.permissions:
if perm.permission_type == 'add':
# user has edit permission on any subpage of perm.page
# (including perm.page itself) that is owned by them
q_expressions.append(
Q(path__startswith=perm.page.path, owner=self.user)
)
elif perm.permission_type == 'edit':
# user has edit permission on any subpage of perm.page
# (including perm.page itself) regardless of owner
q_expressions.append(
Q(path__startswith=perm.page.path)
)
if q_expressions:
all_rules = q_expressions[0]
for expr in q_expressions[1:]:
all_rules = all_rules | expr
return Page.objects.filter(all_rules)
else:
return Page.objects.none()
class PagePermissionTester(object):
def __init__(self, user_perms, page):

View file

@ -1,722 +0,0 @@
from django.test import TestCase, Client
from django.http import HttpRequest, Http404
from django.contrib.auth.models import User
from wagtail.wagtailcore.models import Page, Site
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))
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, '/')

View file

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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, '/')

Binary file not shown.

View file

@ -0,0 +1,157 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Wagtail\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-03-14 23:09+0200\n"
"PO-Revision-Date: 2014-03-14 21:12+0000\n"
"Last-Translator: wdv4758h <wdv4758h@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_TW\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: .\models.py:16 .\templates\wagtaildocs\documents\list.html:9
msgid "Title"
msgstr "標題"
#: .\models.py:17 .\templates\wagtaildocs\documents\list.html:18
msgid "File"
msgstr "文件"
#: .\models.py:21
msgid "Tags"
msgstr "標籤"
#: .\templates\wagtaildocs\chooser\chooser.html:2
#: .\templates\wagtaildocs\edit_handlers\document_chooser_panel.html:11
msgid "Choose a document"
msgstr "選擇一個文件"
#: .\templates\wagtaildocs\chooser\chooser.html:7
#: .\templates\wagtaildocs\chooser\chooser.html:19
msgid "Search"
msgstr "搜尋"
#: .\templates\wagtaildocs\chooser\chooser.html:8
msgid "Upload"
msgstr "上傳"
#: .\templates\wagtaildocs\chooser\chooser.html:34
#: .\templates\wagtaildocs\documents\add.html:25
#: .\templates\wagtaildocs\documents\edit.html:29
msgid "Save"
msgstr "儲存"
#: .\templates\wagtaildocs\chooser\results.html:5
#: .\templates\wagtaildocs\documents\results.html:5
#, python-format
msgid ""
"\n"
" There is one match\n"
" "
msgid_plural ""
"\n"
" There are %(counter)s matches\n"
" "
msgstr[0] "\n 有一個符合"
msgstr[1] "\n 有 $(counter)s 個符合"
#: .\templates\wagtaildocs\chooser\results.html:12
msgid "Latest documents"
msgstr "最新文件"
#: .\templates\wagtaildocs\chooser\results.html:19
#: .\templates\wagtaildocs\documents\results.html:18
#, python-format
msgid "Sorry, no documents match \"<em>%(query_string)s</em>\""
msgstr "對不起,沒有文件符合 \"<em>%(query_string)s</em>\""
#: .\templates\wagtaildocs\documents\_file_field.html:5
msgid "Change document:"
msgstr "修改文件:"
#: .\templates\wagtaildocs\documents\add.html:4
#: .\templates\wagtaildocs\documents\index.html:17
msgid "Add a document"
msgstr "新增一份文件"
#: .\templates\wagtaildocs\documents\add.html:15
msgid "Add document"
msgstr "新增文件"
#: .\templates\wagtaildocs\documents\confirm_delete.html:3
#, python-format
msgid "Delete %(title)s"
msgstr "刪除 %(title)s"
#: .\templates\wagtaildocs\documents\confirm_delete.html:6
#: .\templates\wagtaildocs\documents\edit.html:29
msgid "Delete document"
msgstr "刪除文件"
#: .\templates\wagtaildocs\documents\confirm_delete.html:10
msgid "Are you sure you want to delete this document?"
msgstr "你確定你要刪除這份文件嗎?"
#: .\templates\wagtaildocs\documents\confirm_delete.html:13
msgid "Yes, delete"
msgstr "是的,刪除"
#: .\templates\wagtaildocs\documents\edit.html:4
#, python-format
msgid "Editing %(title)s"
msgstr "編輯 %(title)s"
#: .\templates\wagtaildocs\documents\edit.html:15
msgid "Editing"
msgstr "編輯"
#: .\templates\wagtaildocs\documents\index.html:16
msgid "Documents"
msgstr "文件"
#: .\templates\wagtaildocs\documents\list.html:20
msgid "Uploaded"
msgstr "已上傳"
#: .\templates\wagtaildocs\documents\results.html:21
#, python-format
msgid ""
"You haven't uploaded any documents. Why not <a "
"href=\"%(wagtaildocs_add_document_url)s\">upload one now</a>?"
msgstr "你沒有上傳任何文件。 為什麼不 <a href=\"%(wagtaildocs_add_document_url)s\">上傳一份</a>?"
#: .\templates\wagtaildocs\edit_handlers\document_chooser_panel.html:9
msgid "Clear choice"
msgstr "清除選擇"
#: .\templates\wagtaildocs\edit_handlers\document_chooser_panel.html:10
msgid "Choose another document"
msgstr "選擇另外一份文件"
#: .\views\documents.py:34 .\views\documents.py:43
msgid "Search documents"
msgstr "搜尋文件"
#: .\views\documents.py:83
msgid "Document '{0}' added."
msgstr "文件 '{0}' 已加入"
#: .\views\documents.py:86 .\views\documents.py:115
msgid "The document could not be saved due to errors."
msgstr "這文件因有錯誤而無法建立。"
#: .\views\documents.py:112
msgid "Document '{0}' updated"
msgstr "文件 '{0}' 已更新"
#: .\views\documents.py:134
msgid "Document '{0}' deleted."
msgstr "文件 '{0}' 已刪除"

View file

@ -17,7 +17,7 @@ def embed_to_frontend_html(url):
ratio = "0"
# Render template
render_to_string('wagtailembeds/embed_frontend.html', {
return render_to_string('wagtailembeds/embed_frontend.html', {
'embed': embed,
'ratio': ratio,
})

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Translators: Lihan Li <lilihan.it@gmail.com>, 2014
msgid ""
msgstr ""
"Project-Id-Version: Wagtail\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-02-22 19:56+0200\n"
"PO-Revision-Date: 2014-02-24 17:34+0000\n"
"Last-Translator: wdv4758h <wdv4758h@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_TW\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\forms.py:11
msgid "Please enter a valid URL"
msgstr "請輸入有效的 URL"
#: .\forms.py:15
msgid "URL"
msgstr "URL"
#: .\templates\wagtailembeds\chooser\chooser.html:3
msgid "Insert embed"
msgstr "插入 embed"
#: .\templates\wagtailembeds\chooser\chooser.html:14
msgid "Insert"
msgstr "插入"
#: .\views\chooser.py:34
msgid ""
"There seems to be a problem with your embedly API key. Please check your "
"settings."
msgstr "embedly API key 有問題。請檢查設定。"
#: .\views\chooser.py:36
msgid "Cannot find an embed for this URL."
msgstr "在這個 URL 中無法找到 embed"
#: .\views\chooser.py:38
msgid ""
"There seems to be an error with Embedly while trying to embed this URL. "
"Please try again later."
msgstr ""
"在嵌入這個 URL 的時候Embedly 似乎有錯。"
"請稍候再試."

View file

@ -1,3 +1,3 @@
<div style="padding-bottom: {{ ratio }};" class="responsive-object">
{{ embed.html }}
{{ embed.html|safe }}
</div>

View file

View file

@ -0,0 +1,87 @@
import django.forms
from django.utils.datastructures import SortedDict
class BaseForm(django.forms.Form):
def __init__(self, *args, **kwargs):
kwargs.setdefault('label_suffix', '')
return super(BaseForm, self).__init__(*args, **kwargs)
class FormBuilder():
formfields = SortedDict()
def __init__(self, fields):
for field in fields:
options = self.get_options(field)
f = getattr(self, "create_"+field.field_type+"_field")(field, options)
self.formfields[field.clean_name] = f
def get_options(self, field):
options = {}
options['label'] = field.label
options['help_text'] = field.help_text
options['required'] = field.required
options['initial'] = field.default_value
return options
def create_singleline_field(self, field, options):
# TODO: This is a default value - it may need to be changed
options['max_length'] = 255
return django.forms.CharField(**options)
def create_multiline_field(self, field, options):
return django.forms.CharField(widget=django.forms.Textarea, **options)
def create_date_field(self, field, options):
return django.forms.DateField(**options)
def create_datetime_field(self, field, options):
return django.forms.DateTimeField(**options)
def create_email_field(self, field, options):
return django.forms.EmailField(**options)
def create_url_field(self, field, options):
return django.forms.URLField(**options)
def create_number_field(self, field, options):
return django.forms.DecimalField(**options)
def create_dropdown_field(self, field, options):
options['choices'] = map(
lambda x: (x.strip(), x.strip()),
field.choices.split(',')
)
return django.forms.ChoiceField(**options)
def create_radio_field(self, field, options):
options['choices'] = map(
lambda x: (x.strip(), x.strip()),
field.choices.split(',')
)
return django.forms.ChoiceField(widget=django.forms.RadioSelect, **options)
def create_checkboxes_field(self, field, options):
options['choices'] = [(x.strip(), x.strip()) for x in field.choices.split(',')]
options['initial'] = [x.strip() for x in field.default_value.split(',')]
return django.forms.MultipleChoiceField(
widget=django.forms.CheckboxSelectMultiple, **options
)
def create_checkbox_field(self, field, options):
return django.forms.BooleanField(**options)
def get_form_class(self):
return type('WagtailForm', (BaseForm,), self.formfields)
class SelectDateForm(django.forms.Form):
date_from = django.forms.DateField(
required=False,
widget=django.forms.DateInput(attrs={'placeholder': 'Date from'})
)
date_to = django.forms.DateField(
required=False,
widget=django.forms.DateInput(attrs={'placeholder': 'Date to'})
)

View file

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from wagtail.wagtailcore.compat import AUTH_USER_MODEL, AUTH_USER_MODEL_NAME
class Migration(SchemaMigration):
depends_on = (
("wagtailcore", "0002_initial_data"),
)
def forwards(self, orm):
# Adding model 'FormSubmission'
db.create_table(u'wagtailforms_formsubmission', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('form_data', self.gf('django.db.models.fields.TextField')()),
('page', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wagtailcore.Page'])),
('submit_time', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
))
db.send_create_signal(u'wagtailforms', ['FormSubmission'])
def backwards(self, orm):
# Deleting model 'FormSubmission'
db.delete_table(u'wagtailforms_formsubmission')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
AUTH_USER_MODEL: {
'Meta': {'object_name': AUTH_USER_MODEL_NAME},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['%s']" % AUTH_USER_MODEL}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'wagtailforms.formsubmission': {
'Meta': {'object_name': 'FormSubmission'},
'form_data': ('django.db.models.fields.TextField', [], {}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['wagtailcore.Page']"}),
'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
}
}
complete_apps = ['wagtailforms']

View file

@ -0,0 +1,200 @@
from django.db import models
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
from django.utils.text import slugify
from unidecode import unidecode
import json
import re
from wagtail.wagtailcore.models import Page, Orderable, UserPagePermissionsProxy, get_page_types
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailadmin import tasks
from .forms import FormBuilder
FORM_FIELD_CHOICES = (
('singleline', _('Single line text')),
('multiline', _('Multi-line text')),
('email', _('Email')),
('number', _('Number')),
('url', _('URL')),
('checkbox', _('Checkbox')),
('checkboxes', _('Checkboxes')),
('dropdown', _('Drop down')),
('radio', _('Radio buttons')),
('date', _('Date')),
('datetime', _('Date/time')),
)
HTML_EXTENSION_RE = re.compile(r"(.*)\.html")
class FormSubmission(models.Model):
"""Data for a Form submission."""
form_data = models.TextField()
page = models.ForeignKey(Page)
submit_time = models.DateTimeField(auto_now_add=True)
def get_data(self):
return json.loads(self.form_data)
def __unicode__(self):
return self.form_data
class AbstractFormField(Orderable):
"""Database Fields required for building a Django Form field."""
label = models.CharField(
max_length=255,
help_text=_('The label of the form field')
)
field_type = models.CharField(max_length=16, choices=FORM_FIELD_CHOICES)
required = models.BooleanField(default=True)
choices = models.CharField(
max_length=512,
blank=True,
help_text=_('Comma seperated list of choices. Only applicable in checkboxes, radio and dropdown.')
)
default_value = models.CharField(
max_length=255,
blank=True,
help_text=_('Default value. Comma seperated values supported for checkboxes.')
)
help_text = models.CharField(max_length=255, blank=True)
@property
def clean_name(self):
# unidecode will return an ascii string while slugify wants a
# unicode string on the other hand, slugify returns a safe-string
# which will be converted to a normal str
return str(slugify(unicode(unidecode(self.label))))
panels = [
FieldPanel('label'),
FieldPanel('help_text'),
FieldPanel('required'),
FieldPanel('field_type', classname="formbuilder-type"),
FieldPanel('choices', classname="formbuilder-choices"),
FieldPanel('default_value', classname="formbuilder-default"),
]
class Meta:
abstract = True
ordering = ['sort_order']
_FORM_CONTENT_TYPES = None
def get_form_types():
global _FORM_CONTENT_TYPES
if _FORM_CONTENT_TYPES is None:
_FORM_CONTENT_TYPES = [
ct for ct in get_page_types()
if issubclass(ct.model_class(), AbstractForm)
]
return _FORM_CONTENT_TYPES
def get_forms_for_user(user):
"""Return a queryset of form pages that this user is allowed to access the submissions for"""
editable_pages = UserPagePermissionsProxy(user).editable_pages()
return editable_pages.filter(content_type__in=get_form_types())
class AbstractForm(Page):
"""A Form Page. Pages implementing a form should inhert from it"""
form_builder = FormBuilder
is_abstract = True # Don't display me in "Add"
def __init__(self, *args, **kwargs):
super(AbstractForm, self).__init__(*args, **kwargs)
if not hasattr(self, 'landing_page_template'):
template_wo_ext = re.match(HTML_EXTENSION_RE, self.template).group(1)
self.landing_page_template = template_wo_ext + '_landing.html'
class Meta:
abstract = True
def get_form_parameters(self):
return {}
def process_form_submission(self, form):
# remove csrf_token from form.data
form_data = dict(
i for i in form.data.items()
if i[0] != 'csrfmiddlewaretoken'
)
FormSubmission.objects.create(
form_data=json.dumps(form_data),
page=self,
)
def serve(self, request):
fb = self.form_builder(self.form_fields.all())
form_class = fb.get_form_class()
form_params = self.get_form_parameters()
if request.method == 'POST':
form = form_class(request.POST, **form_params)
if form.is_valid():
self.process_form_submission(form)
# If we have a form_processing_backend call its process method
if hasattr(self, 'form_processing_backend'):
form_processor = self.form_processing_backend()
form_processor.process(self, form)
# render the landing_page
# TODO: It is much better to redirect to it
return render(request, self.landing_page_template, {
'self': self,
})
else:
form = form_class(**form_params)
return render(request, self.template, {
'self': self,
'form': form,
})
def get_page_modes(self):
return [
('form', 'Form'),
('landing', 'Landing page'),
]
def show_as_mode(self, mode):
if mode == 'landing':
return render(self.dummy_request(), self.landing_page_template, {
'self': self,
})
else:
return super(AbstractForm, self).show_as_mode(mode)
class AbstractEmailForm(AbstractForm):
"""A Form Page that sends email. Pages implementing a form to be send to an email should inherit from it"""
is_abstract = True # Don't display me in "Add"
to_address = models.CharField(max_length=255, blank=True, help_text=_("Optional - form submissions will be emailed to this address"))
from_address = models.CharField(max_length=255, blank=True)
subject = models.CharField(max_length=255, blank=True)
def process_form_submission(self, form):
super(AbstractEmailForm, self).process_form_submission(form)
if self.to_address:
content = '\n'.join([x[1].label + ': ' + form.data.get(x[0]) for x in form.fields.items()])
tasks.send_email_task.delay(self.subject, content, [self.to_address], self.from_address,)
class Meta:
abstract = True

View file

@ -0,0 +1,3 @@
$(function(){
});

View file

@ -0,0 +1,15 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Forms" %}{% endblock %}
{% block bodyclass %}menu-forms{% endblock %}
{% block content %}
{% trans "Forms" as forms_str %}
{% trans "Pages" as select_form_str %}
{% include "wagtailadmin/shared/header.html" with title=forms_str subtitle=select_form_str icon="form" %}
<div class="nice-padding">
<div id="form-results" class="forms">
{% include "wagtailforms/results_forms.html" %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,68 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% load localize %}
{% block titletag %}{% blocktrans with form_title=form_page.title|capfirst %}Submissions of {{ form_title }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-snippets{% endblock %}
{% block extra_js %}
{% get_localized_datepicker_js %}
{% get_date_format_override as format_override %}
<script>
window.overrideDateInputFormat ='{{ format_override }}';
$(function() {
if(window.overrideDateInputFormat && window.overrideDateInputFormat !='') {
$('#id_date_from').datepicker({
dateFormat: window.overrideDateInputFormat, constrainInput: false, /* showOn: 'button', */ firstDay: 1
});
$('#id_date_to').datepicker({
dateFormat: window.overrideDateInputFormat, constrainInput: false, /* showOn: 'button', */ firstDay: 1
});
} else {
$('#id_date_from').datepicker({
constrainInput: false, /* showOn: 'button', */ firstDay: 1
});
$('#id_date_to').datepicker({
constrainInput: false, /* showOn: 'button', */ firstDay: 1
});
}
});
</script>
{% endblock %}
{% block content %}
<header class="nice-padding">
<form action="" method="get">
<div class="row">
<div class="left">
<div class="col">
<h1 class="icon icon-form">
{% blocktrans with form_title=form_page.title|capfirst %}Form data <span>{{ form_title }}</span>{% endblocktrans %}
</h1>
</div>
<div class="col search-bar">
<ul class="fields row rowflush">
{% for field in select_date_form %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field input_classes="field-small iconfield icon-date" li_classes="col4" %}
{% endfor %}
<li class="submit col2">
<button name="action" value="filter" class="button">{% trans 'Filter' %}</button>
</li>
</ul>
</div>
</div>
<div class="right">
<button name="action" value="CSV" class="button bicolor icon icon-download">{% trans 'Download CSV' %}</button>
</div>
</div>
</form>
</header>
<div class="nice-padding">
{% if submissions %}
{% include "wagtailforms/list_submissions.html" %}
{% include "wagtailadmin/shared/pagination_nav.html" with items=submissions is_searching=False linkurl='-' %}
{# Here we pass an invalid non-empty URL name as linkurl to generate pagination links with the URL path omitted #}
{% else %}
<p class="no-results-message">{% blocktrans with title=form_page.title %}There have been no submissions of the '{{ title }}' form.{% endblocktrans %}</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,23 @@
{% load i18n %}
<table class="listing">
<col width="50%"/>
<col width="50%"/>
<thead>
<tr>
<th class="title">{% trans "Title" %}</th>
<th class="type">{% trans "Origin" %}</th>
</tr>
</thead>
<tbody>
{% for fp in form_pages %}
<tr>
<td class="title">
<h2><a href="{% url 'wagtailforms_list_submissions' fp.id %}">{{ fp|capfirst }}</a></h2>
</td>
<td class="type">
<small><a href="{% url 'wagtailadmin_pages_edit' fp.id %}" class="nolink">{{ fp.content_type.name |capfirst }} ({{ fp.content_type.app_label }}.{{ fp.content_type.model }})</a></small>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,27 @@
{% load i18n %}
<div class="overflow">
<table class="listing">
<col />
<col />
<col />
<thead>
<tr>
<th>{% trans "Submission Date" %}</th>
{% for heading in data_headings %}
<th>{{ heading }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in data_rows %}
<tr>
{% for cell in row %}
<td>
{{ cell }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>

View file

@ -0,0 +1,8 @@
{% load i18n %}
{% if form_pages %}
{% include "wagtailforms/list_forms.html" %}
{% include "wagtailadmin/shared/pagination_nav.html" with items=form_pages linkurl="wagtailforms_index" %}
{% else %}
<p>{% trans "No form pages have been created." %}</p>
{% endif %}

View file

@ -0,0 +1,63 @@
from django.test import TestCase
from wagtail.wagtailcore.models import Page
from wagtail.wagtailforms.models import FormSubmission
class TestFormSubmission(TestCase):
fixtures = ['test.json']
def test_get_form(self):
response = self.client.get('/contact-us/')
self.assertContains(response, """<label for="id_your-email">Your email</label>""")
self.assertNotContains(response, "Thank you for your feedback")
def test_post_invalid_form(self):
response = self.client.post('/contact-us/', {
'your-email': 'bob', 'your-message': 'hello world'
})
self.assertNotContains(response, "Thank you for your feedback")
self.assertContains(response, "Enter a valid email address.")
def test_post_valid_form(self):
response = self.client.post('/contact-us/', {
'your-email': 'bob@example.com', 'your-message': 'hello world'
})
self.assertNotContains(response, "Your email")
self.assertContains(response, "Thank you for your feedback")
form_page = Page.objects.get(url_path='/home/contact-us/')
self.assertTrue(FormSubmission.objects.filter(page=form_page, form_data__contains='hello world').exists())
class TestFormsBackend(TestCase):
fixtures = ['test.json']
def test_cannot_see_forms_without_permission(self):
form_page = Page.objects.get(url_path='/home/contact-us/')
self.client.login(username='eventeditor', password='password')
response = self.client.get('/admin/forms/')
self.assertFalse(form_page in response.context['form_pages'])
def test_can_see_forms_with_permission(self):
form_page = Page.objects.get(url_path='/home/contact-us/')
self.client.login(username='siteeditor', password='password')
response = self.client.get('/admin/forms/')
self.assertTrue(form_page in response.context['form_pages'])
def test_can_get_submissions(self):
form_page = Page.objects.get(url_path='/home/contact-us/')
self.client.login(username='siteeditor', password='password')
response = self.client.get('/admin/forms/submissions/%d/' % form_page.id)
self.assertEqual(len(response.context['data_rows']), 2)
response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014' % form_page.id)
self.assertEqual(len(response.context['data_rows']), 1)
response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014&action=CSV' % form_page.id)
data_line = response.content.split("\n")[1]
self.assertTrue('new@example.com' in data_line)

View file

@ -0,0 +1,9 @@
from django.conf.urls import patterns, url
urlpatterns = patterns(
'wagtail.wagtailforms.views',
url(r'^$', 'index', name='wagtailforms_index'),
url(r'^submissions/(\d+)/$', 'list_submissions', name='wagtailforms_list_submissions'),
)

Some files were not shown because too many files have changed in this diff Show more