Merge branch 'master' into autodoc

Conflicts:
	runtests.py
This commit is contained in:
Karl Hobley 2014-07-11 12:23:25 +01:00
commit 4bfd0e5a20
202 changed files with 6646 additions and 1792 deletions

View file

@ -1,10 +1,12 @@
# Python releases to test
language: python
# Test matrix
python:
- 2.7
# Django releases
- 2.7
- 3.2
- 3.4
env:
- DJANGO_VERSION=Django==1.6.2
- DJANGO_VERSION=Django==1.6.5
#- DJANGO_VERSION=Django==1.7.0
# Services
services:
- redis-server

View file

@ -1,10 +1,16 @@
Changelog
=========
0.4 (xx.xx.20xx)
0.4 (10.07.2014)
~~~~~~~~~~~~~~~~
* ElasticUtils/pyelasticsearch swapped for elasticsearch-py
* Python 3.2, 3.3 and 3.4 support
* Added scheduled publishing
* Added support for private (password-protected) pages
* Added frontend cache invalidator
* Added sitemap generator
* Added notification preferences
* Added a new way to configure searchable/filterable fields on models
* Added 'original' as a resizing rule supported by the 'image' tag
* Hallo.js updated to version 1.0.4
* Snippets are now ordered alphabetically
@ -15,16 +21,31 @@ Changelog
* Added styleguide (mainly for wagtail developers)
* Aesthetic improvements to preview experience
* 'image' tag now accepts extra keyword arguments to be output as attributes on the img tag
* Login screen redirects to dashboard if user is already logged in
* Renamed some template tag libraries
* Any extra arguments given to serve are now passed through to get_context and get_template
* Added an 'attrs' property to image rendition objects to output src, width, height and alt attributes all in one go
* Added 'construct_whitelister_element_rules' hook for customising the HTML whitelist used when saving rich text fields
* Added 'in_menu' and 'not_in_menu' methods to PageQuerySet
* Added 'get_next_siblings' and 'get_prev_siblings' to Page
* Added init_new_page signal
* Added page_published signal
* Added copy method to Page to allow copying of pages
* Added ``search`` method to ``PageQuerySet``
* Added ``get_indexed_objects`` allowing developers to customise which objects get added to the search index
* Major refactor of Elasticsearch backend
* Use ``match`` instead of ``query_string`` queries
* Fields are now indexed in Elasticsearch with their correct type
* Filter fields are no longer included in '_all' (in Elasticsearch)
* Fields with partial matching are now indexed together into '_partials'
* Fix: Animated GIFs are now coalesced before resizing
* Fix: Wand backend clones images before modifying them
* Fix: Admin breadcrumb now positioned correctly on mobile
* Fix: Page chooser breadcrumb now updates the chooser modal instead of linking to Explorer
* Fix: Embeds - Fixed crash when no HTML field is sent back from the embed provider
* Fix: Multiple sites with same hostname but different ports are now allowed
* Fix: No longer possible to create multiple sites with is_default_site = True
0.3.1 (03.06.2014)
~~~~~~~~~~~~~~~~~~

View file

@ -2,7 +2,7 @@
:target: https://travis-ci.org/torchbox/wagtail
.. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master&zxcv1
:target: https://coveralls.io/r/torchbox/wagtail?branch=master
:target: https://coveralls.io/r/torchbox/wagtail?branch=master
.. image:: https://pypip.in/v/wagtail/badge.png?zxcv
:target: https://crate.io/packages/wagtail/
@ -24,7 +24,9 @@ Wagtail is a Django content management system built originally for the `Royal Co
* Support for tree-based content organisation
* Optional preview->submit->approve workflow
* Fast out of the box. `Varnish <https://www.varnish-cache.org/>`_-friendly if you need it
* Excellent test coverage
* A simple `form builder <http://docs.wagtail.io/en/latest/form_builder.html>`_
* Optional `static site generation <http://docs.wagtail.io/en/latest/static_site_generation.html>`_
* Excellent `test coverage <https://coveralls.io/r/torchbox/wagtail?branch=master>`_
Find out more at `wagtail.io <http://wagtail.io/>`_.
@ -35,16 +37,26 @@ Getting started
* To get you up and running quickly, we've provided a demonstration site with all the configuration in place, at `github.com/torchbox/wagtaildemo <https://github.com/torchbox/wagtaildemo/>`_; see the `README <https://github.com/torchbox/wagtaildemo/blob/master/README.md>`_ for installation instructions.
* See the `Getting Started <http://wagtail.readthedocs.org/en/latest/gettingstarted.html#getting-started>`_ docs for installation (with the demo app) on a fresh Debian/Ubuntu box with production-ready dependencies, on OS X and on a Vagrant box.
* `Serafeim Papastefanos <https://github.com/spapas>`_ has written a `tutorial <http://spapas.github.io/2014/02/13/wagtail-tutorial/>`_ with all the steps to build a simple Wagtail site from scratch.
* We've also provided a skeletal django-template to get started on a blank site: https://github.com/torchbox/wagtail-template
Documentation
~~~~~~~~~~~~~
Available at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_. and always being updated.
Available at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_ and always being updated.
Compatibility
~~~~~~~~~~~~~
Wagtail supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
Wagtail supports Django 1.6.2+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4.
Django 1.7 support is in progress pending further release candidate testing.
Wagtail's dependencies are summarised at `requirements.io <https://requires.io/github/torchbox/wagtail/requirements>`_.
Contributing
~~~~~~~~~~~~
If you're a Python or Django developer, fork the repo and get stuck in! Send us a useful pull request and we'll post you a `t-shirt <https://twitter.com/WagtailCMS/status/432166799464210432/photo/1>`_. Our immediate priorities are better docs, more tests, internationalisation and localisation.
If you're a Python or Django developer, fork the repo and get stuck in!
We suggest you start by checking the `Help develop me! <https://github.com/torchbox/wagtail/issues?labels=Help+develop+me%21>`_ label and the `coding guidelines <http://wagtail.readthedocs.org/en/latest/contributing.html#coding-guidelines>`_.
Send us a useful pull request and we'll post you a `t-shirt <https://twitter.com/WagtailCMS/status/432166799464210432/photo/1>`_.
We also welcome `translations <http://wagtail.readthedocs.org/en/latest/contributing.html#translations>`_ for Wagtail's interface.

View file

@ -171,15 +171,18 @@ 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:
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()``
#. Wagtail checks the hostname of the request to determine which ``Site`` record will handle this request.
#. Starting from the root page of that site, Wagtail traverses the page tree, calling the ``route()`` method and letting each page model decide whether it will handle the request itself or pass it on to a child page.
#. The page responsible for handling the request returns a ``RouteResult`` object from ``route()``, which identifies the page along with any additional args/kwargs to be passed to ``serve()``.
#. Wagtail calls ``serve()``, which 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.
@ -205,6 +208,7 @@ Properties:
* status_string
* subpage_types
* indexed_fields
* preview_modes
Methods:
@ -217,8 +221,7 @@ Methods:
* get_descendants
* get_siblings
* search
* get_page_modes
* show_as_mode
* serve_preview
Page Queryset

View file

@ -192,8 +192,9 @@ More control over the ``img`` tag
Wagtail provides two shorcuts to give greater control over the ``img`` element:
**1. Adding attributes to the {% image %} tag**
.. versionadded:: 0.4
**Adding attributes to the {% image %} tag**
Extra attributes can be specified with the syntax ``attribute="value"``:
@ -201,25 +202,31 @@ Extra attributes can be specified with the syntax ``attribute="value"``:
{% image self.photo width-400 class="foo" id="bar" %}
No validation is performed on attributes add in this way by the developer. It's possible to add `src`, `width`, `height` and `alt` of your own that might conflict with those generated by the tag itself.
No validation is performed on attributes added in this way so it's possible to add `src`, `width`, `height` and `alt` of your own that might conflict with those generated by the tag itself.
**Generating the image "as"**
**2. Generating the image "as foo" to access individual properties**
Wagtail can assign the image data to another object using Django's ``as`` syntax:
Wagtail can assign the image data to another variable using Django's ``as`` syntax:
.. code-block:: django
{% image self.photo width-400 as tmp_photo %}
<img src="{{ tmp_photo.src }}" width="{{ tmp_photo.width }}"
<img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}"
height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
.. Note::
The image property used for the ``src`` attribute is actually ``image.url``, not ``image.src``.
.. versionadded:: 0.4
The ``attrs`` shortcut
-----------------------
You can also use the ``attrs`` property as a shorthand to output the ``src``, ``width``, ``height`` and ``alt`` attributes in one go:
.. versionadded:: 0.4
You can also use the ``attrs`` property as a shorthand to output the attributes ``src``, ``width``, ``height`` and ``alt`` in one go:
.. code-block:: django

View file

@ -59,7 +59,7 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'Wagtail Documentation'
project = u'Wagtail'
copyright = u'2014, Torchbox'
# The version info for the project you're documenting, acts as replacement for
@ -67,9 +67,9 @@ copyright = u'2014, Torchbox'
# built documents.
#
# The short X.Y version.
version = '0.1'
version = '0.4'
# The full version, including alpha/beta/rc tags.
release = '0.1'
release = '0.4'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -15,6 +15,7 @@ Coding guidelines
~~~~~~~~~~~~~~~~~
* PEP8. We ask that all Python contributions adhere to the `PEP8 <http://www.python.org/dev/peps/pep-0008/>`_ style guide, apart from the restriction on line length (E501). The `pep8 tool <http://pep8.readthedocs.org/en/latest/>`_ makes it easy to check your code, e.g. ``pep8 --ignore=E501 your_file.py``.
* Python 2 and 3 compatibility. All contributions should support Python 2 and 3 and we recommend using the `six <https://pythonhosted.org/six/>`_ compatibility library (use the pip version installed as a dependency, not the version bundled with Django).
* Tests. Wagtail has a suite of tests, which we are committed to improving and expanding. We run continuous integration at `travis-ci.org/torchbox/wagtail <https://travis-ci.org/torchbox/wagtail>`_ to ensure that no commits or pull requests introduce test failures. If your contributions add functionality to Wagtail, please include the additional tests to cover it; if your contributions alter existing functionality, please update the relevant tests accordingly.
Styleguide

View file

@ -1,28 +1,26 @@
Editing API
===========
Defining models with the Editing API
====================================
.. note::
This documentation is currently being written.
Wagtail provides a highly-customizable editing interface consisting of several components:
* **Fields** — built-in content types to augment the basic types provided by Django.
* **Fields** — built-in content types to augment the basic types provided by Django
* **Panels** — the basic editing blocks for fields, groups of fields, and related object clusters
* **Choosers** — interfaces for finding related objects in a ForeignKey relationship
Configuring your models to use these components will shape the Wagtail editor to your needs. Wagtail also provides an API for injecting custom CSS and Javascript for further customization, including extending the hallo.js rich text editor.
Configuring your models to use these components will shape the Wagtail editor to your needs. Wagtail also provides an API for injecting custom CSS and JavaScript for further customization, including extending the hallo.js rich text editor.
There is also an Edit Handler API for creating your own Wagtail editor components.
Defining Panels
~~~~~~~~~~~~~~~
A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types, you just need to add a panel for each field you want to show in the Wagtail page editor, in the order you want them to appear.
A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types; implementors just need to add a panel for each field they want to show in the Wagtail page editor, in the order they want them to appear.
There are three types of panels:
There are four basic types of panels:
``FieldPanel( field_name, classname=None )``
This is the panel used for basic Django field types. ``field_name`` is the name of the class property used in your model definition. ``classname`` is a string of optional CSS classes given to the panel which are used in formatting and scripted interactivity. By default, panels are formatted as inset fields. The CSS class ``full`` can be used to format the panel so it covers the full width of the Wagtail page editor. The CSS class ``title`` can be used to mark a field as the source for auto-generated slug strings.
@ -30,10 +28,21 @@ There are three types of panels:
``MultiFieldPanel( children, heading="", classname=None )``
This panel condenses several ``FieldPanel`` s or choosers, from a list or tuple, under a single ``heading`` string.
``InlinePanel( base_model, relation_name, panels=None, label='', help_text='' )``
``InlinePanel( base_model, relation_name, panels=None, classname=None, label='', help_text='' )``
This panel allows for the creation of a "cluster" of related objects over a join to a separate model, such as a list of related links or slides to an image carousel. This is a very powerful, but tricky feature which will take some space to cover, so we'll skip over it for now. For a full explanation on the usage of ``InlinePanel``, see :ref:`inline_panels`.
Wagtail provides a tabbed interface to help organize panels. ``content_panels`` is the main tab, used for the meat of your model content. The other, ``promote_panels``, is suggested for organizing metadata about the content, such as SEO information and other machine-readable information. Since you're writing the panel definitions, you can organize them however you want.
``FieldRowPanel( children, classname=None)``
This panel is purely aesthetic. It creates a columnar layout in the editing interface, where each of the child Panels appears alongside each other rather than below. Use of FieldRowPanel particularly helps reduce the "snow-blindness" effect of seeing so many fields on the page, for complex models. It also improves the perceived association between fields of a similar nature. For example if you created a model representing an "Event" which had a starting date and ending date, it may be intuitive to find the start and end date on the same "row".
FieldRowPanel should be used in combination with ``col*`` classnames added to each of the child Panels of the FieldRowPanel. The Wagtail editing interface is layed out using a grid system, in which the maximum width of the editor is 12 columns wide. Classes ``col1``-``col12`` can be applied to each child of a FieldRowPanel. The class ``col3`` will ensure that field appears 3 columns wide or a quarter the width. ``col4`` would cause the field to be 4 columns wide, or a third the width.
**(In addition to these four, there are also Chooser Panels, detailed below.)**
Wagtail provides a tabbed interface to help organize panels. Three such tabs are provided:
* ``content_panels`` is the main tab, used for the bulk of your model's fields.
* ``promote_panels`` is suggested for organizing fields regarding the promotion of the page around the site and the Internet. For example, a field to dictate whether the page should show in site-wide menus, descriptive text that should appear in site search results, SEO-friendly titles, OpenGraph meta tag content and other machine-readable information.
* ``settings_panels`` is essentially for non-copy fields. By default it contains the page's scheduled publishing fields. Other suggested fields could include a field to switch between one layout/style and another.
Let's look at an example of a panel definition:
@ -55,7 +64,10 @@ Let's look at an example of a panel definition:
ExamplePage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('body', classname="full"),
FieldPanel('date'),
FieldRowPanel([
FieldPanel('start_date', classname="col3"),
FieldPanel('end_date', classname="col3"),
]),
ImageChooserPanel('splash_image'),
DocumentChooserPanel('free_download'),
PageChooserPanel('related_page'),
@ -119,7 +131,7 @@ One of the features of Wagtail is a unified image library, which you can access
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
ImageChooserPanel('cover'),
# ...
@ -225,7 +237,7 @@ Snippets are vanilla Django models you create yourself without a Wagtail-provide
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
SnippetChooserPanel('advert', Advert),
# ...
@ -239,7 +251,7 @@ 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/
.. _Django model field reference: https://docs.djangoproject.com/en/dev/ref/models/fields/
Full-Width Input
@ -254,6 +266,12 @@ Titles
Use ``classname="title"`` to make Page's built-in title field stand out with more vertical padding.
Col*
------
Fields within a ``FieldRowPanel`` can have their width dictated in terms of the number of columns it should span. The ``FieldRowPanel`` is always considered to be 12 columns wide regardless of browser size or the nesting of ``FieldRowPanel`` in any other type of panel. Specify a number of columns thus: ``col3``, ``col4``, ``col6`` etc (up to 12). The resulting width with be *relative* to the full width of the ``FieldRowPanel``.
Required Fields
---------------
@ -356,7 +374,7 @@ For more on ``django-modelcluster``, visit `the django-modelcluster github proje
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.
To inject javascript into the Wagtail page editor, see the :ref:`insert_editor_js <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
@ -366,11 +384,9 @@ hallo.js plugin names are prefixed with the ``"IKS."`` namespace, but the ``name
For information on developing custom hallo.js plugins, see the project's page: https://github.com/bergie/hallo
Edit Handler API
~~~~~~~~~~~~~~~~
Admin Hooks
-----------
@ -380,12 +396,29 @@ Registering functions with a Wagtail hook follows the following pattern:
.. code-block:: python
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
hooks.register('hook', function)
Where ``'hook'`` is one of the following hook strings and ``function`` is a function you've defined to handle the hook.
.. _before_serve_page:
``before_serve_page``
.. versionadded:: 0.4
Called when Wagtail is about to serve a page. The callable passed into the hook will receive the page object, the request object, and the args and kwargs that will be passed to the page's ``serve()`` method. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``serve()`` on the page.
.. code-block:: python
from wagtail.wagtailcore import hooks
def block_googlebot(page, request, serve_args, serve_kwargs):
if request.META.get('HTTP_USER_AGENT') == 'GoogleBot':
return HttpResponse("<h1>bad googlebot no cookie</h1>")
hooks.register('before_serve_page', block_googlebot)
.. _construct_wagtail_edit_bird:
``construct_wagtail_edit_bird``
@ -393,7 +426,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
.. code-block:: python
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
class UserbarPuppyLinkItem(object):
def render(self, request):
@ -414,7 +447,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.utils.safestring import mark_safe
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
class WelcomePanel(object):
order = 50
@ -440,7 +473,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.http import HttpResponse
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
def do_after_page_create(request, page):
return HttpResponse("Congrats on making content!", content_type="text/plain")
@ -468,7 +501,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.http import HttpResponse
from django.conf.urls import url
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
def admin_view( request ):
return HttpResponse( \
@ -484,13 +517,13 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
.. _construct_main_menu:
``construct_main_menu``
Add, remove, or alter ``MenuItem`` objects from the Wagtail admin menu. The callable passed to this hook must take a ``request`` object and a list of ``menu_items``; it must return a list of menu items. New items can be constructed from the ``MenuItem`` class by passing in: a ``label`` which will be the text in the menu item, the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up), CSS class ``name`` applied to the wrapping ``<li>`` of the menu item as ``"menu-{name}"``, CSS ``classnames`` which are used to give the link an icon, and an ``order`` integer which determine's the item's place in the menu.
Add, remove, or alter ``MenuItem`` objects from the Wagtail admin menu. The callable passed to this hook must take a ``request`` object and a list of ``menu_items``; it must return a list of menu items. New items can be constructed from the ``MenuItem`` class by passing in: a ``label`` which will be the text in the menu item, the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up), CSS class ``name`` applied to the wrapping ``<li>`` of the menu item as ``"menu-{name}"``, CSS ``classnames`` which are used to give the link an icon, and an ``order`` integer which determine's the item's place in the menu.
.. code-block:: python
from django.core.urlresolvers import reverse
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
def construct_main_menu(request, menu_items):
@ -510,7 +543,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.utils.html import format_html, format_html_join
from django.conf import settings
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
def editor_js():
js_files = [
@ -538,7 +571,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from django.utils.html import format_html
from django.conf import settings
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
def editor_css():
return format_html('<link rel="stylesheet" href="' \
@ -549,7 +582,8 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
.. _construct_whitelister_element_rules:
``construct_whitelister_element_rules``
.. versionadded:: 0.4
.. versionadded:: 0.4
Customise the rules that define which HTML elements are allowed in rich text areas. By default only a limited set of HTML elements and attributes are whitelisted - all others are stripped out. The callables passed into this hook must return a dict, which maps element names to handler functions that will perform some kind of manipulation of the element. These handler functions receive the element as a `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_ Tag object.
The ``wagtail.wagtailcore.whitelist`` module provides a few helper functions to assist in defining these handlers: ``allow_without_attributes``, a handler which preserves the element but strips out all of its attributes, and ``attribute_rule`` which accepts a dict specifying how to handle each attribute, and returns a handler function. This dict will map attribute names to either True (indicating that the attribute should be kept), False (indicating that it should be dropped), or a callable (which takes the initial attribute value and returns either a final value for the attribute, or None to drop the attribute).
@ -558,7 +592,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
.. code-block:: python
from wagtail.wagtailadmin import hooks
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes
def whitelister_element_rules():
@ -577,6 +611,7 @@ On loading, Wagtail will search for any app with the file ``image_formats.py`` a
As an example, add a "thumbnail" format:
.. code-block:: python
# image_formats.py
from wagtail.wagtailimages.formats import Format, register_image_format
@ -611,4 +646,3 @@ Custom Choosers
Tests
-----

View file

@ -10,3 +10,4 @@ Wagtail allows you to manage all of your documents and images through their own
documents
images
snippets

View file

@ -1,6 +1,9 @@
Snippets
~~~~~~~~
.. note::
Documentation currently incomplete and in draft status
.. UNSURE HOW TO WRITE THIS AS THE ADVERT EXAMPLE IN WAGTAIL DEMO IS NOT A PARTICULARLY HELPFUL USE CASE.
When creating a page on a website, it is a common occurance to want to add in a piece of content that already exists on another page. An example of this would be a person's contact details, or an advert that you want to simply show on every page of your site, without having to manually apply it.

View file

@ -0,0 +1,119 @@
.. _frontend_cache_purging:
Frontend cache purging
======================
Many websites use a frontend cache such as Varnish, Squid or Cloudflare to gain extra performance. The downside of using a frontend cache though is that they don't respond well to updating content and will often keep an old version of a page cached after it has been updated.
This document describes how to configure Wagtail to purge old versions of pages from a frontend cache whenever a page gets updated.
Setting it up
~~~~~~~~~~~~~
Firstly, add ``"wagtail.contrib.wagtailfrontendcache"`` to your INSTALLED_APPS:
.. code-block:: python
INSTALLED_APPS = [
...
"wagtail.contrib.wagtailfrontendcache"
]
The ``wagtailfrontendcache`` module provides a set of signal handlers which will automatically purge the cache whenever a page is published or deleted. You should register these somewhere at the top of your ``urls.py`` file:
.. code-block:: python
# urls.py
from wagtail.contrib.wagtailfrontendcache.signal_handlers import register_signal_handlers
register_signal_handlers()
You then need to set the ``WAGTAILFRONTENDCACHE_LOCATION`` setting to the URL of your Varnish/Squid cache server. This must be a direct connection to the server and cannot go through another proxy. By default, this is set to ``http://127.0.0.1:8000`` which is very likely incorrect.
Finally, make sure you have configured your frontend cache to accept PURGE requests:
- `Varnish <https://www.varnish-cache.org/docs/3.0/tutorial/purging.html>`_
- `Squid <http://wiki.squid-cache.org/SquidFaq/OperatingSquid#How_can_I_purge_an_object_from_my_cache.3F>`_
Advanced useage
~~~~~~~~~~~~~~~
Purging more than one URL per page
----------------------------------
By default, Wagtail will only purge one URL per page. If your page has more than one URL to be purged, you will need to override the ``get_cached_paths`` method on your page type.
.. code-block:: python
class BlogIndexPage(Page):
def get_blog_items(self):
# This returns a Django paginator of blog items in this section
return Paginator(self.get_children().live().type(BlogPage), 10)
def get_cached_paths(self):
# Yield the main URL
yield '/'
# Yield one URL per page in the paginator to make sure all pages are purged
for page_number in range(1, self.get_blog_items().num_pages):
yield '/?page=' + str(page_number)
Purging index pages
-------------------
Another problem is pages that list other pages (such as a blog index) will not be purged when a blog entry gets added, changed or deleted. You may want to purge the blog index page so the updates are added into the listing quickly.
This can be solved by using the ``purge_page_from_cache`` utility function which can be found in the ``wagtail.contrib.wagtailfrontendcache.utils`` module.
Let's take the the above BlogIndexPage as an example. We need to register a signal handler to run when one of the BlogPages get updated/deleted. This signal handler should call the ``purge_page_from_cache`` function on all BlogIndexPages that contain the BlogPage being updated/deleted.
.. code-block:: python
# models.py
from django.db.models.signals import pre_delete
from wagtail.wagtailcore.signals import page_published
from wagtail.contrib.wagtailfrontendcache.utils import purge_page_from_cache
...
def blog_page_changed(blog_page):
# Find all the live BlogIndexPages that contain this blog_page
for blog_index in BlogIndexPage.objects.live():
if blog_page in blog_index.get_blog_items().object_list:
# Purge this blog index
purge_page_from_cache(blog_index)
@register(page_published, sender=BlogPage):
def blog_published_handler(instance):
blog_page_changed(instance)
@register(pre_delete, sender=BlogPage)
def blog_deleted_handler(instance):
blog_page_changed(instance)
Purging individual URLs
-----------------------
``wagtail.contrib.wagtailfrontendcache.utils`` provides another utils function called ``purge_url_from_cache``. As the name suggests, this purges an individual URL from the cache.
For example, this could be useful for purging a single page of blogs:
.. code-block:: python
from wagtail.contrib.wagtailfrontendcache.utils import purge_url_from_cache
# Purge the first page of the blog index
purge_url_from_cache(blog_index.url + '?page=1')

View file

@ -40,7 +40,7 @@ Once you've experimented with the demo app and are ready to build your pages via
On OS X
~~~~~~~
Install `pip <http://pip.readthedocs.org/en/latest/installing.html>`_ and `virtualenvwrapper <http://virtualenvwrapper.readthedocs.org/en/latest/>`_ if you don't have them already. Then, in your terminal::
Install `pip <http://pip.readthedocs.org/en/latest/installing.html>`__ and `virtualenvwrapper <http://virtualenvwrapper.readthedocs.org/en/latest/>`_ if you don't have them already. Then, in your terminal::
mkvirtualenv wagtaildemo
git clone https://github.com/torchbox/wagtaildemo.git
@ -75,10 +75,10 @@ Wagtail instance available as the basis for your new site:
./manage.py runserver 0.0.0.0:8000
- This will make the app accessible on the host machine as
`localhost:8111 <http://localhost:8111>`_ - you can access the Wagtail admin
interface at `localhost:8111/admin <http://localhost:8111/admin>`_. The codebase
is located on the host machine, exported to the VM as a shared folder; code
editing and Git operations will generally be done on the host.
`localhost:8111 <http://localhost:8111>`_ - you can access the Wagtail admin
interface at `localhost:8111/admin <http://localhost:8111/admin>`_. The codebase
is located on the host machine, exported to the VM as a shared folder; code
editing and Git operations will generally be done on the host.
Using Docker
~~~~~~~~~~~~
@ -101,7 +101,7 @@ use the following steps:
Required dependencies
=====================
- `pip <https://github.com/pypa/pip>`_
- `pip <https://github.com/pypa/pip>`__
- `libjpeg <http://ijg.org/>`_
- `libxml2 <http://xmlsoft.org/>`_
- `libxslt <http://xmlsoft.org/XSLT/>`_

View file

@ -3,7 +3,7 @@ Welcome to Wagtail's documentation
Wagtail is a modern, flexible CMS, built on Django.
It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
It supports Django 1.6.2+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4. Django 1.7 support is in progress pending further release candidate testing.
.. toctree::
:maxdepth: 3
@ -13,14 +13,19 @@ It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support
building_your_site/index
editing_api
snippets
wagtail_search
search/index
form_builder
model_recipes
advanced_topics
deploying
performance
private_pages
static_site_generation
frontend_cache_purging
sitemap_generation
management_commands
contributing
support
roadmap
editor_manual/index
releases/index

View file

@ -0,0 +1,69 @@
.. _management_commands:
Management commands
===================
.. _publish_scheduled_pages:
publish_scheduled_pages
-----------------------
:code:`./manage.py publish_scheduled_pages`
This command publishes or unpublishes pages that have had these actions scheduled by an editor. It is recommended to run this command once an hour.
.. _fixtree:
fixtree
-------
:code:`./manage.py fixtree`
This command scans for errors in your database and attempts to fix any issues it finds.
.. _move_pages:
move_pages
----------
:code:`manage.py move_pages from to`
This command moves a selection of pages from one section of the tree to another.
Options:
- **from**
This is the **id** of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children.
- **to**
This is the **id** of the page to move pages to.
.. _update_index:
update_index
------------
:code:`./manage.py update_index`
This command rebuilds the search index from scratch. It is only required when using Elasticsearch.
It is recommended to run this command once a week and at the following times:
- whenever any pages have been created through a script (after an import, for example)
- whenever any changes have been made to models or search configuration
The search may not return any results while this command is running, so avoid running it at peak times.
.. _search_garbage_collect:
search_garbage_collect
----------------------
:code:`./manage.py search_garbage_collect`
Wagtail keeps a log of search queries that are popular on your website. On high traffic websites, this log may get big and you may want to clean out old search queries. This command cleans out all search query logs that are more than one week old.

View file

@ -38,6 +38,8 @@ Consider this example from the Wagtail demo site's ``models.py``, which serves a
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.
.. _overriding_route_method:
Adding Endpoints with Custom route() Methods
--------------------------------------------
@ -58,8 +60,8 @@ Wagtail routes requests by iterating over the path components (separated with a
# find a matching child or 404
try:
subpage = self.get_children().get(slug=child_slug)
except Page.DoesNotExist:
raise Http404
except Page.DoesNotExist:
raise Http404
# delegate further routing
return subpage.specific.route(request, remaining_components)
@ -67,13 +69,16 @@ Wagtail routes requests by iterating over the path components (separated with a
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)
# Return a RouteResult that will tell Wagtail to call
# this page's serve() method
return RouteResult(self)
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.
``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 returning a ``RouteResult`` object or raising a 404 error.
The ``RouteResult`` object (defined in wagtail.wagtailcore.url_routing) encapsulates all the information Wagtail needs to call a page's ``serve()`` method and return a final response: this information consists of the page object, and any additional args / kwargs to be passed to ``serve()``.
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.
@ -82,6 +87,7 @@ First, ``models.py``:
.. code-block:: python
from django.shortcuts import render
from wagtail.wagtailcore.url_routing import RouteResult
...
@ -89,15 +95,20 @@ First, ``models.py``:
def route(self, request, path_components):
if path_components:
return render(request, self.template, {
'self': self,
'echo': ' '.join(path_components),
})
# tell Wagtail to call self.serve() with an additional 'path_components' kwarg
return RouteResult(self, kwargs={'path_components': path_components})
else:
if self.live:
return self.serve(request)
else:
raise Http404
# tell Wagtail to call self.serve() with no further args
return RouteResult(self)
else:
raise Http404
def serve(self, path_components=[]):
render(request, self.template, {
'self': self,
'echo': ' '.join(path_components),
})
Echoer.content_panels = [
FieldPanel('title', classname="full title"),
@ -107,7 +118,7 @@ First, ``models.py``:
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.
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.
Now, once creating a new ``Echoer`` page in the Wagtail admin titled "Echo Base," requests such as::
@ -117,6 +128,12 @@ Will return::
tauntaun kennel bed and breakfast
Be careful if you're introducing new required arguments to the ``serve()`` method - Wagtail still needs to be able to display a default view of the page for previewing and moderation, and by default will attempt to do this by calling ``serve()`` with a request object and no further arguments. If your ``serve()`` method does not accept that as a method signature, you will need to override the page's ``serve_preview()`` method to call ``serve()`` with suitable arguments:
.. code-block:: python
def serve_preview(self, request, mode_name):
return self.serve(request, color='purple')
.. _tagging:
@ -190,11 +207,11 @@ Load Alternate Templates by Overriding get_template()
Page Modes
----------
Preview Modes
-------------
get_page_modes
show_as_mode
preview_modes
serve_preview

View file

@ -1,7 +1,7 @@
Performance
===========
Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eeking out the most from your installation.
Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation.
Editor interface
~~~~~~~~~~~~~~~~
@ -41,4 +41,4 @@ Public users
Caching proxy
-------------
To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both `Varnish <http://www.varnish-cache.org/>`_ and `Squid <http://www.squid-cache.org/>`_ have been tested in production. Hosted proxies like `Cloudflare <https://www.cloudflare.com/>`_ should also work well.
To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both `Varnish <http://www.varnish-cache.org/>`_ and `Squid <http://www.squid-cache.org/>`_ have been tested in production. Hosted proxies like `Cloudflare <https://www.cloudflare.com/>`_ should also work well.

64
docs/private_pages.rst Normal file
View file

@ -0,0 +1,64 @@
.. _private_pages:
Private pages
=============
Users with publish permission on a page can set it to be private by clicking the 'Privacy' control in the top right corner of the page explorer or editing interface, and setting a password. Users visiting this page, or any of its subpages, will be prompted to enter a password before they can view the page.
Private pages work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default "password required" form is only a bare-bones HTML page, and site implementers may wish to replace this with a page customised to their site design.
Setting up a global "password required" page
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By setting ``PASSWORD_REQUIRED_TEMPLATE`` in your Django settings file, you can specify the path of a template which will be used for all "password required" forms on the site (except for page types that specifically override it - see below):
.. code-block:: python
PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html'
This template will receive the same set of context variables that the blocked page would pass to its own template via ``get_context()`` - including ``self`` to refer to the page object itself - plus the following additional variables (which override any of the page's own context variables of the same name):
- **form** - A Django form object for the password prompt; this will contain a field named ``password`` as its only visible field. A number of hidden fields may also be present, so the page must loop over ``form.hidden_fields`` if not using one of Django's rendering helpers such as ``form.as_p``.
- **action_url** - The URL that the password form should be submitted to, as a POST request.
A basic template suitable for use as ``PASSWORD_REQUIRED_TEMPLATE`` might look like this:
.. code-block:: django
<!DOCTYPE HTML>
<html>
<head>
<title>Password required</title>
</head>
<body>
<h1>Password required</h1>
<p>You need a password to access this page.</p>
<form action="{{ action_url }}" method="POST">
{% csrf_token %}
{{ form.non_field_errors }}
<div>
{{ form.password.errors }}
{{ form.password.label_tag }}
{{ form.password }}
</div>
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<input type="submit" value="Continue" />
</form>
</body>
</html>
Setting a "password required" page for a specific page type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The attribute ``password_required_template`` can be defined on a page model to use a custom template for the "password required" view, for that page type only. For example, if a site had a page type for displaying embedded videos along with a description, it might choose to use a custom "password required" template that displays the video description as usual, but shows the password form in place of the video embed.
.. code-block:: python
class VideoPage(Page):
...
password_required_template = 'video/password_required.html'

205
docs/releases/0.4.rst Normal file
View file

@ -0,0 +1,205 @@
=========================
Wagtail 0.4 release notes
=========================
.. contents::
:local:
:depth: 1
What's new
==========
Private Pages
~~~~~~~~~~~~~
Wagtail now supports password protecting pages on the frontend, allowing sections of your website to be made private.
:ref:`private_pages`
Python 3 support
~~~~~~~~~~~~~~~~
Wagtail now supports Python 3.2, 3.3 and 3.4.
Scheduled publishing
~~~~~~~~~~~~~~~~~~~~
Editors can now schedule pages to be published or unpublished at specified times.
A new management command has been added (:ref:`publish_scheduled_pages`) to publish pages that have been scheduled by an editor.
Search on QuerySet with Elasticsearch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's now possible to perform searches with Elasticsearch on ``PageQuerySet`` objects:
>>> from wagtail.wagtailcore.models import Page
>>> Page.objects.live().descendant_of(events_index).search("Hello")
[<Page: Event 1>, <Page: Event 2>]
Sitemap generation
~~~~~~~~~~~~~~~~~~
A new module has been added (``wagtail.contrib.wagtailsitemaps``) which produces XML sitemaps for Wagtail sites.
:ref:`sitemap_generation`
Front-end cache invalidation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A new module has been added (``wagtail.contrib.wagtailfrontendcache``) which invalidates pages in a frontend cache when they are updated or deleted in Wagtail.
:ref:`frontend_cache_purging`
Notification preferences
~~~~~~~~~~~~~~~~~~~~~~~~
Users can now decide which notifications they receive from Wagtail using a new "Notification preferences" section located in the account settings.
Minor features
~~~~~~~~~~~~~~
Core
----
* Any extra arguments given to ``Page.serve`` are now passed through to ``get_context`` and ``get_template``
* Added ``in_menu`` and ``not_in_menu`` methods to ``PageQuerySet``
* Added ``search`` method to ``PageQuerySet``
* Added ``get_next_siblings`` and ``get_prev_siblings`` to ``Page``
* Added ``page_published`` signal
* Added ``copy`` method to ``Page`` to allow copying of pages
* Added ``construct_whitelister_element_rules`` hook for customising the HTML whitelist used when saving ``RichText`` fields
* Support for setting a ``subpage_types`` property on ``Page`` models, to define which page types are allowed as subpages
Admin
-----
* Removed the "More" section from the menu
* Added pagination to page listings
* Added a new datetime picker widget
* Updated hallo.js to version 1.0.4
* Aesthetic improvements to preview experience
* Login screen redirects to dashboard if user is already logged in
* Snippets are now ordered alphabetically
* Added ``init_new_page`` signal
Search
------
* Added a new way to configure searchable/filterable fields on models
* Added ``get_indexed_objects`` allowing developers to customise which objects get added to the search index
* Major refactor of Elasticsearch backend
* Use ``match`` instead of ``query_string`` queries
* Fields are now indexed in Elasticsearch with their correct type
* Filter fields are no longer included in '_all'
* Fields with partial matching are now indexed together into '_partials'
Images
------
* Added ``original`` as a resizing rule supported by the ``{% image %}`` tag
* ``image`` tag now accepts extra keyword arguments to be output as attributes on the img tag
* Added an ``attrs`` property to image rendition objects to output ``src``, ``width``, ``height`` and ``alt`` attributes all in one go
Other
-----
* Added styleguide, for Wagtail developers
Bug fixes
~~~~~~~~~
* Animated GIFs are now coalesced before resizing
* The Wand backend clones images before modifying them
* The admin breadcrumb is now positioned correctly on mobile
* The page chooser breadcrumb now updates the chooser modal instead of linking to Explorer
* Embeds - fixed crash when no HTML field is sent back from the embed provider
* Multiple sites with same hostname but different ports are now allowed
* It is no longer possible to create multiple sites with ``is_default_site = True``
Backwards-incompatible changes
==============================
ElasticUtils replaced with elasticsearch-py
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are using the elasticsearch backend, you must install the ``elasticsearch`` module into your environment.
.. note::
If you are using an older version of Elasticsearch (< 1.0) you must install ``elasticsearch`` version 0.4.x.
Addition of ``expired`` column may break old data migrations involving pages
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The scheduled publishing mechanism adds an ``expired`` field to wagtailcore.Page, defaulting to False. Any application code working with Page objects should be unaffected, but any code that creates page records using direct SQL, or within existing South migrations using South's frozen ORM, will fail as this code will be unaware of the ``expired`` database column. To fix a South migration that fails in this way, add the following line to the ``'wagtailcore.page'`` entry at the bottom of the migration file:
.. code-block:: python
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
Deprecated features
===================
Template tag libraries renamed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following template tag libraries have been renamed:
* ``pageurl`` => ``wagtailcore_tags``
* ``rich_text`` => ``wagtailcore_tags``
* ``embed_filters`` => ``wagtailembeds_tags``
* ``image_tags`` => ``wagtailimages_tags``
The old names will continue to work, but output a ``DeprecationWarning`` - you are advised to update any ``{% load %}`` tags in your templates to refer to the new names.
New search field configuration format
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``indexed_fields`` is now deprecated and has been replaced by a new search field configuration format called ``search_fields``. See :ref:`wagtailsearch_for_python_developers` for how to define a ``search_fields`` property on your models.
``Page.route`` method should now return a ``RouteResult``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Previously, the ``route`` method called ``serve`` and returned an ``HttpResponse`` object. This has now been split up so ``serve`` is called separately and ``route`` must now return a RouteResult object.
If you are overriding ``Page.route`` on any of your page models, you will need to update the method to return a ``RouteResult`` object. The old method of returning an ``HttpResponse`` will continue to work, but this will throw a ``DeprecationWarning`` and bypass the ``before_serve_page`` hook, which means in particular that :ref:`private_pages` will not work on those page types. See :ref:`overriding_route_method`.
Wagtailadmins ``hooks`` module has moved to wagtailcore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you use any ``wagtail_hooks.py`` files in your project, you may have an import like: ``from wagtail.wagtailadmin import hooks``
Change this to: ``from wagtail.wagtailcore import hooks``
Miscellaneous
~~~~~~~~~~~~~
* ``Page.show_as_mode`` replaced with ``Page.serve_preview``
* ``Page.get_page_modes`` method replaced with ``Page.preview_modes`` property
* ``Page.get_other_siblings`` replaced with ``Page.get_siblings(inclusive=False)``

27
docs/releases/0.5.rst Normal file
View file

@ -0,0 +1,27 @@
==========================================
Wagtail 0.5 release notes - IN DEVELOPMENT
==========================================
.. contents::
:local:
:depth: 1
Whats new
=========
Minor features
~~~~~~~~~~~~~~
Bug fixes
~~~~~~~~~
Backwards incompatible changes
==============================
Deprecated features
===================

8
docs/releases/index.rst Normal file
View file

@ -0,0 +1,8 @@
Release notes
=============
.. toctree::
:maxdepth: 1
0.5
0.4

66
docs/search/backends.rst Normal file
View file

@ -0,0 +1,66 @@
.. _wagtailsearch_backends:
========
Backends
========
Wagtail can degrade to a database-backed text search, but we strongly recommend `Elasticsearch`_.
.. _Elasticsearch: http://www.elasticsearch.org/
.. _wagtailsearch_backends_database:
Database Backend
================
The default DB search backend uses Django's ``__icontains`` filter.
Elasticsearch Backend
=====================
Prerequisites are the Elasticsearch service itself and, via pip, the `elasticsearch-py`_ package:
.. code-block:: guess
pip install elasticsearch
.. note::
If you are using Elasticsearch < 1.0, install elasticsearch-py version 0.4.5: ```pip install elasticsearch==0.4.5```
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 elasticsearch-py. 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
- Configure ``URLS`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS``
- Run ``./manage.py update_index``
.. _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
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

@ -0,0 +1,41 @@
.. _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 in addition to results from 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 %}

View file

@ -0,0 +1,152 @@
.. _wagtailsearch_for_python_developers:
=====================
For Python developers
=====================
Basic usage
===========
All searches are performed on Django QuerySets. Wagtail provides a ``search`` method on the queryset for all page models:
.. code-block:: python
# Search future EventPages
>>> from wagtail.wagtailcore.models import EventPage
>>> EventPage.objects.filter(date__gt=timezone.now()).search("Hello world!")
All methods of ``PageQuerySet`` are supported by wagtailsearch:
.. code-block:: python
# Search all live EventPages that are under the events index
>>> EventPage.objects.live().descendant_of(events_index).search("Event")
[<EventPage: Event 1>, <EventPage: Event 2>]
Indexing extra fields
=====================
Fields need to be explicitly added to the search configuration in order for you to be able to search/filter on them.
You can add new fields to the search index by overriding the ``search_fields`` property and appending a list of extra ``SearchField``/``FilterField`` objects to it.
The default value of ``search_fields`` (as set in ``Page``) indexes the ``title`` field as a ``SearchField`` and some other generally useful fields as ``FilterField`` rules.
Quick example
-------------
This creates an ``EventPage`` model with two fields ``description`` and ``date``. ``description`` is indexed as a ``SearchField`` and ``date`` is indexed as a ``FilterField``
.. code-block:: python
from wagtail.wagtailsearch import indexed
class EventPage(Page):
description = models.TextField()
date = models.DateField()
search_fields = Page.search_fields + ( # Inherit search_fields from Page
indexed.SearchField('description'),
indexed.FilterField('date'),
)
# Get future events which contain the string "Christmas" in the title or description
>>> EventPage.objects.filter(date__gt=timezone.now()).search("Christmas")
``indexed.SearchField``
-----------------------
These are added to the search index and are used for performing full-text searches on your models. These would usually be text fields.
Options
```````
- **partial_match** (boolean) - Setting this to true allows results to be matched on parts of words. For example, this is set on the title field by default so a page titled "Hello World!" will be found if the user only types "Hel" into the search box.
- **boost** (number) - This allows you to set fields as being more important than others. Setting this to a high number on a field will make pages with matches in that field to be ranked higher. By default, this is set to 100 on the title field and 1 on all other fields.
- **es_extra** (dict) - This field is to allow the developer to set or override any setting on the field in the ElasticSearch mapping. Use this if you want to make use of any ElasticSearch features that are not yet supported in Wagtail.
``indexed.FilterField``
-----------------------
These are added to the search index but are not used for full-text searches. Instead, they allow you to run filters on your search results.
Indexing callables and other attributes
---------------------------------------
.. note::
This is not supported in the :ref:`wagtailsearch_backends_database`
Search/filter fields do not need to be Django fields, they could be any method or attribute on your class.
One use for this is indexing ``get_*_display`` methods Django creates automatically for fields with choices.
.. code-block:: python
from wagtail.wagtailsearch import indexed
class EventPage(Page):
IS_PRIVATE_CHOICES = (
(False, "Public"),
(True, "Private"),
)
is_private = models.BooleanField(choices=IS_PRIVATE_CHOICES)
search_fields = Page.search_fields + (
# Index the human-readable string for searching
indexed.SearchField('get_is_private_display'),
# Index the boolean value for filtering
indexed.FilterField('is_private'),
)
Indexing non-page models
========================
Any Django model can be indexed and searched.
To do this, inherit from ``indexed.Indexed`` and add some ``search_fields`` to the model.
.. code-block:: python
from wagtail.wagtailsearch import indexed
class Book(models.Model, indexed.Indexed):
title = models.CharField(max_length=255)
genre = models.CharField(max_length=255, choices=GENRE_CHOICES)
author = models.ForeignKey(Author)
published_date = models.DateTimeField()
search_fields = (
indexed.SearchField('title', partial_match=True, boost=10),
indexed.SearchField('get_genre_display'),
indexed.FilterField('genre'),
indexed.FilterField('author'),
indexed.FilterField('published_date'),
)
# As this model doesn't have a search method in its QuerySet, we have to call search directly on the backend
>>> from wagtail.wagtailsearch.backends import get_search_backend
>>> s = get_search_backend()
# Run a search for a book by Roald Dahl
>>> roald_dahl = Author.objects.get(name="Roald Dahl")
>>> s.search("chocolate factory", Book.objects.filter(author=roald_dahl))
[<Book: Charlie and the chocolate factory>]

View file

@ -1,10 +1,10 @@
.. _search:
.. _wagtailsearch_frontend_views:
Search
======
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.
Frontend views
==============
Default Page Search
-------------------
@ -71,43 +71,6 @@ The search view provides a context with a few useful variables.
``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
--------------------------------------
@ -142,30 +105,30 @@ Finally, we'll use JQuery to make the asynchronous requests and handle the inter
// when there's something in the input box, make the query
searchBox.on('input', function() {
if( searchBox.val() == ''){
resultsBox.html('');
return;
resultsBox.html('');
return;
}
// make the request to the Wagtail JSON search view
$.ajax({
url: wagtailJSONSearchURL + "?q=" + searchBox.val(),
dataType: "json"
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);
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);
console.log(data);
});
});
@ -178,12 +141,12 @@ Results are returned as a JSON object with this structure:
{
[
{
title: "Lumpy Space Princess",
url: "/oh-my-glob/"
title: "Lumpy Space Princess",
url: "/oh-my-glob/"
},
{
title: "Lumpy Space",
url: "/no-smooth-posers/"
title: "Lumpy Space",
url: "/no-smooth-posers/"
},
...
]
@ -199,67 +162,8 @@ The AJAX interface uses the same view as the normal HTML search, ``wagtailsearch
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:
Custom Search Views
-------------------
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 `elasticsearch-py`_ package:
.. code-block:: guess
pip install elasticsearch
.. note::
If you are using Elasticsearch < 1.0, install elasticsearch-py version 0.4.5: ```pip install elasticsearch==0.4.5```
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 elasticsearch-py. 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
- Configure ``URLS`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS``
- Run ``./manage.py update_index``
.. _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
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``.
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.

17
docs/search/index.rst Normal file
View file

@ -0,0 +1,17 @@
.. _wagtailsearch:
Search
======
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.
.. toctree::
:maxdepth: 2
for_python_developers
frontend_views
editors_picks
backends

View file

@ -3,7 +3,7 @@
Configuring Django for Wagtail
==============================
To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see `Writing your first Django app`_. Your project directory will look like the following::
To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see `Writing your first Django app <https://docs.djangoproject.com/en/dev/intro/tutorial01/>`_. Your project directory will look like the following::
myproject/
myproject/
@ -19,13 +19,7 @@ To install Wagtail completely from scratch, create a new Django project and an a
views.py
manage.py
From your app directory, you can safely remove ``admin.py`` and ``views.py``, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to ``settings.py`` and urlconfs to ``urls.py``. For a more complete view of what's defined in these files, see `Django Settings`_ and `Django URL Dispatcher`_.
.. _Writing your first Django app: https://docs.djangoproject.com/en/dev/intro/tutorial01/
.. _Django Settings: https://docs.djangoproject.com/en/dev/topics/settings/
.. _Django URL Dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/
From your app directory, you can safely remove ``admin.py`` and ``views.py``, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to ``settings.py`` and urlconfs to ``urls.py``. For a more complete view of what's defined in these files, see `Django Settings <https://docs.djangoproject.com/en/dev/topics/settings/>`__ and `Django URL Dispatcher <https://docs.djangoproject.com/en/dev/topics/http/urls/>`_.
What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see :ref:`complete_example_config`.
@ -246,6 +240,16 @@ Email Notifications
Wagtail sends email notifications when content is submitted for moderation, and when the content is accepted or rejected. This setting lets you pick which email address these automatic notifications will come from. If omitted, Django will fall back to using the ``DEFAULT_FROM_EMAIL`` variable if set, and ``webmaster@localhost`` if not.
Private Pages
-------------
.. code-block:: python
PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html'
This is the path to the Django template which will be used to display the "password required" form when a user accesses a private page. For more details, see the :ref:`private_pages` documentation.
Other Django Settings Used by Wagtail
-------------------------------------
@ -266,9 +270,7 @@ Other Django Settings Used by Wagtail
TEMPLATE_CONTEXT_PROCESSORS
USE_I18N
For information on what these settings do, see `Django Settings`_.
.. _Django Settings: https://docs.djangoproject.com/en/dev/ref/settings/
For information on what these settings do, see `Django Settings <https://docs.djangoproject.com/en/dev/ref/settings/>`__.
Search Signal Handlers

View file

@ -0,0 +1,60 @@
.. _sitemap_generation:
Sitemap generation
==================
This document describes how to create XML sitemaps for your Wagtail website using the ``wagtail.contrib.wagtailsitemaps`` module.
Basic configuration
~~~~~~~~~~~~~~~~~~~
You firstly need to add ``"wagtail.contrib.wagtailsitemaps"`` to INSTALLED_APPS in your Django settings file:
.. code-block:: python
INSTALLED_APPS = [
...
"wagtail.contrib.wagtailsitemaps",
]
Then, in urls.py, you need to add a link to the ``wagtail.contrib.wagtailsitemaps.views.sitemap`` view which generates the sitemap:
.. code-block:: python
from wagtail.contrib.wagtailsitemaps.views import sitemap
urlpatterns = patterns('',
...
url('^sitemap\.xml$', sitemap),
)
You should now be able to browse to "/sitemap.xml" and see the sitemap working. By default, all published pages in your website will be added to the site map.
Customising
~~~~~~~~~~~
URLs
----
The Page class defines a ``get_sitemap_urls`` method which you can override to customise sitemaps per page instance. This method must return a list of dictionaries, one dictionary per URL entry in the sitemap. You can exclude pages from the sitemap by returning an empty list.
Each dictionary can contain the following:
- **location** (required) - This is the full URL path to add into the sitemap.
- **lastmod** - A python date or datetime set to when the page was last modified.
- **changefreq**
- **priority**
You can add more but you will need to override the ``wagtailsitemaps/sitemap.xml`` template in order for them to be displayed in the sitemap.
Cache
-----
By default, sitemaps are cached for 100 minutes. You can change this by setting ``WAGTAILSITEMAPS_CACHE_TIMEOUT`` in your Django settings to the number of seconds you would like the cache to last for.

View file

@ -25,8 +25,19 @@ Then add ``django_medusa`` and ``wagtail.contrib.wagtailmedusa`` to ``INSTALLED_
]
Rendering
~~~~~~~~~
To render a site, run ``./manage.py staticsitegen``. This will render the entire website and place the HTML in a folder called 'medusa_output'. The static and media folders need to be copied into this folder manually after the rendering is complete. This feature inherits django-medusa's ability to render your static site to Amazon S3 or Google App Engine; see the `medusa docs <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``.
Advanced topics
~~~~~~~~~~~~~~~
Replacing GET parameters with custom routing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--------------------------------------------
Pages which require GET parameters (e.g. for pagination) don't generate suitable filenames for generated HTML files so they need to be changed to use custom routing instead.
@ -34,6 +45,8 @@ For example, let's say we have a Blog Index which uses pagination. We can overri
.. code:: python
from wagtail.wagtailcore.url_routing import RouteResult
class BlogIndex(Page):
...
@ -43,7 +56,7 @@ For example, let's say we have a Blog Index which uses pagination. We can overri
def route(self, request, path_components):
if self.live and len(path_components) == 2 and path_components[0] == 'page':
try:
return self.serve(request, page=int(path_components[1]))
return RouteResult(self, kwargs={'page': int(path_components[1])})
except (TypeError, ValueError):
pass
@ -51,7 +64,7 @@ For example, let's say we have a Blog Index which uses pagination. We can overri
Rendering pages which use custom routing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
----------------------------------------
For page types that override the ``route`` method, we need to let django medusa know which URLs it responds on. This is done by overriding the ``get_static_site_paths`` method to make it yield one string per URL path.
@ -72,12 +85,4 @@ For example, the BlogIndex above would need to yield one URL for each page of re
yield path
Rendering
~~~~~~~~~
To render a site, run ``./manage.py staticsitegen``. This will render the entire website and place the HTML in a folder called 'medusa_output'. The static and media folders need to be copied into this folder manually after the rendering is complete. This feature inherits django-medusa's ability to render your static site to Amazon S3 or Google App Engine; see the `medusa docs <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``.
.. _django medusa: https://github.com/mtigas/django-medusa

View file

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

View file

@ -1,5 +1,8 @@
#!/usr/bin/env python
import sys
try:
from setuptools import setup, find_packages
except ImportError:
@ -16,9 +19,35 @@ except ImportError:
pass
PY3 = sys.version_info[0] == 3
install_requires = [
"Django>=1.6.2,<1.7",
"South>=0.8.4",
"django-compressor>=1.4",
"django-libsass>=0.2",
"django-modelcluster>=0.3",
"django-taggit==0.11.2",
"django-treebeard==2.0",
"Pillow>=2.3.0",
"beautifulsoup4>=4.3.2",
"lxml>=3.3.0",
"Unidecode>=0.04.14",
"six==1.7.3",
'requests==2.3.0',
]
if not PY3:
install_requires += [
"unicodecsv>=0.9.4"
]
setup(
name='wagtail',
version='0.3.1',
version='0.4',
description='A Django content management system focused on flexibility and user experience',
author='Matthew Westcott',
author_email='matthew.westcott@torchbox.com',
@ -37,23 +66,13 @@ setup(
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Framework :: Django',
'Topic :: Internet :: WWW/HTTP :: Site Management',
],
install_requires=[
"Django>=1.6.2,<1.7",
"South>=0.8.4",
"django-compressor>=1.3",
"django-libsass>=0.1",
"django-modelcluster>=0.1",
"django-taggit==0.11.2",
"django-treebeard==2.0",
"Pillow>=2.3.0",
"beautifulsoup4>=4.3.2",
"lxml>=3.3.0",
'unicodecsv>=0.9.4',
'Unidecode>=0.04.14',
"BeautifulSoup==3.2.1", # django-compressor gets confused if we have lxml but not BS3 installed
],
install_requires=install_requires,
zip_safe=False,
)

64
tox.ini
View file

@ -1,15 +1,18 @@
[deps]
dj16=
Django>=1.6,<1.7
pyelasticsearch==0.6.1
elasticutils==0.8.2
elasticsearch==1.1.0
mock==1.0.1
[tox]
envlist =
py26-dj16-postgres,
py26-dj16-sqlite,
py27-dj16-postgres,
py27-dj16-sqlite
py27-dj16-sqlite,
py32-dj16-postgres,
py33-dj16-postgres,
py34-dj16-postgres
# mysql not currently supported
# (wagtail.wagtailimages.tests.TestImageEditView currently fails with a
@ -17,6 +20,12 @@ envlist =
# py26-dj16-mysql
# py27-dj16-mysql
# South fails with sqlite on python3, because it tries to use DryRunMigrator which uses iteritems
# py32-dj16-sqlite,
# py33-dj16-sqlite,
# py34-dj16-sqlite
[testenv]
commands=./runtests.py
@ -24,7 +33,7 @@ commands=./runtests.py
basepython=python2.6
deps =
{[deps]dj16}
psycopg2==2.5.2
psycopg2==2.5.3
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
@ -48,7 +57,7 @@ setenv =
basepython=python2.7
deps =
{[deps]dj16}
psycopg2==2.5.2
psycopg2==2.5.3
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
@ -67,3 +76,48 @@ deps =
setenv =
DATABASE_ENGINE=django.db.backends.mysql
DATABASE_USER=wagtail
[testenv:py32-dj16-postgres]
basepython=python3.2
deps =
{[deps]dj16}
psycopg2==2.5.3
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
[testenv:py32-dj16-sqlite]
basepython=python3.2
deps =
{[deps]dj16}
setenv =
DATABASE_ENGINE=django.db.backends.sqlite3
[testenv:py33-dj16-postgres]
basepython=python3.3
deps =
{[deps]dj16}
psycopg2==2.5.3
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
[testenv:py33-dj16-sqlite]
basepython=python3.3
deps =
{[deps]dj16}
setenv =
DATABASE_ENGINE=django.db.backends.sqlite3
[testenv:py34-dj16-postgres]
basepython=python3.4
deps =
{[deps]dj16}
psycopg2==2.5.3
setenv =
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2
[testenv:py34-dj16-sqlite]
basepython=python3.4
deps =
{[deps]dj16}
setenv =
DATABASE_ENGINE=django.db.backends.sqlite3

View file

@ -0,0 +1,25 @@
from django.db import models
from django.db.models.signals import post_delete
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.signals import page_published
from wagtail.contrib.wagtailfrontendcache.utils import purge_page_from_cache
def page_published_signal_handler(instance, **kwargs):
purge_page_from_cache(instance)
def post_delete_signal_handler(instance, **kwargs):
purge_page_from_cache(instance)
def register_signal_handlers():
# Get list of models that are page types
indexed_models = [model for model in models.get_models() if issubclass(model, Page)]
# Loop through list and register signal handlers for each one
for model in indexed_models:
page_published.connect(page_published_signal_handler, sender=model)
post_delete.connect(post_delete_signal_handler, sender=model)

View file

@ -0,0 +1,40 @@
import requests
from requests.adapters import HTTPAdapter
from django.conf import settings
class CustomHTTPAdapter(HTTPAdapter):
"""
Requests will always send requests to whatever server is in the netloc
part of the URL. This is a problem with purging the cache as this netloc
may point to a different server (such as an nginx instance running in
front of the cache).
This class allows us to send a purge request directly to the cache server
with the host header still set correctly. It does this by changing the "url"
parameter of get_connection to always point to the cache server. Requests
will then use this connection to purge the page.
"""
def __init__(self, cache_url):
self.cache_url = cache_url
super(CustomHTTPAdapter, self).__init__()
def get_connection(self, url, proxies=None):
return super(CustomHTTPAdapter, self).get_connection(self.cache_url, proxies)
def purge_url_from_cache(url):
# Get session
cache_server_url = getattr(settings, 'WAGTAILFRONTENDCACHE_LOCATION', 'http://127.0.0.1:8000/')
session = requests.Session()
session.mount('http://', CustomHTTPAdapter(cache_server_url))
# Send purge request to cache
session.request('PURGE', url)
def purge_page_from_cache(page):
# Purge cached paths from cache
for path in page.specific.get_cached_paths():
purge_url_from_cache(page.full_url + path[1:])

View file

@ -0,0 +1,21 @@
from django.template.loader import render_to_string
class Sitemap(object):
template = 'wagtailsitemaps/sitemap.xml'
def __init__(self, site):
self.site = site
def get_pages(self):
return self.site.root_page.get_descendants(inclusive=True).live().public().order_by('path')
def get_urls(self):
for page in self.get_pages():
for url in page.get_sitemap_urls():
yield url
def render(self):
return render_to_string(self.template, {
'urlset': self.get_urls()
})

View file

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

View file

@ -0,0 +1,94 @@
from django.test import TestCase
from django.core.cache import cache
from wagtail.wagtailcore.models import Page, PageViewRestriction, Site
from wagtail.tests.models import SimplePage
from .sitemap_generator import Sitemap
class TestSitemapGenerator(TestCase):
def setUp(self):
self.home_page = Page.objects.get(id=2)
self.child_page = self.home_page.add_child(instance=SimplePage(
title="Hello world!",
slug='hello-world',
live=True,
))
self.unpublished_child_page = self.home_page.add_child(instance=SimplePage(
title="Unpublished",
slug='unpublished',
live=False,
))
self.protected_child_page = self.home_page.add_child(instance=SimplePage(
title="Protected",
slug='protected',
live=True,
))
PageViewRestriction.objects.create(page=self.protected_child_page, password='hello')
self.site = Site.objects.get(is_default_site=True)
def test_get_pages(self):
sitemap = Sitemap(self.site)
pages = sitemap.get_pages()
self.assertIn(self.child_page.page_ptr, pages)
self.assertNotIn(self.unpublished_child_page.page_ptr, pages)
self.assertNotIn(self.protected_child_page.page_ptr, pages)
def test_get_urls(self):
sitemap = Sitemap(self.site)
urls = [url['location'] for url in sitemap.get_urls()]
self.assertIn('http://localhost/', urls) # Homepage
self.assertIn('http://localhost/hello-world/', urls) # Child page
def test_render(self):
sitemap = Sitemap(self.site)
xml = sitemap.render()
# Check that a URL has made it into the xml
self.assertIn('http://localhost/hello-world/', xml)
# Make sure the unpublished page didn't make it into the xml
self.assertNotIn('http://localhost/unpublished/', xml)
# Make sure the protected page didn't make it into the xml
self.assertNotIn('http://localhost/protected/', xml)
class TestSitemapView(TestCase):
def test_sitemap_view(self):
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailsitemaps/sitemap.xml')
self.assertEqual(response['Content-Type'], 'text/xml; charset=utf-8')
def test_sitemap_view_cache(self):
cache_key = 'wagtail-sitemap:%d' % Site.objects.get(is_default_site=True).id
# Check that the key is not in the cache
self.assertFalse(cache.has_key(cache_key))
# Hit the view
first_response = self.client.get('/sitemap.xml')
self.assertEqual(first_response.status_code, 200)
self.assertTemplateUsed(first_response, 'wagtailsitemaps/sitemap.xml')
# Check that the key is in the cache
self.assertTrue(cache.has_key(cache_key))
# Hit the view again. Should come from the cache this time
second_response = self.client.get('/sitemap.xml')
self.assertEqual(second_response.status_code, 200)
self.assertTemplateNotUsed(second_response, 'wagtailsitemaps/sitemap.xml') # Sitemap should not be re rendered
# Check that the content is the same
self.assertEqual(first_response.content, second_response.content)

View file

@ -0,0 +1,23 @@
from django.http import HttpResponse
from django.core.cache import cache
from django.conf import settings
from .sitemap_generator import Sitemap
def sitemap(request):
cache_key = 'wagtail-sitemap:' + str(request.site.id)
sitemap_xml = cache.get(cache_key)
if not sitemap_xml:
# Rerender sitemap
sitemap = Sitemap(request.site)
sitemap_xml = sitemap.render()
cache.set(cache_key, sitemap_xml, getattr(settings, 'WAGTAILSITEMAPS_CACHE_TIMEOUT', 6000))
# Build response
response = HttpResponse(sitemap_xml)
response['Content-Type'] = "text/xml; charset=utf-8"
return response

View file

@ -42,9 +42,10 @@
<li class="color-teal">color-teal</li>
<li class="color-teal-darker">color-teal-darker</li>
<li class="color-teal-dark">color-teal-dark</li>
<li class="color-red">color-red</li>
<li class="color-orange">color-orange</li>
<li class="color-green">color-green</li>
</ul>
<ul>
<li class="color-salmon">color-salmon</li>
<li class="color-salmon-light">color-salmon-light</li>
</ul>
<ul>
<li class="color-grey-1">color-grey-1</li>
@ -54,6 +55,12 @@
<li class="color-grey-4">color-grey-4</li>
<li class="color-grey-5">color-grey-5</li>
</ul>
<ul>
<li class="color-red">color-red</li>
<li class="color-orange">color-orange</li>
<li class="color-green">color-green</li>
</ul>
</section>
<section id="typography">
@ -407,6 +414,7 @@
<li class="icon icon-warning">warning</li>
<li class="icon icon-success">success</li>
<li class="icon icon-date">date</li>
<li class="icon icon-time">time</li>
<li class="icon icon-form">form</li>
</ul>

View file

@ -1,16 +1,10 @@
from django import forms
from django.db import models
from django.shortcuts import render
from django.utils.translation import ugettext as _
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailcore.fields import RichTextField
CHOICES = (
@ -23,6 +17,7 @@ class ExampleForm(forms.Form):
url = forms.URLField(required=True)
email = forms.EmailField(max_length=254)
date = forms.DateField()
time = forms.TimeField()
select = forms.ChoiceField(choices=CHOICES)
boolean = forms.BooleanField(required=False)

View file

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

View file

@ -23,7 +23,7 @@
"model": "wagtailcore.page",
"fields": {
"title": "Welcome to the Wagtail test site!",
"numchild": 3,
"numchild": 5,
"show_in_menus": false,
"live": true,
"depth": 2,
@ -85,6 +85,17 @@
}
},
{
"pk": 1,
"model": "tests.eventpagespeaker",
"fields": {
"page": 4,
"first_name": "Santa",
"last_name": "Claus",
"sort_order": 0
}
},
{
"pk": 5,
"model": "wagtailcore.page",
@ -246,6 +257,79 @@
}
},
{
"pk": 10,
"model": "wagtailcore.page",
"fields": {
"title": "Old style route method",
"numchild": 0,
"show_in_menus": true,
"live": true,
"depth": 3,
"content_type": ["tests", "pagewitholdstyleroutemethod"],
"path": "000100010004",
"url_path": "/home/old-style-route/",
"slug": "old-style-route"
}
},
{
"pk": 10,
"model": "tests.pagewitholdstyleroutemethod",
"fields": {
"content": "<p>Test with old style route method</p>"
}
},
{
"pk": 11,
"model": "wagtailcore.page",
"fields": {
"title": "Secret plans",
"numchild": 1,
"show_in_menus": true,
"live": true,
"depth": 3,
"content_type": ["tests", "simplepage"],
"path": "000100010005",
"url_path": "/home/secret-plans/",
"slug": "secret-plans"
}
},
{
"pk": 11,
"model": "tests.simplepage",
"fields": {
"content": "<p>muahahahaha</p>"
}
},
{
"pk": 12,
"model": "wagtailcore.page",
"fields": {
"title": "Steal underpants",
"numchild": 0,
"show_in_menus": true,
"live": true,
"depth": 4,
"content_type": ["tests", "eventpage"],
"path": "0001000100050001",
"url_path": "/home/secret-plans/steal-underpants/",
"slug": "steal-underpants"
}
},
{
"pk": 12,
"model": "tests.eventpage",
"fields": {
"date_from": "2015-07-04",
"audience": "private",
"location": "Marks and Spencer",
"body": "<p>meet in the menswear department at noon</p>",
"cost": "free"
}
},
{
"pk": 1,
"model": "wagtailcore.site",
@ -478,6 +562,14 @@
"submit_time": "2014-01-01T12:00:00.000Z"
}
},
{
"pk": 1,
"model": "tests.advert",
"fields": {
"text": "test_advert",
"url": "http://www.example.com"
}
},
{
"pk": 1,
"model": "wagtaildocs.Document",
@ -495,5 +587,14 @@
"width": 0,
"height": 0
}
},
{
"pk": 1,
"model": "wagtailcore.pageviewrestriction",
"fields": {
"page": 11,
"password": "swordfish"
}
}
]

