mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-12 17:23:15 +00:00
Merge branch 'master' into test-with-custom-user-model
Conflicts: wagtail/tests/models.py wagtail/tests/utils.py wagtail/wagtailcore/tests/test_page_permissions.py wagtail/wagtaildocs/tests.py
This commit is contained in:
commit
7a2e4e76e9
172 changed files with 3183 additions and 1143 deletions
10
.travis.yml
10
.travis.yml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ Changelog
|
|||
0.4 (xx.xx.20xx)
|
||||
~~~~~~~~~~~~~~~~
|
||||
* ElasticUtils/pyelasticsearch swapped for elasticsearch-py
|
||||
* Python 3.2, 3.3 and 3.4 support
|
||||
* Added scheduled publishing
|
||||
* Added frontend cache invalidator
|
||||
* Added sitemap generator
|
||||
* Added notification preferences
|
||||
* Added 'original' as a resizing rule supported by the 'image' tag
|
||||
* Hallo.js updated to version 1.0.4
|
||||
|
|
@ -15,16 +19,23 @@ 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
|
||||
* 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)
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
21
README.rst
21
README.rst
|
|
@ -2,7 +2,7 @@
|
|||
:target: https://travis-ci.org/torchbox/wagtail
|
||||
|
||||
.. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master&zxcv1
|
||||
:target: https://coveralls.io/r/torchbox/wagtail?branch=master
|
||||
:target: https://coveralls.io/r/torchbox/wagtail?branch=master
|
||||
|
||||
.. image:: https://pypip.in/v/wagtail/badge.png?zxcv
|
||||
:target: https://crate.io/packages/wagtail/
|
||||
|
|
@ -24,7 +24,9 @@ Wagtail is a Django content management system built originally for the `Royal Co
|
|||
* Support for tree-based content organisation
|
||||
* Optional preview->submit->approve workflow
|
||||
* Fast out of the box. `Varnish <https://www.varnish-cache.org/>`_-friendly if you need it
|
||||
* Excellent test coverage
|
||||
* A simple `form builder <http://docs.wagtail.io/en/latest/form_builder.html>`_
|
||||
* Optional `static site generation <http://docs.wagtail.io/en/latest/static_site_generation.html>`_
|
||||
* Excellent `test coverage <https://coveralls.io/r/torchbox/wagtail?branch=master>`_
|
||||
|
||||
Find out more at `wagtail.io <http://wagtail.io/>`_.
|
||||
|
||||
|
|
@ -35,16 +37,25 @@ Getting started
|
|||
* To get you up and running quickly, we've provided a demonstration site with all the configuration in place, at `github.com/torchbox/wagtaildemo <https://github.com/torchbox/wagtaildemo/>`_; see the `README <https://github.com/torchbox/wagtaildemo/blob/master/README.md>`_ for installation instructions.
|
||||
* See the `Getting Started <http://wagtail.readthedocs.org/en/latest/gettingstarted.html#getting-started>`_ docs for installation (with the demo app) on a fresh Debian/Ubuntu box with production-ready dependencies, on OS X and on a Vagrant box.
|
||||
* `Serafeim Papastefanos <https://github.com/spapas>`_ has written a `tutorial <http://spapas.github.io/2014/02/13/wagtail-tutorial/>`_ with all the steps to build a simple Wagtail site from scratch.
|
||||
* We've also provided a skeletal django-template to get started on a blank site: https://github.com/torchbox/wagtail-template
|
||||
|
||||
Documentation
|
||||
~~~~~~~~~~~~~
|
||||
Available at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_. and always being updated.
|
||||
Available at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_ and always being updated.
|
||||
|
||||
Compatibility
|
||||
~~~~~~~~~~~~~
|
||||
Wagtail supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support are in progress.
|
||||
Wagtail supports Django 1.6.2+ on Python 2.6, 2.7, 3.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.
|
||||
|
||||
Send us a useful pull request and we'll post you a `t-shirt <https://twitter.com/WagtailCMS/status/432166799464210432/photo/1>`_.
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
# ...
|
||||
|
|
@ -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
|
||||
---------------
|
||||
|
||||
|
|
@ -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,7 +396,7 @@ Registering functions with a Wagtail hook follows the following pattern:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
hooks.register('hook', function)
|
||||
|
||||
|
|
@ -393,7 +409,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
class UserbarPuppyLinkItem(object):
|
||||
def render(self, request):
|
||||
|
|
@ -414,7 +430,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
|
|||
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
class WelcomePanel(object):
|
||||
order = 50
|
||||
|
|
@ -440,7 +456,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
|
|||
|
||||
from django.http import HttpResponse
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
def do_after_page_create(request, page):
|
||||
return HttpResponse("Congrats on making content!", content_type="text/plain")
|
||||
|
|
@ -468,7 +484,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
|
|||
from django.http import HttpResponse
|
||||
from django.conf.urls import url
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
def admin_view( request ):
|
||||
return HttpResponse( \
|
||||
|
|
@ -484,13 +500,13 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
|
|||
.. _construct_main_menu:
|
||||
|
||||
``construct_main_menu``
|
||||
Add, remove, or alter ``MenuItem`` objects from the Wagtail admin menu. The callable passed to this hook must take a ``request`` object and a list of ``menu_items``; it must return a list of menu items. New items can be constructed from the ``MenuItem`` class by passing in: a ``label`` which will be the text in the menu item, the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up), CSS class ``name`` applied to the wrapping ``<li>`` of the menu item as ``"menu-{name}"``, CSS ``classnames`` which are used to give the link an icon, and an ``order`` integer which determine's the item's place in the menu.
|
||||
Add, remove, or alter ``MenuItem`` objects from the Wagtail admin menu. The callable passed to this hook must take a ``request`` object and a list of ``menu_items``; it must return a list of menu items. New items can be constructed from the ``MenuItem`` class by passing in: a ``label`` which will be the text in the menu item, the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up), CSS class ``name`` applied to the wrapping ``<li>`` of the menu item as ``"menu-{name}"``, CSS ``classnames`` which are used to give the link an icon, and an ``order`` integer which determine's the item's place in the menu.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
|
||||
def construct_main_menu(request, menu_items):
|
||||
|
|
@ -510,7 +526,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
|
|||
from django.utils.html import format_html, format_html_join
|
||||
from django.conf import settings
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
def editor_js():
|
||||
js_files = [
|
||||
|
|
@ -538,7 +554,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
|
|||
from django.utils.html import format_html
|
||||
from django.conf import settings
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
def editor_css():
|
||||
return format_html('<link rel="stylesheet" href="' \
|
||||
|
|
@ -558,7 +574,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes
|
||||
|
||||
def whitelister_element_rules():
|
||||
|
|
@ -611,4 +627,3 @@ Custom Choosers
|
|||
|
||||
Tests
|
||||
-----
|
||||
|
||||
|
|
|
|||
102
docs/frontend_cache_purging.rst
Normal file
102
docs/frontend_cache_purging.rst
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
Frontend cache purging
|
||||
======================
|
||||
|
||||
Many websites use a frontend cache such as Varnish, Squid or Cloudflare to gain extra performance. The downside of using a frontend cache though is that they don't respond well to updating content and will often keep an old version of a page cached after it has been updated.
|
||||
|
||||
This document describes how to configure Wagtail to purge old versions of pages from a frontend cache whenever a page gets updated.
|
||||
|
||||
|
||||
Setting it up
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Firstly, add ``"wagtail.contrib.wagtailfrontendcache"`` to your INSTALLED_APPS:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
|
||||
"wagtail.contrib.wagtailfrontendcache"
|
||||
]
|
||||
|
||||
|
||||
The ``wagtailfrontendcache`` module provides a set of signal handlers which will automatically purge the cache whenever a page is published or deleted. You should register these somewhere at the top of your ``urls.py`` file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# urls.py
|
||||
from wagtail.contrib.wagtailfrontendcache.signal_handlers import register_signal_handlers
|
||||
|
||||
register_signal_handlers()
|
||||
|
||||
|
||||
You then need to set the ``WAGTAILFRONTENDCACHE_LOCATION`` setting to the URL of your Varnish/Squid cache server. This must be a direct connection to the server and cannot go through another proxy. By default, this is set to ``http://127.0.0.1:8000`` which is very likely incorrect.
|
||||
|
||||
Finally, make sure you have configured your frontend cache to accept PURGE requests:
|
||||
|
||||
- `Varnish <https://www.varnish-cache.org/docs/3.0/tutorial/purging.html>`_
|
||||
- `Squid <http://wiki.squid-cache.org/SquidFaq/OperatingSquid#How_can_I_purge_an_object_from_my_cache.3F>`_
|
||||
|
||||
|
||||
Advanced useage
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Purging more than one URL per page
|
||||
----------------------------------
|
||||
|
||||
By default, Wagtail will only purge one URL per page. If your page has more than one URL to be purged, you will need to override the ``get_cached_paths`` method on your page type.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class BlogIndexPage(Page):
|
||||
def get_blog_items(self):
|
||||
# This returns a Django paginator of blog items in this section
|
||||
return Paginator(self.get_children().live().type(BlogPage), 10)
|
||||
|
||||
def get_cached_paths(self):
|
||||
# Yield the main URL
|
||||
yield '/'
|
||||
|
||||
# Yield one URL per page in the paginator to make sure all pages are purged
|
||||
for page_number in range(1, self.get_blog_items().num_pages):
|
||||
yield '/?page=' + str(page_number)
|
||||
|
||||
|
||||
Purging index pages
|
||||
-------------------
|
||||
|
||||
Another problem is pages that list other pages (such as a blog index) will not be purged when a blog entry gets added, changed or deleted. You may want to purge the blog index page so the updates are added into the listing quickly.
|
||||
|
||||
This can be solved by using the ``purge_page_from_cache`` utility function which can be found in the ``wagtail.contrib.wagtailfrontendcache.utils`` module.
|
||||
|
||||
Let's take the the above BlogIndexPage as an example. We need to register a signal handler to run when one of the BlogPages get updated/deleted. This signal handler should call the ``purge_page_from_cache`` function on all BlogIndexPages that contain the BlogPage being updated/deleted.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# models.py
|
||||
from django.db.models.signals import pre_delete
|
||||
|
||||
from wagtail.wagtailcore.signals import page_published
|
||||
from wagtail.contrib.wagtailfrontendcache.utils import purge_page_from_cache
|
||||
|
||||
|
||||
...
|
||||
|
||||
|
||||
def blog_page_changed(blog_page):
|
||||
# Find all the live BlogIndexPages that contain this blog_page
|
||||
for blog_index in BlogIndexPage.objects.live():
|
||||
if blog_page in blog_index.get_blog_items().object_list:
|
||||
# Purge this blog index
|
||||
purge_page_from_cache(blog_index)
|
||||
|
||||
|
||||
@register(page_published, sender=BlogPage):
|
||||
def blog_published_handler(instance):
|
||||
blog_page_changed(instance)
|
||||
|
||||
|
||||
@register(pre_delete, sender=BlogPage)
|
||||
def blog_deleted_handler(instance):
|
||||
blog_page_changed(instance)
|
||||
|
|
@ -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
|
||||
|
|
@ -20,6 +20,7 @@ It supports Django 1.6.2+ on Python 2.6 and 2.7. Django 1.7 and Python 3 support
|
|||
deploying
|
||||
performance
|
||||
static_site_generation
|
||||
management_commands
|
||||
contributing
|
||||
support
|
||||
roadmap
|
||||
|
|
|
|||
52
docs/management_commands.rst
Normal file
52
docs/management_commands.rst
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
Management commands
|
||||
===================
|
||||
|
||||
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
|
||||
-------
|
||||
|
||||
:code:`./manage.py fixtree`
|
||||
|
||||
This command scans for errors in your database and attempts to fix any issues it finds.
|
||||
|
||||
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
|
||||
------------
|
||||
|
||||
: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
|
||||
----------------------
|
||||
|
||||
: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.
|
||||
58
docs/sitemap_generation.rst
Normal file
58
docs/sitemap_generation.rst
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
Sitemap generation
|
||||
==================
|
||||
|
||||
This document describes how to create XML sitemaps for your Wagtail website using the ``wagtail.contrib.wagtailsitemaps`` module.
|
||||
|
||||
|
||||
Basic configuration
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You firstly need to add ``"wagtail.contrib.wagtailsitemaps"`` to INSTALLED_APPS in your Django settings file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
|
||||
"wagtail.contrib.wagtailsitemaps",
|
||||
]
|
||||
|
||||
|
||||
Then, in urls.py, you need to add a link to the ``wagtail.contrib.wagtailsitemaps.views.sitemap`` view which generates the sitemap:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.contrib.wagtailsitemaps.views import sitemap
|
||||
|
||||
urlpatterns = patterns('',
|
||||
...
|
||||
|
||||
url('^sitemap\.xml$', sitemap),
|
||||
)
|
||||
|
||||
|
||||
You should now be able to browse to "/sitemap.xml" and see the sitemap working. By default, all published pages in your website will be added to the site map.
|
||||
|
||||
|
||||
Customising
|
||||
~~~~~~~~~~~
|
||||
|
||||
URLs
|
||||
----
|
||||
|
||||
The Page class defines a ``get_sitemap_urls`` method which you can override to customise sitemaps per page instance. This method must return a list of dictionaries, one dictionary per URL entry in the sitemap. You can exclude pages from the sitemap by returning an empty list.
|
||||
|
||||
Each dictionary can contain the following:
|
||||
|
||||
- **location** (required) - This is the full URL path to add into the sitemap.
|
||||
- **lastmod** - A python date or datetime set to when the page was last modified.
|
||||
- **changefreq**
|
||||
- **priority**
|
||||
|
||||
You can add more but you will need to override the ``wagtailsitemaps/sitemap.xml`` template in order for them to be displayed in the sitemap.
|
||||
|
||||
|
||||
Cache
|
||||
-----
|
||||
|
||||
By default, sitemaps are cached for 100 minutes. You can change this by setting ``WAGTAILSITEMAPS_CACHE_TIMEOUT`` in your Django settings to the number of seconds you would like the cache to last for.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -51,7 +62,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 +83,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ if not settings.configured:
|
|||
STATIC_ROOT=STATIC_ROOT,
|
||||
MEDIA_ROOT=MEDIA_ROOT,
|
||||
USE_TZ=True,
|
||||
TIME_ZONE='UTC',
|
||||
STATICFILES_FINDERS=(
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'compressor.finders.CompressorFinder',
|
||||
|
|
@ -85,6 +86,7 @@ if not settings.configured:
|
|||
'wagtail.wagtailredirects',
|
||||
'wagtail.wagtailforms',
|
||||
'wagtail.contrib.wagtailstyleguide',
|
||||
'wagtail.contrib.wagtailsitemaps',
|
||||
'wagtail.tests',
|
||||
],
|
||||
|
||||
|
|
|
|||
49
setup.py
49
setup.py
|
|
@ -1,5 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
try:
|
||||
from setuptools import setup, find_packages
|
||||
except ImportError:
|
||||
|
|
@ -16,6 +19,32 @@ except ImportError:
|
|||
pass
|
||||
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
|
||||
install_requires = [
|
||||
"Django>=1.6.2,<1.7",
|
||||
"South>=0.8.4",
|
||||
"django-compressor>=1.3",
|
||||
"django-libsass>=0.1",
|
||||
"django-modelcluster>=0.1",
|
||||
"django-taggit==0.11.2",
|
||||
"django-treebeard==2.0",
|
||||
"Pillow>=2.3.0",
|
||||
"beautifulsoup4>=4.3.2",
|
||||
"lxml>=3.3.0",
|
||||
"Unidecode>=0.04.14",
|
||||
"six==1.7.3",
|
||||
'requests==2.3.0',
|
||||
]
|
||||
|
||||
|
||||
if not PY3:
|
||||
install_requires += [
|
||||
"unicodecsv>=0.9.4"
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name='wagtail',
|
||||
version='0.3.1',
|
||||
|
|
@ -37,23 +66,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
64
tox.ini
|
|
@ -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
|
||||
|
|
|
|||
0
wagtail/contrib/wagtailfrontendcache/__init__.py
Normal file
0
wagtail/contrib/wagtailfrontendcache/__init__.py
Normal file
0
wagtail/contrib/wagtailfrontendcache/models.py
Normal file
0
wagtail/contrib/wagtailfrontendcache/models.py
Normal file
25
wagtail/contrib/wagtailfrontendcache/signal_handlers.py
Normal file
25
wagtail/contrib/wagtailfrontendcache/signal_handlers.py
Normal 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)
|
||||
35
wagtail/contrib/wagtailfrontendcache/utils.py
Normal file
35
wagtail/contrib/wagtailfrontendcache/utils.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class CustomHTTPAdapter(HTTPAdapter):
|
||||
"""
|
||||
Requests will always send requests to whatever server is in the netloc
|
||||
part of the URL. This is a problem with purging the cache as this netloc
|
||||
may point to a different server (such as an nginx instance running in
|
||||
front of the cache).
|
||||
|
||||
This class allows us to send a purge request directly to the cache server
|
||||
with the host header still set correctly. It does this by changing the "url"
|
||||
parameter of get_connection to always point to the cache server. Requests
|
||||
will then use this connection to purge the page.
|
||||
"""
|
||||
def __init__(self, cache_url):
|
||||
self.cache_url = cache_url
|
||||
super(CustomHTTPAdapter, self).__init__()
|
||||
|
||||
def get_connection(self, url, proxies=None):
|
||||
return super(CustomHTTPAdapter, self).get_connection(self.cache_url, proxies)
|
||||
|
||||
|
||||
def purge_page_from_cache(page):
|
||||
# Get session
|
||||
cache_server_url = getattr(settings, 'WAGTAILFRONTENDCACHE_LOCATION', 'http://127.0.0.1:8000/')
|
||||
session = requests.Session()
|
||||
session.mount('http://', CustomHTTPAdapter(cache_server_url))
|
||||
|
||||
# Purge paths from cache
|
||||
for path in page.get_cached_paths():
|
||||
session.request('PURGE', page.full_url + path[1:])
|
||||
0
wagtail/contrib/wagtailsitemaps/__init__.py
Normal file
0
wagtail/contrib/wagtailsitemaps/__init__.py
Normal file
0
wagtail/contrib/wagtailsitemaps/models.py
Normal file
0
wagtail/contrib/wagtailsitemaps/models.py
Normal file
21
wagtail/contrib/wagtailsitemaps/sitemap_generator.py
Normal file
21
wagtail/contrib/wagtailsitemaps/sitemap_generator.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from django.template.loader import render_to_string
|
||||
|
||||
|
||||
class Sitemap(object):
|
||||
template = 'wagtailsitemaps/sitemap.xml'
|
||||
|
||||
def __init__(self, site):
|
||||
self.site = site
|
||||
|
||||
def get_pages(self):
|
||||
return self.site.root_page.get_descendants(inclusive=True).live().order_by('path')
|
||||
|
||||
def get_urls(self):
|
||||
for page in self.get_pages():
|
||||
for url in page.get_sitemap_urls():
|
||||
yield url
|
||||
|
||||
def render(self):
|
||||
return render_to_string(self.template, {
|
||||
'urlset': self.get_urls()
|
||||
})
|
||||
|
|
@ -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>
|
||||
83
wagtail/contrib/wagtailsitemaps/tests.py
Normal file
83
wagtail/contrib/wagtailsitemaps/tests.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from django.test import TestCase
|
||||
from django.core.cache import cache
|
||||
|
||||
from wagtail.wagtailcore.models import Page, Site
|
||||
from wagtail.tests.models import SimplePage
|
||||
|
||||
from .sitemap_generator import Sitemap
|
||||
|
||||
|
||||
class TestSitemapGenerator(TestCase):
|
||||
def setUp(self):
|
||||
self.home_page = Page.objects.get(id=2)
|
||||
|
||||
self.child_page = self.home_page.add_child(instance=SimplePage(
|
||||
title="Hello world!",
|
||||
slug='hello-world',
|
||||
live=True,
|
||||
))
|
||||
|
||||
self.unpublished_child_page = self.home_page.add_child(instance=SimplePage(
|
||||
title="Unpublished",
|
||||
slug='unpublished',
|
||||
live=False,
|
||||
))
|
||||
|
||||
self.site = Site.objects.get(is_default_site=True)
|
||||
|
||||
def test_get_pages(self):
|
||||
sitemap = Sitemap(self.site)
|
||||
pages = sitemap.get_pages()
|
||||
|
||||
self.assertIn(self.child_page.page_ptr, pages)
|
||||
self.assertNotIn(self.unpublished_child_page.page_ptr, pages)
|
||||
|
||||
def test_get_urls(self):
|
||||
sitemap = Sitemap(self.site)
|
||||
urls = [url['location'] for url in sitemap.get_urls()]
|
||||
|
||||
self.assertIn('/', urls) # Homepage
|
||||
self.assertIn('/hello-world/', urls) # Child page
|
||||
|
||||
def test_render(self):
|
||||
sitemap = Sitemap(self.site)
|
||||
xml = sitemap.render()
|
||||
|
||||
# Check that a URL has made it into the xml
|
||||
self.assertIn('/hello-world/', xml)
|
||||
|
||||
# Make sure the unpublished page didn't make it into the xml
|
||||
self.assertNotIn('/unpublished/', xml)
|
||||
|
||||
|
||||
class TestSitemapView(TestCase):
|
||||
def test_sitemap_view(self):
|
||||
response = self.client.get('/sitemap.xml')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailsitemaps/sitemap.xml')
|
||||
self.assertEqual(response['Content-Type'], 'text/xml; charset=utf-8')
|
||||
|
||||
def test_sitemap_view_cache(self):
|
||||
cache_key = 'wagtail-sitemap:%d' % Site.objects.get(is_default_site=True).id
|
||||
|
||||
# Check that the key is not in the cache
|
||||
self.assertFalse(cache.has_key(cache_key))
|
||||
|
||||
# Hit the view
|
||||
first_response = self.client.get('/sitemap.xml')
|
||||
|
||||
self.assertEqual(first_response.status_code, 200)
|
||||
self.assertTemplateUsed(first_response, 'wagtailsitemaps/sitemap.xml')
|
||||
|
||||
# Check that the key is in the cache
|
||||
self.assertTrue(cache.has_key(cache_key))
|
||||
|
||||
# Hit the view again. Should come from the cache this time
|
||||
second_response = self.client.get('/sitemap.xml')
|
||||
|
||||
self.assertEqual(second_response.status_code, 200)
|
||||
self.assertTemplateNotUsed(second_response, 'wagtailsitemaps/sitemap.xml') # Sitemap should not be re rendered
|
||||
|
||||
# Check that the content is the same
|
||||
self.assertEqual(first_response.content, second_response.content)
|
||||
23
wagtail/contrib/wagtailsitemaps/views.py
Normal file
23
wagtail/contrib/wagtailsitemaps/views.py
Normal 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
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
29
wagtail/tests/fixtures/test.json
vendored
29
wagtail/tests/fixtures/test.json
vendored
|
|
@ -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",
|
||||
|
|
@ -477,5 +488,23 @@
|
|||
"page": 8,
|
||||
"submit_time": "2014-01-01T12:00:00.000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "wagtaildocs.Document",
|
||||
"fields": {
|
||||
"title": "test document",
|
||||
"created_at": "2014-01-01T12:00:00.000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "wagtailimages.Image",
|
||||
"fields": {
|
||||
"title": "test image",
|
||||
"created_at": "2014-01-01T12:00:00.000Z",
|
||||
"width": 0,
|
||||
"height": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from django.db import models
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from modelcluster.fields import ParentalKey
|
||||
|
||||
|
|
@ -306,6 +307,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)
|
||||
|
|
@ -315,7 +317,7 @@ class Advert(models.Model):
|
|||
FieldPanel('text'),
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.text
|
||||
|
||||
|
||||
|
|
@ -328,18 +330,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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{% load pageurl %}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{% load pageurl %}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{% load pageurl %}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% load pageurl %}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<ul>
|
||||
{% for event in events %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{% load pageurl %}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from wagtail.wagtailcore import urls as wagtail_urls
|
|||
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
||||
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
||||
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls
|
||||
from wagtail.contrib.wagtailsitemaps.views import sitemap
|
||||
|
||||
# Signal handlers
|
||||
from wagtail.wagtailsearch import register_signal_handlers as wagtailsearch_register_signal_handlers
|
||||
|
|
@ -15,6 +16,8 @@ urlpatterns = patterns('',
|
|||
url(r'^search/', include(wagtailsearch_frontend_urls)),
|
||||
url(r'^documents/', include(wagtaildocs_urls)),
|
||||
|
||||
url(r'^sitemap\.xml$', sitemap),
|
||||
|
||||
# For anything not caught by a more specific rule above, hand over to
|
||||
# Wagtail's serving mechanism
|
||||
url(r'', include(wagtail_urls)),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.six.moves.urllib.parse import urlparse, ParseResult
|
||||
from django.http import QueryDict
|
||||
|
||||
# We need to make sure that we're using the same unittest library that Django uses internally
|
||||
# Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes
|
||||
|
||||
def editor_css():
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
@ -588,7 +601,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 +610,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 +635,7 @@ Page.promote_panels = [
|
|||
FieldPanel('search_description'),
|
||||
], ugettext_lazy('Common page configuration')),
|
||||
]
|
||||
|
||||
Page.settings_panels = [
|
||||
PublishingPanel()
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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'));
|
||||
|
||||
|
|
|
|||
|
|
@ -2829,6 +2829,7 @@
|
|||
} else {
|
||||
|
||||
}
|
||||
|
||||
return this.toolbar.css('left', this.element.offset().left - 2);
|
||||
},
|
||||
_updatePosition: function(position) {},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
@ -261,7 +264,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
|
|||
left:0;
|
||||
top:0;
|
||||
width:2em;
|
||||
line-height:2em;
|
||||
line-height:1.85em;
|
||||
height:100%;
|
||||
text-align:center;
|
||||
background-color:rgba(0,0,0,0.2);
|
||||
|
|
@ -275,7 +278,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
|
|||
&:before{
|
||||
width:2em;
|
||||
font-size:0.9rem;
|
||||
line-height:2em;
|
||||
line-height:1.65em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,21 +303,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 +430,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{
|
||||
|
|
@ -697,9 +746,14 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
|
|||
.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 +769,12 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
|
|||
}
|
||||
|
||||
&.bicolor{
|
||||
padding-left:3.5em;
|
||||
padding-left:3.7em;
|
||||
|
||||
&:before{
|
||||
width:2.2em;
|
||||
line-height:2.45em;
|
||||
width:2em;
|
||||
line-height:2.2em;
|
||||
font-size:1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -758,8 +813,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -97,13 +97,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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -17,6 +17,7 @@
|
|||
<glyph unicode="5" d="M135 424h241v-23h-241zM405 247l-127-124v222h-45v-220l-125 122-33-32 181-181 181 181z" />
|
||||
<glyph unicode="6" d="M136 424h241v-23h-241zM108 122l126 124v-222h45v220l126-122 32 32-181 181-181-181z" />
|
||||
<glyph unicode="7" 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="8" 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="9" 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="?" 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="A" 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 |
Binary file not shown.
Binary file not shown.
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,20 +244,20 @@
|
|||
top:0px;
|
||||
left:0px;
|
||||
width:3.3em;
|
||||
background-color:$color-teal;
|
||||
padding:0;
|
||||
margin:0 0 0 -20px;
|
||||
cursor:pointer;
|
||||
|
||||
a{
|
||||
font-size: 0em;
|
||||
line-height: 0;
|
||||
.button{
|
||||
@include border-radius(0);
|
||||
overflow: visible;
|
||||
display:block;
|
||||
display:inline-block;
|
||||
padding:0;
|
||||
width:3.45em;
|
||||
background-color:$color-salmon;
|
||||
|
||||
&:before{
|
||||
position:relative;
|
||||
color:white;
|
||||
padding:0;
|
||||
line-height:1.8em; /* specific height required as parent 'a' has no height */
|
||||
font-size:1.4rem;
|
||||
|
|
@ -260,7 +269,6 @@
|
|||
|
||||
.multiple{
|
||||
padding:0;
|
||||
max-height:0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -16,11 +16,9 @@
|
|||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
{% blocktrans with last_mod=page.get_latest_revision.created_at %}Last modified: {{ last_mod }}{% endblocktrans %}
|
||||
{% if page.get_latest_revision.user %}
|
||||
{% blocktrans with modified_by=page.get_latest_revision.user.get_full_name|default:page.get_latest_revision.user.username %}by {{ modified_by }}{% endblocktrans %}
|
||||
{% if request.user.email %}
|
||||
{% if page.get_latest_revision.user.email %}
|
||||
<span class="avatar small icon icon-user"><img src="{% gravatar_url page.get_latest_revision.user.email 25 %}" /></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if include_self %}
|
||||
<li><a href="{% if choosing %}{% url 'wagtailadmin_choose_page_child' page.id %}?{{ querystring }}{% else %}{% url 'wagtailadmin_explore' page.id %}{% endif %}" {% if choosing %}class="navigate-pages"{% endif %}>>{{ page.title }}</a></li>
|
||||
<li><a href="{% if choosing %}{% url 'wagtailadmin_choose_page_child' page.id %}?{{ querystring }}{% else %}{% url 'wagtailadmin_explore' page.id %}{% endif %}" {% if choosing %}class="navigate-pages"{% endif %}>{{ page.title }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@
|
|||
### <img src="{% gravatar_url sometemplatevariable %}">
|
||||
### just make sure to update the "default" image path below
|
||||
|
||||
import urllib
|
||||
import hashlib
|
||||
|
||||
from six import b
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
|
@ -30,8 +32,8 @@ class GravatarUrlNode(template.Node):
|
|||
default = "blank"
|
||||
size = int(self.size) * 2 # requested at retina size by default and scaled down at point of use with css
|
||||
|
||||
gravatar_url = "//www.gravatar.com/avatar/" + hashlib.md5(email.lower()).hexdigest() + "?"
|
||||
gravatar_url += urllib.urlencode({'s': str(size), 'd': default})
|
||||
gravatar_url = "//www.gravatar.com/avatar/" + hashlib.md5(b(email.lower())).hexdigest() + "?"
|
||||
gravatar_url += urlencode({'s': str(size), 'd': default})
|
||||
|
||||
return gravatar_url
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django import template
|
||||
from django.core import urlresolvers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import get_navigation_menu_items, UserPagePermissionsProxy
|
||||
from wagtail.wagtailcore.utils import camelcase_to_underscore
|
||||
|
||||
|
|
@ -95,4 +97,4 @@ def hook_output(hook_name):
|
|||
Note that the output is not escaped - it is the hook function's responsibility to escape unsafe content.
|
||||
"""
|
||||
snippets = [fn() for fn in hooks.get_hooks(hook_name)]
|
||||
return u''.join(snippets)
|
||||
return ''.join(snippets)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
|
|
@ -5,7 +7,7 @@ from django.contrib.auth.models import Group, Permission
|
|||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core import mail
|
||||
|
||||
from wagtail.tests.utils import unittest, WagtailTestUtils
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
from wagtail.wagtailusers.models import UserProfile
|
||||
|
||||
|
||||
|
|
@ -199,9 +201,9 @@ class TestAccountSection(TestCase, WagtailTestUtils):
|
|||
"""
|
||||
# Post new values to the notification preferences page
|
||||
post_data = {
|
||||
'submitted_notifications': u'false',
|
||||
'approved_notifications': u'false',
|
||||
'rejected_notifications': u'true',
|
||||
'submitted_notifications': 'false',
|
||||
'approved_notifications': 'false',
|
||||
'rejected_notifications': 'true',
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_account_notification_preferences'), post_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core import mail
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils import timezone
|
||||
|
||||
from wagtail.tests.models import SimplePage, EventPage, StandardIndex, StandardChild, BusinessIndex, BusinessChild, BusinessSubIndex
|
||||
from wagtail.tests.utils import unittest, WagtailTestUtils
|
||||
from wagtail.tests.models import SimplePage, EventPage, StandardIndex, BusinessIndex, BusinessChild, BusinessSubIndex
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
from wagtail.wagtailcore.models import Page, PageRevision
|
||||
from wagtail.wagtailcore.signals import page_published
|
||||
from wagtail.wagtailusers.models import UserProfile
|
||||
|
||||
|
||||
|
|
@ -170,7 +174,71 @@ class TestPageCreation(TestCase, WagtailTestUtils):
|
|||
self.assertIsInstance(page, SimplePage)
|
||||
self.assertFalse(page.live)
|
||||
|
||||
def test_create_simplepage_scheduled(self):
|
||||
go_live_at = timezone.now() + timedelta(days=1)
|
||||
expire_at = timezone.now() + timedelta(days=2)
|
||||
post_data = {
|
||||
'title': "New page!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'go_live_at': str(go_live_at).split('.')[0],
|
||||
'expire_at': str(expire_at).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
|
||||
|
||||
# Should be redirected to explorer page
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Find the page and check the scheduled times
|
||||
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
|
||||
self.assertEqual(page.go_live_at.date(), go_live_at.date())
|
||||
self.assertEqual(page.expire_at.date(), expire_at.date())
|
||||
self.assertEqual(page.expired, False)
|
||||
self.assertTrue(page.status_string, "draft")
|
||||
|
||||
# No revisions with approved_go_live_at
|
||||
self.assertFalse(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
def test_create_simplepage_scheduled_go_live_before_expiry(self):
|
||||
post_data = {
|
||||
'title': "New page!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0],
|
||||
'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that a form error was raised
|
||||
self.assertFormError(response, 'form', 'go_live_at', "Go live date/time must be before expiry date/time")
|
||||
self.assertFormError(response, 'form', 'expire_at', "Go live date/time must be before expiry date/time")
|
||||
|
||||
def test_create_simplepage_scheduled_expire_in_the_past(self):
|
||||
post_data = {
|
||||
'title': "New page!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that a form error was raised
|
||||
self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
|
||||
|
||||
def test_create_simplepage_post_publish(self):
|
||||
# Connect a mock signal handler to page_published signal
|
||||
signal_fired = [False]
|
||||
signal_page = [None]
|
||||
def page_published_handler(sender, instance, **kwargs):
|
||||
signal_fired[0] = True
|
||||
signal_page[0] = instance
|
||||
page_published.connect(page_published_handler)
|
||||
|
||||
# Post
|
||||
post_data = {
|
||||
'title': "New page!",
|
||||
'content': "Some content",
|
||||
|
|
@ -188,6 +256,39 @@ class TestPageCreation(TestCase, WagtailTestUtils):
|
|||
self.assertIsInstance(page, SimplePage)
|
||||
self.assertTrue(page.live)
|
||||
|
||||
# Check that the page_published signal was fired
|
||||
self.assertTrue(signal_fired[0])
|
||||
self.assertEqual(signal_page[0], page)
|
||||
self.assertEqual(signal_page[0], signal_page[0].specific)
|
||||
|
||||
def test_create_simplepage_post_publish_scheduled(self):
|
||||
go_live_at = timezone.now() + timedelta(days=1)
|
||||
expire_at = timezone.now() + timedelta(days=2)
|
||||
post_data = {
|
||||
'title': "New page!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'action-publish': "Publish",
|
||||
'go_live_at': str(go_live_at).split('.')[0],
|
||||
'expire_at': str(expire_at).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
|
||||
|
||||
# Should be redirected to explorer page
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Find the page and check it
|
||||
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
|
||||
self.assertEqual(page.go_live_at.date(), go_live_at.date())
|
||||
self.assertEqual(page.expire_at.date(), expire_at.date())
|
||||
self.assertEqual(page.expired, False)
|
||||
|
||||
# A revision with approved_go_live_at should exist now
|
||||
self.assertTrue(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists())
|
||||
# But Page won't be live
|
||||
self.assertFalse(page.live)
|
||||
self.assertTrue(page.status_string, "scheduled")
|
||||
|
||||
def test_create_simplepage_post_submit(self):
|
||||
# Create a moderator user for testing email
|
||||
moderator = get_user_model().objects.create_superuser('moderator', 'moderator@email.com', 'password')
|
||||
|
|
@ -246,7 +347,6 @@ class TestPageCreation(TestCase, WagtailTestUtils):
|
|||
response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', 100000)))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@unittest.expectedFailure # FIXME: Crashes!
|
||||
def test_create_nonpagetype(self):
|
||||
response = self.client.get(reverse('wagtailadmin_pages_create', args=('wagtailimages', 'image', self.root_page.id)))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
|
@ -328,7 +428,72 @@ class TestPageEdit(TestCase, WagtailTestUtils):
|
|||
child_page_new = SimplePage.objects.get(id=self.child_page.id)
|
||||
self.assertTrue(child_page_new.has_unpublished_changes)
|
||||
|
||||
def test_edit_post_scheduled(self):
|
||||
go_live_at = timezone.now() + timedelta(days=1)
|
||||
expire_at = timezone.now() + timedelta(days=2)
|
||||
post_data = {
|
||||
'title': "I've been edited!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'go_live_at': str(go_live_at).split('.')[0],
|
||||
'expire_at': str(expire_at).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
|
||||
|
||||
# Should be redirected to explorer page
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
child_page_new = SimplePage.objects.get(id=self.child_page.id)
|
||||
|
||||
# The page will still be live
|
||||
self.assertTrue(child_page_new.live)
|
||||
|
||||
# A revision with approved_go_live_at should not exist
|
||||
self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
# But a revision with go_live_at and expire_at in their content json *should* exist
|
||||
self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(go_live_at.date())).exists())
|
||||
self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(expire_at.date())).exists())
|
||||
|
||||
def test_edit_scheduled_go_live_before_expiry(self):
|
||||
post_data = {
|
||||
'title': "I've been edited!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0],
|
||||
'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that a form error was raised
|
||||
self.assertFormError(response, 'form', 'go_live_at', "Go live date/time must be before expiry date/time")
|
||||
self.assertFormError(response, 'form', 'expire_at', "Go live date/time must be before expiry date/time")
|
||||
|
||||
def test_edit_scheduled_expire_in_the_past(self):
|
||||
post_data = {
|
||||
'title': "I've been edited!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that a form error was raised
|
||||
self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
|
||||
|
||||
def test_page_edit_post_publish(self):
|
||||
# Connect a mock signal handler to page_published signal
|
||||
signal_fired = [False]
|
||||
signal_page = [None]
|
||||
def page_published_handler(sender, instance, **kwargs):
|
||||
signal_fired[0] = True
|
||||
signal_page[0] = instance
|
||||
page_published.connect(page_published_handler)
|
||||
|
||||
# Tests publish from edit page
|
||||
post_data = {
|
||||
'title': "I've been edited!",
|
||||
|
|
@ -345,9 +510,85 @@ class TestPageEdit(TestCase, WagtailTestUtils):
|
|||
child_page_new = SimplePage.objects.get(id=self.child_page.id)
|
||||
self.assertEqual(child_page_new.title, post_data['title'])
|
||||
|
||||
# Check that the page_published signal was fired
|
||||
self.assertTrue(signal_fired[0])
|
||||
self.assertEqual(signal_page[0], child_page_new)
|
||||
self.assertEqual(signal_page[0], signal_page[0].specific)
|
||||
|
||||
# The page shouldn't have "has_unpublished_changes" flag set
|
||||
self.assertFalse(child_page_new.has_unpublished_changes)
|
||||
|
||||
def test_edit_post_publish_scheduled(self):
|
||||
go_live_at = timezone.now() + timedelta(days=1)
|
||||
expire_at = timezone.now() + timedelta(days=2)
|
||||
post_data = {
|
||||
'title': "I've been edited!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'action-publish': "Publish",
|
||||
'go_live_at': str(go_live_at).split('.')[0],
|
||||
'expire_at': str(expire_at).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
|
||||
|
||||
# Should be redirected to explorer page
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
child_page_new = SimplePage.objects.get(id=self.child_page.id)
|
||||
|
||||
# The page should not be live anymore
|
||||
self.assertFalse(child_page_new.live)
|
||||
|
||||
# Instead a revision with approved_go_live_at should now exist
|
||||
self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
def test_edit_post_publish_now_an_already_scheduled(self):
|
||||
# First let's publish a page with a go_live_at in the future
|
||||
go_live_at = timezone.now() + timedelta(days=1)
|
||||
expire_at = timezone.now() + timedelta(days=2)
|
||||
post_data = {
|
||||
'title': "I've been edited!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'action-publish': "Publish",
|
||||
'go_live_at': str(go_live_at).split('.')[0],
|
||||
'expire_at': str(expire_at).split('.')[0],
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
|
||||
|
||||
# Should be redirected to explorer page
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
child_page_new = SimplePage.objects.get(id=self.child_page.id)
|
||||
|
||||
# The page should not be live anymore
|
||||
self.assertFalse(child_page_new.live)
|
||||
|
||||
# Instead a revision with approved_go_live_at should now exist
|
||||
self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
# Now, let's edit it and publish it right now
|
||||
go_live_at = timezone.now()
|
||||
post_data = {
|
||||
'title': "I've been edited!",
|
||||
'content': "Some content",
|
||||
'slug': 'hello-world',
|
||||
'action-publish': "Publish",
|
||||
'go_live_at': "",
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
|
||||
|
||||
# Should be redirected to explorer page
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
child_page_new = SimplePage.objects.get(id=self.child_page.id)
|
||||
|
||||
# The page should be live now
|
||||
self.assertTrue(child_page_new.live)
|
||||
|
||||
# And a revision with approved_go_live_at should not exist
|
||||
self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
def test_page_edit_post_submit(self):
|
||||
# Create a moderator user for testing email
|
||||
moderator = get_user_model().objects.create_superuser('moderator', 'moderator@email.com', 'password')
|
||||
|
|
@ -643,6 +884,14 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
|
|||
"""
|
||||
This posts to the approve moderation view and checks that the page was approved
|
||||
"""
|
||||
# Connect a mock signal handler to page_published signal
|
||||
signal_fired = [False]
|
||||
signal_page = [None]
|
||||
def page_published_handler(sender, instance, **kwargs):
|
||||
signal_fired[0] = True
|
||||
signal_page[0] = instance
|
||||
page_published.connect(page_published_handler)
|
||||
|
||||
# Post
|
||||
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )), {
|
||||
'foo': "Must post something or the view won't see this as a POST request",
|
||||
|
|
@ -654,6 +903,11 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
|
|||
# Page must be live
|
||||
self.assertTrue(Page.objects.get(id=self.page.id).live)
|
||||
|
||||
# Check that the page_published signal was fired
|
||||
self.assertTrue(signal_fired[0])
|
||||
self.assertEqual(signal_page[0], self.page)
|
||||
self.assertEqual(signal_page[0], signal_page[0].specific)
|
||||
|
||||
def test_approve_moderation_view_bad_revision_id(self):
|
||||
"""
|
||||
This tests that the approve moderation view handles invalid revision ids correctly
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from django.test import TestCase
|
||||
from wagtail.tests.models import SimplePage, EventPage
|
||||
from wagtail.tests.utils import unittest, WagtailTestUtils
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailadmin.tasks import send_email_task
|
||||
from django.core.urlresolvers import reverse
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
from django.conf.urls import url
|
||||
from django.conf import settings
|
||||
|
||||
from wagtail.wagtailadmin.forms import LoginForm, PasswordResetForm
|
||||
from wagtail.wagtailadmin.forms import PasswordResetForm
|
||||
from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ from django.conf import settings
|
|||
from django.template import RequestContext
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailadmin.forms import SearchForm
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page, PageRevision, UserPagePermissionsProxy
|
||||
|
||||
from wagtail.wagtaildocs.models import Document
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@ from django.contrib import messages
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList
|
||||
from wagtail.wagtailadmin.forms import SearchForm
|
||||
from wagtail.wagtailadmin import tasks, hooks, signals
|
||||
from wagtail.wagtailadmin import tasks, signals
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page, PageRevision
|
||||
from wagtail.wagtailcore.signals import page_published
|
||||
|
||||
|
||||
@permission_required('wagtailadmin.access_admin')
|
||||
|
|
@ -115,12 +118,17 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
|
|||
except ContentType.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
# Get class
|
||||
page_class = content_type.model_class()
|
||||
|
||||
# Make sure the class is a descendant of Page
|
||||
if not issubclass(page_class, Page):
|
||||
raise Http404
|
||||
|
||||
# page must be in the list of allowed subpage types for this parent ID
|
||||
if content_type not in parent_page.clean_subpage_types():
|
||||
raise PermissionDenied
|
||||
|
||||
page_class = content_type.model_class()
|
||||
|
||||
page = page_class(owner=request.user)
|
||||
edit_handler_class = get_page_edit_handler(page_class)
|
||||
form_class = edit_handler_class.get_form_class(page_class)
|
||||
|
|
@ -136,23 +144,66 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
|
|||
return slug
|
||||
form.fields['slug'].clean = clean_slug
|
||||
|
||||
# Stick another validator into the form to check that the scheduled publishing settings are set correctly
|
||||
def clean():
|
||||
cleaned_data = form_class.clean(form)
|
||||
|
||||
# Go live must be before expire
|
||||
go_live_at = cleaned_data.get('go_live_at')
|
||||
expire_at = cleaned_data.get('expire_at')
|
||||
|
||||
if go_live_at and expire_at:
|
||||
if go_live_at > expire_at:
|
||||
msg = _('Go live date/time must be before expiry date/time')
|
||||
form._errors['go_live_at'] = form.error_class([msg])
|
||||
form._errors['expire_at'] = form.error_class([msg])
|
||||
del cleaned_data['go_live_at']
|
||||
del cleaned_data['expire_at']
|
||||
|
||||
# Expire must be in the future
|
||||
expire_at = cleaned_data.get('expire_at')
|
||||
|
||||
if expire_at and expire_at < timezone.now():
|
||||
form._errors['expire_at'] = form.error_class([_('Expiry date/time must be in the future')])
|
||||
del cleaned_data['expire_at']
|
||||
|
||||
return cleaned_data
|
||||
form.clean = clean
|
||||
|
||||
if form.is_valid():
|
||||
page = form.save(commit=False) # don't save yet, as we need treebeard to assign tree params
|
||||
|
||||
is_publishing = bool(request.POST.get('action-publish')) and parent_page_perms.can_publish_subpage()
|
||||
is_submitting = bool(request.POST.get('action-submit'))
|
||||
go_live_at = form.cleaned_data.get('go_live_at')
|
||||
future_go_live = go_live_at and go_live_at > timezone.now()
|
||||
approved_go_live_at = None
|
||||
|
||||
if is_publishing:
|
||||
page.live = True
|
||||
page.has_unpublished_changes = False
|
||||
page.expired = False
|
||||
if future_go_live:
|
||||
page.live = False
|
||||
# Set approved_go_live_at only if is publishing
|
||||
# and the future_go_live is actually in future
|
||||
approved_go_live_at = go_live_at
|
||||
else:
|
||||
page.live = True
|
||||
else:
|
||||
page.live = False
|
||||
page.has_unpublished_changes = True
|
||||
|
||||
parent_page.add_child(instance=page) # assign tree parameters - will cause page to be saved
|
||||
page.save_revision(user=request.user, submitted_for_moderation=is_submitting)
|
||||
|
||||
# Pass approved_go_live_at to save_revision
|
||||
page.save_revision(
|
||||
user=request.user,
|
||||
submitted_for_moderation=is_submitting,
|
||||
approved_go_live_at=approved_go_live_at
|
||||
)
|
||||
|
||||
if is_publishing:
|
||||
page_published.send(sender=page_class, instance=page)
|
||||
messages.success(request, _("Page '{0}' published.").format(page.title))
|
||||
elif is_submitting:
|
||||
messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title))
|
||||
|
|
@ -167,7 +218,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
|
|||
|
||||
return redirect('wagtailadmin_explore', page.get_parent().id)
|
||||
else:
|
||||
messages.error(request, _("The page could not be created due to errors."))
|
||||
messages.error(request, _("The page could not be created due to validation errors"))
|
||||
edit_handler = edit_handler_class(instance=page, form=form)
|
||||
else:
|
||||
signals.init_new_page.send(sender=create, page=page, parent=parent_page)
|
||||
|
|
@ -210,15 +261,54 @@ def edit(request, page_id):
|
|||
return slug
|
||||
form.fields['slug'].clean = clean_slug
|
||||
|
||||
# Stick another validator into the form to check that the scheduled publishing settings are set correctly
|
||||
def clean():
|
||||
cleaned_data = form_class.clean(form)
|
||||
|
||||
# Go live must be before expire
|
||||
go_live_at = cleaned_data.get('go_live_at')
|
||||
expire_at = cleaned_data.get('expire_at')
|
||||
|
||||
if go_live_at and expire_at:
|
||||
if go_live_at > expire_at:
|
||||
msg = _('Go live date/time must be before expiry date/time')
|
||||
form._errors['go_live_at'] = form.error_class([msg])
|
||||
form._errors['expire_at'] = form.error_class([msg])
|
||||
del cleaned_data['go_live_at']
|
||||
del cleaned_data['expire_at']
|
||||
|
||||
# Expire must be in the future
|
||||
expire_at = cleaned_data.get('expire_at')
|
||||
|
||||
if expire_at and expire_at < timezone.now():
|
||||
form._errors['expire_at'] = form.error_class([_('Expiry date/time must be in the future')])
|
||||
del cleaned_data['expire_at']
|
||||
|
||||
return cleaned_data
|
||||
form.clean = clean
|
||||
|
||||
if form.is_valid():
|
||||
is_publishing = bool(request.POST.get('action-publish')) and page_perms.can_publish()
|
||||
is_submitting = bool(request.POST.get('action-submit'))
|
||||
go_live_at = form.cleaned_data.get('go_live_at')
|
||||
future_go_live = go_live_at and go_live_at > timezone.now()
|
||||
approved_go_live_at = None
|
||||
|
||||
if is_publishing:
|
||||
page.live = True
|
||||
page.has_unpublished_changes = False
|
||||
page.expired = False
|
||||
if future_go_live:
|
||||
page.live = False
|
||||
# Set approved_go_live_at only if publishing
|
||||
approved_go_live_at = go_live_at
|
||||
else:
|
||||
page.live = True
|
||||
form.save()
|
||||
page.revisions.update(submitted_for_moderation=False)
|
||||
# Clear approved_go_live_at for older revisions
|
||||
page.revisions.update(
|
||||
submitted_for_moderation=False,
|
||||
approved_go_live_at=None,
|
||||
)
|
||||
else:
|
||||
# not publishing the page
|
||||
if page.live:
|
||||
|
|
@ -230,9 +320,14 @@ def edit(request, page_id):
|
|||
page.has_unpublished_changes = True
|
||||
form.save()
|
||||
|
||||
page.save_revision(user=request.user, submitted_for_moderation=is_submitting)
|
||||
page.save_revision(
|
||||
user=request.user,
|
||||
submitted_for_moderation=is_submitting,
|
||||
approved_go_live_at=approved_go_live_at
|
||||
)
|
||||
|
||||
if is_publishing:
|
||||
page_published.send(sender=page.__class__, instance=page)
|
||||
messages.success(request, _("Page '{0}' published.").format(page.title))
|
||||
elif is_submitting:
|
||||
messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title))
|
||||
|
|
@ -248,10 +343,11 @@ def edit(request, page_id):
|
|||
return redirect('wagtailadmin_explore', page.get_parent().id)
|
||||
else:
|
||||
messages.error(request, _("The page could not be saved due to validation errors"))
|
||||
|
||||
edit_handler = edit_handler_class(instance=page, form=form)
|
||||
errors_debug = (
|
||||
repr(edit_handler.form.errors)
|
||||
+ repr([(name, formset.errors) for (name, formset) in edit_handler.form.formsets.iteritems() if formset.errors])
|
||||
+ repr([(name, formset.errors) for (name, formset) in edit_handler.form.formsets.items() if formset.errors])
|
||||
)
|
||||
else:
|
||||
form = form_class(instance=page)
|
||||
|
|
@ -439,6 +535,8 @@ def unpublish(request, page_id):
|
|||
parent_id = page.get_parent().id
|
||||
page.live = False
|
||||
page.save()
|
||||
# Since page is unpublished clear the approved_go_live_at of all revisions
|
||||
page.revisions.update(approved_go_live_at=None)
|
||||
messages.success(request, _("Page '{0}' unpublished.").format(page.title))
|
||||
return redirect('wagtailadmin_explore', parent_id)
|
||||
|
||||
|
|
@ -541,7 +639,8 @@ def get_page_edit_handler(page_class):
|
|||
if page_class not in PAGE_EDIT_HANDLERS:
|
||||
PAGE_EDIT_HANDLERS[page_class] = TabbedInterface([
|
||||
ObjectList(page_class.content_panels, heading='Content'),
|
||||
ObjectList(page_class.promote_panels, heading='Promote')
|
||||
ObjectList(page_class.promote_panels, heading='Promote'),
|
||||
ObjectList(page_class.settings_panels, heading='Settings', classname="settings")
|
||||
])
|
||||
|
||||
return PAGE_EDIT_HANDLERS[page_class]
|
||||
|
|
@ -601,6 +700,7 @@ def approve_moderation(request, revision_id):
|
|||
|
||||
if request.POST:
|
||||
revision.publish()
|
||||
page_published.send(sender=revision.page.__class__, instance=revision.page.specific)
|
||||
messages.success(request, _("Page '{0}' published.").format(revision.page.title))
|
||||
tasks.send_notification.delay(revision.id, 'approved', request.user.id)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from django.shortcuts import render
|
|||
from django.contrib.auth.decorators import permission_required
|
||||
|
||||
from wagtail.wagtailadmin.userbar import EditPageItem, AddPageItem, ApproveModerationEditPageItem, RejectModerationEditPageItem
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page, PageRevision
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
# A setting that can be used in foreign key declarations
|
||||
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
|
||||
|
|
|
|||
38
wagtail/wagtailcore/hooks.py
Normal file
38
wagtail/wagtailcore/hooks.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
from django.conf import settings
|
||||
try:
|
||||
from importlib import import_module
|
||||
except ImportError:
|
||||
# for Python 2.6, fall back on django.utils.importlib (deprecated as of Django 1.7)
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
_hooks = {}
|
||||
|
||||
# TODO: support 'register' as a decorator:
|
||||
# @hooks.register('construct_main_menu')
|
||||
# def construct_main_menu(menu_items):
|
||||
# ...
|
||||
|
||||
|
||||
def register(hook_name, fn):
|
||||
if hook_name not in _hooks:
|
||||
_hooks[hook_name] = []
|
||||
_hooks[hook_name].append(fn)
|
||||
|
||||
_searched_for_hooks = False
|
||||
|
||||
|
||||
def search_for_hooks():
|
||||
global _searched_for_hooks
|
||||
if not _searched_for_hooks:
|
||||
for app_module in settings.INSTALLED_APPS:
|
||||
try:
|
||||
import_module('%s.wagtail_hooks' % app_module)
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
_searched_for_hooks = True
|
||||
|
||||
|
||||
def get_hooks(hook_name):
|
||||
search_for_hooks()
|
||||
return _hooks.get(hook_name, [])
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import json
|
||||
from optparse import make_option
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import dateparse, timezone
|
||||
from wagtail.wagtailcore.models import Page, PageRevision
|
||||
|
||||
|
||||
def revision_date_expired(r):
|
||||
expiry_str = json.loads(r.content_json).get('expire_at')
|
||||
if not expiry_str:
|
||||
return False
|
||||
expire_at = dateparse.parse_datetime(expiry_str)
|
||||
if expire_at < timezone.now():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option(
|
||||
'--dryrun',
|
||||
action='store_true',
|
||||
dest='dryrun',
|
||||
default=False,
|
||||
help='Dry run -- don\'t change anything.'),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dryrun = False
|
||||
if options['dryrun']:
|
||||
print("Will do a dry run.")
|
||||
dryrun = True
|
||||
|
||||
# 1. get all expired pages with live = True
|
||||
expired_pages = Page.objects.filter(
|
||||
live=True,
|
||||
expire_at__lt=timezone.now()
|
||||
)
|
||||
if dryrun:
|
||||
if expired_pages:
|
||||
print("Expired pages to be deactivated:")
|
||||
print("Expiry datetime\t\tSlug\t\tName")
|
||||
print("---------------\t\t----\t\t----")
|
||||
for ep in expired_pages:
|
||||
print("{0}\t{1}\t{2}".format(
|
||||
ep.expire_at.strftime("%Y-%m-%d %H:%M"),
|
||||
ep.slug,
|
||||
ep.title
|
||||
))
|
||||
else:
|
||||
print("No expired pages to be deactivated found.")
|
||||
else:
|
||||
expired_pages.update(expired=True, live=False)
|
||||
|
||||
# 2. get all page revisions for moderation that have been expired
|
||||
expired_revs = [
|
||||
r for r in PageRevision.objects.filter(
|
||||
submitted_for_moderation=True
|
||||
) if revision_date_expired(r)
|
||||
]
|
||||
if dryrun:
|
||||
print("---------------------------------")
|
||||
if expired_revs:
|
||||
print("Expired revisions to be dropped from moderation queue:")
|
||||
print("Expiry datetime\t\tSlug\t\tName")
|
||||
print("---------------\t\t----\t\t----")
|
||||
for er in expired_revs:
|
||||
rev_data = json.loads(er.content_json)
|
||||
print("{0}\t{1}\t{2}".format(
|
||||
dateparse.parse_datetime(
|
||||
rev_data.get('expire_at')
|
||||
).strftime("%Y-%m-%d %H:%M"),
|
||||
rev_data.get('slug'),
|
||||
rev_data.get('title')
|
||||
))
|
||||
else:
|
||||
print("No expired revision to be dropped from moderation.")
|
||||
else:
|
||||
for er in expired_revs:
|
||||
er.submitted_for_moderation = False
|
||||
er.save()
|
||||
|
||||
# 3. get all revisions that need to be published
|
||||
revs_for_publishing = PageRevision.objects.filter(
|
||||
approved_go_live_at__lt=timezone.now()
|
||||
)
|
||||
if dryrun:
|
||||
print("---------------------------------")
|
||||
if revs_for_publishing:
|
||||
print("Revisions to be published:")
|
||||
print("Go live datetime\t\tSlug\t\tName")
|
||||
print("---------------\t\t\t----\t\t----")
|
||||
for rp in revs_for_publishing:
|
||||
rev_data = json.loads(rp.content_json)
|
||||
print("{0}\t\t{1}\t{2}".format(
|
||||
rp.approved_go_live_at.strftime("%Y-%m-%d %H:%M"),
|
||||
rev_data.get('slug'),
|
||||
rev_data.get('title')
|
||||
))
|
||||
else:
|
||||
print("No pages to go live.")
|
||||
else:
|
||||
for rp in revs_for_publishing:
|
||||
# just run publish for the revision -- since the approved go
|
||||
# live datetime is before now it will make the page live
|
||||
rp.publish()
|
||||
|
|
@ -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 SchemaMigration
|
||||
|
|
@ -11,18 +13,18 @@ class Migration(SchemaMigration):
|
|||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'Site'
|
||||
db.create_table(u'wagtailcore_site', (
|
||||
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
db.create_table('wagtailcore_site', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('hostname', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, db_index=True)),
|
||||
('port', self.gf('django.db.models.fields.IntegerField')(default=80)),
|
||||
('root_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='sites_rooted_here', to=orm['wagtailcore.Page'])),
|
||||
('is_default_site', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
))
|
||||
db.send_create_signal(u'wagtailcore', ['Site'])
|
||||
db.send_create_signal('wagtailcore', ['Site'])
|
||||
|
||||
# Adding model 'Page'
|
||||
db.create_table(u'wagtailcore_page', (
|
||||
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
db.create_table('wagtailcore_page', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('path', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
|
||||
('depth', self.gf('django.db.models.fields.PositiveIntegerField')()),
|
||||
('numchild', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
|
||||
|
|
@ -37,55 +39,55 @@ class Migration(SchemaMigration):
|
|||
('show_in_menus', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
('search_description', self.gf('django.db.models.fields.TextField')(blank=True)),
|
||||
))
|
||||
db.send_create_signal(u'wagtailcore', ['Page'])
|
||||
db.send_create_signal('wagtailcore', ['Page'])
|
||||
|
||||
# Adding model 'PageRevision'
|
||||
db.create_table(u'wagtailcore_pagerevision', (
|
||||
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
db.create_table('wagtailcore_pagerevision', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='revisions', to=orm['wagtailcore.Page'])),
|
||||
('submitted_for_moderation', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[AUTH_USER_MODEL], null=True, blank=True)),
|
||||
('content_json', self.gf('django.db.models.fields.TextField')()),
|
||||
))
|
||||
db.send_create_signal(u'wagtailcore', ['PageRevision'])
|
||||
db.send_create_signal('wagtailcore', ['PageRevision'])
|
||||
|
||||
# Adding model 'GroupPagePermission'
|
||||
db.create_table(u'wagtailcore_grouppagepermission', (
|
||||
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
db.create_table('wagtailcore_grouppagepermission', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('group', self.gf('django.db.models.fields.related.ForeignKey')(related_name='page_permissions', to=orm['auth.Group'])),
|
||||
('page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='group_permissions', to=orm['wagtailcore.Page'])),
|
||||
('permission_type', self.gf('django.db.models.fields.CharField')(max_length=20)),
|
||||
))
|
||||
db.send_create_signal(u'wagtailcore', ['GroupPagePermission'])
|
||||
db.send_create_signal('wagtailcore', ['GroupPagePermission'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'Site'
|
||||
db.delete_table(u'wagtailcore_site')
|
||||
db.delete_table('wagtailcore_site')
|
||||
|
||||
# Deleting model 'Page'
|
||||
db.delete_table(u'wagtailcore_page')
|
||||
db.delete_table('wagtailcore_page')
|
||||
|
||||
# Deleting model 'PageRevision'
|
||||
db.delete_table(u'wagtailcore_pagerevision')
|
||||
db.delete_table('wagtailcore_pagerevision')
|
||||
|
||||
# Deleting model 'GroupPagePermission'
|
||||
db.delete_table(u'wagtailcore_grouppagepermission')
|
||||
db.delete_table('wagtailcore_grouppagepermission')
|
||||
|
||||
|
||||
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'})
|
||||
},
|
||||
AUTH_USER_MODEL: {
|
||||
|
|
@ -93,40 +95,40 @@ class Migration(SchemaMigration):
|
|||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
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'})
|
||||
},
|
||||
u'wagtailcore.grouppagepermission': {
|
||||
'wagtailcore.grouppagepermission': {
|
||||
'Meta': {'object_name': 'GroupPagePermission'},
|
||||
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}),
|
||||
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
|
||||
},
|
||||
u'wagtailcore.page': {
|
||||
'wagtailcore.page': {
|
||||
'Meta': {'object_name': 'Page'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['contenttypes.ContentType']"}),
|
||||
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['%s']" % AUTH_USER_MODEL}),
|
||||
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': "orm['%s']" % AUTH_USER_MODEL}),
|
||||
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
|
|
@ -135,22 +137,22 @@ class Migration(SchemaMigration):
|
|||
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
u'wagtailcore.pagerevision': {
|
||||
'wagtailcore.pagerevision': {
|
||||
'Meta': {'object_name': 'PageRevision'},
|
||||
'content_json': ('django.db.models.fields.TextField', [], {}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s']" % AUTH_USER_MODEL, 'null': 'True', 'blank': 'True'})
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % AUTH_USER_MODEL, 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'wagtailcore.site': {
|
||||
'wagtailcore.site': {
|
||||
'Meta': {'object_name': 'Site'},
|
||||
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
|
||||
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
|
||||
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': "orm['wagtailcore.Page']"})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -61,17 +63,17 @@ class Migration(DataMigration):
|
|||
orm['wagtailcore.page'].objects.all().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'})
|
||||
},
|
||||
AUTH_USER_MODEL: {
|
||||
|
|
@ -79,40 +81,40 @@ class Migration(DataMigration):
|
|||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
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'})
|
||||
},
|
||||
u'wagtailcore.grouppagepermission': {
|
||||
'wagtailcore.grouppagepermission': {
|
||||
'Meta': {'object_name': 'GroupPagePermission'},
|
||||
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}),
|
||||
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
|
||||
},
|
||||
u'wagtailcore.page': {
|
||||
'wagtailcore.page': {
|
||||
'Meta': {'object_name': 'Page'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['contenttypes.ContentType']"}),
|
||||
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['%s']" % AUTH_USER_MODEL}),
|
||||
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': "orm['%s']" % AUTH_USER_MODEL}),
|
||||
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
|
|
@ -121,22 +123,22 @@ class Migration(DataMigration):
|
|||
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
u'wagtailcore.pagerevision': {
|
||||
'wagtailcore.pagerevision': {
|
||||
'Meta': {'object_name': 'PageRevision'},
|
||||
'content_json': ('django.db.models.fields.TextField', [], {}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s']" % AUTH_USER_MODEL, 'null': 'True', 'blank': 'True'})
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % AUTH_USER_MODEL, 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'wagtailcore.site': {
|
||||
'wagtailcore.site': {
|
||||
'Meta': {'object_name': 'Site'},
|
||||
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
|
||||
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
|
||||
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': "orm['wagtailcore.Page']"})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
# -*- 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 SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Removing unique constraint on 'Site', fields ['hostname']
|
||||
db.delete_unique('wagtailcore_site', ['hostname'])
|
||||
|
||||
# Adding unique constraint on 'Site', fields ['hostname', 'port']
|
||||
db.create_unique('wagtailcore_site', ['hostname', 'port'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'Site', fields ['hostname', 'port']
|
||||
db.delete_unique('wagtailcore_site', ['hostname', 'port'])
|
||||
|
||||
# Adding unique constraint on 'Site', fields ['hostname']
|
||||
db.create_unique('wagtailcore_site', ['hostname'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'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': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'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'}),
|
||||
'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'})
|
||||
},
|
||||
'wagtailcore.grouppagepermission': {
|
||||
'Meta': {'object_name': 'GroupPagePermission'},
|
||||
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
|
||||
},
|
||||
'wagtailcore.page': {
|
||||
'Meta': {'object_name': 'Page'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['contenttypes.ContentType']"}),
|
||||
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': "orm['auth.User']"}),
|
||||
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
|
||||
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
'wagtailcore.pagerevision': {
|
||||
'Meta': {'object_name': 'PageRevision'},
|
||||
'content_json': ('django.db.models.fields.TextField', [], {}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'wagtailcore.site': {
|
||||
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
|
||||
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
|
||||
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': "orm['wagtailcore.Page']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['wagtailcore']
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# -*- 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 SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'PageRevision.approved_go_live_at'
|
||||
db.add_column('wagtailcore_pagerevision', 'approved_go_live_at',
|
||||
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Page.go_live_at'
|
||||
db.add_column('wagtailcore_page', 'go_live_at',
|
||||
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Page.expire_at'
|
||||
db.add_column('wagtailcore_page', 'expire_at',
|
||||
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Page.expired'
|
||||
db.add_column('wagtailcore_page', 'expired',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'PageRevision.approved_go_live_at'
|
||||
db.delete_column('wagtailcore_pagerevision', 'approved_go_live_at')
|
||||
|
||||
# Deleting field 'Page.go_live_at'
|
||||
db.delete_column('wagtailcore_page', 'go_live_at')
|
||||
|
||||
# Deleting field 'Page.expire_at'
|
||||
db.delete_column('wagtailcore_page', 'expire_at')
|
||||
|
||||
# Deleting field 'Page.expired'
|
||||
db.delete_column('wagtailcore_page', 'expired')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'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': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'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'}),
|
||||
'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'})
|
||||
},
|
||||
'wagtailcore.grouppagepermission': {
|
||||
'Meta': {'object_name': 'GroupPagePermission'},
|
||||
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
|
||||
},
|
||||
'wagtailcore.page': {
|
||||
'Meta': {'object_name': 'Page'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['contenttypes.ContentType']"}),
|
||||
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'expire_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
|
||||
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': "orm['auth.User']"}),
|
||||
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
|
||||
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
'wagtailcore.pagerevision': {
|
||||
'Meta': {'object_name': 'PageRevision'},
|
||||
'approved_go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'content_json': ('django.db.models.fields.TextField', [], {}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['wagtailcore.Page']"}),
|
||||
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'wagtailcore.site': {
|
||||
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
|
||||
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
|
||||
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': "orm['wagtailcore.Page']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['wagtailcore']
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
from StringIO import StringIO
|
||||
from urlparse import urlparse
|
||||
import warnings
|
||||
|
||||
import six
|
||||
from six import string_types
|
||||
from six import StringIO
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
from modelcluster.models import ClusterableModel
|
||||
|
||||
from django.db import models, connection, transaction
|
||||
|
|
@ -14,8 +17,11 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from django.contrib.auth.models import Group
|
||||
from django.conf import settings
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from treebeard.mp_tree import MP_Node
|
||||
|
||||
|
|
@ -26,29 +32,48 @@ from wagtail.wagtailsearch import Indexed, get_search_backend
|
|||
|
||||
|
||||
class SiteManager(models.Manager):
|
||||
def get_by_natural_key(self, hostname):
|
||||
return self.get(hostname=hostname)
|
||||
def get_by_natural_key(self, hostname, port):
|
||||
return self.get(hostname=hostname, port=port)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Site(models.Model):
|
||||
hostname = models.CharField(max_length=255, unique=True, db_index=True)
|
||||
hostname = models.CharField(max_length=255, db_index=True)
|
||||
port = models.IntegerField(default=80, help_text=_("Set this to something other than 80 if you need a specific port number to appear in URLs (e.g. development on port 8000). Does not affect request handling (so port forwarding still works)."))
|
||||
root_page = models.ForeignKey('Page', related_name='sites_rooted_here')
|
||||
is_default_site = models.BooleanField(default=False, help_text=_("If true, this site will handle requests for all other hostnames that do not have a site entry of their own"))
|
||||
|
||||
def natural_key(self):
|
||||
return (self.hostname,)
|
||||
class Meta:
|
||||
unique_together = ('hostname', 'port')
|
||||
|
||||
def __unicode__(self):
|
||||
def natural_key(self):
|
||||
return (self.hostname, self.port)
|
||||
|
||||
def __str__(self):
|
||||
return self.hostname + ("" if self.port == 80 else (":%d" % self.port)) + (" [default]" if self.is_default_site else "")
|
||||
|
||||
@staticmethod
|
||||
def find_for_request(request):
|
||||
"""Find the site object responsible for responding to this HTTP request object"""
|
||||
"""
|
||||
Find the site object responsible for responding to this HTTP
|
||||
request object. Try:
|
||||
- unique hostname first
|
||||
- then hostname and port
|
||||
- if there is no matching hostname at all, or no matching
|
||||
hostname:port combination, fall back to the unique default site,
|
||||
or raise an exception
|
||||
NB this means that high-numbered ports on an extant hostname may
|
||||
still be routed to a different hostname which is set as the default
|
||||
"""
|
||||
try:
|
||||
hostname = request.META['HTTP_HOST'].split(':')[0]
|
||||
# find a Site matching this specific hostname
|
||||
return Site.objects.get(hostname=hostname)
|
||||
hostname = request.META['HTTP_HOST'].split(':')[0] # KeyError here goes to the final except clause
|
||||
try:
|
||||
# find a Site matching this specific hostname
|
||||
return Site.objects.get(hostname=hostname) # Site.DoesNotExist here goes to the final except clause
|
||||
except Site.MultipleObjectsReturned:
|
||||
# as there were more than one, try matching by port too
|
||||
port = request.META['SERVER_PORT'] # KeyError here goes to the final except clause
|
||||
return Site.objects.get(hostname=hostname, port=int(port)) # Site.DoesNotExist here goes to the final except clause
|
||||
except (Site.DoesNotExist, KeyError):
|
||||
# If no matching site exists, or request does not specify an HTTP_HOST (which
|
||||
# will often be the case for the Django test client), look for a catch-all Site.
|
||||
|
|
@ -64,6 +89,24 @@ class Site(models.Model):
|
|||
else:
|
||||
return 'http://%s:%d' % (self.hostname, self.port)
|
||||
|
||||
def clean_fields(self, exclude=None):
|
||||
super(Site, self).clean_fields(exclude)
|
||||
# Only one site can have the is_default_site flag set
|
||||
try:
|
||||
default = Site.objects.get(is_default_site=True)
|
||||
except Site.DoesNotExist:
|
||||
pass
|
||||
except Site.MultipleObjectsReturned:
|
||||
raise
|
||||
else:
|
||||
if self.is_default_site and self.pk != default.pk:
|
||||
raise ValidationError(
|
||||
{'is_default_site': [
|
||||
_("%(hostname)s is already configured as the default site. You must unset that before you can save this site as default.")
|
||||
% { 'hostname': default.hostname }
|
||||
]}
|
||||
)
|
||||
|
||||
# clear the wagtail_site_root_paths cache whenever Site records are updated
|
||||
def save(self, *args, **kwargs):
|
||||
result = super(Site, self).save(*args, **kwargs)
|
||||
|
|
@ -216,9 +259,8 @@ class PageBase(models.base.ModelBase):
|
|||
PAGE_MODEL_CLASSES.append(cls)
|
||||
|
||||
|
||||
class Page(MP_Node, ClusterableModel, Indexed):
|
||||
__metaclass__ = PageBase
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, Indexed)):
|
||||
title = models.CharField(max_length=255, help_text=_("The page title as you'd like it to be seen by the public"))
|
||||
slug = models.SlugField(help_text=_("The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/"))
|
||||
# TODO: enforce uniqueness on slug field per parent (will have to be done at the Django
|
||||
|
|
@ -233,6 +275,10 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
show_in_menus = models.BooleanField(default=False, help_text=_("Whether a link to this page will appear in automatically generated menus"))
|
||||
search_description = models.TextField(blank=True)
|
||||
|
||||
go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
|
||||
expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
|
||||
expired = models.BooleanField(default=False, editable=False)
|
||||
|
||||
indexed_fields = {
|
||||
'title': {
|
||||
'type': 'string',
|
||||
|
|
@ -257,7 +303,7 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
# created as
|
||||
self.content_type = ContentType.objects.get_for_model(self)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
is_abstract = True # don't offer Page in the list of page types a superuser can create
|
||||
|
|
@ -327,7 +373,7 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
SET url_path = %s || substring(url_path from %s)
|
||||
WHERE path LIKE %s AND id <> %s
|
||||
"""
|
||||
cursor.execute(update_statement,
|
||||
cursor.execute(update_statement,
|
||||
[new_url_path, len(old_url_path) + 1, self.path + '%', self.id])
|
||||
|
||||
@cached_property
|
||||
|
|
@ -373,8 +419,13 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
else:
|
||||
raise Http404
|
||||
|
||||
def save_revision(self, user=None, submitted_for_moderation=False):
|
||||
return self.revisions.create(content_json=self.to_json(), user=user, submitted_for_moderation=submitted_for_moderation)
|
||||
def save_revision(self, user=None, submitted_for_moderation=False, approved_go_live_at=None):
|
||||
return self.revisions.create(
|
||||
content_json=self.to_json(),
|
||||
user=user,
|
||||
submitted_for_moderation=submitted_for_moderation,
|
||||
approved_go_live_at=approved_go_live_at,
|
||||
)
|
||||
|
||||
def get_latest_revision(self):
|
||||
return self.revisions.order_by('-created_at').first()
|
||||
|
|
@ -401,8 +452,8 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
|
||||
def serve(self, request, *args, **kwargs):
|
||||
return TemplateResponse(
|
||||
request,
|
||||
self.get_template(request, *args, **kwargs),
|
||||
request,
|
||||
self.get_template(request, *args, **kwargs),
|
||||
self.get_context(request, *args, **kwargs)
|
||||
)
|
||||
|
||||
|
|
@ -488,7 +539,7 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
else:
|
||||
res = []
|
||||
for page_type in cls.subpage_types:
|
||||
if isinstance(page_type, basestring):
|
||||
if isinstance(page_type, string_types):
|
||||
try:
|
||||
app_label, model_name = page_type.split(".")
|
||||
except ValueError:
|
||||
|
|
@ -533,13 +584,22 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
@property
|
||||
def status_string(self):
|
||||
if not self.live:
|
||||
return "draft"
|
||||
if self.expired:
|
||||
return "expired"
|
||||
elif self.approved_schedule:
|
||||
return "scheduled"
|
||||
else:
|
||||
return "draft"
|
||||
else:
|
||||
if self.has_unpublished_changes:
|
||||
return "live + draft"
|
||||
else:
|
||||
return "live"
|
||||
|
||||
@property
|
||||
def approved_schedule(self):
|
||||
return self.revisions.exclude(approved_go_live_at__isnull=True).exists()
|
||||
|
||||
def has_unpublished_subtree(self):
|
||||
"""
|
||||
An awkwardly-defined flag used in determining whether unprivileged editors have
|
||||
|
|
@ -562,6 +622,43 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
new_self.save()
|
||||
new_self._update_descendant_url_paths(old_url_path, new_url_path)
|
||||
|
||||
def copy(self, recursive=False, to=None, update_attrs=None):
|
||||
# Make a copy
|
||||
page_copy = Page.objects.get(id=self.id).specific
|
||||
page_copy.pk = None
|
||||
page_copy.id = None
|
||||
page_copy.depth = None
|
||||
page_copy.numchild = 0
|
||||
page_copy.path = None
|
||||
|
||||
if update_attrs:
|
||||
for field, value in update_attrs.items():
|
||||
setattr(page_copy, field, value)
|
||||
|
||||
if to:
|
||||
page_copy = to.add_child(instance=page_copy)
|
||||
else:
|
||||
page_copy = self.add_sibling(instance=page_copy)
|
||||
|
||||
# Copy child objects
|
||||
specific_self = self.specific
|
||||
for child_relation in getattr(specific_self._meta, 'child_relations', []):
|
||||
parental_key_name = child_relation.field.attname
|
||||
child_objects = getattr(specific_self, child_relation.get_accessor_name(), None)
|
||||
|
||||
if child_objects:
|
||||
for child_object in child_objects.all():
|
||||
child_object.pk = None
|
||||
setattr(child_object, parental_key_name, page_copy.id)
|
||||
child_object.save()
|
||||
|
||||
# Copy child pages
|
||||
if recursive:
|
||||
for child_page in self.get_children():
|
||||
child_page.specific.copy(recursive=True, to=page_copy)
|
||||
|
||||
return page_copy
|
||||
|
||||
def permissions_for_user(self, user):
|
||||
"""
|
||||
Return a PagePermissionsTester object defining what actions the user can perform on this page
|
||||
|
|
@ -629,12 +726,28 @@ class Page(MP_Node, ClusterableModel, Indexed):
|
|||
"""
|
||||
return self.serve(self.dummy_request())
|
||||
|
||||
def get_cached_paths(self):
|
||||
"""
|
||||
This returns a list of paths to invalidate in a frontend cache
|
||||
"""
|
||||
return ['/']
|
||||
|
||||
def get_sitemap_urls(self):
|
||||
latest_revision = self.get_latest_revision()
|
||||
|
||||
return [
|
||||
{
|
||||
'location': self.url,
|
||||
'lastmod': latest_revision.created_at if latest_revision else None
|
||||
}
|
||||
]
|
||||
|
||||
def get_static_site_paths(self):
|
||||
"""
|
||||
This is a generator of URL paths to feed into a static site generator
|
||||
Override this if you would like to create static versions of subpages
|
||||
"""
|
||||
# Yield paths for this page
|
||||
# Yield path for this page
|
||||
yield '/'
|
||||
|
||||
# Yield paths for child pages
|
||||
|
|
@ -713,12 +826,14 @@ class SubmittedRevisionsManager(models.Manager):
|
|||
return super(SubmittedRevisionsManager, self).get_queryset().filter(submitted_for_moderation=True)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PageRevision(models.Model):
|
||||
page = models.ForeignKey('Page', related_name='revisions')
|
||||
submitted_for_moderation = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)
|
||||
content_json = models.TextField()
|
||||
approved_go_live_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
objects = models.Manager()
|
||||
submitted_revisions = SubmittedRevisionsManager()
|
||||
|
|
@ -752,14 +867,27 @@ class PageRevision(models.Model):
|
|||
|
||||
def publish(self):
|
||||
page = self.as_page_object()
|
||||
page.live = True
|
||||
if page.go_live_at and page.go_live_at > timezone.now():
|
||||
# if we have a go_live in the future don't make the page live
|
||||
page.live = False
|
||||
# Instead set the approved_go_live_at of this revision
|
||||
self.approved_go_live_at = page.go_live_at
|
||||
self.save()
|
||||
# And clear the the approved_go_live_at of any other revisions
|
||||
page.revisions.exclude(id=self.id).update(approved_go_live_at=None)
|
||||
else:
|
||||
page.live = True
|
||||
# If page goes live clear the approved_go_live_at of all revisions
|
||||
page.revisions.update(approved_go_live_at=None)
|
||||
page.expired = False # When a page is published it can't be expired
|
||||
page.save()
|
||||
self.submitted_for_moderation = False
|
||||
page.revisions.update(submitted_for_moderation=False)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return '"' + unicode(self.page) + '" at ' + unicode(self.created_at)
|
||||
|
||||
|
||||
PAGE_PERMISSION_TYPE_CHOICES = [
|
||||
('add', 'Add'),
|
||||
('edit', 'Edit'),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from wagtail.wagtaildocs.models import Document
|
|||
from wagtail.wagtailimages.models import get_image_model
|
||||
from wagtail.wagtailimages.formats import get_image_format
|
||||
|
||||
from wagtail.wagtailadmin import hooks
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
|
||||
# Define a set of 'embed handlers' and 'link handlers'. These handle the translation
|
||||
|
|
@ -166,8 +166,8 @@ class DbWhitelister(Whitelister):
|
|||
def clean(cls, html):
|
||||
if not cls.has_loaded_custom_whitelist_rules:
|
||||
for fn in hooks.get_hooks('construct_whitelister_element_rules'):
|
||||
cls.element_rules = dict(
|
||||
cls.element_rules.items() + fn().items())
|
||||
cls.element_rules = cls.element_rules.copy()
|
||||
cls.element_rules.update(fn())
|
||||
cls.has_loaded_custom_whitelist_rules = True
|
||||
|
||||
return super(DbWhitelister, cls).clean(html)
|
||||
|
|
|
|||
4
wagtail/wagtailcore/signals.py
Normal file
4
wagtail/wagtailcore/signals.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
|
||||
page_published = Signal(providing_args=['instance'])
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
from StringIO import StringIO
|
||||
from datetime import timedelta
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.http import HttpRequest, Http404
|
||||
from six import StringIO
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core import management
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
|
||||
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
|
||||
from wagtail.tests.models import EventPage, EventIndex, SimplePage
|
||||
from wagtail.wagtailcore.models import Page, PageRevision
|
||||
from wagtail.tests.models import SimplePage
|
||||
|
||||
|
||||
class TestFixTreeCommand(TestCase):
|
||||
|
|
@ -87,3 +88,107 @@ class TestReplaceTextCommand(TestCase):
|
|||
|
||||
# Check that its now about easter
|
||||
self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter")
|
||||
|
||||
|
||||
class TestPublishScheduledPagesCommand(TestCase):
|
||||
def setUp(self):
|
||||
# Find root page
|
||||
self.root_page = Page.objects.get(id=2)
|
||||
|
||||
def test_go_live_page_will_be_published(self):
|
||||
page = SimplePage(
|
||||
title="Hello world!",
|
||||
slug="hello-world",
|
||||
live=False,
|
||||
go_live_at=timezone.now() - timedelta(days=1),
|
||||
)
|
||||
self.root_page.add_child(instance=page)
|
||||
|
||||
page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1))
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertFalse(p.live)
|
||||
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
management.call_command('publish_scheduled_pages')
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertTrue(p.live)
|
||||
self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
def test_future_go_live_page_will_not_be_published(self):
|
||||
page = SimplePage(
|
||||
title="Hello world!",
|
||||
slug="hello-world",
|
||||
live=False,
|
||||
go_live_at=timezone.now() + timedelta(days=1),
|
||||
)
|
||||
self.root_page.add_child(instance=page)
|
||||
|
||||
page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1))
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertFalse(p.live)
|
||||
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
management.call_command('publish_scheduled_pages')
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertFalse(p.live)
|
||||
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
|
||||
|
||||
def test_expired_page_will_be_unpublished(self):
|
||||
page = SimplePage(
|
||||
title="Hello world!",
|
||||
slug="hello-world",
|
||||
live=True,
|
||||
expire_at=timezone.now() - timedelta(days=1),
|
||||
)
|
||||
self.root_page.add_child(instance=page)
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertTrue(p.live)
|
||||
|
||||
management.call_command('publish_scheduled_pages')
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertFalse(p.live)
|
||||
self.assertTrue(p.expired)
|
||||
|
||||
def test_future_expired_page_will_not_be_unpublished(self):
|
||||
page = SimplePage(
|
||||
title="Hello world!",
|
||||
slug="hello-world",
|
||||
live=True,
|
||||
expire_at=timezone.now() + timedelta(days=1),
|
||||
)
|
||||
self.root_page.add_child(instance=page)
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertTrue(p.live)
|
||||
|
||||
management.call_command('publish_scheduled_pages')
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertTrue(p.live)
|
||||
self.assertFalse(p.expired)
|
||||
|
||||
def test_expired_pages_are_dropped_from_mod_queue(self):
|
||||
page = SimplePage(
|
||||
title="Hello world!",
|
||||
slug="hello-world",
|
||||
live=False,
|
||||
expire_at=timezone.now() - timedelta(days=1),
|
||||
)
|
||||
self.root_page.add_child(instance=page)
|
||||
|
||||
page.save_revision(submitted_for_moderation=True)
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertFalse(p.live)
|
||||
self.assertTrue(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists())
|
||||
|
||||
management.call_command('publish_scheduled_pages')
|
||||
|
||||
p = Page.objects.get(slug='hello-world')
|
||||
self.assertFalse(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists())
|
||||
|
|
|
|||
|
|
@ -1,39 +1,101 @@
|
|||
from StringIO import StringIO
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.http import HttpRequest, Http404
|
||||
from django.core import management
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
|
||||
from wagtail.wagtailcore.models import Page, Site
|
||||
from wagtail.tests.models import EventPage, EventIndex, SimplePage
|
||||
|
||||
|
||||
class TestSiteRouting(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
||||
def setUp(self):
|
||||
self.default_site = Site.objects.get(is_default_site=True)
|
||||
events_page = Page.objects.get(url_path='/home/events/')
|
||||
about_page = Page.objects.get(url_path='/home/about-us/')
|
||||
self.events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
|
||||
self.alternate_port_events_site = Site.objects.create(hostname='events.example.com', root_page=events_page, port='8765')
|
||||
self.about_site = Site.objects.create(hostname='about.example.com', root_page=about_page)
|
||||
self.unrecognised_port = '8000'
|
||||
self.unrecognised_hostname = 'unknown.site.com'
|
||||
|
||||
def test_no_host_header_routes_to_default_site(self):
|
||||
# requests without a Host: header should be directed to the default site
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
self.assertEqual(Site.find_for_request(request), self.default_site)
|
||||
|
||||
def test_valid_headers_route_to_specific_site(self):
|
||||
# requests with a known Host: header should be directed to the specific site
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = self.events_site.hostname
|
||||
request.META['SERVER_PORT'] = self.events_site.port
|
||||
self.assertEqual(Site.find_for_request(request), self.events_site)
|
||||
|
||||
def test_ports_in_request_headers_are_respected(self):
|
||||
# ports in the Host: header should be respected
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = self.alternate_port_events_site.hostname
|
||||
request.META['SERVER_PORT'] = self.alternate_port_events_site.port
|
||||
self.assertEqual(Site.find_for_request(request), self.alternate_port_events_site)
|
||||
|
||||
def test_unrecognised_host_header_routes_to_default_site(self):
|
||||
# requests with an unrecognised Host: header should be directed to the default site
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = self.unrecognised_hostname
|
||||
request.META['SERVER_PORT'] = '80'
|
||||
self.assertEqual(Site.find_for_request(request), self.default_site)
|
||||
|
||||
def test_unrecognised_port_and_default_host_routes_to_default_site(self):
|
||||
# requests to the default host on an unrecognised port should be directed to the default site
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = self.default_site.hostname
|
||||
request.META['SERVER_PORT'] = self.unrecognised_port
|
||||
self.assertEqual(Site.find_for_request(request), self.default_site)
|
||||
|
||||
def test_unrecognised_port_and_unrecognised_host_routes_to_default_site(self):
|
||||
# requests with an unrecognised Host: header _and_ an unrecognised port
|
||||
# hould be directed to the default site
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = self.unrecognised_hostname
|
||||
request.META['SERVER_PORT'] = self.unrecognised_port
|
||||
self.assertEqual(Site.find_for_request(request), self.default_site)
|
||||
|
||||
def test_unrecognised_port_on_known_hostname_routes_there_if_no_ambiguity(self):
|
||||
# requests on an unrecognised port should be directed to the site with
|
||||
# matching hostname if there is no ambiguity
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = self.about_site.hostname
|
||||
request.META['SERVER_PORT'] = self.unrecognised_port
|
||||
self.assertEqual(Site.find_for_request(request), self.about_site)
|
||||
|
||||
def test_unrecognised_port_on_known_hostname_routes_to_default_site_if_ambiguity(self):
|
||||
# requests on an unrecognised port should be directed to the default
|
||||
# site, even if their hostname (but not port) matches more than one
|
||||
# other entry
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = self.events_site.hostname
|
||||
request.META['SERVER_PORT'] = self.unrecognised_port
|
||||
self.assertEqual(Site.find_for_request(request), self.default_site)
|
||||
|
||||
def test_port_in_http_host_header_is_ignored(self):
|
||||
# port in the HTTP_HOST header is ignored
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = "%s:%s" % (self.events_site.hostname, self.events_site.port)
|
||||
request.META['SERVER_PORT'] = self.alternate_port_events_site.port
|
||||
self.assertEqual(Site.find_for_request(request), self.alternate_port_events_site)
|
||||
|
||||
|
||||
class TestRouting(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
||||
def test_find_site_for_request(self):
|
||||
default_site = Site.objects.get(is_default_site=True)
|
||||
events_page = Page.objects.get(url_path='/home/events/')
|
||||
events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
|
||||
|
||||
# requests without a Host: header should be directed to the default site
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
self.assertEqual(Site.find_for_request(request), default_site)
|
||||
|
||||
# requests with a known Host: header should be directed to the specific site
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = 'events.example.com'
|
||||
self.assertEqual(Site.find_for_request(request), events_site)
|
||||
|
||||
# requests with an unrecognised Host: header should be directed to the default site
|
||||
request = HttpRequest()
|
||||
request.path = '/'
|
||||
request.META['HTTP_HOST'] = 'unknown.example.com'
|
||||
self.assertEqual(Site.find_for_request(request), default_site)
|
||||
|
||||
def test_urls(self):
|
||||
default_site = Site.objects.get(is_default_site=True)
|
||||
homepage = Page.objects.get(url_path='/home/')
|
||||
|
|
@ -245,3 +307,82 @@ class TestPrevNextSiblings(TestCase):
|
|||
|
||||
# First element must always be the current page
|
||||
self.assertEqual(final_event.get_prev_siblings(inclusive=True).first(), final_event)
|
||||
|
||||
|
||||
class TestCopyPage(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
||||
def test_copy_page_copies(self):
|
||||
about_us = SimplePage.objects.get(url_path='/home/about-us/')
|
||||
|
||||
# Copy it
|
||||
new_about_us = about_us.copy(update_attrs={'title': "New about us", 'slug': 'new-about-us'})
|
||||
|
||||
# Check that new_about_us is correct
|
||||
self.assertIsInstance(new_about_us, SimplePage)
|
||||
self.assertEqual(new_about_us.title, "New about us")
|
||||
self.assertEqual(new_about_us.slug, 'new-about-us')
|
||||
|
||||
# Check that new_about_us is a different page
|
||||
self.assertNotEqual(about_us.id, new_about_us.id)
|
||||
|
||||
# Check that the url path was updated
|
||||
self.assertEqual(new_about_us.url_path, '/home/new-about-us/')
|
||||
|
||||
def test_copy_page_copies_child_objects(self):
|
||||
christmas_event = EventPage.objects.get(url_path='/home/events/christmas/')
|
||||
|
||||
# Copy it
|
||||
new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
|
||||
|
||||
# Check that the speakers were copied
|
||||
self.assertEqual(new_christmas_event.speakers.count(), 1, "Child objects weren't copied")
|
||||
|
||||
# Check that the speakers weren't removed from old page
|
||||
self.assertEqual(christmas_event.speakers.count(), 1, "Child objects were removed from the original page")
|
||||
|
||||
def test_copy_page_copies_child_objects_with_nonspecific_class(self):
|
||||
# Get chrismas page as Page instead of EventPage
|
||||
christmas_event = Page.objects.get(url_path='/home/events/christmas/')
|
||||
|
||||
# Copy it
|
||||
new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
|
||||
|
||||
# Check that the type of the new page is correct
|
||||
self.assertIsInstance(new_christmas_event, EventPage)
|
||||
|
||||
# Check that the speakers were copied
|
||||
self.assertEqual(new_christmas_event.speakers.count(), 1, "Child objects weren't copied")
|
||||
|
||||
def test_copy_page_copies_recursively(self):
|
||||
events_index = EventIndex.objects.get(url_path='/home/events/')
|
||||
|
||||
# Copy it
|
||||
new_events_index = events_index.copy(recursive=True, update_attrs={'title': "New events index", 'slug': 'new-events-index'})
|
||||
|
||||
# Get christmas event
|
||||
old_christmas_event = events_index.get_children().filter(slug='christmas').first()
|
||||
new_christmas_event = new_events_index.get_children().filter(slug='christmas').first()
|
||||
|
||||
# Check that the event exists in both places
|
||||
self.assertNotEqual(new_christmas_event, None, "Child pages weren't copied")
|
||||
self.assertNotEqual(old_christmas_event, None, "Child pages were removed from original page")
|
||||
|
||||
# Check that the url path was updated
|
||||
self.assertEqual(new_christmas_event.url_path, '/home/new-events-index/christmas/')
|
||||
|
||||
def test_copy_page_copies_recursively_with_child_objects(self):
|
||||
events_index = EventIndex.objects.get(url_path='/home/events/')
|
||||
|
||||
# Copy it
|
||||
new_events_index = events_index.copy(recursive=True, update_attrs={'title': "New events index", 'slug': 'new-events-index'})
|
||||
|
||||
# Get christmas event
|
||||
old_christmas_event = events_index.get_children().filter(slug='christmas').first()
|
||||
new_christmas_event = new_events_index.get_children().filter(slug='christmas').first()
|
||||
|
||||
# Check that the speakers were copied
|
||||
self.assertEqual(new_christmas_event.specific.speakers.count(), 1, "Child objects weren't copied")
|
||||
|
||||
# Check that the speakers weren't removed from old page
|
||||
self.assertEqual(old_christmas_event.specific.speakers.count(), 1, "Child objects were removed from the original page")
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
from StringIO import StringIO
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.http import HttpRequest, Http404
|
||||
from django.core import management
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
|
||||
from wagtail.tests.models import EventPage, EventIndex, SimplePage
|
||||
from wagtail.wagtailcore.models import Page, UserPagePermissionsProxy
|
||||
from wagtail.tests.models import EventPage
|
||||
|
||||
|
||||
class TestPagePermission(TestCase):
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
from StringIO import StringIO
|
||||
from django.test import TestCase
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.http import HttpRequest, Http404
|
||||
from django.core import management
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
|
||||
from wagtail.tests.models import EventPage, EventIndex, SimplePage
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.tests.models import EventPage
|
||||
|
||||
|
||||
class TestPageQuerySet(TestCase):
|
||||
|
|
|
|||
262
wagtail/wagtailcore/tests/test_rich_text.py
Normal file
262
wagtail/wagtailcore/tests/test_rich_text.py
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
from mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from wagtail.wagtailcore.rich_text import (
|
||||
ImageEmbedHandler,
|
||||
MediaEmbedHandler,
|
||||
PageLinkHandler,
|
||||
DocumentLinkHandler,
|
||||
DbWhitelister,
|
||||
extract_attrs,
|
||||
expand_db_html
|
||||
)
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class TestImageEmbedHandler(TestCase):
|
||||
fixtures = ['wagtail/tests/fixtures/test.json']
|
||||
|
||||
def test_get_db_attributes(self):
|
||||
soup = BeautifulSoup(
|
||||
'<b data-id="test-id" data-format="test-format" data-alt="test-alt">foo</b>'
|
||||
)
|
||||
tag = soup.b
|
||||
result = ImageEmbedHandler.get_db_attributes(tag)
|
||||
self.assertEqual(result,
|
||||
{'alt': 'test-alt',
|
||||
'id': 'test-id',
|
||||
'format': 'test-format'})
|
||||
|
||||
def test_expand_db_attributes_page_does_not_exist(self):
|
||||
result = ImageEmbedHandler.expand_db_attributes(
|
||||
{'id': 0},
|
||||
False
|
||||
)
|
||||
self.assertEqual(result, '<img>')
|
||||
|
||||
@patch('wagtail.wagtailimages.models.Image')
|
||||
@patch('django.core.files.File')
|
||||
def test_expand_db_attributes_not_for_editor(self, mock_file, mock_image):
|
||||
result = ImageEmbedHandler.expand_db_attributes(
|
||||
{'id': 1,
|
||||
'alt': 'test-alt',
|
||||
'format': 'left'},
|
||||
False
|
||||
)
|
||||
self.assertIn('<img class="richtext-image left"', result)
|
||||
|
||||
@patch('wagtail.wagtailimages.models.Image')
|
||||
@patch('django.core.files.File')
|
||||
def test_expand_db_attributes_for_editor(self, mock_file, mock_image):
|
||||
result = ImageEmbedHandler.expand_db_attributes(
|
||||
{'id': 1,
|
||||
'alt': 'test-alt',
|
||||
'format': 'left'},
|
||||
True
|
||||
)
|
||||
self.assertIn('<img data-embedtype="image" data-id="1" data-format="left" data-alt="test-alt" class="richtext-image left"', result)
|
||||
|
||||
@patch('wagtail.wagtailimages.models.Image')
|
||||
@patch('django.core.files.File')
|
||||
def test_expand_db_attributes_for_editor_throws_exception(self, mock_file, mock_image):
|
||||
result = ImageEmbedHandler.expand_db_attributes(
|
||||
{'id': 1,
|
||||
'format': 'left'},
|
||||
True
|
||||
)
|
||||
self.assertEqual(result, '')
|
||||
|
||||
|
||||
class TestMediaEmbedHandler(TestCase):
|
||||
def test_get_db_attributes(self):
|
||||
soup = BeautifulSoup(
|
||||
'<b data-url="test-url">foo</b>'
|
||||
)
|
||||
tag = soup.b
|
||||
result = MediaEmbedHandler.get_db_attributes(tag)
|
||||
self.assertEqual(result,
|
||||
{'url': 'test-url'})
|
||||
|
||||
@patch('wagtail.wagtailembeds.embeds.oembed')
|
||||
def test_expand_db_attributes_for_editor(self, oembed):
|
||||
oembed.return_value = {
|
||||
'title': 'test title',
|
||||
'author_name': 'test author name',
|
||||
'provider_name': 'test provider name',
|
||||
'type': 'test type',
|
||||
'thumbnail_url': 'test thumbnail url',
|
||||
'width': 'test width',
|
||||
'height': 'test height',
|
||||
'html': 'test html'
|
||||
}
|
||||
result = MediaEmbedHandler.expand_db_attributes(
|
||||
{'url': 'http://www.youtube.com/watch/'},
|
||||
True
|
||||
)
|
||||
self.assertIn('<div class="embed-placeholder" contenteditable="false" data-embedtype="media" data-url="http://www.youtube.com/watch/">', result)
|
||||
self.assertIn('<h3>test title</h3>', result)
|
||||
self.assertIn('<p>URL: http://www.youtube.com/watch/</p>', result)
|
||||
self.assertIn('<p>Provider: test provider name</p>', result)
|
||||
self.assertIn('<p>Author: test author name</p>', result)
|
||||
self.assertIn('<img src="test thumbnail url" alt="test title">', result)
|
||||
|
||||
@patch('wagtail.wagtailembeds.embeds.oembed')
|
||||
def test_expand_db_attributes_not_for_editor(self, oembed):
|
||||
oembed.return_value = {
|
||||
'title': 'test title',
|
||||
'author_name': 'test author name',
|
||||
'provider_name': 'test provider name',
|
||||
'type': 'test type',
|
||||
'thumbnail_url': 'test thumbnail url',
|
||||
'width': 'test width',
|
||||
'height': 'test height',
|
||||
'html': 'test html'
|
||||
}
|
||||
result = MediaEmbedHandler.expand_db_attributes(
|
||||
{'url': 'http://www.youtube.com/watch/'},
|
||||
False
|
||||
)
|
||||
self.assertIn('test html', result)
|
||||
|
||||
|
||||
class TestPageLinkHandler(TestCase):
|
||||
fixtures = ['wagtail/tests/fixtures/test.json']
|
||||
|
||||
def test_get_db_attributes(self):
|
||||
soup = BeautifulSoup(
|
||||
'<a data-id="test-id">foo</a>'
|
||||
)
|
||||
tag = soup.a
|
||||
result = PageLinkHandler.get_db_attributes(tag)
|
||||
self.assertEqual(result,
|
||||
{'id': 'test-id'})
|
||||
|
||||
def test_expand_db_attributes_page_does_not_exist(self):
|
||||
result = PageLinkHandler.expand_db_attributes(
|
||||
{'id': 0},
|
||||
False
|
||||
)
|
||||
self.assertEqual(result, '<a>')
|
||||
|
||||
def test_expand_db_attributes_for_editor(self):
|
||||
result = PageLinkHandler.expand_db_attributes(
|
||||
{'id': 1},
|
||||
True
|
||||
)
|
||||
self.assertEqual(result,
|
||||
'<a data-linktype="page" data-id="1" href="None">')
|
||||
|
||||
def test_expand_db_attributes_not_for_editor(self):
|
||||
result = PageLinkHandler.expand_db_attributes(
|
||||
{'id': 1},
|
||||
False
|
||||
)
|
||||
self.assertEqual(result, '<a href="None">')
|
||||
|
||||
|
||||
class TestDocumentLinkHandler(TestCase):
|
||||
fixtures = ['wagtail/tests/fixtures/test.json']
|
||||
|
||||
def test_get_db_attributes(self):
|
||||
soup = BeautifulSoup(
|
||||
'<a data-id="test-id">foo</a>'
|
||||
)
|
||||
tag = soup.a
|
||||
result = DocumentLinkHandler.get_db_attributes(tag)
|
||||
self.assertEqual(result,
|
||||
{'id': 'test-id'})
|
||||
|
||||
def test_expand_db_attributes_document_does_not_exist(self):
|
||||
result = DocumentLinkHandler.expand_db_attributes(
|
||||
{'id': 0},
|
||||
False
|
||||
)
|
||||
self.assertEqual(result, '<a>')
|
||||
|
||||
def test_expand_db_attributes_for_editor(self):
|
||||
result = DocumentLinkHandler.expand_db_attributes(
|
||||
{'id': 1},
|
||||
True
|
||||
)
|
||||
self.assertEqual(result,
|
||||
'<a data-linktype="document" data-id="1" href="/documents/1/">')
|
||||
|
||||
def test_expand_db_attributes_not_for_editor(self):
|
||||
result = DocumentLinkHandler.expand_db_attributes(
|
||||
{'id': 1},
|
||||
False
|
||||
)
|
||||
self.assertEqual(result,
|
||||
'<a href="/documents/1/">')
|
||||
|
||||
|
||||
class TestDbWhiteLister(TestCase):
|
||||
def test_clean_tag_node_div(self):
|
||||
soup = BeautifulSoup(
|
||||
'<div>foo</div>'
|
||||
)
|
||||
tag = soup.div
|
||||
self.assertEqual(tag.name, 'div')
|
||||
DbWhitelister.clean_tag_node(soup, tag)
|
||||
self.assertEqual(tag.name, 'p')
|
||||
|
||||
def test_clean_tag_node_with_data_embedtype(self):
|
||||
soup = BeautifulSoup(
|
||||
'<p><a data-embedtype="image" data-id=1 data-format="left" data-alt="bar" irrelevant="baz">foo</a></p>'
|
||||
)
|
||||
tag = soup.p
|
||||
DbWhitelister.clean_tag_node(soup, tag)
|
||||
self.assertEqual(str(tag),
|
||||
'<p><embed alt="bar" embedtype="image" format="left" id="1"/></p>')
|
||||
|
||||
def test_clean_tag_node_with_data_linktype(self):
|
||||
soup = BeautifulSoup(
|
||||
'<a data-linktype="document" data-id="1" irrelevant="baz">foo</a>'
|
||||
)
|
||||
tag = soup.a
|
||||
DbWhitelister.clean_tag_node(soup, tag)
|
||||
self.assertEqual(str(tag), '<a id="1" linktype="document">foo</a>')
|
||||
|
||||
def test_clean_tag_node(self):
|
||||
soup = BeautifulSoup(
|
||||
'<a irrelevant="baz">foo</a>'
|
||||
)
|
||||
tag = soup.a
|
||||
DbWhitelister.clean_tag_node(soup, tag)
|
||||
self.assertEqual(str(tag), '<a>foo</a>')
|
||||
|
||||
|
||||
class TestExtractAttrs(TestCase):
|
||||
def test_extract_attr(self):
|
||||
html = '<a foo="bar" baz="quux">snowman</a>'
|
||||
result = extract_attrs(html)
|
||||
self.assertEqual(result, {'foo': 'bar', 'baz': 'quux'})
|
||||
|
||||
|
||||
class TestExpandDbHtml(TestCase):
|
||||
def test_expand_db_html_with_linktype(self):
|
||||
html = '<a id="1" linktype="document">foo</a>'
|
||||
result = expand_db_html(html)
|
||||
self.assertEqual(result, '<a>foo</a>')
|
||||
|
||||
def test_expand_db_html_no_linktype(self):
|
||||
html = '<a id="1">foo</a>'
|
||||
result = expand_db_html(html)
|
||||
self.assertEqual(result, '<a id="1">foo</a>')
|
||||
|
||||
@patch('wagtail.wagtailembeds.embeds.oembed')
|
||||
def test_expand_db_html_with_embed(self, oembed):
|
||||
oembed.return_value = {
|
||||
'title': 'test title',
|
||||
'author_name': 'test author name',
|
||||
'provider_name': 'test provider name',
|
||||
'type': 'test type',
|
||||
'thumbnail_url': 'test thumbnail url',
|
||||
'width': 'test width',
|
||||
'height': 'test height',
|
||||
'html': 'test html'
|
||||
}
|
||||
html = '<embed embedtype="media" url="http://www.youtube.com/watch" />'
|
||||
result = expand_db_html(html)
|
||||
self.assertIn('test html', result)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from bs4 import BeautifulSoup, NavigableString
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from django.test import TestCase
|
||||
from wagtail.wagtailcore.whitelist import (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
from StringIO import StringIO
|
||||
from django.test import TestCase
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.http import HttpRequest, Http404
|
||||
from django.core import management
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
|
||||
from wagtail.tests.models import EventPage, EventIndex, SimplePage
|
||||
from wagtail.wagtailcore.models import Page, Site
|
||||
from wagtail.tests.models import SimplePage
|
||||
|
||||
|
||||
class TestPageUrlTags(TestCase):
|
||||
|
|
@ -15,22 +10,25 @@ class TestPageUrlTags(TestCase):
|
|||
def test_pageurl_tag(self):
|
||||
response = self.client.get('/events/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
|
||||
self.assertContains(response,
|
||||
'<a href="/events/christmas/">Christmas</a>')
|
||||
|
||||
def test_slugurl_tag(self):
|
||||
response = self.client.get('/events/christmas/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, '<a href="/events/">Back to events index</a>')
|
||||
self.assertContains(response,
|
||||
'<a href="/events/">Back to events index</a>')
|
||||
|
||||
|
||||
class TestIssue7(TestCase):
|
||||
"""
|
||||
This tests for an issue where if a site root page was moved, all the page
|
||||
urls in that site would change to None.
|
||||
This tests for an issue where if a site root page was moved, all
|
||||
the page urls in that site would change to None.
|
||||
|
||||
The issue was caused by the 'wagtail_site_root_paths' cache variable not being
|
||||
cleared when a site root page was moved. Which left all the child pages
|
||||
thinking that they are no longer in the site and return None as their url.
|
||||
The issue was caused by the 'wagtail_site_root_paths' cache
|
||||
variable not being cleared when a site root page was moved. Which
|
||||
left all the child pages thinking that they are no longer in the
|
||||
site and return None as their url.
|
||||
|
||||
Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
|
||||
Discussion: https://github.com/torchbox/wagtail/issues/7
|
||||
|
|
@ -67,12 +65,13 @@ class TestIssue7(TestCase):
|
|||
|
||||
class TestIssue157(TestCase):
|
||||
"""
|
||||
This tests for an issue where if a site root pages slug was changed, all the page
|
||||
urls in that site would change to None.
|
||||
This tests for an issue where if a site root pages slug was
|
||||
changed, all the page urls in that site would change to None.
|
||||
|
||||
The issue was caused by the 'wagtail_site_root_paths' cache variable not being
|
||||
cleared when a site root page was changed. Which left all the child pages
|
||||
thinking that they are no longer in the site and return None as their url.
|
||||
The issue was caused by the 'wagtail_site_root_paths' cache
|
||||
variable not being cleared when a site root page was changed.
|
||||
Which left all the child pages thinking that they are no longer in
|
||||
the site and return None as their url.
|
||||
|
||||
Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
|
||||
Discussion: https://github.com/torchbox/wagtail/issues/157
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
A generic HTML whitelisting engine, designed to accommodate subclassing to override
|
||||
specific rules.
|
||||
"""
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
from bs4 import BeautifulSoup, NavigableString, Tag
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
ALLOWED_URL_SCHEMES = ['', 'http', 'https', 'ftp', 'mailto', 'tel']
|
||||
|
|
@ -28,7 +29,7 @@ def attribute_rule(allowed_attrs):
|
|||
* if the lookup returns a truthy value, keep the attribute; if falsy, drop it
|
||||
"""
|
||||
def fn(tag):
|
||||
for attr, val in tag.attrs.items():
|
||||
for attr, val in list(tag.attrs.items()):
|
||||
rule = allowed_attrs.get(attr)
|
||||
if rule:
|
||||
if callable(rule):
|
||||
|
|
@ -82,7 +83,7 @@ class Whitelister(object):
|
|||
attributes"""
|
||||
doc = BeautifulSoup(html, 'lxml')
|
||||
cls.clean_node(doc, doc)
|
||||
return unicode(doc)
|
||||
return doc.decode()
|
||||
|
||||
@classmethod
|
||||
def clean_node(cls, doc, node):
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue