Merge branch 'master' into 146-display-calculated-usage

This commit is contained in:
Tom Talbot 2014-07-22 16:30:31 +01:00
commit d7c3ba25ea
73 changed files with 9518 additions and 1487 deletions

View file

@ -1,6 +1,19 @@
Changelog
=========
0.5 (xx.xx.20xx)
~~~~~~~~~~~~~~~~
* Added RoutablePage model to allow embedding Django-style URL routing within a page
* Explorer nav now rendered separately and fetched with AJAX when needed
* Added decorator syntax for hooks
* Replaced lxml dependency with html5lib, to simplify installation
* Added page_unpublished signal
0.4.1 (14.07.2014)
~~~~~~~~~~~~~~~~~~
* ElasticSearch backend now respects the backward-compatible URLS configuration setting, in addition to HOSTS
* Documentation fixes
0.4 (10.07.2014)
~~~~~~~~~~~~~~~~
* ElasticUtils/pyelasticsearch swapped for elasticsearch-py

View file

@ -197,13 +197,13 @@ In addition to the model fields provided, ``Page`` has many properties and metho
.. automodule:: wagtail.wagtailcore.models
.. autoclass:: Page
.. method:: specific
.. autoattribute:: specific
Return this page in its most specific subclassed form.
.. autoattribute:: specific_class
.. automethod:: url
.. autoattribute:: url
.. automethod:: full_url
.. autoattribute:: full_url
.. automethod:: relative_url
@ -217,7 +217,7 @@ In addition to the model fields provided, ``Page`` has many properties and metho
.. automethod:: get_template
.. automethod:: preview_modes
.. autoattribute:: preview_modes
.. automethod:: serve_preview
@ -381,6 +381,8 @@ Examples:
.. automethod:: public
See: :ref:`private_pages`
.. note::
This doesn't filter out unpublished pages. If you want to only have published public pages, use ``.live().public()``
@ -394,6 +396,8 @@ Examples:
.. automethod:: search
See: :ref:`wagtailsearch_for_python_developers`
Example:
.. code-block:: python

View file

@ -33,6 +33,9 @@ sys.path.insert(0, os.path.abspath('..'))
# be configured
os.environ['DJANGO_SETTINGS_MODULE'] = 'wagtail.tests.settings'
# Use SQLite3 database engine so it doesn't attempt to use psycopg2 on RTD
os.environ['DATABASE_ENGINE'] = 'django.db.backends.sqlite3'
# -- General configuration ------------------------------------------------

View file

@ -392,15 +392,28 @@ Admin Hooks
On loading, Wagtail will search for any app with the file ``wagtail_hooks.py`` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a ``Page`` object is saved or when the main menu is constructed.
Registering functions with a Wagtail hook follows the following pattern:
.. versionadded:: 0.5
Decorator syntax was added in 0.5; earlier versions only supported ``hooks.register`` as an ordinary function call.
Registering functions with a Wagtail hook is done through the ``@hooks.register`` decorator:
.. code-block:: python
from wagtail.wagtailcore import hooks
hooks.register('hook', function)
@hooks.register('name_of_hook')
def my_hook_function(arg1, arg2...)
# your code here
Where ``'hook'`` is one of the following hook strings and ``function`` is a function you've defined to handle the hook.
Alternatively, ``hooks.register`` can be called as an ordinary function, passing in the name of the hook and a handler function defined elsewhere:
.. code-block:: python
hooks.register('name_of_hook', my_hook_function)
The available hooks are:
.. _before_serve_page:
@ -413,11 +426,11 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from wagtail.wagtailcore import hooks
@hooks.register('before_serve_page')
def block_googlebot(page, request, serve_args, serve_kwargs):
if request.META.get('HTTP_USER_AGENT') == 'GoogleBot':
return HttpResponse("<h1>bad googlebot no cookie</h1>")
hooks.register('before_serve_page', block_googlebot)
.. _construct_wagtail_edit_bird:
@ -433,10 +446,10 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
+ 'target="_parent" class="action icon icon-wagtail">Puppies!</a></li>'
@hooks.register('construct_wagtail_edit_bird')
def add_puppy_link_item(request, items):
return items.append( UserbarPuppyLinkItem() )
hooks.register('construct_wagtail_edit_bird', add_puppy_link_item)
.. _construct_homepage_panels:
@ -459,10 +472,10 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
</section>
""")
@hooks.register('construct_homepage_panels')
def add_another_welcome_panel(request, panels):
return panels.append( WelcomePanel() )
hooks.register('construct_homepage_panels', add_another_welcome_panel)
.. _after_create_page:
@ -475,9 +488,10 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from wagtail.wagtailcore import hooks
@hooks.register('after_create_page')
def do_after_page_create(request, page):
return HttpResponse("Congrats on making content!", content_type="text/plain")
hooks.register('after_create_page', do_after_page_create)
.. _after_edit_page:
@ -508,11 +522,11 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
"I have approximate knowledge of many things!", \
content_type="text/plain")
@hooks.register('register_admin_urls')
def urlconf_time():
return [
url(r'^how_did_you_almost_know_my_name/$', admin_view, name='frank' ),
]
hooks.register('register_admin_urls', urlconf_time)
.. _construct_main_menu:
@ -526,11 +540,11 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
menu_items.append(
MenuItem( 'Frank', reverse('frank'), classnames='icon icon-folder-inverse', order=10000)
)
hooks.register('construct_main_menu', construct_main_menu)
.. _insert_editor_js:
@ -545,6 +559,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from wagtail.wagtailcore import hooks
@hooks.register('insert_editor_js')
def editor_js():
js_files = [
'demo/js/hallo-plugins/hallo-demo-plugin.js',
@ -559,7 +574,7 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
</script>
"""
)
hooks.register('insert_editor_js', editor_js)
.. _insert_editor_css:
@ -573,11 +588,11 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from wagtail.wagtailcore import hooks
@hooks.register('insert_editor_css')
def editor_css():
return format_html('<link rel="stylesheet" href="' \
+ settings.STATIC_URL \
+ 'demo/css/vendor/font-awesome/css/font-awesome.min.css">')
hooks.register('insert_editor_css', editor_css)
.. _construct_whitelister_element_rules:
@ -595,12 +610,12 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes
@hooks.register('construct_whitelister_element_rules')
def whitelister_element_rules():
return {
'blockquote': allow_without_attributes,
'a': attribute_rule({'href': check_url, 'target': True}),
}
hooks.register('construct_whitelister_element_rules', whitelister_element_rules)
Image Formats in the Rich Text Editor

View file

@ -3,6 +3,8 @@
Frontend cache purging
======================
.. versionadded:: 0.4
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.

View file

@ -16,6 +16,7 @@ It supports Django 1.6.2+ on Python 2.6, 2.7, 3.2, 3.3 and 3.4. Django 1.7 suppo
search/index
form_builder
model_recipes
routable_page
advanced_topics
deploying
performance

View file

@ -3,11 +3,13 @@ Performance
Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation.
Editor interface
~~~~~~~~~~~~~~~~
We have tried to minimise external dependencies for a working installation of Wagtail, in order to make it as simple as possible to get going. However, a number of default settings can be configured for better performance:
Cache
-----
@ -25,16 +27,19 @@ We recommend `Redis <http://redis.io/>`_ as a fast, persistent cache. Install Re
Without a persistent cache, Wagtail will recreate all compressable assets at each server start, e.g. when any files change under ```./manage.py runserver```.
Search
------
Wagtail has strong support for `Elasticsearch <http://www.elasticsearch.org/>`_ - both in the editor interface and for users of your site - but can fall back to a database search if Elasticsearch isn't present. Elasticsearch is faster and more powerful than the Django ORM for text search, so we recommend installing it or using a hosted service like `Searchly <http://www.searchly.com/>`_.
Database
--------
Wagtail is tested on SQLite, and should work on other Django-supported database backends, but we recommend PostgreSQL for production use.
Public users
~~~~~~~~~~~~
@ -42,3 +47,7 @@ Caching proxy
-------------
To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both `Varnish <http://www.varnish-cache.org/>`_ and `Squid <http://www.squid-cache.org/>`_ have been tested in production. Hosted proxies like `Cloudflare <https://www.cloudflare.com/>`_ should also work well.
.. versionadded:: 0.4
Wagtail supports automatic cache invalidation for Varnish/Squid. See :ref:`frontend_cache_purging` for more information.