View file

@ -1,6 +1,9 @@
from django.db import models
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.encoding import python_2_unicode_compatible
from modelcluster.fields import ParentalKey
from wagtail.wagtailcore.models import Page, Orderable
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel, InlinePanel, PageChooserPanel
@ -8,6 +11,7 @@ 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
from wagtail.wagtailsearch import indexed
EVENT_AUDIENCE_CHOICES = (
@ -103,6 +107,19 @@ class SimplePage(Page):
content = models.TextField()
class PageWithOldStyleRouteMethod(Page):
"""
Prior to Wagtail 0.4, the route() method on Page returned an HttpResponse
rather than a Page instance. As subclasses of Page may override route,
we need to continue accepting this convention (albeit as a deprecated API).
"""
content = models.TextField()
template = 'tests/simple_page.html'
def route(self, request, path_components):
return self.serve(request)
# Event page
class EventPageCarouselItem(Orderable, CarouselItem):
@ -163,6 +180,8 @@ class EventPage(Page):
indexed_fields = ('get_audience_display', 'location', 'body')
search_name = "Event"
password_required_template = 'tests/event_page_password_required.html'
EventPage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('date_from'),
@ -259,6 +278,7 @@ FormPage.content_panels = [
# Snippets
@python_2_unicode_compatible
class Advert(models.Model):
url = models.URLField(null=True, blank=True)
text = models.CharField(max_length=255)
@ -268,7 +288,7 @@ class Advert(models.Model):
FieldPanel('text'),
]
def __unicode__(self):
def __str__(self):
return self.text
@ -281,18 +301,20 @@ register_snippet(Advert)
# to ensure specific [in]correct register ordering
# AlphaSnippet is registered during TestSnippetOrdering
@python_2_unicode_compatible
class AlphaSnippet(models.Model):
text = models.CharField(max_length=255)
def __unicode__(self):
def __str__(self):
return self.text
# ZuluSnippet is registered during TestSnippetOrdering
@python_2_unicode_compatible
class ZuluSnippet(models.Model):
text = models.CharField(max_length=255)
def __unicode__(self):
def __str__(self):
return self.text
@ -310,3 +332,60 @@ class BusinessSubIndex(Page):
class BusinessChild(Page):
subpage_types = []
class SearchTest(models.Model, indexed.Indexed):
title = models.CharField(max_length=255)
content = models.TextField()
live = models.BooleanField(default=False)
published_date = models.DateField(null=True)
search_fields = [
indexed.SearchField('title', partial_match=True),
indexed.SearchField('content'),
indexed.SearchField('callable_indexed_field'),
indexed.FilterField('title'),
indexed.FilterField('live'),
indexed.FilterField('published_date'),
]
def callable_indexed_field(self):
return "Callable"
class SearchTestChild(SearchTest):
subtitle = models.CharField(max_length=255, null=True, blank=True)
extra_content = models.TextField()
search_fields = SearchTest.search_fields + [
indexed.SearchField('subtitle', partial_match=True),
indexed.SearchField('extra_content'),
]
class SearchTestOldConfig(models.Model, indexed.Indexed):
"""
This tests that the Indexed class can correctly handle models that
use the old "indexed_fields" configuration format.
"""
indexed_fields = {
# A search field with predictive search and boosting
'title': {
'type': 'string',
'analyzer': 'edgengram_analyzer',
'boost': 100,
},
# A filter field
'live': {
'type': 'boolean',
'index': 'not_analyzed',
},
}
class SearchTestOldConfigList(models.Model, indexed.Indexed):
"""
This tests that the Indexed class can correctly handle models that
use the old "indexed_fields" configuration format using a list.
"""
indexed_fields = ['title', 'content']

View file

@ -28,9 +28,8 @@ STATICFILES_FINDERS = (
'compressor.finders.CompressorFinder',
)
MEDIA_ROOT=MEDIA_ROOT
USE_TZ = True
TIME_ZONE = 'UTC'
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'django.core.context_processors.request',
@ -72,6 +71,7 @@ INSTALLED_APPS = [
'wagtail.wagtailredirects',
'wagtail.wagtailforms',
'wagtail.contrib.wagtailstyleguide',
'wagtail.contrib.wagtailsitemaps',
'wagtail.tests',
]

View file

@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<title>{{ self.title }}</title>
</head>
<body>
<h1>{{ self.title }}</h1>
<p>This event is invitation only. Please enter your password to see the details.</p>
<form action="{{ action_url }}" method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Continue" />
</form>
</body>
</html>

View file

@ -3,18 +3,17 @@ from django.conf.urls import patterns, include, url
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls
# Signal handlers
from wagtail.wagtailsearch import register_signal_handlers as wagtailsearch_register_signal_handlers
wagtailsearch_register_signal_handlers()
from wagtail.wagtailsearch import urls as wagtailsearch_urls
from wagtail.contrib.wagtailsitemaps.views import sitemap
urlpatterns = patterns('',
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^search/', include(wagtailsearch_frontend_urls)),
url(r'^search/', include(wagtailsearch_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
url(r'^sitemap\.xml$', sitemap),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's serving mechanism
url(r'', include(wagtail_urls)),

View file

@ -1,7 +1,8 @@
from django.test import TestCase
from contextlib import contextmanager
import warnings
from django.contrib.auth.models import User
from django.utils.six.moves.urllib.parse import urlparse, ParseResult
from django.http import QueryDict
from django.utils import six
# We need to make sure that we're using the same unittest library that Django uses internally
# Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors
@ -24,3 +25,17 @@ class WagtailTestUtils(object):
self.client.login(username='test', password='password')
return user
def assertRegex(self, *args, **kwargs):
six.assertRegex(self, *args, **kwargs)
@staticmethod
@contextmanager
def ignore_deprecation_warnings():
with warnings.catch_warnings(record=True) as warning_list: # catch all warnings
yield
# rethrow all warnings that were not DeprecationWarnings
for w in warning_list:
if not issubclass(w.category, DeprecationWarning):
warnings.showwarning(message=w.message, category=w.category, filename=w.filename, lineno=w.lineno, file=w.file, line=w.line)

View file

@ -1,4 +1,6 @@
from wagtail.wagtailadmin import hooks
from django.http import HttpResponse
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes
def editor_css():
@ -17,3 +19,9 @@ def whitelister_element_rules():
'a': attribute_rule({'href': check_url, 'target': True}),
}
hooks.register('construct_whitelister_element_rules', whitelister_element_rules)
def block_googlebot(page, request, serve_args, serve_kwargs):
if request.META.get('HTTP_USER_AGENT') == 'GoogleBot':
return HttpResponse("<h1>bad googlebot no cookie</h1>")
hooks.register('before_serve_page', block_googlebot)

View file

@ -1,6 +1,9 @@
from __future__ import unicode_literals
import copy
import re
import datetime
from six import string_types
from six import text_type
from taggit.forms import TagWidget
from modelcluster.forms import ClusterForm, ClusterFormMetaclass
@ -9,13 +12,10 @@ from django.template.loader import render_to_string
from django.template.defaultfilters import addslashes
from django.utils.safestring import mark_safe
from django import forms
from django.db import models
from django.forms.models import fields_for_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured, ValidationError
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.conf import settings
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from wagtail.wagtailcore.models import Page
@ -72,7 +72,7 @@ class WagtailAdminModelFormMetaclass(ClusterFormMetaclass):
new_class = super(WagtailAdminModelFormMetaclass, cls).__new__(cls, name, bases, attrs)
return new_class
WagtailAdminModelForm = WagtailAdminModelFormMetaclass('WagtailAdminModelForm', (ClusterForm,), {})
WagtailAdminModelForm = WagtailAdminModelFormMetaclass(str('WagtailAdminModelForm'), (ClusterForm,), {})
# Now, any model forms built off WagtailAdminModelForm instead of ModelForm should pick up
# the nice form fields defined in FORM_FIELD_OVERRIDES.
@ -108,7 +108,7 @@ def get_form_for_model(
# Give this new form class a reasonable name.
class_name = model.__name__ + str('Form')
form_class_attrs = {
'Meta': type('Meta', (object,), attrs)
'Meta': type(str('Meta'), (object,), attrs)
}
return WagtailAdminModelFormMetaclass(class_name, (WagtailAdminModelForm,), form_class_attrs)
@ -140,6 +140,10 @@ def extract_panel_definitions_from_model_class(model, exclude=None):
return panels
def set_page_edit_handler(page_class, handlers):
page_class.handlers = handlers
class EditHandler(object):
"""
Abstract class providing sensible default behaviours for objects implementing
@ -183,19 +187,21 @@ class EditHandler(object):
heading = ""
help_text = ""
def object_classnames(self):
def classes(self):
"""
Additional classnames to add to the <li class="object"> when rendering this
within an ObjectList
Additional CSS classnames to add to whatever kind of object this is at output.
Subclasses of EditHandler should override this, invoking super(B, self).classes() to
append more classes specific to the situation.
"""
return ""
def field_classnames(self):
"""
Additional classnames to add to the <li> when rendering this within a
<ul class="fields">
"""
return ""
classes = []
try:
classes.append(self.classname)
except AttributeError:
pass
return classes
def field_type(self):
"""
@ -239,12 +245,12 @@ class EditHandler(object):
"""
rendered_fields = self.rendered_fields()
missing_fields_html = [
unicode(self.form[field_name])
text_type(self.form[field_name])
for field_name in self.form.fields
if field_name not in rendered_fields
]
return mark_safe(u''.join(missing_fields_html))
return mark_safe(''.join(missing_fields_html))
def render_form_content(self):
"""
@ -261,12 +267,6 @@ class BaseCompositeEditHandler(EditHandler):
"""
_widget_overrides = None
def object_classnames(self):
try:
return "multi-field " + self.classname
except (AttributeError, TypeError):
return "multi-field"
@classmethod
def widget_overrides(cls):
if cls._widget_overrides is None:
@ -304,7 +304,7 @@ class BaseCompositeEditHandler(EditHandler):
}))
def render_js(self):
return mark_safe(u'\n'.join([handler.render_js() for handler in self.children]))
return mark_safe('\n'.join([handler.render_js() for handler in self.children]))
def rendered_fields(self):
result = []
@ -319,26 +319,41 @@ class BaseTabbedInterface(BaseCompositeEditHandler):
def TabbedInterface(children):
return type('_TabbedInterface', (BaseTabbedInterface,), {'children': children})
return type(str('_TabbedInterface'), (BaseTabbedInterface,), {'children': children})
class BaseObjectList(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/object_list.html"
def ObjectList(children, heading=""):
return type('_ObjectList', (BaseObjectList,), {
def ObjectList(children, heading="", classname=""):
return type(str('_ObjectList'), (BaseObjectList,), {
'children': children,
'heading': heading,
'classname': classname
})
class BaseFieldRowPanel(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/field_row_panel.html"
def FieldRowPanel(children, classname=""):
return type(str('_FieldRowPanel'), (BaseFieldRowPanel,), {
'children': children,
'classname': classname,
})
class BaseMultiFieldPanel(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/multi_field_panel.html"
def classes(self):
classes = super(BaseMultiFieldPanel, self).classes()
classes.append("multi-field")
return classes
def MultiFieldPanel(children, heading="", classname=None):
return type('_MultiFieldPanel', (BaseMultiFieldPanel,), {
def MultiFieldPanel(children, heading="", classname=""):
return type(str('_MultiFieldPanel'), (BaseMultiFieldPanel,), {
'children': children,
'heading': heading,
'classname': classname,
@ -353,25 +368,23 @@ class BaseFieldPanel(EditHandler):
self.heading = self.bound_field.label
self.help_text = self.bound_field.help_text
def object_classnames(self):
try:
return "single-field " + self.classname
except (AttributeError, TypeError):
return "single-field"
def classes(self):
classes = super(BaseFieldPanel, self).classes();
if self.bound_field.field.required:
classes.append("required")
if self.bound_field.errors:
classes.append("error")
classes.append(self.field_type())
classes.append("single-field")
return classes
def field_type(self):
return camelcase_to_underscore(self.bound_field.field.__class__.__name__)
def field_classnames(self):
classname = self.field_type()
if self.bound_field.field.required:
classname += " required"
if self.bound_field.errors:
classname += " error"
return classname
object_template = "wagtailadmin/edit_handlers/field_panel_object.html"
object_template = "wagtailadmin/edit_handlers/single_field_panel.html"
def render_as_object(self):
return mark_safe(render_to_string(self.object_template, {
@ -401,8 +414,8 @@ class BaseFieldPanel(EditHandler):
return [self.field_name]
def FieldPanel(field_name, classname=None):
return type('_FieldPanel', (BaseFieldPanel,), {
def FieldPanel(field_name, classname=""):
return type(str('_FieldPanel'), (BaseFieldPanel,), {
'field_name': field_name,
'classname': classname,
})
@ -414,7 +427,7 @@ class BaseRichTextFieldPanel(BaseFieldPanel):
def RichTextFieldPanel(field_name):
return type('_RichTextFieldPanel', (BaseRichTextFieldPanel,), {
return type(str('_RichTextFieldPanel'), (BaseRichTextFieldPanel,), {
'field_name': field_name,
})
@ -470,7 +483,7 @@ class BasePageChooserPanel(BaseChooserPanel):
def target_content_type(cls):
if cls._target_content_type is None:
if cls.page_type:
if isinstance(cls.page_type, basestring):
if isinstance(cls.page_type, string_types):
# translate the passed model name into an actual model class
from django.db.models import get_model
try:
@ -505,7 +518,7 @@ class BasePageChooserPanel(BaseChooserPanel):
def PageChooserPanel(field_name, page_type=None):
return type('_PageChooserPanel', (BasePageChooserPanel,), {
return type(str('_PageChooserPanel'), (BasePageChooserPanel,), {
'field_name': field_name,
'page_type': page_type,
})
@ -562,6 +575,11 @@ class BaseInlinePanel(EditHandler):
child_edit_handler_class(instance=subform.instance, form=subform)
)
# if this formset is valid, it may have been re-ordered; respect that
# in case the parent form errored and we need to re-render
if self.formset.can_order and self.formset.is_valid():
self.children = sorted(self.children, key=lambda x: x.form.cleaned_data['ORDER'])
empty_form = self.formset.empty_form
empty_form.fields['DELETE'].widget = forms.HiddenInput()
if self.formset.can_order:
@ -588,7 +606,7 @@ class BaseInlinePanel(EditHandler):
def InlinePanel(base_model, relation_name, panels=None, label='', help_text=''):
rel = getattr(base_model, relation_name).related
return type('_InlinePanel', (BaseInlinePanel,), {
return type(str('_InlinePanel'), (BaseInlinePanel,), {
'relation_name': relation_name,
'related': rel,
'panels': panels,
@ -597,10 +615,23 @@ def InlinePanel(base_model, relation_name, panels=None, label='', help_text=''):
})
# This allows users to include the publishing panel in their own per-model override
# without having to write these fields out by hand, potentially losing 'classname'
# and therefore the associated styling of the publishing panel
def PublishingPanel():
return MultiFieldPanel([
FieldRowPanel([
FieldPanel('go_live_at'),
FieldPanel('expire_at'),
], classname="label-above"),
], ugettext_lazy('Scheduled publishing'), classname="publishing")
# Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some.
Page.content_panels = [
FieldPanel('title', classname="full title"),
]
Page.promote_panels = [
MultiFieldPanel([
FieldPanel('slug'),
@ -609,3 +640,7 @@ Page.promote_panels = [
FieldPanel('search_description'),
], ugettext_lazy('Common page configuration')),
]
Page.settings_panels = [
PublishingPanel()
]

View file

@ -75,3 +75,20 @@ class PasswordResetForm(PasswordResetForm):
raise forms.ValidationError(_("This email address is not recognised."))
return cleaned_data
class PageViewRestrictionForm(forms.Form):
restriction_type = forms.ChoiceField(label="Visibility", choices=[
('none', ugettext_lazy("Public")),
('password', ugettext_lazy("Private, accessible with the following password")),
], widget=forms.RadioSelect)
password = forms.CharField(required=False)
def clean(self):
cleaned_data = super(PageViewRestrictionForm, self).clean()
if cleaned_data.get('restriction_type') == 'password' and not cleaned_data.get('password'):
self._errors["password"] = self.error_class([_('This field is required.')])
del cleaned_data['password']
return cleaned_data

View file

@ -1,38 +1,11 @@
from django.conf import settings
try:
from importlib import import_module
except ImportError:
# for Python 2.6, fall back on django.utils.importlib (deprecated as of Django 1.7)
from django.utils.importlib import import_module
# The 'hooks' module is now part of wagtailcore.
# Imports are provided here for backwards compatibility
_hooks = {}
import warnings
# TODO: support 'register' as a decorator:
# @hooks.register('construct_main_menu')
# def construct_main_menu(menu_items):
# ...
warnings.warn(
"The wagtail.wagtailadmin.hooks module has been moved. "
"Use wagtail.wagtailcore.hooks instead.", DeprecationWarning)
def register(hook_name, fn):
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(fn)
_searched_for_hooks = False
def search_for_hooks():
global _searched_for_hooks
if not _searched_for_hooks:
for app_module in settings.INSTALLED_APPS:
try:
import_module('%s.wagtail_hooks' % app_module)
except ImportError:
continue
_searched_for_hooks = True
def get_hooks(hook_name):
search_for_hooks()
return _hooks.get(hook_name, [])
from wagtail.wagtailcore.hooks import register, get_hooks

View file

@ -1,3 +1,7 @@
from __future__ import unicode_literals
from six import text_type
from django.utils.text import slugify
from django.utils.html import format_html
@ -7,10 +11,10 @@ class MenuItem(object):
self.label = label
self.url = url
self.classnames = classnames
self.name = (name or slugify(unicode(label)))
self.name = (name or slugify(text_type(label)))
self.order = order
def render_html(self):
return format_html(
u"""<li class="menu-{0}"><a href="{1}" class="{2}">{3}</a></li>""",
"""<li class="menu-{0}"><a href="{1}" class="{2}">{3}</a></li>""",
self.name, self.url, self.classnames, self.label)

View file

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
@ -20,7 +22,7 @@ class Migration(DataMigration):
wagtailadmin_content_type = orm['contenttypes.ContentType'].objects.create(
app_label='wagtailadmin', model='admin', name='Wagtail admin')
admin_permission = orm['auth.permission'].objects.create(
content_type=wagtailadmin_content_type, codename='access_admin', name=u'Can access Wagtail admin')
content_type=wagtailadmin_content_type, codename='access_admin', name='Can access Wagtail admin')
for group in orm['auth.group'].objects.filter(name__in=['Editors', 'Moderators']):
group.permissions.add(admin_permission)
@ -32,23 +34,23 @@ class Migration(DataMigration):
wagtailadmin_content_type.delete()
models = {
u'auth.group': {
'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'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'})
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "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'},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', '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'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'contenttypes.contenttype': {
'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'}),
'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'})
},

View file

@ -1,97 +0,0 @@
(function(jQuery) {
return jQuery.widget('IKS.halloToolbarFixed', {
toolbar: null,
options: {
parentElement: 'body',
editable: null,
toolbar: null,
affix: true,
affixTopOffset: 2
},
_create: function() {
var el, widthToAdd,
_this = this;
this.toolbar = this.options.toolbar;
this.toolbar.show();
jQuery(this.options.parentElement).append(this.toolbar);
this._bindEvents();
jQuery(window).resize(function(event) {
return _this.setPosition();
});
jQuery(window).scroll(function(event) {
return _this.setPosition();
});
if (this.options.parentElement === 'body') {
el = jQuery(this.element);
widthToAdd = parseFloat(el.css('padding-left'));
widthToAdd += parseFloat(el.css('padding-right'));
widthToAdd += parseFloat(el.css('border-left-width'));
widthToAdd += parseFloat(el.css('border-right-width'));
widthToAdd += (parseFloat(el.css('outline-width'))) * 2;
widthToAdd += (parseFloat(el.css('outline-offset'))) * 2;
return jQuery(this.toolbar).css("width", el.width() + widthToAdd);
}
},
_getPosition: function(event, selection) {
var offset, position, width;
if (!event) {
return;
}
width = parseFloat(this.element.css('outline-width'));
offset = width + parseFloat(this.element.css('outline-offset'));
return position = {
top: this.element.offset().top - this.toolbar.outerHeight() - offset,
left: this.element.offset().left - offset
};
},
_getCaretPosition: function(range) {
var newRange, position, tmpSpan;
tmpSpan = jQuery("<span/>");
newRange = rangy.createRange();
newRange.setStart(range.endContainer, range.endOffset);
newRange.insertNode(tmpSpan.get(0));
position = {
top: tmpSpan.offset().top,
left: tmpSpan.offset().left
};
tmpSpan.remove();
return position;
},
setPosition: function() {
var elementBottom, elementTop, height, offset, scrollTop, topOffset;
if (this.options.parentElement !== 'body') {
return;
}
this.toolbar.css('top', this.element.offset().top - this.toolbar.outerHeight());
if (this.options.affix) {
this.toolbar.removeClass('affixed');
scrollTop = jQuery(window).scrollTop();
offset = this.element.offset();
height = this.element.height();
topOffset = this.options.affixTopOffset;
elementTop = offset.top - (this.toolbar.height() + this.options.affixTopOffset);
elementBottom = (height - topOffset) + (offset.top - this.toolbar.height());
if (scrollTop > elementTop && scrollTop < elementBottom) {
this.toolbar.addClass('affixed');
this.toolbar.css('top', this.options.affixTopOffset);
}
} else {
}
return this.toolbar;
},
_updatePosition: function(position) {},
_bindEvents: function() {
var _this = this;
this.element.on('halloactivated', function(event, data) {
_this.setPosition();
return _this.toolbar.show();
});
return this.element.on('hallodeactivated', function(event, data) {
return _this.toolbar.hide();
});
}
});
})(jQuery);

View file

@ -1,11 +1,14 @@
"use strict";
var halloPlugins = {
'halloformat': {},
'halloheadings': {formatBlocks: ["p", "h2", "h3", "h4", "h5"]},
'hallolists': {},
'hallohr': {},
'halloreundo': {},
'hallowagtaillink': {},
'hallowagtaillink': {}
};
function registerHalloPlugin(name, opts) {
halloPlugins[name] = (opts || {});
}
@ -30,7 +33,7 @@ function makeRichTextEditable(id) {
richText.hallo({
toolbar: 'halloToolbarFixed',
toolbarcssClass: 'testy',
toolbarCssClass: (input.closest('.object').hasClass('full')) ? 'full' : '',
plugins: halloPlugins
}).bind('hallomodified', function(event, data) {
input.val(data.content);
@ -57,6 +60,7 @@ function initDateChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
timepicker: false,
scrollInput:false,
format: 'Y-m-d',
i18n: {
lang: window.dateTimePickerTranslations
@ -66,6 +70,7 @@ function initDateChooser(id) {
} else {
$('#' + id).datetimepicker({
timepicker: false,
scrollInput:false,
format: 'Y-m-d',
});
}
@ -75,6 +80,7 @@ function initTimeChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
datepicker: false,
scrollInput:false,
format: 'H:i',
i18n: {
lang: window.dateTimePickerTranslations
@ -93,6 +99,7 @@ function initDateTimeChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
format: 'Y-m-d H:i',
scrollInput:false,
i18n: {
lang: window.dateTimePickerTranslations
},
@ -197,7 +204,7 @@ function InlinePanel(opts) {
self.updateMoveButtonDisabledStates = function() {
if (opts.canOrder) {
forms = self.formsUl.children('li:visible');
var forms = self.formsUl.children('li:visible');
forms.each(function(i) {
$('ul.controls .inline-child-move-up', this).toggleClass('disabled', i === 0).toggleClass('enabled', i !== 0);
$('ul.controls .inline-child-move-down', this).toggleClass('disabled', i === forms.length - 1).toggleClass('enabled', i != forms.length - 1);
@ -240,7 +247,10 @@ function InlinePanel(opts) {
}
self.initChildControls(fixPrefix(opts.emptyChildFormPrefix));
if (opts.canOrder) {
$(fixPrefix('#id_' + opts.emptyChildFormPrefix + '-ORDER')).val(formCount);
/* NB form hidden inputs use 0-based index and only increment formCount *after* this function is run.
Therefore formcount and order are currently equal and order must be incremented
to ensure it's *greater* than previous item */
$(fixPrefix('#id_' + opts.emptyChildFormPrefix + '-ORDER')).val(formCount + 1);
}
self.updateMoveButtonDisabledStates();
@ -331,7 +341,7 @@ $(function() {
/* Set up behaviour of preview button */
$('.action-preview').click(function(e) {
e.preventDefault();
$this = $(this);
var $this = $(this);
var previewWindow = window.open($this.data('placeholder'), $this.data('windowname'));

View file

@ -0,0 +1,18 @@
$(function() {
/* Interface to set permissions from the explorer / editor */
$('a.action-set-privacy').click(function() {
ModalWorkflow({
'url': this.href,
'responses': {
'setPermission': function(isPublic) {
if (isPublic) {
$('.privacy-indicator').removeClass('private').addClass('public');
} else {
$('.privacy-indicator').removeClass('public').addClass('private');
}
}
}
});
return false;
});
});

View file

@ -2829,6 +2829,7 @@
} else {
}
return this.toolbar.css('left', this.element.offset().left - 2);
},
_updatePosition: function(position) {},

View file

@ -246,7 +246,7 @@
.xdsoft_calendar td.xdsoft_default,
.xdsoft_calendar td.xdsoft_current,
.xdsoft_timepicker .xdsoft_time_box > div > div.xdsoft_current{
background: $color-orange;
background: $color-salmon;
color:#fff;
font-weight: 700;
}

View file

@ -121,8 +121,8 @@
color:lighten($color-grey-2,30%);
-webkit-font-smoothing: auto;
font-size:0.80em;
margin:0 0.5em;
background:white url("#{$static-root}bg-dark-diag.svg");
margin:0 0.5em 0.5em;
background:white url( "#{$static-root}bg-dark-diag.svg");
&.primary{
color:$color-grey-2;
@ -136,6 +136,26 @@ a.status-tag.primary:hover{
color:$color-teal;
}
.privacy-indicator {
.label-private, .label-public{
&:before{
font-size:1.5em;
color:$color-teal;
}
}
&.public {
.label-private {
display: none;
}
}
&.private {
.label-public {
display: none;
}
}
}
/* free tagging tags from taggit */
.tag{
background-color:$color-teal;
@ -174,6 +194,7 @@ a.tag:hover{
}
}
/* make a block-level element inline */
.inline{
display:inline;

View file

@ -1,3 +1,9 @@
/*
These are the generic stylings for forms of any type.
If you're styling something specific to the page editing interface,
it probably ought to go in layouts/page-editor.scss
*/
form {
ul, li{
list-style-type:none;
@ -6,9 +12,6 @@ form {
margin:0;
padding:0;
}
li{
@include row();
}
}
fieldset{
@ -66,6 +69,10 @@ input, textarea, select, .richtext, .tagit{
outline:none;
background-color:$color-input-focus;
}
&:disabled, &[disabled], &:disabled:hover, &[disabled]:hover{
background-color:inherit;
cursor:not-allowed;
}
}
/* select boxes */
@ -132,6 +139,7 @@ input[type=radio]:before{
display:block;
content:"K";
width: 1em;
height:1em;
line-height: 1.1em;
padding: 4px;
background-color: white;
@ -261,7 +269,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
left:0;
top:0;
width:2em;
line-height:2em;
line-height:1.85em;
height:100%;
text-align:center;
background-color:rgba(0,0,0,0.2);
@ -275,7 +283,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
&:before{
width:2em;
font-size:0.9rem;
line-height:2em;
line-height:1.65em;
}
}
@ -300,21 +308,13 @@ input[type=submit], input[type=reset], input[type=button], button{
}
}
button.icon{
&:before,
&:after{
line-height:0;
}
}
.multiple{
@include transition(max-height 10s ease);
padding:0;
max-height:10000px;
max-width:1024px - 50px;
overflow:hidden;
> li{
@include row();
position:relative;
background-color:white;
padding:1em 10em 1em 1.5em; /* 10em padding leaves room for controls */
@ -435,58 +435,112 @@ li.focused > .help{
.boolean_field .help, .radio .help{
opacity:1;
}
.iconfield {
position:relative;
/*
This is expected to go on the parent of the input/select/textarea
so in most cases .input
*/
.iconfield, /* generic */
.date_field,
.time_field,
.date_time_field,
.url_field{
.input{
position:relative;
&:before, &:after{
font-family:wagtail;
position:absolute;
top:0.5em;
line-height:100%;
font-size:2em;
color:$color-grey-3;
}
&:before{
left:0.3em;
}
&:after{
right:0.5em;
}
}
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;
/* smaller fields required slight repositioning of icons */
&.field-small{
.input{
&:before, &:after{
font-size:1.3rem; /* REMs are necessary here because IE doesn't treat generated content correctly */
top:0.3em;
}
&: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{
.date_field,
.date_time_field{
.input:before{
@extend .icon-date:before;
}
}
.time_field{
.input:before{
@extend .icon-time:before;
}
}
.url_field{
.input:before{
@extend .icon-link:before;
}
}
/* This is specifically for model that are a generated set of checkboxes/radios */
.model_multiple_choice_field .input li,
.choice_field .input li{
label{
display:block;
width:auto;
float:none;
}
}
.fields > li,
.field-col{
@include clearfix();
padding-top:0.5em;
padding-bottom:1.2em;
}
.field-content .input li{
label{
width:auto;
float:none;
}
.field-row{
@include clearfix();
/* negative margin the bottom so it doesn't add too much space */
margin-bottom:-1.2em;
}
.input{
clear:both;
}
/* field sizing */
/* field sizing and alignment */
.field-small{
input, textarea, select, .richtext, .tagit{
@ -692,14 +746,20 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
.choice_field &,
.model_multiple_choice_field &,
.boolean_field &,
.choice_field &,
.model_choice_field &,
.image_field &,
.file_field &{
padding-top:0;
}
}
.boolean_field &{
padding-bottom:0;
.label-above{
.field > label{
display:block;
padding:0 0 0.8em 0;
float:none;
width:auto;
}
}
@ -715,11 +775,12 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
}
&.bicolor{
padding-left:3.5em;
padding-left:3.7em;
&:before{
width:2.2em;
line-height:2.45em;
width:2em;
line-height:2.2em;
font-size:1.1rem;
}
}
@ -758,8 +819,14 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
@include row();
}
.field-col{
float:left;
padding-left:0 !important;
}
.field-content{
@include column(10);
padding-right:0;
}
padding-left:0;
}
}

View file

@ -5,6 +5,10 @@ header{
margin-bottom:2em;
color:white;
a{
color:white;
}
h1, h2{
margin:0;
color:white;
@ -97,19 +101,6 @@ header{
}
}
/* 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;

View file

@ -96,7 +96,6 @@
.icon-unlocked:before {
content: "p";
}
.icon-doc-full-inverse:before {
content: "r";
}
@ -210,9 +209,9 @@
}
.icon-spinner:after{
width:1em;
animation: spin 1s infinite;
-webkit-animation: spin 1s infinite;
-moz-animation: spin 1s infinite;
animation: spin 0.5s infinite linear;
-webkit-animation: spin 0.5s infinite linear;
-moz-animation: spin 0.5s infinite linear;
content:"1";
}
.icon-pick:before{
@ -236,6 +235,9 @@
.icon-date:before{
content:"7";
}
.icon-time:before{
content:"8";
}
.icon-success:before{
content:"9";
}
@ -248,6 +250,7 @@
.icon-form:before{
content:"$";
}
.icon.text-replace{
font-size:0em;
line-height:0;

View file

@ -343,7 +343,6 @@ ul.listing{
background:$color-teal-darker;
}
}
}
}
ul.listing{
@ -357,6 +356,8 @@ table.listing{
/* explorer specific tweaks */
.page-explorer .listing {
position:relative;
.index{
color:white;
background-color:$color-header-bg;
@ -366,6 +367,14 @@ table.listing{
padding-bottom:1.5em;
}
.privacy-indicator{
font-size:1em;
opacity:1;
position:absolute;
right:5%;
top:2em;
}
.title{
h2{
color:white;
@ -376,9 +385,15 @@ table.listing{
color:white;
}
}
}
}
.privacy-indicator{
font-size:0.9em;
opacity:0.7;
}
.table-headers{
.ord{
padding-right:0;

View file

@ -1,32 +1,30 @@
.tab-nav{
@include clearfix();
@include row();
padding:0;
background:$color-grey-4;
li{
list-style-type:none;
width:48%;
width:33%;
float:left;
padding:0;
position:relative;
&:before,&:after{
display:none;
}
margin-right:1px;
}
a{
@include transition(border-color 0.2s ease);
background-color:lighten($color-teal-darker, 3%);
outline:none;
line-height:3em;
text-transform:uppercase;
font-weight:700;
font-size:1.2em;
text-decoration:none;
display:block;
padding:0 20px;
padding:0.7em;
color:white;
border-top:0.3em solid lighten($color-teal-darker, 3%);
border-bottom:1px solid transparent;
max-height:1.2em;
overflow:hidden;
&:hover{
color:white;
@ -45,7 +43,6 @@
min-width:0.9em;
color:white;
background:$color-red;
content:attr(data-count);
padding:0 0.3em;
line-height:1.4em;
@ -61,10 +58,21 @@
border-top:0.3em solid $color-grey-1;
}
li.settings a{
&:before{
font-family:wagtail;
vertical-align:middle;
text-transform:none;
content:"w";
margin-right:0.5em;
font-size:1.2em;
}
}
/* For cases where tab-nav should merge with header */
&.merged{
background-color:$color-header-bg;
margin-top:0;
background-color:$color-header-bg;
}
}
.tab-content{
@ -79,14 +87,27 @@
}
@media screen and (min-width: $breakpoint-mobile){
.tab-nav li{
width:auto;
padding:0;
margin-left:0.7em;
}
.tab-nav{
/* For cases where tab-nav should merge with header */
&.merged{
background-color:$color-header-bg;
}
.tab-nav a{
padding:0 50px;
li{
width:auto;
padding:0;
margin-left:0.7em;
}
a{
padding-left:$desktop-nice-padding - 10;
padding-right:$desktop-nice-padding - 10;
}
li.settings a{
padding-left:2em;
padding-right:2em;
}
}
.modal-content .tab-nav li{

View file

@ -1,6 +1,29 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M632.913 707.493l-173.647-173.649v-232.782h105.469v189.094l142.759 142.757zM512 90.125c-232.995 0-421.875 188.88-421.875 421.875s188.88 421.875 421.875 421.875 421.875-188.88 421.875-421.875-188.88-421.875-421.875-421.875zM512 828.406c-174.747 0-316.406-141.659-316.406-316.406s141.659-316.406 316.406-316.406c174.747 0 316.406 141.659 316.406 316.406s-141.659 316.406-316.406 316.406z"
],
"tags": [
"clock",
"time",
"schedule"
],
"grid": 16
},
"properties": {
"id": 72,
"order": 9,
"prevSize": 32,
"code": 56,
"name": "clock",
"ligatures": ""
},
"setIdx": 0,
"iconIdx": 72
},
{
"icon": {
"paths": [
@ -20,7 +43,7 @@
"name": "lock39copy",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 0
},
{
@ -42,7 +65,7 @@
"name": "lock39-open",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 1
},
{
@ -63,7 +86,7 @@
"name": "form",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 2
},
{
@ -82,7 +105,7 @@
"name": "uni61",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 3
},
{
@ -101,7 +124,7 @@
"name": "uni62",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 4
},
{
@ -120,7 +143,7 @@
"name": "uni63",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 5
},
{
@ -139,7 +162,7 @@
"name": "uni64",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 6
},
{
@ -158,7 +181,7 @@
"name": "uni65",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 7
},
{
@ -177,7 +200,7 @@
"name": "uni66",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 8
},
{
@ -196,7 +219,7 @@
"name": "uni67",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 9
},
{
@ -215,7 +238,7 @@
"name": "uni69",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 10
},
{
@ -234,7 +257,7 @@
"name": "uni6A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 11
},
{
@ -253,7 +276,7 @@
"name": "uni6B",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 12
},
{
@ -272,7 +295,7 @@
"name": "uni6C",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 13
},
{
@ -291,7 +314,7 @@
"name": "uni6E",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 14
},
{
@ -310,7 +333,7 @@
"name": "uni68",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 15
},
{
@ -329,7 +352,7 @@
"name": "uni6F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 16
},
{
@ -348,7 +371,7 @@
"name": "uni71",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 17
},
{
@ -367,7 +390,7 @@
"name": "uni72",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 18
},
{
@ -386,7 +409,7 @@
"name": "uni73",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 19
},
{
@ -405,7 +428,7 @@
"name": "uni74",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 20
},
{
@ -424,7 +447,7 @@
"name": "uni75",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 21
},
{
@ -443,7 +466,7 @@
"name": "uni76",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 22
},
{
@ -462,7 +485,7 @@
"name": "uni77",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 23
},
{
@ -481,7 +504,7 @@
"name": "uni78",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 24
},
{
@ -500,7 +523,7 @@
"name": "uni7A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 25
},
{
@ -519,7 +542,7 @@
"name": "uni41",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 26
},
{
@ -538,7 +561,7 @@
"name": "uni42",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 27
},
{
@ -557,7 +580,7 @@
"name": "uni44",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 28
},
{
@ -576,7 +599,7 @@
"name": "uni43",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 29
},
{
@ -595,7 +618,7 @@
"name": "uni45",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 30
},
{
@ -614,7 +637,7 @@
"name": "uni46",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 31
},
{
@ -633,7 +656,7 @@
"name": "uni47",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 32
},
{
@ -652,7 +675,7 @@
"name": "uni48",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 33
},
{
@ -671,7 +694,7 @@
"name": "uni49",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 34
},
{
@ -690,7 +713,7 @@
"name": "uni4A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 35
},
{
@ -709,7 +732,7 @@
"name": "uni4B",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 36
},
{
@ -728,7 +751,7 @@
"name": "uni4C",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 37
},
{
@ -747,7 +770,7 @@
"name": "uni4D",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 38
},
{
@ -766,7 +789,7 @@
"name": "uni4E",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 39
},
{
@ -785,7 +808,7 @@
"name": "uni4F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 40
},
{
@ -804,7 +827,7 @@
"name": "uni50",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 41
},
{
@ -823,7 +846,7 @@
"name": "uni51",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 42
},
{
@ -842,7 +865,7 @@
"name": "uni79",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 43
},
{
@ -861,7 +884,7 @@
"name": "uni52",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 44
},
{
@ -880,7 +903,7 @@
"name": "uni54",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 45
},
{
@ -899,7 +922,7 @@
"name": "uni57",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 46
},
{
@ -918,7 +941,7 @@
"name": "uni58",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 47
},
{
@ -937,7 +960,7 @@
"name": "uni59",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 48
},
{
@ -956,7 +979,7 @@
"name": "uni5A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 49
},
{
@ -975,7 +998,7 @@
"name": "uni56",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 50
},
{
@ -994,7 +1017,7 @@
"name": "uni31",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 51
},
{
@ -1013,7 +1036,7 @@
"name": "uni55",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 52
},
{
@ -1032,7 +1055,7 @@
"name": "uni33",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 53
},
{
@ -1051,7 +1074,7 @@
"name": "uni32",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 54
},
{
@ -1070,7 +1093,7 @@
"name": "uni35",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 55
},
{
@ -1089,7 +1112,7 @@
"name": "uni36",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 56
},
{
@ -1108,7 +1131,7 @@
"name": "uni30",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 57
},
{
@ -1127,7 +1150,7 @@
"name": "uni3F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 58
},
{
@ -1146,7 +1169,7 @@
"name": "uni21",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 59
},
{
@ -1165,7 +1188,7 @@
"name": "uni39",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 60
},
{
@ -1184,7 +1207,7 @@
"name": "uni53",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 61
},
{
@ -1203,7 +1226,7 @@
"name": "uni34",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 62
},
{
@ -1222,7 +1245,7 @@
"name": "uni37",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 63
}
],
@ -1245,13 +1268,14 @@
"baseline": 6.25,
"whitespace": 50
},
"showMetadata": false,
"showMetrics": true,
"useClassSelector": false,
"classSelector": ".icon",
"embed": false
"resetPoint": 58880
},
"imagePref": {
"color": 0,
"height": 32,
"columns": 16,
"margin": 16
},
"imagePref": {},
"historySize": 100,
"showCodes": true,
"search": "",

View file

@ -17,6 +17,7 @@
<glyph unicode="&#x35;" d="M135 424h241v-23h-241zM405 247l-127-124v222h-45v-220l-125 122-33-32 181-181 181 181z" />
<glyph unicode="&#x36;" d="M136 424h241v-23h-241zM108 122l126 124v-222h45v220l126-122 32 32-181 181-181-181z" />
<glyph unicode="&#x37;" d="M387.836 13.063h-263.672c-43.671 0-79.102 35.431-79.102 79.101v263.672c0 34.607 22.248 63.446 52.734 74.158v-34.607c0-22.248 18.127-39.551 39.551-39.551s39.551 17.303 39.551 39.551v39.551h158.203v-39.551c0-22.248 18.127-39.551 39.551-39.551s39.551 17.303 39.551 39.551v34.607c30.487-10.712 52.735-39.551 52.735-74.158v-263.672c0-43.671-35.431-79.101-79.101-79.101zM414.203 303.101h-316.406v-210.938c0-14.832 11.535-26.367 26.367-26.367h263.672c14.832 0 26.367 11.536 26.367 26.367zM308.735 171.265h52.735v-52.735h-52.735zM308.735 250.367h52.735v-52.734h-52.735zM229.633 171.265h52.734v-52.735h-52.734zM229.633 250.367h52.734v-52.734h-52.734zM150.531 171.265h52.734v-52.735h-52.734zM150.531 250.367h52.734v-52.734h-52.734zM374.652 382.203c-7.416 0-13.183 5.768-13.183 13.184v39.551h26.367v-39.551c0-7.416-5.768-13.184-13.183-13.184zM137.347 382.203c-7.416 0-13.183 5.768-13.183 13.184v39.551h26.367v-39.551c0-7.416-5.768-13.184-13.184-13.184z" />
<glyph unicode="&#x38;" d="M316.457 126.253l-86.823 86.825v116.391h52.734v-94.547l71.38-71.379zM256 434.938c-116.498 0-210.938-94.44-210.938-210.938s94.44-210.938 210.938-210.938 210.938 94.44 210.938 210.938-94.44 210.938-210.938 210.938zM256 65.797c-87.374 0-158.203 70.829-158.203 158.203s70.829 158.203 158.203 158.203c87.374 0 158.203-70.829 158.203-158.203s-70.829-158.203-158.203-158.203z" />
<glyph unicode="&#x39;" d="M256 449c-123.926 0-225-101.074-225-225s101.074-225 225-225c123.926 0 225 101.074 225 225s-101.074 225-225 225zM220.844 120.289l-102.832 103.711 39.551 39.551 63.281-64.16 135.351 135.351 39.551-39.551z" />
<glyph unicode="&#x3f;" d="M253.188 445.25c60.938 0 112.5-20.625 156.563-62.813 43.125-42.188 65.625-93.75 67.5-154.688 0-60.938-20.625-113.438-63.75-156.563-42.188-44.063-93.75-66.563-154.688-68.438-60.938 0-113.438 20.625-156.563 63.75-44.063 42.188-66.563 93.75-67.5 154.688s19.688 113.438 62.813 156.563c43.125 44.063 94.688 66.563 155.625 67.5zM252.25 89.938c9.375 0 17.813 2.813 23.438 8.438 5.625 6.563 9.375 14.063 9.375 22.5 0 10.313-1.875 17.813-8.438 24.375-5.625 5.625-14.063 8.438-23.438 8.438 0 0-0.938 0-0.938 0-9.375 0-16.875-2.813-22.5-8.438-6.563-5.625-9.375-13.125-10.313-22.5 0-9.375 2.813-16.875 9.375-23.438 5.625-5.625 13.125-9.375 22.5-9.375 0 0 0.938 0 0.938 0zM331.938 247.438c8.438 10.313 12.188 22.5 12.188 37.5 0 24.375-8.438 43.125-25.313 55.313s-38.438 17.813-64.688 17.813c-20.625 0-37.5-3.75-49.688-12.188-22.5-13.125-33.75-36.563-34.688-70.313 0 0 0-1.875 0-1.875s52.5 0 52.5 0c0 0 0 1.875 0 1.875 0 8.438 2.813 16.875 7.5 26.25 5.625 7.5 14.063 11.25 26.25 11.25 13.125 0 21.563-2.813 25.313-9.375 4.688-6.563 7.5-13.125 7.5-21.563 0-5.625-2.813-12.188-7.5-18.75-2.813-3.75-6.563-7.5-10.313-9.375 0 0-2.813-1.875-2.813-1.875-1.875-1.875-3.75-3.75-7.5-5.625-2.813-1.875-6.563-4.688-9.375-7.5-3.75-1.875-6.563-4.688-10.313-7.5s-6.563-5.625-8.438-8.438c-3.75-6.563-6.563-18.75-8.438-37.5 0 0 0-3.75 0-3.75s52.5 0 52.5 0c0 0 0 1.875 0 1.875 0 3.75 0 8.438 1.875 13.125 1.875 6.563 5.625 12.188 13.125 17.813 0 0 13.125 8.438 13.125 8.438 15 11.25 23.438 18.75 27.188 24.375z" />
<glyph unicode="&#x41;" d="M232 109l176 175c3 4 5 8 5 13s-2 9-5 13l-29 29c-4 4-8 6-13 6s-10-2-13-6l-134-133-60 60c-3 4-8 5-13 5s-9-1-13-5l-29-29c-3-4-5-8-5-13s2-9 5-13l103-102c3-4 7-6 12-6s10 2 13 6zM475 361v-274c0-23-8-42-24-58s-35-24-58-24h-274c-23 0-42 8-58 24s-24 35-24 58v274c0 23 8 42 24 58s35 24 58 24h274c23 0 42-8 58-24s24-35 24-58z" />

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -44,6 +44,7 @@ form{
}
}
label{
width:auto;
color:white;
}
input[type=submit]{
@ -83,24 +84,9 @@ form{
.field{
padding:0;
}
.iconfield:before{
.iconfield .input:before{
display:none;
}
.full label{
@include border-radius(2px);
text-transform:uppercase;
padding:2px 5px;
position:absolute;
top:0;
left:0;
margin-top:-1px;
font-size:0.7em;
z-index:1;
margin:0.2em 0;
line-height:1.5em;
font-weight:normal;
}
}
/* Special full-width, one-off fields i.e a single text or textarea input */
.full input{
@ -173,7 +159,7 @@ form{
margin:0px (-$desktop-nice-padding);
.iconfield{
&:before{
.input:before{
display:inline-block;
position: absolute;
color:$color-grey-4;

View file

@ -25,9 +25,6 @@
}
}
.objects{
background:url("#{$static-root}bg-dark-diag.svg");
}
.object{
position:relative;
overflow:hidden;
@ -80,9 +77,9 @@
> h2, &.single-field label{
-webkit-font-smoothing: auto;
background:$color-grey-3;
background:$color-salmon-light;
text-transform:uppercase;
padding:0.9em 0 0.9em 4em;
padding:0.9em 0 0.9em 4.1em;
font-size:0.95em;
margin:0 0 0.2em 0;
line-height:1.5em;
@ -92,10 +89,10 @@
left:0;
right:0;
z-index:1;
text-shadow:1px 1px 1px rgba(255,255,255,0.5);
@include box-shadow(0 0 7px 0 rgba(0,0,0,0.4));
overflow:hidden;
&:before{
text-shadow:none;
font-family:wagtail;
text-transform:none;
content:"q";
@ -108,10 +105,11 @@
line-height:1.8em;
left:0px;
width:1.7em;
opacity:0.15;
color:white;
padding:0;
margin:0;
cursor:pointer;
background-color:$color-salmon;
}
}
@ -186,6 +184,17 @@
}
}
/* special panel for the publishing fields, requires a bit more pizzazz */
&.publishing{
h2:before{
content:"7";
font-size:2.4em;
line-height:1.4em;
width:1.4em;
}
}
&.title input,
&.title textarea{
font-size:2em;
@ -235,7 +244,6 @@
top:0px;
left:0px;
width:3.3em;
background-color:$color-teal;
padding:0;
margin:0 0 0 -20px;
cursor:pointer;
@ -246,6 +254,7 @@
display:inline-block;
padding:0;
width:3.45em;
background-color:$color-salmon;
&:before{
position:relative;
@ -260,7 +269,6 @@
.multiple{
padding:0;
max-height:0px;
}
}

View file

@ -68,6 +68,12 @@ section{
.color-grey-5{
background-color:$color-grey-5;
}
.color-salmon{
background-color:$color-salmon;
}
.color-salmon-light{
background-color:$color-salmon-light;
}
}

View file

@ -452,15 +452,6 @@ input[type="submit"] {
*overflow: visible; /* 4 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* 1. Address box sizing set to content-box in IE 8/9.
* 2. Remove excess padding in IE 8/9.

View file

@ -3,10 +3,13 @@
.hallotoolbar{
position:absolute;
left:$mobile-nice-padding;
z-index:5;
margin-top:4em;
margin-left:0em;
margin-left:1.2em;
}
/* full is added to hallotoolbar when invoked on a field set to the full layout style */
.hallotoolbar.full{
margin-left:$mobile-nice-padding;
}
.hallotoolbar.affixed{
position:fixed;
@ -150,7 +153,7 @@
}
@media screen and (min-width: $breakpoint-mobile){
.hallotoolbar{
left:$menu-width + $desktop-nice-padding;
.hallotoolbar.full{
margin-left:$desktop-nice-padding;
}
}

View file

@ -27,9 +27,11 @@ $breakpoint-desktop-larger: 100em; /* 1600px */
$color-teal: #43b1b0;
$color-teal-darker: darken($color-teal, 10%);
$color-teal-dark: #246060;
$color-red: #f7474e;
$color-red: #cd3238;
$color-orange:#e9b04d;
$color-green: #189370;
$color-salmon: #f37e77;
$color-salmon-light: #fcf2f2;
/* darker to lighter */
$color-grey-1: #333333;

View file

@ -4,31 +4,28 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from wagtail.wagtailsearch import Indexed, get_search_backend
from wagtail.wagtailsearch import indexed
from wagtail.wagtailsearch.backends import get_search_backend
class TagSearchable(Indexed):
class TagSearchable(indexed.Indexed):
"""
Mixin to provide a 'search' method, searching on the 'title' field and tags,
for models that provide those things.
"""
indexed_fields = {
'title': {
'type': 'string',
'analyzer': 'edgengram_analyzer',
'boost': 10,
},
'get_tags': {
'type': 'string',
'analyzer': 'edgengram_analyzer',
'boost': 10,
},
}
search_fields = (
indexed.SearchField('title', partial_match=True, boost=10),
indexed.SearchField('get_tags', partial_match=True, boost=10)
)
@property
def get_tags(self):
return ' '.join([tag.name for tag in self.tags.all()])
return ' '.join([tag.name for tag in self.prefetched_tags()])
@classmethod
def get_indexed_objects(cls):
return super(TagSearchable, cls).get_indexed_objects().prefetch_related('tagged_items__tag')
@classmethod
def search(cls, q, results_per_page=None, page=1, prefetch_tags=False, filters={}):

View file

@ -1,4 +1,4 @@
{% extends "wagtailadmin/edit_handlers/field_panel_field.html" %}
{% extends "wagtailadmin/shared/field.html" %}
{% load i18n %}
{% comment %}
Either the chosen or unchosen div will be shown, depending on the presence

View file

@ -1,23 +1 @@
<div class="field">
{{ 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>
{% include "wagtailadmin/shared/field.html" %}

View file

@ -0,0 +1,7 @@
<ul class="field-row {{ self.classes|join:" " }}">
{% for child in self.children %}
<li class="field-col {{ child.classes|join:" " }}">
{{ child.render_as_field }}
</li>
{% endfor %}
</ul>

View file

@ -2,9 +2,7 @@
<legend>{{ self.heading }}</legend>
<ul class="fields">
{% for child in self.children %}
<li {% if child.field_classnames %}class="{{ child.field_classnames }}"{% endif %}>
{{ child.render_as_field }}
</li>
<li class="{{ child.classes|join:" " }}">{{ child.render_as_field }}</li>
{% endfor %}
</ul>
</fieldset>

View file

@ -1,6 +1,6 @@
<ul class="objects">
{% for child in self.children %}
<li class="object {{ child.object_classnames }}">
<li class="object {{ child.classes|join:" " }}">
{% if child.heading %}
<h2>{{ child.heading }}</h2>
{% endif %}

View file

@ -1,6 +1,6 @@
<fieldset>
<legend>{{ self.heading }}</legend>
<ul class="fields">
<li class="{{ self.field_classnames }}">{{ field_content }}</li>
<li>{{ field_content }}</li>
</ul>
</fieldset>

View file

@ -1,12 +1,12 @@
<ul class="tab-nav merged">
{% for child in self.children %}
<li class="{% if forloop.first %}active{% endif %}"><a href="#{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">{{ child.heading }}</a></li>
<li class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}"><a href="#{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">{{ child.heading }}</a></li>
{% endfor %}
</ul>
<div class="tab-content">
{% for child in self.children %}
<section id="{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">
<section id="{{ child.heading|slugify }}" class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}">
{{ child.render_as_object }}
</section>
{% endfor %}

View file

@ -28,17 +28,17 @@
<ul class="fields">
<li class="full">
<div class="field">
<div class="field iconfield">
{{ form.username.label_tag }}
<div class="input iconfield icon-user">
<div class="input icon-user">
{{ form.username }}
</div>
</div>
</li>
<li class="full">
<div class="field">
<div class="field iconfield">
{{ form.password.label_tag }}
<div class="input iconfield icon-password">
<div class="input icon-password">
{{ form.password }}
</div>
</div>

View file

@ -0,0 +1,8 @@
{% load i18n %}
{% trans "Page privacy" as title_str %}
{% include "wagtailadmin/shared/header.html" with title=title_str icon="locked" %}
<div class="nice-padding">
<p>{% trans "This page has been made private by a parent page." %}</p>
<p>{% trans "You can edit the privacy settings on:" %} <a href="{% url 'wagtailadmin_pages_edit' page_with_restriction.id %}">{{ page_with_restriction.title }}</a>
</div>

View file

@ -0,0 +1,16 @@
{% load i18n %}
{% trans "Page privacy" as title_str %}
{% include "wagtailadmin/shared/header.html" with title=title_str icon="locked" %}
<div class="nice-padding">
<p>{% trans "<b>Note:</b> privacy changes apply to all children of this page too." %}</p>
<form action="{% url 'wagtailadmin_pages_set_privacy' page.id %}" method="POST">
{% csrf_token %}
<ul class="fields">
{% include "wagtailadmin/shared/field_as_li.html" with field=form.restriction_type %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password li_classes="password-field" %}
</ul>
<input type="submit" value="Save" />
</form>
</div>

View file

@ -0,0 +1,19 @@
function(modal) {
$('form', modal.body).submit(function() {
modal.postForm(this.action, $(this).serialize());
return false;
});
var restrictionTypePasswordField = $("input[name='restriction_type'][value='password']", modal.body);
var passwordField = $("#id_password", modal.body);
function refreshFormFields() {
if (restrictionTypePasswordField.is(':checked')) {
passwordField.removeAttr('disabled');
} else {
passwordField.attr('disabled', true);
}
}
refreshFormFields();
$("input[name='restriction_type']", modal.body).change(refreshFormFields);
}

View file

@ -0,0 +1,4 @@
function(modal) {
modal.respond('setPermission', {% if is_public %}true{% else %}false{% endif %});
modal.close();
}

View file

@ -16,13 +16,12 @@
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/tag-it.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/expanding_formset.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/modal-workflow.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-wagtail-toolbar.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-hr.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/page-editor.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/page-chooser.js"></script>
<script src="{{ STATIC_URL }}admin/js/urlify.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/privacy-indicator.js"></script>
{% hook_output 'insert_editor_js' %}
{% endcompress %}

View file

@ -0,0 +1,23 @@
{% load i18n wagtailadmin_tags %}
{% test_page_is_public page as is_public %}
{% if not page_perms %}
{% page_permissions page as page_perms %}
{% endif %}
<div class="privacy-indicator {% if is_public %}public{% else %}private{% endif %}">
{% trans "Privacy" %}
{% if page_perms.can_set_view_restrictions %}
<a href="{% url 'wagtailadmin_pages_set_privacy' page.id %}" class="status-tag primary action-set-privacy">
{# labels are shown/hidden in CSS according to the 'private' / 'public' class on view-permission-indicator #}
<span class="label-public icon icon-unlocked">{% trans 'Public' %}</span>
<span class="label-private icon icon-locked">{% trans 'Private' %}</span>
</a>
{% else %}
{% if is_public %}
<span class="label-public status-tag primary icon icon-unlocked ">{% trans 'Public' %}</span>
{% else %}
<span class="label-private status-tag primary icon icon-locked">{% trans 'Private' %}</span>
{% endif %}
{% endif %}
</div>

View file

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

View file

@ -6,6 +6,7 @@
{% block bodyclass %}menu-explorer page-editor{% endblock %}
{% block content %}
{% page_permissions page as page_perms %}
<header class="merged tab-merged nice-padding">
{% include "wagtailadmin/shared/breadcrumb.html" with page=page %}
@ -14,7 +15,14 @@
<h1 class="icon icon-doc-empty-inverse">{% blocktrans with title=page.title %}Editing <span>{{ title }}</span>{% endblocktrans %}</h1>
</div>
<div class="right col3">
{% trans "Status:" %} {% if page.live %}<a href="{{ page.url }}" class="status-tag {% if page.live %}primary{% endif %}">{{ page.status_string }}</a>{% else %}<span class="status-tag">{{ page.status_string }}</span>{% endif %}
{% trans "Status" %}
{% if page.live %}
<a href="{{ page.url }}" class="status-tag {% if page.live %}primary{% endif %}">{{ page.status_string }}</a>
{% else %}
<span class="status-tag">{{ page.status_string }}</span>
{% endif %}
{% include "wagtailadmin/pages/_privacy_indicator.html" with page=page page_perms=page_perms only %}
</div>
</div>
</header>
@ -22,8 +30,7 @@
<form id="page-edit-form" action="{% url 'wagtailadmin_pages_edit' page.id %}" method="POST">
{% csrf_token %}
{{ edit_handler.render_form_content }}
{% page_permissions page as page_perms %}
<footer>
<ul>
<li class="actions">
@ -47,12 +54,12 @@
<li class="actions preview">
{% trans 'Preview' as preview_label %}
{% if display_modes|length > 1 %}
{% if preview_modes|length > 1 %}
<div class="dropdown dropup dropdown-button match-width">
{% include "wagtailadmin/pages/_preview_button_on_edit.html" with label=preview_label icon=1 %}
<div class="dropdown-toggle icon icon-arrow-up"></div>
<ul role="menu">
{% for mode_name, mode_display_name in display_modes %}
{% for mode_name, mode_display_name in preview_modes %}
<li>
{% include "wagtailadmin/pages/_preview_button_on_edit.html" with mode=mode_name label=mode_display_name %}
</li>
@ -70,7 +77,7 @@
{% blocktrans with last_mod=page.get_latest_revision.created_at %}Last modified: {{ last_mod }}{% endblocktrans %}
{% if page.get_latest_revision.user %}
{% blocktrans with modified_by=page.get_latest_revision.user.get_full_name|default:page.get_latest_revision.user.username %}by {{ modified_by }}{% endblocktrans %}
{% if request.user.email %}
{% if page.get_latest_revision.user.email %}
<span class="avatar small icon icon-user"><img src="{% gravatar_url page.get_latest_revision.user.email 25 %}" /></span>
{% endif %}
{% endif %}

View file

@ -1,6 +1,5 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% load wagtailadmin_tags %}
{% load i18n wagtailadmin_tags compress %}
{% block titletag %}{% blocktrans with title=parent_page.title %}Exploring {{ title }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-explorer page-explorer {% if ordering == 'ord' %}reordering{% endif %}{% endblock %}
@ -17,10 +16,13 @@
{% page_permissions parent_page as parent_page_perms %}
{% include "wagtailadmin/pages/list.html" with sortable=1 allow_navigation=1 full_width=1 parent_page=parent_page orderable=parent_page_perms.can_reorder_children %}
</form>
{% endblock %}
{% block extra_js %}
{% comment %} modal-workflow is required by the view restrictions interface {% endcomment %}
<script src="{{ STATIC_URL }}wagtailadmin/js/modal-workflow.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/privacy-indicator.js"></script>
<script type="text/javascript">
{% if ordering == 'ord' %}
$(function() {

View file

@ -31,23 +31,42 @@
<tr class="index {% if not parent_page.live %} inactive{% endif %} {% if moving or choosing %}{% if parent_page.can_choose %}can-choose{% endif %}{% endif %}">
<td class="title" {% if orderable %}colspan="2"{% endif %}>
{% if moving %}
{% if parent_page.can_choose %}
<h2><a href="{% url 'wagtailadmin_pages_move_confirm' page_to_move.id parent_page.id %}">{{ parent_page.title }}</a></h2>
{% else %}
<h2>{{ parent_page.title }}</h2>
{% endif %}
<h2>
{% if parent_page.can_choose %}
<a href="{% url 'wagtailadmin_pages_move_confirm' page_to_move.id parent_page.id %}">{{ parent_page.title }}</a>
{% else %}
{{ parent_page.title }}
{% endif %}
{% test_page_is_public parent_page as is_public %}
{% if not is_public %}
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
{% endif %}
</h2>
{% elif choosing %}
{% if parent_page.can_choose %}
<h2><a class="choose-page" href="#{{ parent_page.id }}" data-id="{{ parent_page.id }}" data-title="{{ parent_page.title }}" data-url="{{ parent_page.url }}">{{ parent_page.title }}</a></h2>
{% else %}
<h2>{{ parent_page.title }}</h2>
{% endif %}
<h2>
{% if parent_page.can_choose %}
<a class="choose-page" href="#{{ parent_page.id }}" data-id="{{ parent_page.id }}" data-title="{{ parent_page.title }}" data-url="{{ parent_page.url }}">{{ parent_page.title }}</a>
{% else %}
{{ parent_page.title }}
{% endif %}
{% test_page_is_public parent_page as is_public %}
{% if not is_public %}
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
{% endif %}
</h2>
{% else %}
{% if parent_page_perms.can_edit and 'edit' not in hide_actions|default:'' %}
<h2><a href="{% url 'wagtailadmin_pages_edit' parent_page.id %}">{{ parent_page.title }}</a></h2>
{% else %}
<h2>{{ parent_page.title }}</h2>
{% endif %}
<h2>
{% if parent_page_perms.can_edit and 'edit' not in hide_actions|default:'' %}
<a href="{% url 'wagtailadmin_pages_edit' parent_page.id %}">{{ parent_page.title }}</a>
{% else %}
{{ parent_page.title }}
{% endif %}
</h2>
{% include "wagtailadmin/pages/_privacy_indicator.html" with page=parent_page %}
<ul class="actions">
{% if parent_page_perms.can_edit and 'edit' not in hide_actions|default:'' %}
<li><a href="{% url 'wagtailadmin_pages_edit' parent_page.id %}" class="button button-small">{% trans 'Edit' %}</a></li>
@ -159,6 +178,11 @@
{{ page.title }}
{% endif %}
{% endif %}
{% test_page_is_public page as is_public %}
{% if not is_public %}
<span class="privacy-indicator icon icon-locked" title="This page is protected from public view"></span>
{% endif %}
</h2>
{% if not moving and not choosing %}
<ul class="actions">
@ -249,4 +273,4 @@
</li>
</ul>
</div>
{% endif %}
{% endif %}

View file

@ -0,0 +1,25 @@
{% load wagtailadmin_tags %}
<div class="field {{ field.field_classnames }} {{ field|fieldtype }} {{ field_classes }}">
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ field.input_classnames }} {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
{# This span only used on rare occasions by certain types of input #}
<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 %}
</div>
</div>

View file

@ -1,25 +1,4 @@
{% load wagtailadmin_tags %}
<li class="{% if field.field.required %}required{% endif %} {{ field.css_classes }} {{ field|fieldtype }} {{ li_classes }} {% if field.errors %}error{% endif %}">
<div class="field">
{{ field.label_tag }}
<div class="field-content">
<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 %}
</div>
</div>
<li class="{% if field.field.required %}required{% endif %} {{ wrapper_classes }} {{ li_classes }} {% if field.errors %}error{% endif %}">
{% include "wagtailadmin/shared/field.html" %}
</li>

View file

@ -8,7 +8,7 @@
<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 input_classes="field-small iconfield icon-search" %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field field_classes="field-small iconfield" input_classes="icon-search" %}
{% endfor %}
<li class="submit visuallyhidden"><input type="submit" value="Search" class="button" /></li>
</ul>

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