View file

@ -3,10 +3,13 @@
Private pages
=============
.. versionadded:: 0.4
Users with publish permission on a page can set it to be private by clicking the 'Privacy' control in the top right corner of the page explorer or editing interface, and setting a password. Users visiting this page, or any of its subpages, will be prompted to enter a password before they can view the page.
Private pages work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default "password required" form is only a bare-bones HTML page, and site implementers may wish to replace this with a page customised to their site design.
Setting up a global "password required" page
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -52,6 +55,7 @@ A basic template suitable for use as ``PASSWORD_REQUIRED_TEMPLATE`` might look l
</body>
</html>
Setting a "password required" page for a specific page type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

9
docs/releases/0.4.1.rst Normal file
View file

@ -0,0 +1,9 @@
===========================
Wagtail 0.4.1 release notes
===========================
Bug fixes
~~~~~~~~~
* ElasticSearch backend now respects the backward-compatible URLS configuration setting, in addition to HOSTS
* Documentation fixes

View file

@ -7,13 +7,44 @@ Wagtail 0.5 release notes - IN DEVELOPMENT
:depth: 1
Whats new
=========
What's new
==========
RoutablePage
~~~~~~~~~~~~
A ``RoutablePage`` model has been added to allow embedding Django-style URL routing within a page.
:ref:`routable_page`
Minor features
~~~~~~~~~~~~~~
Core
----
* Hooks can now be defined using decorator syntax:
.. code-block:: python
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
menu_items.append(
MenuItem('Kittens!', '/kittens/', classnames='icon icon-folder-inverse', order=1000)
)
* The lxml library (used for whitelisting and rewriting of rich text fields) has been replaced with the pure-python html5lib library, to simplify installation.
* A ``page_unpublished`` signal has been added.
Admin
-----
* Explorer nav now rendered separately and fetched with AJAX when needed.
This improves the general performance of the admin interface for large sites.
Bug fixes
~~~~~~~~~

View file

@ -5,4 +5,5 @@ Release notes
:maxdepth: 1
0.5
0.4.1
0.4

5
docs/requirements.txt Normal file
View file

@ -0,0 +1,5 @@
Django>=1.6.2,<1.7
django-modelcluster>=0.3
django-taggit==0.11.2
django-treebeard==2.0
six==1.7.3

88
docs/routable_page.rst Normal file
View file

@ -0,0 +1,88 @@
.. _routable_page:
====================================
Embedding URL configuration in Pages
====================================
.. versionadded:: 0.5
The ``RoutablePage`` class provides a convenient way for a page to respond on multiple sub-URLs with different views. For example, a blog section on a site might provide several different types of index page at URLs like ``/blog/2013/06/``, ``/blog/authors/bob/``, ``/blog/tagged/python/``, all served by the same ``BlogIndex`` page.
A ``RoutablePage`` exists within the page tree like any other page, but URL paths underneath it are checked against a list of patterns, using Django's urlconf scheme. If none of the patterns match, control is passed to subpages as usual (or failing that, a 404 error is thrown).
The basics
==========
To use ``RoutablePage``, you need to make your class inherit from :class:`wagtail.contrib.wagtailroutablepage.models.RoutablePage` and configure the ``subpage_urls`` attribute with your URL configuration.
Here's an example of an ``EventPage`` with three views:
.. code-block:: python
from django.conf.urls import url
from wagtail.contrib.wagtailroutablepage.models import RoutablePage
class EventPage(RoutablePage):
subpage_urls = (
url(r'^$', 'current_events', name='current_events'),
url(r'^past/$', 'past_events', name='past_events'),
url(r'^year/(\d+)/$', 'events_for_year', name='events_for_year'),
)
def current_events(self, request):
"""
View function for the current events page
"""
...
def past_events(self, request):
"""
View function for the current events page
"""
...
def events_for_year(self, request):
"""
View function for the events for year page
"""
...
The ``RoutablePage`` class
==========================
.. automodule:: wagtail.contrib.wagtailroutablepage.models
.. autoclass:: RoutablePage
.. autoattribute:: subpage_urls
Example:
.. code-block:: python
from django.conf.urls import url
subpage_urls = (
url(r'^$', 'serve', name='main'),
url(r'^archive/$', 'archive', name='archive'),
)
.. automethod:: resolve_subpage
Example:
.. code-block:: python
view, args, kwargs = page.resolve_subpage('/past/')
response = view(request, *args, **kwargs)
.. automethod:: reverse_subpage
Example:
.. code-block:: python
url = page.url + page.reverse_subpage('events_for_year', args=('2014', ))

View file

@ -41,11 +41,10 @@ The backend is configured in settings:
'URLS': ['http://localhost:9200'],
'INDEX': 'wagtail',
'TIMEOUT': 5,
'FORCE_NEW': False,
}
}
Other than ``BACKEND`` the keys are optional and default to the values shown. ``FORCE_NEW`` is used by elasticsearch-py. In addition, any other keys are passed directly to the Elasticsearch constructor as case-sensitive keyword arguments (e.g. ``'max_retries': 1``).
Other than ``BACKEND`` the keys are optional and default to the values shown. In addition, any other keys are passed directly to the Elasticsearch constructor as case-sensitive keyword arguments (e.g. ``'max_retries': 1``).
If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly:

View file

@ -31,6 +31,11 @@ All methods of ``PageQuerySet`` are supported by wagtailsearch:
Indexing extra fields
=====================
.. versionchanged:: 0.4
The ``indexed_fields`` configuration format was replaced with ``search_fields``
Fields need to be explicitly added to the search configuration in order for you to be able to search/filter on them.
You can add new fields to the search index by overriding the ``search_fields`` property and appending a list of extra ``SearchField``/``FilterField`` objects to it.

View file

@ -3,6 +3,8 @@
Sitemap generation
==================
.. versionadded:: 0.4
This document describes how to create XML sitemaps for your Wagtail website using the ``wagtail.contrib.wagtailsitemaps`` module.

View file

@ -92,26 +92,49 @@ Then in your own page templates, you can include your snippet template tag with:
{% endblock %}
Binding Pages to Snippets
-------------------------
An alternate strategy for including snippets might involve explicitly binding a specific page object to a specific snippet object. Lets add another snippet class to see how that might work:
In the above example, the list of adverts is a fixed list, displayed as part of the template independently of the page content. This might be what you want for a common panel in a sidebar, say - but in other scenarios you may wish to refer to a snippet within page content. This can be done by defining a foreign key to the snippet model within your page model, and adding a ``SnippetChooserPanel`` to the page's ``content_panels`` definitions. For example, if you wanted to be able to specify an advert to appear on ``BookPage``:
.. code-block:: python
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
# ...
class BookPage(Page):
advert = models.ForeignKey(
'demo.Advert',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
BookPage.content_panels = [
SnippetChooserPanel('advert', Advert),
# ...
]
The snippet could then be accessed within your template as ``self.advert``.
To attach multiple adverts to a page, the ``SnippetChooserPanel`` can be placed on an inline child object of ``BookPage``, rather than on ``BookPage`` itself. Here this child model is named ``BookPageAdvertPlacement`` (so called because there is one such object for each time that an advert is placed on a BookPage):
.. code-block:: python
from django.db import models
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
from wagtail.wagtailsnippets.models import register_snippet
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from modelcluster.fields import ParentalKey
...
class AdvertPlacement(models.Model):
page = ParentalKey('wagtailcore.Page', related_name='advert_placements')
class BookPageAdvertPlacement(Orderable, models.Model):
page = ParentalKey('demo.BookPage', related_name='advert_placements')
advert = models.ForeignKey('demo.Advert', related_name='+')
class Meta:
@ -119,25 +142,27 @@ An alternate strategy for including snippets might involve explicitly binding a
verbose_name_plural = "Advert Placements"
panels = [
PageChooserPanel('page'),
SnippetChooserPanel('advert', Advert),
]
def __unicode__(self):
return self.page.title + " -> " + self.advert.text
register_snippet(AdvertPlacement)
class BookPage(Page):
...
The class ``AdvertPlacement`` has two properties, ``page`` and ``advert``, which point to other models. Wagtail provides a ``PageChooserPanel`` and ``SnippetChooserPanel`` to let us make painless selection of those properties in the Wagtail admin. Note also the ``Meta`` class, which you can stock with the ``verbose_name`` and ``verbose_name_plural`` properties to override the snippet labels in the Wagtail admin. The text representation of the class has also gotten fancy, using both properties to construct a compound label showing the relationship it forms between a page and an Advert.
BookPage.content_panels = [
InlinePanel(BookPage, 'advert_placements', label="Adverts"),
# ...
]
With this snippet in place, we can use the reverse ``related_name`` lookup label ``advert_placements`` to iterate over any placements within our template files. In the template for a ``Page``-derived model, we could include the following:
These child objects are now accessible through the page's ``advert_placements`` property, and from there we can access the linked Advert snippet as ``advert``. In the template for ``BookPage``, we could include the following:
.. code-block:: django
{% if self.advert_placements %}
{% for advert_placement in self.advert_placements.all %}
<p><a href="{{ advert_placement.advert.url }}">{{ advert_placement.advert.text }}</a></p>
{% endfor %}
{% endif %}
{% for advert_placement in self.advert_placements.all %}
<p><a href="{{ advert_placement.advert.url }}">{{ advert_placement.advert.text }}</a></p>
{% endfor %}

View file

@ -1,3 +1,4 @@
#!/usr/bin/env bash
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# For a non-dummy email backend configure Django's EMAIL_BACKEND
@ -12,7 +13,7 @@ PROJECT_ROOT=/usr/local/django
echo "This script overwrites key files, and should only be run on a new box."
read -p "Type 'yes' to confirm: " CONFIRM
[ $CONFIRM== “yes” ] || exit
[ "$CONFIRM" == "yes" ] || exit
read -p "Enter a name for your project [$PROJECT]: " U_PROJECT
if [ ! -z "$U_PROJECT" ]; then
@ -35,7 +36,7 @@ SERVER_IP=`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1`
aptitude update
aptitude -y install git python-pip nginx postgresql redis-server
aptitude -y install postgresql-server-dev-all python-dev libxml2-dev libxslt-dev libjpeg62-dev
aptitude -y install postgresql-server-dev-all python-dev libjpeg62-dev
perl -pi -e "s/^(local\s+all\s+postgres\s+)peer$/\1trust/" /etc/postgresql/9.1/main/pg_hba.conf
service postgresql reload
@ -62,7 +63,7 @@ pip install -r requirements/production.txt
swapoff -v /tmpswap
rm /tmpswap
echo SECRET_KEY = \"`python -c 'import random; print "".join([random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)])'`\" > $PROJECT/settings.local.py
echo SECRET_KEY = \"`python -c 'import random; print "".join([random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)])'`\" > $PROJECT/settings/local.py
echo ALLOWED_HOSTS = [\'$SERVER_IP\',] >> $PROJECT/settings/local.py
createdb -Upostgres $PROJECT
./manage.py syncdb --settings=$PROJECT.settings.production

View file

@ -1,3 +1,4 @@
#!/usr/bin/env bash
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# For a non-dummy email backend configure Django's EMAIL_BACKEND
@ -10,7 +11,7 @@ PROJECT_ROOT=/usr/local/django
echo "This script overwrites key files, and should only be run on a new box."
read -p "Type 'yes' to confirm: " CONFIRM
[ $CONFIRM== “yes” ] || exit
[ "$CONFIRM" == "yes" ] || exit
read -p "Enter a name for your project [$PROJECT]: " U_PROJECT
if [ ! -z "$U_PROJECT" ]; then
@ -33,7 +34,7 @@ SERVER_IP=`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1`
aptitude update
aptitude -y install git python-pip nginx postgresql redis-server
aptitude -y install postgresql-server-dev-all python-dev libxml2-dev libxslt-dev libjpeg62-dev
aptitude -y install postgresql-server-dev-all python-dev libjpeg62-dev
perl -pi -e "s/^(local\s+all\s+postgres\s+)peer$/\1trust/" /etc/postgresql/9.1/main/pg_hba.conf
service postgresql reload
@ -60,7 +61,7 @@ pip install -r requirements/production.txt
swapoff -v /tmpswap
rm /tmpswap
echo SECRET_KEY = \"`python -c 'import random; print "".join([random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)])'`\" > $PROJECT/settings.local.py
echo SECRET_KEY = \"`python -c 'import random; print "".join([random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)])'`\" > $PROJECT/settings/local.py
echo ALLOWED_HOSTS = [\'$SERVER_IP\',] >> $PROJECT/settings/local.py
createdb -Upostgres $PROJECT
./manage.py syncdb --settings=$PROJECT.settings.production

View file

@ -32,7 +32,7 @@ install_requires = [
"django-treebeard==2.0",
"Pillow>=2.3.0",
"beautifulsoup4>=4.3.2",
"lxml>=3.3.0",
"html5lib==0.999",
"Unidecode>=0.04.14",
"six==1.7.3",
'requests==2.3.0',

View file

@ -1,8 +1,7 @@
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.wagtailcore.signals import page_published, page_unpublished
from wagtail.contrib.wagtailfrontendcache.utils import purge_page_from_cache
@ -11,7 +10,7 @@ def page_published_signal_handler(instance, **kwargs):
purge_page_from_cache(instance)
def post_delete_signal_handler(instance, **kwargs):
def page_unpublished_signal_handler(instance, **kwargs):
purge_page_from_cache(instance)
@ -22,4 +21,4 @@ def register_signal_handlers():
# 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)
page_unpublished.connect(page_unpublished_signal_handler, sender=model)

View file

@ -0,0 +1,74 @@
from six import string_types
from django.http import Http404
from django.core.urlresolvers import get_resolver
from django.core.exceptions import ImproperlyConfigured
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.url_routing import RouteResult
class RoutablePage(Page):
"""
This class extends Page by adding methods to allow urlconfs to be embedded inside pages
"""
#: Set this to a tuple of ``django.conf.urls.url`` objects.
subpage_urls = None
def reverse_subpage(self, name, args=None, kwargs=None):
"""
This method does the same job as Djangos' built in "urlresolvers.reverse()" function for subpage urlconfs.
"""
args = args or []
kwargs = kwargs or {}
if self.subpage_urls is None:
raise ImproperlyConfigured("You must set 'subpage_urls' on " + type(self).__name__)
resolver = get_resolver(self.subpage_urls)
return resolver.reverse(name, *args, **kwargs)
def resolve_subpage(self, path):
"""
This finds a view method/function from a URL path.
"""
if self.subpage_urls is None:
raise ImproperlyConfigured("You must set 'subpage_urls' on " + type(self).__name__)
resolver = get_resolver(self.subpage_urls)
view, args, kwargs = resolver.resolve(path)
# If view is a string, find it as an attribute of self
if isinstance(view, string_types):
view = getattr(self, view)
return view, args, kwargs
def route(self, request, path_components):
"""
This hooks the subpage urls into Wagtails routing.
"""
if self.live:
try:
path = '/'
if path_components:
path += '/'.join(path_components) + '/'
view, args, kwargs = self.resolve_subpage(path)
return RouteResult(self, args=(view, args, kwargs))
except Http404:
pass
return super(RoutablePage, self).route(request, path_components)
def serve(self, request, view, args, kwargs):
return view(request, *args, **kwargs)
def serve_preview(self, request, mode_name):
view, args, kwargs = self.resolve_subpage('/')
return view(*args, **kwargs)
is_abstract = True
class Meta:
abstract = True

View file

@ -0,0 +1,82 @@
from django.test import TestCase
from wagtail.wagtailcore.models import Page
from wagtail.tests.models import RoutablePageTest, routable_page_external_view
class TestRoutablePage(TestCase):
def setUp(self):
self.home_page = Page.objects.get(id=2)
self.routable_page = self.home_page.add_child(instance=RoutablePageTest(
title="Routable Page",
slug='routable-page',
live=True,
))
def test_resolve_main_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/')
self.assertEqual(view, self.routable_page.main)
self.assertEqual(args, ())
self.assertEqual(kwargs, {})
def test_resolve_archive_by_year_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/archive/year/2014/')
self.assertEqual(view, self.routable_page.archive_by_year)
self.assertEqual(args, ('2014', ))
self.assertEqual(kwargs, {})
def test_resolve_archive_by_author_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/archive/author/joe-bloggs/')
self.assertEqual(view, self.routable_page.archive_by_author)
self.assertEqual(args, ())
self.assertEqual(kwargs, {'author_slug': 'joe-bloggs'})
def test_resolve_external_view(self):
view, args, kwargs = self.routable_page.resolve_subpage('/external/joe-bloggs/')
self.assertEqual(view, routable_page_external_view)
self.assertEqual(args, ('joe-bloggs', ))
self.assertEqual(kwargs, {})
def test_reverse_main_view(self):
url = self.routable_page.reverse_subpage('main')
self.assertEqual(url, '')
def test_reverse_archive_by_year_view(self):
url = self.routable_page.reverse_subpage('archive_by_year', args=('2014', ))
self.assertEqual(url, 'archive/year/2014/')
def test_reverse_archive_by_author_view(self):
url = self.routable_page.reverse_subpage('archive_by_author', kwargs={'author_slug': 'joe-bloggs'})
self.assertEqual(url, 'archive/author/joe-bloggs/')
def test_reverse_external_view(self):
url = self.routable_page.reverse_subpage('external_view', args=('joe-bloggs', ))
self.assertEqual(url, 'external/joe-bloggs/')
def test_get_main_view(self):
response = self.client.get(self.routable_page.url)
self.assertContains(response, "MAIN VIEW")
def test_get_archive_by_year_view(self):
response = self.client.get(self.routable_page.url + 'archive/year/2014/')
self.assertContains(response, "ARCHIVE BY YEAR: 2014")
def test_get_archive_by_author_view(self):
response = self.client.get(self.routable_page.url + 'archive/author/joe-bloggs/')
self.assertContains(response, "ARCHIVE BY AUTHOR: joe-bloggs")
def test_get_external_view(self):
response = self.client.get(self.routable_page.url + 'external/joe-bloggs/')
self.assertContains(response, "EXTERNAL VIEW: joe-bloggs")

View file

@ -1,6 +1,6 @@
@import "../variables.scss";
@import "../mixins.scss";
@import "../grid.scss";
@import "wagtailadmin/scss/variables.scss";
@import "wagtailadmin/scss/mixins.scss";
@import "wagtailadmin/scss/grid.scss";
section{
border-top:1px solid $color-grey-3;
@ -50,6 +50,9 @@ section{
.color-green{
background-color:$color-green;
}
.color-blue{
background-color:$color-blue;
}
.color-grey-1{
background-color:$color-grey-1;
}

View file

@ -5,7 +5,7 @@
{% block extra_css %}
{% compress css %}
<link rel="stylesheet" href="{{ STATIC_URL }}wagtailadmin/scss/layouts/styleguide.scss" type="text/x-scss" />
<link rel="stylesheet" href="{{ STATIC_URL }}wagtailstyleguide/scss/styleguide.scss" type="text/x-scss" />
{% endcompress %}
{% endblock %}
@ -22,6 +22,7 @@
<ul class="unlist">
<li><a href="#palette">Colour palette</a></li>
<li><a href="#typography">Typography</a></li>
<li><a href="#help">Help text</a></li>
<li><a href="#listings">Listings</a></li>
<li><a href="#buttons">Buttons</a></li>
<li><a href="#dropdowns">Dropdown buttons</a></li>
@ -59,6 +60,7 @@
<li class="color-red">color-red</li>
<li class="color-orange">color-orange</li>
<li class="color-green">color-green</li>
<li class="color-blue">color-blue</li>
</ul>
</section>
@ -88,6 +90,26 @@
</section>
<section id="help">
<h2>Help text</h2>
<p>Help text is not to be confused with the messages that appear in a banner drop down from the top of the screen. Help text are permanent instructions, visible on every page view, that explain or warn about something.
<div class="help-block help-info">
<p>This is help text that might be just for information, explaining what happens next, or drawing the user's attention to something they're about to do</p>
<p>It could be multiple lines</p>
</div>
<p class="help-block help-warning">
A warning message might be output in cases where a user's action could have serious consequences
</p>
<div class="help-block help-critical">
A critical message would probably be rare, in cases where a particularly brittle or dangerously destructive action could be performed and needs to be warned about.
</div>
</section>
<section id="listings">
<h2>Listings</h2>
@ -416,6 +438,7 @@
<li class="icon icon-date">date</li>
<li class="icon icon-time">time</li>
<li class="icon icon-form">form</li>
<li class="icon icon-site">site</li>
</ul>
</section>

View file

@ -8,15 +8,15 @@ from wagtail.wagtailadmin.menu import MenuItem
from . import views
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^styleguide/$', views.index, name='wagtailstyleguide'),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
menu_items.append(
MenuItem(_('Styleguide'), urlresolvers.reverse('wagtailstyleguide'), classnames='icon icon-image', order=1000)
)
hooks.register('construct_main_menu', construct_main_menu)

View file

@ -1,6 +1,8 @@
from django.db import models
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.encoding import python_2_unicode_compatible
from django.conf.urls import url
from django.http import HttpResponse
from modelcluster.fields import ParentalKey
@ -12,6 +14,7 @@ from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
from wagtail.wagtailsnippets.models import register_snippet
from wagtail.wagtailsearch import indexed
from wagtail.contrib.wagtailroutablepage.models import RoutablePage
EVENT_AUDIENCE_CHOICES = (
@ -385,9 +388,31 @@ class SearchTestOldConfig(models.Model, indexed.Indexed):
},
}
class SearchTestOldConfigList(models.Model, indexed.Indexed):
"""
This tests that the Indexed class can correctly handle models that
use the old "indexed_fields" configuration format using a list.
"""
indexed_fields = ['title', 'content']
def routable_page_external_view(request, arg):
return HttpResponse("EXTERNAL VIEW: " + arg)
class RoutablePageTest(RoutablePage):
subpage_urls = (
url(r'^$', 'main', name='main'),
url(r'^archive/year/(\d+)/$', 'archive_by_year', name='archive_by_year'),
url(r'^archive/author/(?P<author_slug>.+)/$', 'archive_by_author', name='archive_by_author'),
url(r'^external/(.+)/$', routable_page_external_view, name='external_view')
)
def archive_by_year(self, request, year):
return HttpResponse("ARCHIVE BY YEAR: " + str(year))
def archive_by_author(self, request, author_slug):
return HttpResponse("ARCHIVE BY AUTHOR: " + author_slug)
def main(self, request):
return HttpResponse("MAIN VIEW")

View file

@ -29,7 +29,6 @@ STATICFILES_FINDERS = (
)
USE_TZ = True
TIME_ZONE = 'UTC'
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'django.core.context_processors.request',
@ -72,6 +71,7 @@ INSTALLED_APPS = [
'wagtail.wagtailforms',
'wagtail.contrib.wagtailstyleguide',
'wagtail.contrib.wagtailsitemaps',
'wagtail.contrib.wagtailroutablepage',
'wagtail.tests',
]

View file

@ -3,14 +3,17 @@ from django.http import HttpResponse
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.whitelist import attribute_rule, check_url, allow_without_attributes
# Register one hook using decorators...
@hooks.register('insert_editor_css')
def editor_css():
return """<link rel="stylesheet" href="/path/to/my/custom.css">"""
hooks.register('insert_editor_css', editor_css)
def editor_js():
return """<script src="/path/to/my/custom.js"></script>"""
hooks.register('insert_editor_js', editor_js)
# And the other using old-style function calls
def whitelister_element_rules():

View file

@ -12,7 +12,7 @@ $(function(){
$('body').addClass('ready');
// Enable toggle to open/close nav
$('#nav-toggle').click(function(){
$(document).on('click', '#nav-toggle', function(){
$('body').toggleClass('nav-open');
if(!$('body').hasClass('nav-open')){
$('body').addClass('nav-closed');
@ -21,12 +21,30 @@ $(function(){
}
});
// Enable swishy section navigation menu
$('.explorer').addClass('dl-menuwrapper').dlmenu({
animationClasses : {
classin : 'dl-animate-in-2',
classout : 'dl-animate-out-2'
// Dynamically load menu on request.
$(document).on('click', '.dl-trigger', function(){
var $this = $(this);
var $explorer = $('#explorer');
$this.addClass('icon-spinner');
if(!$explorer.children().length){
$explorer.load(window.explorer_menu_url, function() {
$this.removeClass('icon-spinner');
$explorer.addClass('dl-menuwrapper').dlmenu({
animationClasses : {
classin : 'dl-animate-in-2',
classout : 'dl-animate-out-2'
}
});
$explorer.dlmenu('openMenu');
});
}else{
$explorer.dlmenu('openMenu');
}
return false;
});
// Resize nav to fit height of window. This is an unimportant bell/whistle to make it look nice
@ -35,9 +53,6 @@ $(function(){
$('.nav-main').each(function(){
var thisHeight = $(this).height();
var footerHeight = $('.footer', $(this)).height();
// $(this).css({'height':thisHeight - footerHeight, 'overflow-y':'scroll'});
// $('> ul', $(this)).height(thisHeight)
});
};
fitNav();
@ -86,22 +101,6 @@ $(function(){
}
});
/* Bulk-selection */
$(document).on('click', 'thead .bulk', function(){
$(this).closest('table').find('tbody .bulk input').each(function(){
$(this).prop('checked', !$(this).prop('checked'));
});
});
$('#menu-search input').bind('focus', function(){
$('#menu-search').addClass('focussed');
}).bind('blur', function(){
$('#menu-search').removeClass('focussed');
});
$('#menu-search').bind('focus click', function(){
$(this).addClass('focussed');
});
/* Header search behaviour */
if(window.headerSearch){
var search_current_index = 0;

View file

@ -194,6 +194,13 @@ a.tag:hover{
}
}
/* general image style */
img{
max-width:100%;
height:auto;
border: 3px solid $color-grey-4;
}
/* make a block-level element inline */
.inline{

View file

@ -250,6 +250,9 @@
.icon-form:before{
content:"$";
}
.icon-site:before{
content:"@";
}
.icon.text-replace{
font-size:0em;

View file

@ -1,3 +1,10 @@
/*
Messages are specific to Django's "Messaging" system which adds messages into the session,
for display on the next page visited. These appear as an animated banner at the top of the page.
For inline help text, see typography.scss
*/
.messages{
position:relative;
z-index:5;

View file

@ -0,0 +1,110 @@
h1,h2,h3,h4,h5,h6{
font-weight:normal;
}
h1{
line-height:1.3em;
font-size:1.5em;
text-transform:uppercase;
color:$color-grey-1;
font-weight:600;
span{
text-transform:none;
font-weight:300;
}
.homepage &{
text-transform:none;
}
}
h2{
text-transform:uppercase;
font-size:1.3em;
font-family:Open Sans;
font-weight:600;
color:$color-grey-2;
.page-explorer &{
text-transform:none;
}
}
a{
outline:none;
color:$color-link;
text-decoration:none;
&:hover{
color:$color-link-hover;
}
}
code{
@include box-shadow(inset 0px 0px 4px 0px rgba(0, 0, 0, 0.2));
background-color:$color-fieldset-hover;
padding:2px 5px;
}
kbd{
@include border-radius(3px);
font-family:Open Sans, Arial, sans-serif;
border:1px solid $color-grey-2;
border-color:rgba(0,0,0,0.2);
padding:0.3em 0.5em;
}
/* Help text formatters */
.help-block{
padding:1em;
margin:1em 0;
p{
margin-top:0;
&:last-child{
margin-bottom:0;
}
}
}
.help-info, .help-warning, .help-critical{
@include border-radius(3px);
border:1px solid $color-grey-4;
padding-left:3.5em;
position:relative;
&:before{
font-family:wagtail;
position:absolute;
left:1em;
top:0.7em;
content:"?";
font-size:1.4em;
}
}
.help-info{
border-color:$color-blue;
&:before{
color:$color-blue;
}
}
.help-warning{
border-color:$color-orange;
&:before{
color:$color-orange;
content:"!";
}
}
.help-critical{
border-color:$color-red;
&:before{
color:$color-red;
content:"!";
}
}

View file

@ -4,6 +4,7 @@
@import "components/explorer.scss";
@import "components/icons.scss";
@import "components/typography.scss";
@import "components/tabs.scss";
@import "components/dropdowns.scss";
@import "components/modals.scss";
@ -35,67 +36,6 @@ body{
}
}
h1,h2,h3,h4,h5,h6{
font-weight:normal;
}
h1{
line-height:1.3em;
font-size:1.5em;
text-transform:uppercase;
color:$color-grey-1;
font-weight:600;
span{
text-transform:none;
font-weight:300;
}
.homepage &{
text-transform:none;
}
}
h2{
text-transform:uppercase;
font-size:1.3em;
font-family:Open Sans;
font-weight:600;
color:$color-grey-2;
.page-explorer &{
text-transform:none;
}
}
a{
outline:none;
color:$color-link;
text-decoration:none;
&:hover{
color:$color-link-hover;
}
}
code{
@include box-shadow(inset 0px 0px 4px 0px rgba(0, 0, 0, 0.2));
background-color:$color-fieldset-hover;
padding:2px 5px;
}
kbd{
@include border-radius(3px);
font-family:Open Sans, Arial, sans-serif;
border:1px solid $color-grey-2;
border-color:rgba(0,0,0,0.2);
padding:0.3em 0.5em;
}
img{
max-width:100%;
height:auto;
border: 3px solid $color-grey-4;
}
.browsermessage{
background-color:$color-red;
color:white;
@ -163,7 +103,6 @@ img{
}
.nav-main{
top: 43px;
bottom: 0px;
overflow: auto;
@ -226,6 +165,15 @@ img{
&:hover{
color:white;
}
/* only really used for spinners */
&:after{
font-size:1.5em;
margin:0;
position:absolute;
right:0.5em;
margin-top:0.15em;
}
}
.avatar{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -27,6 +27,7 @@ $breakpoint-desktop-larger: 100em; /* 1600px */
$color-teal: #43b1b0;
$color-teal-darker: darken($color-teal, 10%);
$color-teal-dark: #246060;
$color-blue: #71b2d4;
$color-red: #cd3238;
$color-orange:#e9b04d;
$color-green: #189370;

View file

@ -22,6 +22,9 @@
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/bootstrap-tab.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/jquery.dlmenu.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/core.js"></script>
<script>
window.explorer_menu_url = "{% url 'wagtailadmin_explorer_nav' %}";
</script>
{% endcompress %}
{% block extra_js %}{% endblock %}

View file

@ -8,11 +8,7 @@
{% main_nav %}
</div>
<nav class="explorer">
<ul class="dl-menu">
{% explorer_nav %}
</ul>
</nav>
<nav id="explorer" class="explorer"></nav>
</div>
<div class="content-wrapper">

View file

@ -1,13 +1,5 @@
{% load wagtailadmin_tags %}
{% for page, children in nodes %}
<li {% if children %}class="has-children"{% endif %}>
<a href="{% url 'wagtailadmin_explore' page.id %}" class="icon icon-folder-open-inverse">{{ page.title }}</a>
{% if children %}
<div class="children icon icon-arrow-right"></div>
<ul class="dl-submenu">
{% explorer_subnav children %}
</ul>
{% endif %}
</li>
{% endfor %}
<ul class="dl-menu">
{% include "wagtailadmin/shared/explorer_nav_child.html" %}
</ul>

View file

@ -0,0 +1,13 @@
{% load wagtailadmin_tags %}
{% for page, children in nodes %}
<li {% if children %}class="has-children"{% endif %}>
<a href="{% url 'wagtailadmin_explore' page.id %}" class="icon icon-folder-open-inverse">{{ page.title }}</a>
{% if children %}
<div class="children icon icon-arrow-right"></div>
<ul class="dl-submenu">
{% explorer_subnav children %}
</ul>
{% endif %}
</li>
{% endfor %}

View file

@ -22,7 +22,7 @@ def explorer_nav():
}
@register.inclusion_tag('wagtailadmin/shared/explorer_nav.html')
@register.inclusion_tag('wagtailadmin/shared/explorer_nav_child.html')
def explorer_subnav(nodes):
return {
'nodes': nodes
@ -32,7 +32,7 @@ def explorer_subnav(nodes):
@register.inclusion_tag('wagtailadmin/shared/main_nav.html', takes_context=True)
def main_nav(context):
menu_items = [
MenuItem(_('Explorer'), '#', classnames='icon icon-folder-open-inverse dl-trigger', order=100),
MenuItem(_('Explorer'), urlresolvers.reverse('wagtailadmin_explore_root'), classnames='icon icon-folder-open-inverse dl-trigger', order=100),
MenuItem(_('Search'), urlresolvers.reverse('wagtailadmin_pages_search'), classnames='icon icon-search', order=200),
]

View file

@ -10,10 +10,21 @@ from django.utils import timezone
from wagtail.tests.models import SimplePage, EventPage, EventPageCarouselItem, StandardIndex, BusinessIndex, BusinessChild, BusinessSubIndex
from wagtail.tests.utils import unittest, WagtailTestUtils
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailcore.signals import page_published
from wagtail.wagtailcore.signals import page_published, page_unpublished
from wagtail.wagtailusers.models import UserProfile
def submittable_timestamp(timestamp):
"""
Helper function to translate a possibly-timezone-aware datetime into the format used in the
go_live_at / expire_at form fields - "YYYY-MM-DD hh:mm", with no timezone indicator.
This will be interpreted as being in the server's timezone (settings.TIME_ZONE), so we
need to pass it through timezone.localtime to ensure that the client and server are in
agreement about what the timestamp means.
"""
return str(timezone.localtime(timestamp)).split('.')[0]
class TestPageExplorer(TestCase, WagtailTestUtils):
def setUp(self):
# Find root page
@ -180,8 +191,8 @@ class TestPageCreation(TestCase, WagtailTestUtils):
'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],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
@ -203,8 +214,8 @@ class TestPageCreation(TestCase, WagtailTestUtils):
'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],
'go_live_at': submittable_timestamp(timezone.now() + timedelta(days=2)),
'expire_at': submittable_timestamp(timezone.now() + timedelta(days=1)),
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
@ -219,7 +230,7 @@ class TestPageCreation(TestCase, WagtailTestUtils):
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
'expire_at': submittable_timestamp(timezone.now() + timedelta(days=-1)),
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
@ -268,8 +279,8 @@ class TestPageCreation(TestCase, WagtailTestUtils):
'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],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
@ -428,14 +439,16 @@ class TestPageEdit(TestCase, WagtailTestUtils):
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)
# put go_live_at and expire_at several days away from the current date, to avoid
# false matches in content_json__contains tests
go_live_at = timezone.now() + timedelta(days=10)
expire_at = timezone.now() + timedelta(days=20)
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],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -459,8 +472,8 @@ class TestPageEdit(TestCase, WagtailTestUtils):
'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],
'go_live_at': submittable_timestamp(timezone.now() + timedelta(days=2)),
'expire_at': submittable_timestamp(timezone.now() + timedelta(days=1)),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -475,7 +488,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
'expire_at': submittable_timestamp(timezone.now() + timedelta(days=-1)),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -525,8 +538,8 @@ class TestPageEdit(TestCase, WagtailTestUtils):
'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],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -550,8 +563,8 @@ class TestPageEdit(TestCase, WagtailTestUtils):
'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],
'go_live_at': submittable_timestamp(go_live_at),
'expire_at': submittable_timestamp(expire_at),
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
@ -796,6 +809,15 @@ class TestPageDelete(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 403)
def test_page_delete_post(self):
# Connect a mock signal handler to page_unpublished signal
signal_fired = [False]
signal_page = [None]
def page_unpublished_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_unpublished.connect(page_unpublished_handler)
# Post
post_data = {'hello': 'world'} # For some reason, this test doesn't work without a bit of POST data
response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )), post_data)
@ -805,6 +827,38 @@ class TestPageDelete(TestCase, WagtailTestUtils):
# Check that the page is gone
self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0)
# Check that the page_unpublished signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], self.child_page)
self.assertEqual(signal_page[0], signal_page[0].specific)
def test_page_delete_notlive_post(self):
# Same as above, but this makes sure the page_unpublished signal is not fired
# when if the page is not live when it is deleted
# Unpublish the page
self.child_page.live = False
self.child_page.save()
# Connect a mock signal handler to page_unpublished signal
signal_fired = [False]
def page_unpublished_handler(sender, instance, **kwargs):
signal_fired[0] = True
page_unpublished.connect(page_unpublished_handler)
# Post
post_data = {'hello': 'world'} # For some reason, this test doesn't work without a bit of POST data
response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
# Check that the page is gone
self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0)
# Check that the page_unpublished signal was not fired
self.assertFalse(signal_fired[0])
class TestPageSearch(TestCase, WagtailTestUtils):
def setUp(self):
@ -954,6 +1008,14 @@ class TestPageUnpublish(TestCase, WagtailTestUtils):
"""
This posts to the unpublish view and checks that the page was unpublished
"""
# Connect a mock signal handler to page_unpublished signal
signal_fired = [False]
signal_page = [None]
def page_unpublished_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_unpublished.connect(page_unpublished_handler)
# Post to the unpublish page
response = self.client.post(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, )), {
'foo': "Must post something or the view won't see this as a POST request",
@ -965,6 +1027,11 @@ class TestPageUnpublish(TestCase, WagtailTestUtils):
# Check that the page was unpublished
self.assertFalse(SimplePage.objects.get(id=self.page.id).live)
# Check that the page_unpublished signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], self.page)
self.assertEqual(signal_page[0], signal_page[0].specific)
class TestApproveRejectModeration(TestCase, WagtailTestUtils):
def setUp(self):

View file

@ -44,3 +44,17 @@ class TestSendEmailTask(TestCase):
self.assertEqual(mail.outbox[0].subject, "Test subject")
self.assertEqual(mail.outbox[0].body, "Test content")
self.assertEqual(mail.outbox[0].to, ["nobody@email.com"])
class TestExplorerNavView(TestCase, WagtailTestUtils):
def setUp(self):
self.homepage = Page.objects.get(id=2).specific
self.login()
def test_explorer_nav_view(self):
response = self.client.get(reverse('wagtailadmin_explorer_nav'))
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html')
self.assertEqual(response.context['nodes'][0][0], self.homepage)

View file

@ -38,6 +38,8 @@ urlpatterns += [
url(r'^failwhale/$', home.error_test, name='wagtailadmin_error_test'),
url(r'^explorer-nav/$', pages.explorer_nav, name='wagtailadmin_explorer_nav'),
url(r'^pages/$', pages.index, name='wagtailadmin_explore_root'),
url(r'^pages/(\d+)/$', pages.index, name='wagtailadmin_explore'),

View file

@ -17,8 +17,15 @@ from wagtail.wagtailadmin.forms import SearchForm
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
from wagtail.wagtailcore.models import Page, PageRevision, get_navigation_menu_items
from wagtail.wagtailcore.signals import page_published, page_unpublished
@permission_required('wagtailadmin.access_admin')
def explorer_nav(request):
return render(request, 'wagtailadmin/shared/explorer_nav.html', {
'nodes': get_navigation_menu_items(),
})
@permission_required('wagtailadmin.access_admin')
@ -376,8 +383,19 @@ def delete(request, page_id):
raise PermissionDenied
if request.POST:
if page.live:
# fetch params to pass to the page_unpublished_signal, before the
# deletion happens
specific_class = page.specific_class
specific_page = page.specific
parent_id = page.get_parent().id
page.delete()
# If the page is live, send the unpublished signal
if page.live:
page_unpublished.send(sender=specific_class, instance=specific_page)
messages.success(request, _("Page '{0}' deleted.").format(page.title))
for fn in hooks.get_hooks('after_delete_page'):
@ -538,9 +556,14 @@ 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)
page_unpublished.send(sender=page.specific_class, instance=page.specific)
messages.success(request, _("Page '{0}' unpublished.").format(page.title))
return redirect('wagtailadmin_explore', parent_id)
return render(request, 'wagtailadmin/pages/confirm_unpublish.html', {

View file

@ -7,13 +7,29 @@ except ImportError:
_hooks = {}
# TODO: support 'register' as a decorator:
# @hooks.register('construct_main_menu')
# def construct_main_menu(menu_items):
# ...
def register(hook_name, fn=None):
"""
Register hook for ``hook_name``. Can be used as a decorator::
@register('hook_name')
def my_hook(...):
pass
or as a function call::
def my_hook(...):
pass
register('hook_name', my_hook)
"""
# Pretend to be a decorator if fn is not supplied
if fn is None:
def decorator(fn):
register(hook_name, fn)
return fn
return decorator
def register(hook_name, fn):
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(fn)

View file

@ -5,7 +5,9 @@ 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
from wagtail.wagtailcore.signals import page_published, page_unpublished
def revision_date_expired(r):
@ -54,8 +56,16 @@ class Command(BaseCommand):
else:
print("No expired pages to be deactivated found.")
else:
# need to get the list of expired pages before the update,
# so that we can fire the page_unpublished signal on them afterwards
expired_pages_list = list(expired_pages)
expired_pages.update(expired=True, live=False)
# Fire page_unpublished signal for all expired pages
for page in expired_pages_list:
page_unpublished.send(sender=page.specific_class, instance=page.specific)
# 2. get all page revisions for moderation that have been expired
expired_revs = [
r for r in PageRevision.objects.filter(
@ -108,3 +118,6 @@ class Command(BaseCommand):
# just run publish for the revision -- since the approved go
# live datetime is before now it will make the page live
rp.publish()
# Fire page_published signal
page_published.send(sender=rp.page.specific_class, instance=rp.page.specific)

View file

@ -381,10 +381,11 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, indexed.Index
cursor.execute(update_statement,
[new_url_path, len(old_url_path) + 1, self.path + '%', self.id])
#: Return this page in its most specific subclassed form.
@cached_property
def specific(self):
"""
Return this page in its most specific subclassed form.
Return this page in its most specific subclassed form.
"""
# the ContentType.objects manager keeps a cache, so this should potentially
# avoid a database lookup over doing self.content_type. I think.
@ -395,11 +396,13 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, indexed.Index
else:
return content_type.get_object_for_this_type(id=self.id)
#: Return the class that this page would be if instantiated in its
#: most specific form
@cached_property
def specific_class(self):
"""
return the class that this page would be if instantiated in its
most specific form
Return the class that this page would be if instantiated in its
most specific form
"""
content_type = ContentType.objects.get_for_id(self.content_type_id)
return content_type.model_class()

View file

@ -157,9 +157,21 @@ LINK_HANDLERS = {
}
# Prepare a whitelisting engine with custom behaviour:
# rewrite any elements with a data-embedtype or data-linktype attribute
class DbWhitelister(Whitelister):
"""
A custom whitelisting engine to convert the HTML as returned by the rich text editor
into the pseudo-HTML format stored in the database (in which images, documents and other
linked objects are identified by ID rather than URL):
* implements a 'construct_whitelister_element_rules' hook so that other apps can modify
the whitelist ruleset (e.g. to permit additional HTML elements beyond those in the base
Whitelister module);
* replaces any element with a 'data-embedtype' attribute with an <embed> element, with
attributes supplied by the handler for that type as defined in EMBED_HANDLERS;
* rewrites the attributes of any <a> element with a 'data-linktype' attribute, as
determined by the handler for that type defined in LINK_HANDLERS, while keeping the
element content intact.
"""
has_loaded_custom_whitelist_rules = False
@classmethod

View file

@ -2,3 +2,4 @@ from django.dispatch import Signal
page_published = Signal(providing_args=['instance'])
page_unpublished = Signal(providing_args=['instance'])

View file

@ -25,9 +25,9 @@ class TestDbWhitelister(TestCase):
self.assertHtmlEqual(expected, output_html)
def test_image_embed_is_rewritten(self):
input_html = '<p>OMG look at this picture of a kitten: <figure data-embedtype="image" data-id="5" data-format="image-with-caption" data-alt="A cute kitten" class="fancy-image"><img src="/media/images/kitten.jpg" width="320" height="200" alt="A cute kitten" /><figcaption>A kitten, yesterday.</figcaption></figure></p>'
input_html = '<p>OMG look at this picture of a kitten:</p><figure data-embedtype="image" data-id="5" data-format="image-with-caption" data-alt="A cute kitten" class="fancy-image"><img src="/media/images/kitten.jpg" width="320" height="200" alt="A cute kitten" /><figcaption>A kitten, yesterday.</figcaption></figure>'
output_html = DbWhitelister.clean(input_html)
expected = '<p>OMG look at this picture of a kitten: <embed embedtype="image" id="5" format="image-with-caption" alt="A cute kitten" /></p>'
expected = '<p>OMG look at this picture of a kitten:</p><embed embedtype="image" id="5" format="image-with-caption" alt="A cute kitten" />'
self.assertHtmlEqual(expected, output_html)
def test_media_embed_is_rewritten(self):

View file

@ -7,6 +7,7 @@ from django.core import management
from django.utils import timezone
from wagtail.wagtailcore.models import Page, PageRevision
from wagtail.wagtailcore.signals import page_published, page_unpublished
from wagtail.tests.models import SimplePage
@ -96,6 +97,15 @@ class TestPublishScheduledPagesCommand(TestCase):
self.root_page = Page.objects.get(id=2)
def test_go_live_page_will_be_published(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)
page = SimplePage(
title="Hello world!",
slug="hello-world",
@ -116,6 +126,11 @@ class TestPublishScheduledPagesCommand(TestCase):
self.assertTrue(p.live)
self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
# 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_future_go_live_page_will_not_be_published(self):
page = SimplePage(
title="Hello world!",
@ -138,6 +153,15 @@ class TestPublishScheduledPagesCommand(TestCase):
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
def test_expired_page_will_be_unpublished(self):
# Connect a mock signal handler to page_unpublished signal
signal_fired = [False]
signal_page = [None]
def page_unpublished_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_unpublished.connect(page_unpublished_handler)
page = SimplePage(
title="Hello world!",
slug="hello-world",
@ -155,6 +179,11 @@ class TestPublishScheduledPagesCommand(TestCase):
self.assertFalse(p.live)
self.assertTrue(p.expired)
# 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_future_expired_page_will_not_be_unpublished(self):
page = SimplePage(
title="Hello world!",

View file

@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse
from wagtail.wagtailcore import hooks
@hooks.register('before_serve_page')
def check_view_restrictions(page, request, serve_args, serve_kwargs):
"""
Check whether there are any view restrictions on this page which are
@ -21,5 +22,3 @@ def check_view_restrictions(page, request, serve_args, serve_kwargs):
initial={'return_url': request.get_full_path()})
action_url = reverse('wagtailcore_authenticate_with_password', args=[restriction.id, page.id])
return page.serve_password_required_response(request, form, action_url)
hooks.register('before_serve_page', check_view_restrictions)

View file

@ -81,7 +81,7 @@ class Whitelister(object):
def clean(cls, html):
"""Clean up an HTML string to contain just the allowed elements /
attributes"""
doc = BeautifulSoup(html, 'lxml')
doc = BeautifulSoup(html, 'html5lib')
cls.clean_node(doc, doc)
return doc.decode()

View file

@ -10,21 +10,22 @@ from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtaildocs import admin_urls
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^documents/', include(admin_urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
if request.user.has_perm('wagtaildocs.add_document'):
menu_items.append(
MenuItem(_('Documents'), urlresolvers.reverse('wagtaildocs_index'), classnames='icon icon-doc-full-inverse', order=400)
)
hooks.register('construct_main_menu', construct_main_menu)
@hooks.register('insert_editor_js')
def editor_js():
js_files = [
'wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js',
@ -42,4 +43,3 @@ def editor_js():
""",
urlresolvers.reverse('wagtaildocs_chooser')
)
hooks.register('insert_editor_js', editor_js)

View file

@ -5,4 +5,4 @@ warnings.warn(
"Use {% load wagtailembeds_tags %} instead.", DeprecationWarning)
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import register, embed, embedly
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import register, embed

View file

@ -19,12 +19,3 @@ def embed(url, max_width=None):
return ''
except:
return ''
@register.filter
def embedly(url, max_width=None):
warnings.warn(
"The 'embedly' filter has been renamed. "
"Use 'embed' instead.", DeprecationWarning)
return embed(url, max_width)

View file

@ -22,7 +22,7 @@ from wagtail.wagtailembeds.embeds import (
AccessDeniedEmbedlyException,
)
from wagtail.wagtailembeds.embeds import embedly as wagtail_embedly, oembed as wagtail_oembed
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import embed as embed_filter, embedly as embedly_filter
from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import embed as embed_filter
class TestEmbeds(TestCase):

View file

@ -7,13 +7,14 @@ from wagtail.wagtailcore import hooks
from wagtail.wagtailembeds import urls
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^embeds/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('insert_editor_js')
def editor_js():
return format_html("""
<script src="{0}{1}"></script>
@ -26,4 +27,3 @@ def editor_js():
'wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js',
urlresolvers.reverse('wagtailembeds_chooser')
)
hooks.register('insert_editor_js', editor_js)

View file

@ -9,20 +9,20 @@ from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailforms import urls
from wagtail.wagtailforms.models import get_forms_for_user
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^forms/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
# show this only if the user has permission to retrieve submissions for at least one form
if get_forms_for_user(request.user).exists():
menu_items.append(
MenuItem(_('Forms'), urlresolvers.reverse('wagtailforms_index'), classnames='icon icon-form', order=700)
)
hooks.register('construct_main_menu', construct_main_menu)
@hooks.register('insert_editor_js')
def editor_js():
return """<script src="%swagtailforms/js/page-editor.js"></script>""" % settings.STATIC_URL
hooks.register('insert_editor_js', editor_js)

View file

@ -10,21 +10,22 @@ from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailimages import urls
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^images/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
if request.user.has_perm('wagtailimages.add_image'):
menu_items.append(
MenuItem(_('Images'), urlresolvers.reverse('wagtailimages_index'), classnames='icon icon-image', order=300)
)
hooks.register('construct_main_menu', construct_main_menu)
@hooks.register('insert_editor_js')
def editor_js():
js_files = [
'wagtailimages/js/hallo-plugins/hallo-wagtailimage.js',
@ -42,4 +43,3 @@ def editor_js():
""",
urlresolvers.reverse('wagtailimages_chooser')
)
hooks.register('insert_editor_js', editor_js)

View file

@ -8,17 +8,17 @@ from wagtail.wagtailredirects import urls
from wagtail.wagtailadmin.menu import MenuItem
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^redirects/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
# TEMPORARY: Only show if the user is a superuser
if request.user.is_superuser:
menu_items.append(
MenuItem(_('Redirects'), urlresolvers.reverse('wagtailredirects_index'), classnames='icon icon-redirect', order=800)
)
hooks.register('construct_main_menu', construct_main_menu)

View file

@ -2,6 +2,8 @@ from __future__ import absolute_import
import json
from six.moves.urllib.parse import urlparse
from django.db import models
from django.db.models.sql.where import SubqueryConstraint
@ -446,17 +448,30 @@ class ElasticSearch(BaseSearch):
super(ElasticSearch, self).__init__(params)
# Get settings
self.es_hosts = params.pop('HOSTS', None)
self.es_urls = params.pop('URLS', ['http://localhost:9200'])
self.es_index = params.pop('INDEX', 'wagtail')
self.es_timeout = params.pop('TIMEOUT', 5)
self.es_force_new = params.pop('FORCE_NEW', False)
self.es_timeout = params.pop('TIMEOUT', 10)
# If HOSTS is not set, convert URLS setting to HOSTS
if self.es_hosts is None:
self.es_hosts = []
for url in self.es_urls:
parsed_url = urlparse(url)
self.es_hosts.append({
'host': parsed_url.hostname,
'port': parsed_url.port or 9200,
'url_prefix': parsed_url.path,
'use_ssl': parsed_url.scheme == 'https',
})
# Get ElasticSearch interface
# Any remaining params are passed into the ElasticSearch constructor
self.es = Elasticsearch(
urls=self.es_urls,
hosts=self.es_hosts,
timeout=self.es_timeout,
force_new=self.es_force_new,
**params)
def reset_index(self):

View file

@ -397,3 +397,54 @@ class TestElasticSearchMappingInheritance(TestCase):
}
self.assertDictEqual(document, expected_result)
class TestBackendConfiguration(TestCase):
def setUp(self):
# Import using a try-catch block to prevent crashes if the elasticsearch-py
# module is not installed
try:
from wagtail.wagtailsearch.backends.elasticsearch import ElasticSearch
except ImportError:
raise unittest.SkipTest("elasticsearch-py not installed")
self.ElasticSearch = ElasticSearch
def test_default_settings(self):
backend = self.ElasticSearch(params={})
self.assertEqual(len(backend.es_hosts), 1)
self.assertEqual(backend.es_hosts[0]['host'], 'localhost')
self.assertEqual(backend.es_hosts[0]['port'], 9200)
self.assertEqual(backend.es_hosts[0]['use_ssl'], False)
def test_hosts(self):
# This tests that HOSTS goes to es_hosts
backend = self.ElasticSearch(params={
'HOSTS': [
{
'host': '127.0.0.1',
'port': 9300,
'use_ssl': True,
}
]
})
self.assertEqual(len(backend.es_hosts), 1)
self.assertEqual(backend.es_hosts[0]['host'], '127.0.0.1')
self.assertEqual(backend.es_hosts[0]['port'], 9300)
self.assertEqual(backend.es_hosts[0]['use_ssl'], True)
def test_urls(self):
# This test backwards compatibility with old URLS setting
backend = self.ElasticSearch(params={
'URLS': ['http://localhost:12345', 'https://127.0.0.1:54321'],
})
self.assertEqual(len(backend.es_hosts), 2)
self.assertEqual(backend.es_hosts[0]['host'], 'localhost')
self.assertEqual(backend.es_hosts[0]['port'], 12345)
self.assertEqual(backend.es_hosts[0]['use_ssl'], False)
self.assertEqual(backend.es_hosts[1]['host'], '127.0.0.1')
self.assertEqual(backend.es_hosts[1]['port'], 54321)
self.assertEqual(backend.es_hosts[1]['use_ssl'], True)

View file

@ -8,17 +8,17 @@ from wagtail.wagtailsearch.urls import admin as admin_urls
from wagtail.wagtailadmin.menu import MenuItem
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^search/', include(admin_urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
# TEMPORARY: Only show if the user is a superuser
if request.user.is_superuser:
menu_items.append(
MenuItem(_('Editors picks'), urlresolvers.reverse('wagtailsearch_editorspicks_index'), classnames='icon icon-pick', order=900)
)
hooks.register('construct_main_menu', construct_main_menu)

View file

@ -11,21 +11,22 @@ from wagtail.wagtailsnippets import urls
from wagtail.wagtailsnippets.permissions import user_can_edit_snippets
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^snippets/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
if user_can_edit_snippets(request.user):
menu_items.append(
MenuItem(_('Snippets'), urlresolvers.reverse('wagtailsnippets_index'), classnames='icon icon-snippet', order=500)
)
hooks.register('construct_main_menu', construct_main_menu)
@hooks.register('insert_editor_js')
def editor_js():
return format_html("""
<script src="{0}{1}"></script>
@ -35,4 +36,3 @@ def editor_js():
'wagtailsnippets/js/snippet-chooser.js',
urlresolvers.reverse('wagtailsnippets_choose_generic')
)
hooks.register('insert_editor_js', editor_js)

View file

@ -8,16 +8,16 @@ from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailusers import urls
@hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^users/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
@hooks.register('construct_main_menu')
def construct_main_menu(request, menu_items):
if request.user.has_module_perms('auth'):
menu_items.append(
MenuItem(_('Users'), urlresolvers.reverse('wagtailusers_index'), classnames='icon icon-user', order=600)
)
hooks.register('construct_main_menu', construct_main_menu)