mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-12 01:03:11 +00:00
merged
This commit is contained in:
commit
d665a59c4e
56 changed files with 1032 additions and 441 deletions
|
|
@ -8,21 +8,22 @@ matrix:
|
|||
- env: TOXENV=py33-dj17-postgres
|
||||
- env: TOXENV=py34-dj17-postgres
|
||||
- env: TOXENV=py34-dj17-sqlite
|
||||
- env: TOXENV=py34-dj17-mysql
|
||||
- env: TOXENV=py27-dj18-postgres
|
||||
# - env: TOXENV=py27-dj18-mysql
|
||||
# - env: TOXENV=py27-dj18-sqlite
|
||||
# - env: TOXENV=py33-dj18-postgres
|
||||
- env: TOXENV=py34-dj18-postgres
|
||||
- env: TOXENV=py34-dj18-sqlite
|
||||
- env: TOXENV=py34-dj18-mysql
|
||||
allow_failures:
|
||||
- env: TOXENV=py27-dj17-mysql
|
||||
- env: TOXENV=py27-dj18-postgres
|
||||
- env: TOXENV=py34-dj18-postgres
|
||||
- env: TOXENV=py34-dj18-sqlite
|
||||
- env: TOXENV=py34-dj18-mysql
|
||||
|
||||
# Services
|
||||
services:
|
||||
- redis-server
|
||||
- elasticsearch
|
||||
|
||||
# Package installation
|
||||
|
|
@ -32,6 +33,8 @@ install:
|
|||
# Pre-test configuration
|
||||
before_script:
|
||||
- psql -c 'create database wagtaildemo;' -U postgres
|
||||
- mysql -e "SET GLOBAL wait_timeout = 36000;"
|
||||
- mysql -e "SET GLOBAL max_allowed_packet = 134209536;"
|
||||
|
||||
# Run the tests
|
||||
script:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
0.9 (xx.xx.xxxx)
|
||||
1.0 (xx.xx.xxxx)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* Added StreamField, a model field for freeform page content
|
||||
* MySQL support
|
||||
* Javascript includes in the admin backend have been moved to the HTML header, to accommodate form widgets that render inline scripts that depend on libraries such as jQuery
|
||||
* Improvements to the layout of the admin menu footer.
|
||||
* Added thousands separator for counters on dashboard
|
||||
|
|
@ -36,6 +38,7 @@ Changelog
|
|||
* Plain text fields in the page editor now use auto-expanding text areas
|
||||
* Date / time pickers now consistently use times without seconds, to prevent Javascript behaviour glitches when focusing / unfocusing fields
|
||||
* Added hooks `register_rich_text_embed_handler` and `register_rich_text_link_handler` for customising link / embed handling within rich text fields
|
||||
* Added hook `construct_homepage_summary_items` for customising the site summary panel on the admin homepage
|
||||
|
||||
|
||||
0.8.6 (10.03.2015)
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@ Wagtail ships with a variety of extra optional modules.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
static_site_generation
|
||||
sitemap_generation
|
||||
staticsitegen
|
||||
sitemaps
|
||||
frontendcache
|
||||
routablepage
|
||||
|
||||
|
||||
``wagtailmedusa``
|
||||
-----------------
|
||||
|
||||
:doc:`static_site_generation`
|
||||
:doc:`staticsitegen`
|
||||
|
||||
Provides a management command that turns a Wagtail site into a set of static HTML files.
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ Provides a management command that turns a Wagtail site into a set of static HTM
|
|||
``wagtailsitemaps``
|
||||
-------------------
|
||||
|
||||
:doc:`sitemap_generation`
|
||||
:doc:`sitemaps`
|
||||
|
||||
Provides a view that generates a Google XML sitemap of your public wagtail content.
|
||||
|
||||
|
|
@ -34,3 +35,11 @@ Provides a view that generates a Google XML sitemap of your public wagtail conte
|
|||
:doc:`frontendcache`
|
||||
|
||||
A module for automatically purging pages from a cache (Varnish, Squid or Cloudflare) when their content is changed.
|
||||
|
||||
|
||||
``wagtailroutablepage``
|
||||
-----------------------
|
||||
|
||||
:doc:`routablepage`
|
||||
|
||||
Provides a way of embedding Django URLconfs into pages.
|
||||
|
|
@ -14,7 +14,7 @@ A ``Page`` using ``RoutablePageMixin`` exists within the page tree like any othe
|
|||
The basics
|
||||
==========
|
||||
|
||||
To use ``RoutablePageMixin``, you need to make your class inherit from both :class:`wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin` and :class:`wagtail.wagtailcore.models.Page`, and configure the ``subpage_urls`` attribute with your URL configuration.
|
||||
To use ``RoutablePageMixin``, you need to make your class inherit from both :class:`wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin` and :class:`wagtail.wagtailcore.models.Page`, then define a ``subpage_urls`` property that returns a tuple of ``django.conf.url`` objects.
|
||||
|
||||
Here's an example of an ``EventPage`` with three views:
|
||||
|
||||
|
|
@ -27,11 +27,13 @@ Here's an example of an ``EventPage`` with three views:
|
|||
|
||||
|
||||
class EventPage(RoutablePageMixin, Page):
|
||||
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'),
|
||||
)
|
||||
@property
|
||||
def subpage_urls(self):
|
||||
return (
|
||||
url(r'^$', self.current_events, name='current_events'),
|
||||
url(r'^past/$', self.past_events, name='past_events'),
|
||||
url(r'^year/(\d+)/$', self.events_for_year, name='events_for_year'),
|
||||
)
|
||||
|
||||
def current_events(self, request):
|
||||
"""
|
||||
|
|
@ -51,6 +53,16 @@ Here's an example of an ``EventPage`` with three views:
|
|||
"""
|
||||
...
|
||||
|
||||
You can also set this as an attribute on the class. This will not work on Django 1.8 and above as the ``url`` function no longer allows strings to be specified as view names.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class EventPage(RoutablePageMixin, Page):
|
||||
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'),
|
||||
)
|
||||
|
||||
The ``RoutablePageMixin`` class
|
||||
===============================
|
||||
|
|
@ -60,6 +72,10 @@ The ``RoutablePageMixin`` class
|
|||
|
||||
.. autoattribute:: subpage_urls
|
||||
|
||||
.. versionchanged:: 0.9
|
||||
|
||||
It was previously recommended to define ``subpage_urls`` as a class attribute. It is now recommend to define it as a property as Django 1.8 and above requires that the view argument to the ``url`` function is a callable and not a string.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -70,16 +86,17 @@ The ``RoutablePageMixin`` class
|
|||
|
||||
|
||||
class MyPage(RoutablePageMixin, Page):
|
||||
subpage_urls = (
|
||||
url(r'^$', 'main', name='main'),
|
||||
url(r'^archive/$', 'archive', name='archive'),
|
||||
url(r'^archive/(?P<year>[0-9]{4})/$', 'archive', name='archive'),
|
||||
)
|
||||
def subpage_urls(self):
|
||||
return (
|
||||
url(r'^$', self.main_view, name='main'),
|
||||
url(r'^archive/$', self.archive_view, name='archive'),
|
||||
url(r'^archive/(?P<year>[0-9]{4})/$', self.archive_view, name='archive'),
|
||||
)
|
||||
|
||||
def main(self, request):
|
||||
def main_view(self, request):
|
||||
...
|
||||
|
||||
def archive(self, request, year=None):
|
||||
def archive_view(self, request, year=None):
|
||||
...
|
||||
|
||||
.. automethod:: resolve_subpage
|
||||
|
|
@ -99,7 +116,6 @@ The ``RoutablePageMixin`` class
|
|||
|
||||
url = page.url + page.reverse_subpage('archive', kwargs={'year': '2014'})
|
||||
|
||||
|
||||
.. _routablepageurl_template_tag:
|
||||
|
||||
The ``routablepageurl`` template tag
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
Core components
|
||||
===============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:titlesonly:
|
||||
|
||||
sites
|
||||
pages/index
|
||||
images/index
|
||||
snippets
|
||||
search/index
|
||||
form_builder
|
||||
|
||||
|
|
@ -13,10 +13,10 @@ Below are some useful links to help you get started with Wagtail.
|
|||
|
||||
|
||||
* **Creating your Wagtail site**
|
||||
* :doc:`core_components/pages/creating_pages`
|
||||
* :doc:`Writing templates <core_components/pages/writing_templates>`
|
||||
* :doc:`core_components/images/index`
|
||||
* :doc:`core_components/search/index`
|
||||
* :doc:`pages/creating_pages`
|
||||
* :doc:`pages/writing_templates`
|
||||
* :doc:`images/index`
|
||||
* :doc:`search/index`
|
||||
* :doc:`howto/third_party_tutorials`
|
||||
|
||||
|
||||
|
|
@ -33,8 +33,12 @@ Index
|
|||
:titlesonly:
|
||||
|
||||
getting_started/index
|
||||
core_components/index
|
||||
contrib_components/index
|
||||
pages/index
|
||||
images/index
|
||||
snippets
|
||||
search/index
|
||||
form_builder
|
||||
contrib/index
|
||||
howto/index
|
||||
reference/index
|
||||
support
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
.. _editing-api:
|
||||
|
||||
Displaying fields with the Editing API
|
||||
======================================
|
||||
|
||||
.. note::
|
||||
This documentation is currently being written.
|
||||
Setting up the page editor interface
|
||||
====================================
|
||||
|
||||
Wagtail provides a highly-customizable editing interface consisting of several components:
|
||||
|
||||
|
|
@ -34,7 +31,7 @@ There are four basic types of panels:
|
|||
``InlinePanel( 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`.
|
||||
|
||||
``FieldRowPanel( children, classname=None)``
|
||||
``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.
|
||||
|
|
@ -425,277 +422,6 @@ 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:
|
||||
|
||||
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.
|
||||
|
||||
.. 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('name_of_hook')
|
||||
def my_hook_function(arg1, arg2...)
|
||||
# your code here
|
||||
|
||||
|
||||
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:
|
||||
|
||||
``before_serve_page``
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Called when Wagtail is about to serve a page. The callable passed into the hook will receive the page object, the request object, and the args and kwargs that will be passed to the page's ``serve()`` method. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``serve()`` on the page.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
@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>")
|
||||
|
||||
|
||||
.. _construct_wagtail_edit_bird:
|
||||
|
||||
``construct_wagtail_edit_bird``
|
||||
Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the ``request`` object and a list of menu objects, ``items``. The menu item objects must have a ``render`` method which can take a ``request`` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
class UserbarPuppyLinkItem(object):
|
||||
def render(self, request):
|
||||
return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
|
||||
+ 'target="_parent" class="action icon icon-wagtail">Puppies!</a></li>'
|
||||
|
||||
@hooks.register('construct_wagtail_edit_bird')
|
||||
def add_puppy_link_item(request, items):
|
||||
return items.append( UserbarPuppyLinkItem() )
|
||||
|
||||
|
||||
.. _construct_homepage_panels:
|
||||
|
||||
``construct_homepage_panels``
|
||||
Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a ``request`` object and a list of ``panels``, objects which have a ``render()`` method returning a string. The objects also have an ``order`` property, an integer used for ordering the panels. The default panels use integers between ``100`` and ``300``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
class WelcomePanel(object):
|
||||
order = 50
|
||||
|
||||
def render(self):
|
||||
return mark_safe("""
|
||||
<section class="panel summary nice-padding">
|
||||
<h3>No, but seriously -- welcome to the admin homepage.</h3>
|
||||
</section>
|
||||
""")
|
||||
|
||||
@hooks.register('construct_homepage_panels')
|
||||
def add_another_welcome_panel(request, panels):
|
||||
return panels.append( WelcomePanel() )
|
||||
|
||||
|
||||
.. _after_create_page:
|
||||
|
||||
``after_create_page``
|
||||
Do something with a ``Page`` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a ``request`` object and a ``page`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page's parent.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
from wagtail.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")
|
||||
|
||||
|
||||
.. _after_edit_page:
|
||||
|
||||
``after_edit_page``
|
||||
Do something with a ``Page`` object after it has been updated. Uses the same behavior as ``after_create_page``.
|
||||
|
||||
.. _after_delete_page:
|
||||
|
||||
``after_delete_page``
|
||||
Do something after a ``Page`` object is deleted. Uses the same behavior as ``after_create_page``.
|
||||
|
||||
.. _register_admin_urls:
|
||||
|
||||
``register_admin_urls``
|
||||
Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see `url dispatcher`_.
|
||||
|
||||
.. _url dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.conf.urls import url
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
def admin_view( request ):
|
||||
return HttpResponse( \
|
||||
"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' ),
|
||||
]
|
||||
|
||||
.. _register_admin_menu_item:
|
||||
|
||||
``register_admin_menu_item``
|
||||
.. versionadded:: 0.6
|
||||
|
||||
Add an item to the Wagtail admin menu. The callable passed to this hook must return an instance of ``wagtail.wagtailadmin.menu.MenuItem``. New items can be constructed from the ``MenuItem`` class by passing in a ``label`` which will be the text in the menu item, and 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). Additionally, the following keyword arguments are accepted:
|
||||
|
||||
:name: an internal name used to identify the menu item; defaults to the slugified form of the label. Also applied as a CSS class to the wrapping ``<li>``, as ``"menu-{name}"``.
|
||||
:classnames: additional classnames applied to the link, used to give it an icon
|
||||
:attrs: additional HTML attributes to apply to the link
|
||||
:order: an integer which determines the item's position in the menu
|
||||
|
||||
``MenuItem`` can be subclassed to customise the HTML output, specify Javascript files required by the menu item, or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (``wagtail/wagtailadmin/menu.py``) for details.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
|
||||
@hooks.register('register_admin_menu_item')
|
||||
def register_frank_menu_item():
|
||||
return MenuItem('Frank', reverse('frank'), classnames='icon icon-folder-inverse', order=10000)
|
||||
|
||||
.. _register_settings_menu_item:
|
||||
|
||||
``register_settings_menu_item``
|
||||
.. versionadded:: 0.7
|
||||
|
||||
As ``register_admin_menu_item``, but registers menu items into the 'Settings' sub-menu rather than the top-level menu.
|
||||
|
||||
.. _construct_main_menu:
|
||||
|
||||
``construct_main_menu``
|
||||
Called just before the Wagtail admin menu is output, to allow the list of menu items to be modified. The callable passed to this hook will receive a ``request`` object and a list of ``menu_items``, and should modify ``menu_items`` in-place as required. Adding menu items should generally be done through the ``register_admin_menu_item`` hook instead - items added through ``construct_main_menu`` will be missing any associated Javascript includes, and their ``is_shown`` check will not be applied.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
@hooks.register('construct_main_menu')
|
||||
def hide_explorer_menu_item_from_frank(request, menu_items):
|
||||
if request.user.username == 'frank':
|
||||
menu_items[:] = [item for item in menu_items if item.name != 'explorer']
|
||||
|
||||
|
||||
.. _insert_editor_js:
|
||||
|
||||
``insert_editor_js``
|
||||
Add additional Javascript files or code snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.utils.html import format_html, format_html_join
|
||||
from django.conf import settings
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
@hooks.register('insert_editor_js')
|
||||
def editor_js():
|
||||
js_files = [
|
||||
'demo/js/hallo-plugins/hallo-demo-plugin.js',
|
||||
]
|
||||
js_includes = format_html_join('\n', '<script src="{0}{1}"></script>',
|
||||
((settings.STATIC_URL, filename) for filename in js_files)
|
||||
)
|
||||
return js_includes + format_html(
|
||||
"""
|
||||
<script>
|
||||
registerHalloPlugin('demoeditor');
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
.. _insert_editor_css:
|
||||
|
||||
``insert_editor_css``
|
||||
Add additional CSS or SCSS files or snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.utils.html import format_html
|
||||
from django.conf import settings
|
||||
|
||||
from wagtail.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">')
|
||||
|
||||
.. _construct_whitelister_element_rules:
|
||||
|
||||
``construct_whitelister_element_rules``
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Customise the rules that define which HTML elements are allowed in rich text areas. By default only a limited set of HTML elements and attributes are whitelisted - all others are stripped out. The callables passed into this hook must return a dict, which maps element names to handler functions that will perform some kind of manipulation of the element. These handler functions receive the element as a `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_ Tag object.
|
||||
|
||||
The ``wagtail.wagtailcore.whitelist`` module provides a few helper functions to assist in defining these handlers: ``allow_without_attributes``, a handler which preserves the element but strips out all of its attributes, and ``attribute_rule`` which accepts a dict specifying how to handle each attribute, and returns a handler function. This dict will map attribute names to either True (indicating that the attribute should be kept), False (indicating that it should be dropped), or a callable (which takes the initial attribute value and returns either a final value for the attribute, or None to drop the attribute).
|
||||
|
||||
For example, the following hook function will add the ``<blockquote>`` element to the whitelist, and allow the ``target`` attribute on ``<a>`` elements:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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}),
|
||||
}
|
||||
|
||||
.. _register_permissions:
|
||||
|
||||
``register_permissions``
|
||||
.. versionadded:: 0.7
|
||||
|
||||
Return a queryset of Permission objects to be shown in the Groups administration area.
|
||||
|
||||
|
||||
Image Formats in the Rich Text Editor
|
||||
-------------------------------------
|
||||
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
Pages
|
||||
=====
|
||||
|
||||
.. note::
|
||||
This documentation is currently being written.
|
||||
|
||||
Wagtail requires a little careful setup to define the types of content that you want to present through your website. The basic unit of content in Wagtail is the ``Page``, and all of your page-level content will inherit basic webpage-related properties from it. But for the most part, you will be defining content yourself, through the construction of Django models using Wagtail's ``Page`` as a base.
|
||||
|
||||
Wagtail organizes content created from your models in a tree, which can have any structure and combination of model objects in it. Wagtail doesn't prescribe ways to organize and interrelate your content, but here we've sketched out some strategies for organizing your models.
|
||||
|
|
@ -19,6 +16,7 @@ The presentation of your content, the actual webpages, includes the normal use o
|
|||
writing_templates
|
||||
model_recipes
|
||||
editing_api
|
||||
streamfield
|
||||
sites
|
||||
advanced_topics/queryset_methods
|
||||
advanced_topics/private_pages
|
||||
advanced_topics/routable_page_mixin
|
||||
449
docs/pages/streamfield.rst
Normal file
449
docs/pages/streamfield.rst
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
.. _streamfield:
|
||||
|
||||
Freeform page content using StreamField
|
||||
=======================================
|
||||
|
||||
StreamField provides a content editing model suitable for pages that do not follow a fixed structure - such as blog posts or news stories, where the text may be interspersed with subheadings, images, pull quotes and video, and perhaps more specialised content types such as maps and charts (or, for a programming blog, code snippets). In this model, these different content types are represented as a sequence of 'blocks', which can be repeated and arranged in any order.
|
||||
|
||||
For further background on StreamField, and why you would use it instead of a rich text field for the article body, see the blog post `Rich text fields and faster horses <https://torchbox.com/blog/rich-text-fields-and-faster-horses/>`__.
|
||||
|
||||
StreamField also offers a rich API to define your own block types, ranging from simple collections of sub-blocks (such as a 'person' block consisting of first name, surname and photograph) to completely custom components with their own editing interface. Within the database, the StreamField content is stored as JSON, ensuring that the full informational content of the field is preserved, rather than just an HTML representation of it.
|
||||
|
||||
|
||||
Using StreamField
|
||||
-----------------
|
||||
|
||||
``StreamField`` is a model field that can be defined within your page model like any other field:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.db import models
|
||||
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailcore.fields import StreamField
|
||||
from wagtail.wagtailcore import blocks
|
||||
from wagtail.wagtailadmin.edit_handlers import FieldPanel, StreamFieldPanel
|
||||
from wagtail.wagtailimages.blocks import ImageChooserBlock
|
||||
|
||||
class BlogPage(Page):
|
||||
author = models.CharField(max_length=255)
|
||||
date = models.DateField("Post date")
|
||||
body = StreamField([
|
||||
('heading', blocks.CharBlock(classname="full title")),
|
||||
('paragraph', blocks.RichTextBlock()),
|
||||
('image', ImageChooserBlock()),
|
||||
])
|
||||
|
||||
BlogPage.content_panels = [
|
||||
FieldPanel('author'),
|
||||
FieldPanel('date'),
|
||||
StreamFieldPanel('body'),
|
||||
]
|
||||
|
||||
|
||||
Note: StreamField is not backwards compatible with other field types such as RichTextField; if you migrate an existing field to StreamField, the existing data will be lost.
|
||||
|
||||
The parameter to ``StreamField`` is a list of (name, block_type) tuples; 'name' is used to identify the block type within templates and the internal JSON representation (and should follow standard Python conventions for variable names: lower-case and underscores, no spaces) and 'block_type' should be a block definition object as described below. (Alternatively, ``StreamField`` can be passed a single ``StreamBlock`` instance - see `Structural block types`_.)
|
||||
|
||||
This defines the set of available block types that can be used within this field. The author of the page is free to use these blocks as many times as desired, in any order.
|
||||
|
||||
Basic block types
|
||||
-----------------
|
||||
|
||||
All block types accept the following optional keyword arguments:
|
||||
|
||||
``default``
|
||||
The default value that a new 'empty' block should receive.
|
||||
|
||||
``label``
|
||||
The label to display in the editor interface when referring to this block - defaults to a prettified version of the block name (or, in a context where no name is assigned - such as within a ``ListBlock`` - the empty string).
|
||||
|
||||
``icon``
|
||||
The name of the icon to display for this block type in the menu of available block types. For a list of icon names, see the Wagtail style guide, which can be enabled by adding ``wagtail.contrib.wagtailstyleguide`` to your project's ``INSTALLED_APPS``.
|
||||
|
||||
``template``
|
||||
The path to a Django template that will be used to render this block on the front end. See `Template rendering`_.
|
||||
|
||||
The basic block types provided by Wagtail are as follows:
|
||||
|
||||
CharBlock
|
||||
~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.CharBlock``
|
||||
|
||||
A single-line text input. The following keyword arguments are accepted:
|
||||
|
||||
``required`` (default: True)
|
||||
If true, the field cannot be left blank.
|
||||
|
||||
``max_length``, ``min_length``
|
||||
Ensures that the string is at most or at least the given length.
|
||||
|
||||
``help_text``
|
||||
Help text to display alongside the field.
|
||||
|
||||
TextBlock
|
||||
~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.TextBlock``
|
||||
|
||||
A multi-line text input. As with ``CharBlock``, the keyword arguments ``required``, ``max_length``, ``min_length`` and ``help_text`` are accepted.
|
||||
|
||||
URLBlock
|
||||
~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.URLBlock``
|
||||
|
||||
A single-line text input that validates that the string is a valid URL. The keyword arguments ``required``, ``max_length``, ``min_length`` and ``help_text`` are accepted.
|
||||
|
||||
BooleanBlock
|
||||
~~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.BooleanBlock``
|
||||
|
||||
A checkbox. The keyword arguments ``required`` and ``help_text`` are accepted. As with Django's ``BooleanField``, a value of ``required=True`` (the default) indicates that the checkbox must be ticked in order to proceed; for a checkbox that can be ticked or unticked, you must explicitly pass in ``required=False``.
|
||||
|
||||
DateBlock
|
||||
~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.DateBlock``
|
||||
|
||||
A date picker. The keyword arguments ``required`` and ``help_text`` are accepted.
|
||||
|
||||
TimeBlock
|
||||
~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.TimeBlock``
|
||||
|
||||
A time picker. The keyword arguments ``required`` and ``help_text`` are accepted.
|
||||
|
||||
DateTimeBlock
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.DateTimeBlock``
|
||||
|
||||
A combined date / time picker. The keyword arguments ``required`` and ``help_text`` are accepted.
|
||||
|
||||
RichTextBlock
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.RichTextBlock``
|
||||
|
||||
A WYSIWYG editor for creating formatted text including links, bold / italics etc.
|
||||
|
||||
RawHTMLBlock
|
||||
~~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.RawHTMLBlock``
|
||||
|
||||
A text area for entering raw HTML which will be rendered unescaped in the page output. The keyword arguments ``required``, ``max_length``, ``min_length`` and ``help_text`` are accepted.
|
||||
|
||||
.. WARNING::
|
||||
When this block is in use, there is nothing to prevent editors from inserting malicious scripts into the page, including scripts that would allow the editor to acquire administrator privileges when another administrator views the page. Do not use this block unless your editors are fully trusted.
|
||||
|
||||
ChoiceBlock
|
||||
~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.ChoiceBlock``
|
||||
|
||||
A dropdown select box for choosing from a list of choices. The following keyword arguments are accepted:
|
||||
|
||||
``choices``
|
||||
A list of choices, in any format accepted by Django's ``choices`` parameter for model fields: https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices
|
||||
|
||||
``required`` (default: True)
|
||||
If true, the field cannot be left blank.
|
||||
|
||||
``help_text``
|
||||
Help text to display alongside the field.
|
||||
|
||||
``ChoiceBlock`` can also be subclassed to produce a reusable block with the same list of choices everywhere it is used. For example, a block definition such as:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
blocks.ChoiceBlock(choices=[
|
||||
('tea', 'Tea'),
|
||||
('coffee', 'Coffee'),
|
||||
], icon='cup')
|
||||
|
||||
|
||||
could be rewritten as a subclass of ChoiceBlock:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DrinksChoiceBlock(blocks.ChoiceBlock):
|
||||
choices = [
|
||||
('tea', 'Tea'),
|
||||
('coffee', 'Coffee'),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
icon = 'cup'
|
||||
|
||||
|
||||
``StreamField`` definitions can then refer to ``DrinksChoiceBlock()`` in place of the full ``ChoiceBlock`` definition.
|
||||
|
||||
PageChooserBlock
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.PageChooserBlock``
|
||||
|
||||
A control for selecting a page object, using Wagtail's page browser. The keyword argument ``required`` is accepted.
|
||||
|
||||
DocumentChooserBlock
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtaildocs.blocks.DocumentChooserBlock``
|
||||
|
||||
A control to allow the editor to select an existing document object, or upload a new one. The keyword argument ``required`` is accepted.
|
||||
|
||||
ImageChooserBlock
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailimages.blocks.ImageChooserBlock``
|
||||
|
||||
A control to allow the editor to select an existing image, or upload a new one. The keyword argument ``required`` is accepted.
|
||||
|
||||
SnippetChooserBlock
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailsnippets.blocks.SnippetChooserBlock``
|
||||
|
||||
A control to allow the editor to select a snippet object. Requires one positional argument: the snippet class to choose from. The keyword argument ``required`` is accepted.
|
||||
|
||||
EmbedBlock
|
||||
~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailembeds.blocks.EmbedBlock``
|
||||
|
||||
A field for the editor to enter a URL to a media item (such as a YouTube video) to appear as embedded media on the page. The keyword arguments ``required``, ``max_length``, ``min_length`` and ``help_text`` are accepted.
|
||||
|
||||
|
||||
Structural block types
|
||||
----------------------
|
||||
|
||||
In addition to the basic block types above, it is possible to define new block types made up of sub-blocks: for example, a 'person' block consisting of sub-blocks for first name, surname and image, or a 'carousel' block consisting of an unlimited number of image blocks. These structures can be nested to any depth, making it possible to have a structure containing a list, or a list of structures.
|
||||
|
||||
StructBlock
|
||||
~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.StructBlock``
|
||||
|
||||
A block consisting of a fixed group of sub-blocks to be displayed together. Takes a list of (name, block_definition) tuples as its first argument:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
('person', blocks.StructBlock([
|
||||
('first_name', blocks.CharBlock(required=True)),
|
||||
('surname', blocks.CharBlock(required=True)),
|
||||
('photo', ImageChooserBlock()),
|
||||
('biography', blocks.RichTextBlock()),
|
||||
], icon='user'))
|
||||
|
||||
|
||||
Alternatively, the list of sub-blocks can be provided in a subclass of StructBlock:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class PersonBlock(blocks.StructBlock):
|
||||
first_name = blocks.CharBlock(required=True)
|
||||
surname = blocks.CharBlock(required=True)
|
||||
photo = ImageChooserBlock()
|
||||
biography = blocks.RichTextBlock()
|
||||
|
||||
class Meta:
|
||||
icon = 'user'
|
||||
|
||||
|
||||
The ``Meta`` class supports the properties ``default``, ``label``, ``icon`` and ``template``; these have the same meanings as when they are passed to the block's constructor.
|
||||
|
||||
This defines ``PersonBlock()`` as a block type that can be re-used as many times as you like within your model definitions:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
body = StreamField([
|
||||
('heading', blocks.CharBlock(classname="full title")),
|
||||
('paragraph', blocks.RichTextBlock()),
|
||||
('image', ImageChooserBlock()),
|
||||
('person', PersonBlock()),
|
||||
])
|
||||
|
||||
|
||||
ListBlock
|
||||
~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.ListBlock``
|
||||
|
||||
A block consisting of many sub-blocks, all of the same type. The editor can add an unlimited number of sub-blocks, and re-order and delete them. Takes the definition of the sub-block as its first argument:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
('ingredients_list', blocks.ListBlock(blocks.CharBlock(label="Ingredient")))
|
||||
|
||||
|
||||
Any block type is valid as the sub-block type, including structural types:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
('ingredients_list', blocks.ListBlock(blocks.StructBlock(
|
||||
('ingredient', blocks.CharBlock(required=True)),
|
||||
('amount', blocks.CharBlock()),
|
||||
)))
|
||||
|
||||
|
||||
StreamBlock
|
||||
~~~~~~~~~~~
|
||||
|
||||
``wagtail.wagtailcore.blocks.StreamBlock``
|
||||
|
||||
A block consisting of a sequence of sub-blocks of different types, which can be mixed and reordered in any order. Used as the overall mechanism of the StreamField itself, but can also be nested or used within other structural block types. Takes a list of (name, block_definition) tuples as its first argument:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
('carousel', blocks.StreamBlock(
|
||||
[
|
||||
('image', ImageChooserBlock()),
|
||||
('quotation', blocks.StructBlock([
|
||||
('text', blocks.TextBlock()),
|
||||
('author', blocks.CharBlock),
|
||||
])),
|
||||
('video', blocks.EmbedBlock()),
|
||||
],
|
||||
icon='cogs'
|
||||
))
|
||||
|
||||
|
||||
As with StructBlock, the list of sub-blocks can also be provided as a subclass of StreamBlock:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CarouselBlock(blocks.StreamBlock):
|
||||
image = ImageChooserBlock()
|
||||
quotation = blocks.StructBlock([
|
||||
('text', blocks.TextBlock()),
|
||||
('author', blocks.CharBlock),
|
||||
])
|
||||
video = blocks.EmbedBlock
|
||||
|
||||
class Meta:
|
||||
icon='cogs'
|
||||
|
||||
|
||||
Since ``StreamField`` accepts an instance of ``StreamBlock`` as a parameter, in place of a list of block types, this makes it possible to re-use a common set block types without repeating definitions:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class HomePage(Page):
|
||||
carousel = StreamField(CarouselBlock())
|
||||
|
||||
|
||||
Template rendering
|
||||
------------------
|
||||
|
||||
The simplest way to render the contents of a StreamField into your template is to output it as a variable, like any other field:
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{{ self.body }}
|
||||
|
||||
|
||||
This will render each block of the stream in turn, wrapped in a ``<div class="block-my_block_name">`` element (where ``my_block_name`` is the block name given in the StreamField definition). If you wish to provide your own HTML markup, you can instead iterate over the field's value to access each block in turn:
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
<article>
|
||||
{% for block in self.body %}
|
||||
<section>{{ block }}</section>
|
||||
{% endfor %}
|
||||
</article>
|
||||
|
||||
|
||||
For more control over the rendering of specific block types, each block object provides ``block_type`` and ``value`` properties:
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
<article>
|
||||
{% for block in self.body %}
|
||||
{% if block.block_type == 'heading' %}
|
||||
<h1>{{ block.value }}</h1>
|
||||
{% else %}
|
||||
<section class="block-{{ block.block_type }}">
|
||||
{{ block }}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</article>
|
||||
|
||||
|
||||
Each block type provides its own front-end HTML rendering mechanism, and this is used for the output of ``{{ block }}``. For most simple block types, such as CharBlock, this will simply output the field's value, but others will provide their own HTML markup; for example, a ListBlock will output the list of child blocks as a ``<ul>`` element (with each child wrapped in an ``<li>`` element and rendered using the child block's own HTML rendering).
|
||||
|
||||
To override this with your own custom HTML rendering, you can pass a ``template`` argument to the block, giving the filename of a template file to be rendered. This is particularly useful for custom block types derived from StructBlock, as the default StructBlock rendering is simple and somewhat generic:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
('person', blocks.StructBlock(
|
||||
[
|
||||
('first_name', blocks.CharBlock(required=True)),
|
||||
('surname', blocks.CharBlock(required=True)),
|
||||
('photo', ImageChooserBlock()),
|
||||
('biography', blocks.RichTextBlock()),
|
||||
],
|
||||
template='myapp/blocks/person.html',
|
||||
icon='user'
|
||||
))
|
||||
|
||||
|
||||
Or, when defined as a subclass of StructBlock:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class PersonBlock(blocks.StructBlock):
|
||||
first_name = blocks.CharBlock(required=True)
|
||||
surname = blocks.CharBlock(required=True)
|
||||
photo = ImageChooserBlock()
|
||||
biography = blocks.RichTextBlock()
|
||||
|
||||
class Meta:
|
||||
template = 'myapp/blocks/person.html'
|
||||
icon = 'user'
|
||||
|
||||
|
||||
Within the template, the block value is accessible as the variable ``self``:
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{% load wagtailimages_tags %}
|
||||
|
||||
<div class="person">
|
||||
{% image self.photo width-400 %}
|
||||
<h2>{{ self.first_name }} {{ self.surname }}</h2>
|
||||
{{ self.bound_blocks.biography.render }}
|
||||
</div>
|
||||
|
||||
|
||||
The line ``self.bound_blocks.biography.render`` warrants further explanation. While blocks such as RichTextBlock are aware of their own rendering, the actual block *values* (as returned when accessing properties of a StructBlock, such as ``self.biography``), are just plain Python values such as strings. To access the block's proper HTML rendering, you must retrieve the 'bound block' - an object which has access to both the rendering method and the value - via the ``bound_blocks`` property.
|
||||
|
||||
|
||||
Custom block types
|
||||
------------------
|
||||
|
||||
If you need to implement a custom UI, or handle a datatype that is not provided by Wagtail's built-in block types (and cannot built up as a structure of existing fields), it is possible to define your own custom block types. For further guidance, refer to the source code of Wagtail's built-in block classes.
|
||||
|
||||
For block types that simply wrap an existing Django form field, Wagtail provides an abstract class ``wagtail.wagtailcore.blocks.FieldBlock`` as a helper. Subclasses just need to set a ``field`` property that returns the form field object:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class IPAddressBlock(FieldBlock):
|
||||
def __init__(self, required=True, help_text=None, **kwargs):
|
||||
self.field = forms.GenericIPAddressField(required=required, help_text=help_text)
|
||||
super(IPAddressBlock, self).__init__(**kwargs)
|
||||
|
||||
|
||||
Migrations
|
||||
----------
|
||||
|
||||
As with any model field in Django, any changes to a model definition that affect a StreamField will result in a migration file that contains a 'frozen' copy of that field definition. Since a StreamField definition is more complex than a typical model field, there is an increased likelihood of definitions from your project being imported into the migration - which would cause problems later on if those definitions are moved or deleted.
|
||||
|
||||
To mitigate this, StructBlock, StreamBlock and ChoiceBlock implement additional logic to ensure that any subclasses of these blocks are deconstructed to plain instances of StructBlock, StreamBlock and ChoiceBlock - in this way, the migrations avoid having any references to your custom class definitions. This is possible because these block types provide a standard pattern for inheritance, and know how to reconstruct the block definition for any subclass that follows that pattern.
|
||||
|
||||
If you subclass any other block class, such as ``FieldBlock``, you will need to either keep that class definition in place for the lifetime of your project, or implement a `custom deconstruct method <https://docs.djangoproject.com/en/1.7/topics/migrations/#custom-deconstruct-method>`__ that expresses your block entirely in terms of classes that are guaranteed to remain in place. Similarly, if you customise a StructBlock, StreamBlock or ChoiceBlock subclass to the point where it can no longer be expressed as an instance of the basic block type - for example, if you add extra arguments to the constructor - you will need to provide your own ``deconstruct`` method.
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
For Front End developers
|
||||
========================
|
||||
Writing templates
|
||||
=================
|
||||
|
||||
.. contents:: Contents
|
||||
:local:
|
||||
275
docs/reference/hooks.rst
Normal file
275
docs/reference/hooks.rst
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
|
||||
.. _admin_hooks:
|
||||
|
||||
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.
|
||||
|
||||
.. 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('name_of_hook')
|
||||
def my_hook_function(arg1, arg2...)
|
||||
# your code here
|
||||
|
||||
|
||||
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:
|
||||
|
||||
``before_serve_page``
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Called when Wagtail is about to serve a page. The callable passed into the hook will receive the page object, the request object, and the args and kwargs that will be passed to the page's ``serve()`` method. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``serve()`` on the page.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
@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>")
|
||||
|
||||
|
||||
.. _construct_wagtail_edit_bird:
|
||||
|
||||
``construct_wagtail_edit_bird``
|
||||
Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the ``request`` object and a list of menu objects, ``items``. The menu item objects must have a ``render`` method which can take a ``request`` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
class UserbarPuppyLinkItem(object):
|
||||
def render(self, request):
|
||||
return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
|
||||
+ 'target="_parent" class="action icon icon-wagtail">Puppies!</a></li>'
|
||||
|
||||
@hooks.register('construct_wagtail_edit_bird')
|
||||
def add_puppy_link_item(request, items):
|
||||
return items.append( UserbarPuppyLinkItem() )
|
||||
|
||||
|
||||
.. _construct_homepage_panels:
|
||||
|
||||
``construct_homepage_panels``
|
||||
Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a ``request`` object and a list of ``panels``, objects which have a ``render()`` method returning a string. The objects also have an ``order`` property, an integer used for ordering the panels. The default panels use integers between ``100`` and ``300``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
class WelcomePanel(object):
|
||||
order = 50
|
||||
|
||||
def render(self):
|
||||
return mark_safe("""
|
||||
<section class="panel summary nice-padding">
|
||||
<h3>No, but seriously -- welcome to the admin homepage.</h3>
|
||||
</section>
|
||||
""")
|
||||
|
||||
@hooks.register('construct_homepage_panels')
|
||||
def add_another_welcome_panel(request, panels):
|
||||
return panels.append( WelcomePanel() )
|
||||
|
||||
|
||||
.. _construct_homepage_summary_items:
|
||||
|
||||
``construct_homepage_summary_items``
|
||||
.. versionadded:: 1.0
|
||||
|
||||
Add or remove items from the 'site summary' bar on the admin homepage (which shows the number of pages and other object that exist on the site). The callable passed into this hook should take a ``request`` object and a list of ``SummaryItem`` objects to be modified as required. These objects have a ``render()`` method, which returns an HTML string, and an ``order`` property, which is an integer that specifies the order in which the items will appear.
|
||||
|
||||
|
||||
.. _after_create_page:
|
||||
|
||||
``after_create_page``
|
||||
Do something with a ``Page`` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a ``request`` object and a ``page`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page's parent.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
from wagtail.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")
|
||||
|
||||
|
||||
.. _after_edit_page:
|
||||
|
||||
``after_edit_page``
|
||||
Do something with a ``Page`` object after it has been updated. Uses the same behavior as ``after_create_page``.
|
||||
|
||||
.. _after_delete_page:
|
||||
|
||||
``after_delete_page``
|
||||
Do something after a ``Page`` object is deleted. Uses the same behavior as ``after_create_page``.
|
||||
|
||||
.. _register_admin_urls:
|
||||
|
||||
``register_admin_urls``
|
||||
Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see `url dispatcher`_.
|
||||
|
||||
.. _url dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.conf.urls import url
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
def admin_view( request ):
|
||||
return HttpResponse( \
|
||||
"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' ),
|
||||
]
|
||||
|
||||
.. _register_admin_menu_item:
|
||||
|
||||
``register_admin_menu_item``
|
||||
.. versionadded:: 0.6
|
||||
|
||||
Add an item to the Wagtail admin menu. The callable passed to this hook must return an instance of ``wagtail.wagtailadmin.menu.MenuItem``. New items can be constructed from the ``MenuItem`` class by passing in a ``label`` which will be the text in the menu item, and 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). Additionally, the following keyword arguments are accepted:
|
||||
|
||||
:name: an internal name used to identify the menu item; defaults to the slugified form of the label. Also applied as a CSS class to the wrapping ``<li>``, as ``"menu-{name}"``.
|
||||
:classnames: additional classnames applied to the link, used to give it an icon
|
||||
:attrs: additional HTML attributes to apply to the link
|
||||
:order: an integer which determines the item's position in the menu
|
||||
|
||||
``MenuItem`` can be subclassed to customise the HTML output, specify Javascript files required by the menu item, or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (``wagtail/wagtailadmin/menu.py``) for details.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
|
||||
@hooks.register('register_admin_menu_item')
|
||||
def register_frank_menu_item():
|
||||
return MenuItem('Frank', reverse('frank'), classnames='icon icon-folder-inverse', order=10000)
|
||||
|
||||
.. _register_settings_menu_item:
|
||||
|
||||
``register_settings_menu_item``
|
||||
.. versionadded:: 0.7
|
||||
|
||||
As ``register_admin_menu_item``, but registers menu items into the 'Settings' sub-menu rather than the top-level menu.
|
||||
|
||||
.. _construct_main_menu:
|
||||
|
||||
``construct_main_menu``
|
||||
Called just before the Wagtail admin menu is output, to allow the list of menu items to be modified. The callable passed to this hook will receive a ``request`` object and a list of ``menu_items``, and should modify ``menu_items`` in-place as required. Adding menu items should generally be done through the ``register_admin_menu_item`` hook instead - items added through ``construct_main_menu`` will be missing any associated Javascript includes, and their ``is_shown`` check will not be applied.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
@hooks.register('construct_main_menu')
|
||||
def hide_explorer_menu_item_from_frank(request, menu_items):
|
||||
if request.user.username == 'frank':
|
||||
menu_items[:] = [item for item in menu_items if item.name != 'explorer']
|
||||
|
||||
|
||||
.. _insert_editor_js:
|
||||
|
||||
``insert_editor_js``
|
||||
Add additional Javascript files or code snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.utils.html import format_html, format_html_join
|
||||
from django.conf import settings
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
@hooks.register('insert_editor_js')
|
||||
def editor_js():
|
||||
js_files = [
|
||||
'demo/js/hallo-plugins/hallo-demo-plugin.js',
|
||||
]
|
||||
js_includes = format_html_join('\n', '<script src="{0}{1}"></script>',
|
||||
((settings.STATIC_URL, filename) for filename in js_files)
|
||||
)
|
||||
return js_includes + format_html(
|
||||
"""
|
||||
<script>
|
||||
registerHalloPlugin('demoeditor');
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
.. _insert_editor_css:
|
||||
|
||||
``insert_editor_css``
|
||||
Add additional CSS or SCSS files or snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.utils.html import format_html
|
||||
from django.conf import settings
|
||||
|
||||
from wagtail.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">')
|
||||
|
||||
.. _construct_whitelister_element_rules:
|
||||
|
||||
``construct_whitelister_element_rules``
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Customise the rules that define which HTML elements are allowed in rich text areas. By default only a limited set of HTML elements and attributes are whitelisted - all others are stripped out. The callables passed into this hook must return a dict, which maps element names to handler functions that will perform some kind of manipulation of the element. These handler functions receive the element as a `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_ Tag object.
|
||||
|
||||
The ``wagtail.wagtailcore.whitelist`` module provides a few helper functions to assist in defining these handlers: ``allow_without_attributes``, a handler which preserves the element but strips out all of its attributes, and ``attribute_rule`` which accepts a dict specifying how to handle each attribute, and returns a handler function. This dict will map attribute names to either True (indicating that the attribute should be kept), False (indicating that it should be dropped), or a callable (which takes the initial attribute value and returns either a final value for the attribute, or None to drop the attribute).
|
||||
|
||||
For example, the following hook function will add the ``<blockquote>`` element to the whitelist, and allow the ``target`` attribute on ``<a>`` elements:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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}),
|
||||
}
|
||||
|
||||
.. _register_permissions:
|
||||
|
||||
``register_permissions``
|
||||
.. versionadded:: 0.7
|
||||
|
||||
Return a queryset of Permission objects to be shown in the Groups administration area.
|
||||
|
|
@ -6,5 +6,6 @@ Reference
|
|||
:maxdepth: 2
|
||||
|
||||
management_commands
|
||||
hooks
|
||||
project_template
|
||||
page
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
==========================================
|
||||
Wagtail 0.9 release notes - IN DEVELOPMENT
|
||||
Wagtail 1.0 release notes - IN DEVELOPMENT
|
||||
==========================================
|
||||
|
||||
.. contents::
|
||||
|
|
@ -10,6 +10,18 @@ Wagtail 0.9 release notes - IN DEVELOPMENT
|
|||
What's changed
|
||||
==============
|
||||
|
||||
StreamField - a field type for freeform content
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
StreamField provides an editing model for freeform content such as blog posts and news stories, allowing diverse content types such as text, images, headings, video and more specialised types such as maps and charts to be mixed in any order. See :ref:`streamfield`.
|
||||
|
||||
|
||||
MySQL support
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Wagtail now officially supports MySQL as a database backend.
|
||||
|
||||
|
||||
Minor changes
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
|
@ -65,6 +77,7 @@ Admin
|
|||
* Reversing ``django.contrib.auth.admin.login`` will no longer lead to Wagtails login view (making it easier to have front end views)
|
||||
* Added cache-control headers to all admin views. This allows Varnish/Squid/CDN to run on vanilla settings in front of a Wagtail site
|
||||
* Date / time pickers now consistently use times without seconds, to prevent Javascript behaviour glitches when focusing / unfocusing fields
|
||||
* Added hook ``construct_homepage_summary_items`` for customising the site summary panel on the admin homepage
|
||||
|
||||
|
||||
Project template
|
||||
|
|
@ -4,7 +4,7 @@ Release notes
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
0.9
|
||||
1.0
|
||||
0.8.6
|
||||
0.8.5
|
||||
0.8.4
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
Django>=1.6.2,<1.8
|
||||
django-modelcluster>=0.5
|
||||
django-taggit==0.12.2
|
||||
django-taggit==0.12.3
|
||||
django-treebeard==3.0
|
||||
six>=1.7.0
|
||||
|
|
|
|||
|
|
@ -28,9 +28,6 @@ Prerequisites are the Elasticsearch service itself and, via pip, the `elasticsea
|
|||
|
||||
pip install elasticsearch
|
||||
|
||||
.. note::
|
||||
If you are using Elasticsearch < 1.0, install elasticsearch-py version 0.4.5: ```pip install elasticsearch==0.4.5```
|
||||
|
||||
The backend is configured in settings:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -62,4 +59,4 @@ If you prefer not to run an Elasticsearch server in development or production, t
|
|||
Rolling Your Own
|
||||
================
|
||||
|
||||
Wagtail search backends implement the interface defined in ``wagtail/wagtail/wagtailsearch/backends/base.py``. At a minimum, the backend's ``search()`` method must return a collection of objects or ``model.objects.none()``. For a fully-featured search backend, examine the Elasticsearch backend code in ``elasticsearch.py``.
|
||||
Wagtail search backends implement the interface defined in ``wagtail/wagtail/wagtailsearch/backends/base.py``. At a minimum, the backend's ``search()`` method must return a collection of objects or ``model.objects.none()``. For a fully-featured search backend, examine the Elasticsearch backend code in ``elasticsearch.py``.
|
||||
|
|
@ -96,7 +96,7 @@ Next, let's look at the template itself:
|
|||
.. code-block:: django
|
||||
|
||||
{% extends "mysite/base.html" %}
|
||||
{% load pageurl %}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
{% block title %}Search{% if search_results %} Results{% endif %}{% endblock %}
|
||||
|
||||
34
tox.ini
34
tox.ini
|
|
@ -24,7 +24,7 @@ dj17 =
|
|||
Django>=1.7.1,<1.8
|
||||
|
||||
dj18 =
|
||||
https://github.com/django/django/archive/stable/1.8.x.zip#egg=django
|
||||
https://github.com/django/django/archive/stable/1.8.x.tar.gz#egg=django
|
||||
|
||||
py2 =
|
||||
unicodecsv>=0.9.4
|
||||
|
|
@ -43,6 +43,7 @@ envlist =
|
|||
py33-dj17-postgres,
|
||||
py33-dj17-sqlite,
|
||||
py34-dj17-postgres,
|
||||
py34-dj17-mysql,
|
||||
py34-dj17-sqlite,
|
||||
py27-dj18-postgres,
|
||||
py27-dj18-sqlite,
|
||||
|
|
@ -51,6 +52,7 @@ envlist =
|
|||
py33-dj18-sqlite,
|
||||
py34-dj18-postgres,
|
||||
py34-dj18-sqlite
|
||||
py34-dj18-mysql,
|
||||
|
||||
|
||||
[testenv]
|
||||
|
|
@ -82,7 +84,7 @@ deps =
|
|||
{[deps]base}
|
||||
{[deps]py2}
|
||||
{[deps]dj17}
|
||||
MySQL-python==1.2.5
|
||||
mysqlclient==1.3.6
|
||||
setenv =
|
||||
DATABASE_ENGINE=django.db.backends.mysql
|
||||
DATABASE_USER=wagtail
|
||||
|
|
@ -127,6 +129,19 @@ deps =
|
|||
setenv =
|
||||
DATABASE_ENGINE=django.db.backends.sqlite3
|
||||
|
||||
[testenv:py34-dj17-mysql]
|
||||
basepython=python3.4
|
||||
deps =
|
||||
{[deps]base}
|
||||
{[deps]py3}
|
||||
{[deps]dj17}
|
||||
mysqlclient==1.3.6
|
||||
setenv =
|
||||
DATABASE_ENGINE=django.db.backends.mysql
|
||||
DATABASE_USER=wagtail
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_USER=root
|
||||
|
||||
|
||||
[testenv:py27-dj18-postgres]
|
||||
basepython=python2.7
|
||||
|
|
@ -153,7 +168,7 @@ deps =
|
|||
{[deps]base}
|
||||
{[deps]py2}
|
||||
{[deps]dj18}
|
||||
MySQL-python==1.2.5
|
||||
mysqlclient==1.3.6
|
||||
setenv =
|
||||
DATABASE_ENGINE=django.db.backends.mysql
|
||||
DATABASE_USER=wagtail
|
||||
|
|
@ -197,3 +212,16 @@ deps =
|
|||
{[deps]dj18}
|
||||
setenv =
|
||||
DATABASE_ENGINE=django.db.backends.sqlite3
|
||||
|
||||
[testenv:py34-dj18-mysql]
|
||||
basepython=python3.4
|
||||
deps =
|
||||
{[deps]base}
|
||||
{[deps]py3}
|
||||
{[deps]dj18}
|
||||
mysqlclient==1.3.6
|
||||
setenv =
|
||||
DATABASE_ENGINE=django.db.backends.mysql
|
||||
DATABASE_USER=wagtail
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_USER=root
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ class RoutablePageMixin(object):
|
|||
This class can be mixed in to a Page subclass to allow urlconfs to be
|
||||
embedded inside pages.
|
||||
"""
|
||||
#: Set this to a tuple of ``django.conf.urls.url`` objects.
|
||||
#: This attribute should contain a tuple of ``django.conf.url`` objects.
|
||||
#: It can also be set to a property that returns a tuple of ``django.conf.url`` objects.
|
||||
subpage_urls = None
|
||||
|
||||
def reverse_subpage(self, name, args=None, kwargs=None):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Minimal requirements
|
||||
Django>=1.7.1,<1.8
|
||||
wagtail==0.8.6
|
||||
wagtail==1.0b1
|
||||
|
||||
# Recommended components (require additional setup):
|
||||
# psycopg2==2.5.2
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ DATABASES = {
|
|||
'default': {
|
||||
'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.sqlite3'),
|
||||
'NAME': os.environ.get('DATABASE_NAME', 'wagtail'),
|
||||
'USER': os.environ.get('DATABASE_USER', None),
|
||||
'PASSWORD': os.environ.get('DATABASE_PASS', None),
|
||||
'HOST': os.environ.get('DATABASE_HOST', None),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
class RemovedInWagtail10Warning(DeprecationWarning):
|
||||
class RemovedInWagtail11Warning(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
class RemovedInWagtail11Warning(PendingDeprecationWarning):
|
||||
class RemovedInWagtail12Warning(PendingDeprecationWarning):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from taggit.managers import TaggableManager
|
|||
from wagtail.wagtailadmin import widgets
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailcore.utils import camelcase_to_underscore, resolve_model_string
|
||||
from wagtail.utils.deprecation import RemovedInWagtail11Warning
|
||||
from wagtail.utils.deprecation import RemovedInWagtail12Warning
|
||||
|
||||
|
||||
# Form field properties to override whenever we encounter a model field
|
||||
|
|
@ -670,7 +670,7 @@ class InlinePanel(object):
|
|||
"InlinePanel no longer needs to be passed a model parameter. "
|
||||
"InlinePanel({classname}, '{relname}') should be changed to InlinePanel('{relname}')".format(
|
||||
classname=args[0].__name__, relname=self.relation_name
|
||||
), RemovedInWagtail11Warning, stacklevel=2)
|
||||
), RemovedInWagtail12Warning, stacklevel=2)
|
||||
else:
|
||||
raise TypeError("InlinePanel() takes exactly 1 argument (%d given)" % len(args))
|
||||
|
||||
|
|
|
|||
49
wagtail/wagtailadmin/site_summary.py
Normal file
49
wagtail/wagtailadmin/site_summary.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from django.template import RequestContext
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page
|
||||
|
||||
|
||||
class SummaryItem(object):
|
||||
order = 100
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def get_context(self):
|
||||
return {}
|
||||
|
||||
def render(self):
|
||||
return render_to_string(self.template, self.get_context(),
|
||||
RequestContext(self.request))
|
||||
|
||||
|
||||
class PagesSummaryItem(SummaryItem):
|
||||
order = 100
|
||||
template = 'wagtailadmin/home/site_summary_pages.html'
|
||||
|
||||
def get_context(self):
|
||||
return {
|
||||
'total_pages': Page.objects.count() - 1, # subtract 1 because the root node is not a real page
|
||||
}
|
||||
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
def add_pages_summary_item(request, items):
|
||||
items.append(PagesSummaryItem(request))
|
||||
|
||||
|
||||
class SiteSummaryPanel(object):
|
||||
name = 'site_summary'
|
||||
order = 100
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
self.summary_items = []
|
||||
for fn in hooks.get_hooks('construct_homepage_summary_items'):
|
||||
fn(request, self.summary_items)
|
||||
|
||||
def render(self):
|
||||
return render_to_string('wagtailadmin/home/site_summary.html', {
|
||||
'summary_items': sorted(self.summary_items, key=lambda p: p.order),
|
||||
}, RequestContext(self.request))
|
||||
|
|
@ -2,32 +2,8 @@
|
|||
<section class="panel summary nice-padding">
|
||||
<h2 class="visuallyhidden">{% trans "Site summary" %}</h2>
|
||||
<ul class="stats">
|
||||
<li class="icon icon-doc-empty-inverse">
|
||||
<a href="{% url 'wagtailadmin_explore_root' %}">
|
||||
{% blocktrans count counter=total_pages with total_pages|intcomma as total %}
|
||||
<span>{{ total }}</span> Page
|
||||
{% plural %}
|
||||
<span>{{ total }}</span> Pages
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="icon icon-image">
|
||||
<a href="{% url 'wagtailimages_index' %}">
|
||||
{% blocktrans count counter=total_images with total_images|intcomma as total %}
|
||||
<span>{{ total }}</span> Image
|
||||
{% plural %}
|
||||
<span>{{ total }}</span> Images
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="icon icon-doc-full-inverse">
|
||||
<a href="{% url 'wagtaildocs_index' %}">
|
||||
{% blocktrans count counter=total_docs with total_docs|intcomma as total %}
|
||||
<span>{{ total }}</span> Document
|
||||
{% plural %}
|
||||
<span>{{ total }}</span> Documents
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</li>
|
||||
{% for item in summary_items %}
|
||||
{{ item.render }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
<li class="icon icon-doc-empty-inverse">
|
||||
<a href="{% url 'wagtailadmin_explore_root' %}">
|
||||
{% blocktrans count counter=total_pages with total_pages|intcomma as total %}
|
||||
<span>{{ total }}</span> Page
|
||||
{% plural %}
|
||||
<span>{{ total }}</span> Pages
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -22,7 +22,7 @@ from wagtail.wagtailcore.models import Page, Site
|
|||
from wagtail.wagtailcore.fields import RichTextArea
|
||||
from wagtail.tests.models import PageChooserModel, EventPageChooserModel, EventPage, EventPageSpeaker, SimplePage
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
from wagtail.utils.deprecation import RemovedInWagtail11Warning
|
||||
from wagtail.utils.deprecation import RemovedInWagtail12Warning
|
||||
|
||||
|
||||
class TestGetFormForModel(TestCase):
|
||||
|
|
@ -532,9 +532,9 @@ class TestInlinePanel(TestCase, WagtailTestUtils):
|
|||
with warnings.catch_warnings(record=True) as w:
|
||||
SpeakerInlinePanelDef = InlinePanel(EventPage, 'speakers', label="Speakers")
|
||||
|
||||
# Check that a RemovedInWagtail11Warning has been triggered
|
||||
# Check that a RemovedInWagtail12Warning has been triggered
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertTrue(issubclass(w[-1].category, RemovedInWagtail11Warning))
|
||||
self.assertTrue(issubclass(w[-1].category, RemovedInWagtail12Warning))
|
||||
self.assertTrue("InlinePanel(EventPage, 'speakers') should be changed to InlinePanel('speakers')" in str(w[-1].message))
|
||||
|
||||
SpeakerInlinePanel = SpeakerInlinePanelDef.bind_to_model(EventPage)
|
||||
|
|
|
|||
|
|
@ -3,33 +3,13 @@ from django.conf import settings
|
|||
from django.template import RequestContext
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from wagtail.wagtailadmin.forms import SearchForm
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page, PageRevision, UserPagePermissionsProxy
|
||||
from wagtail.wagtailcore.models import PageRevision, UserPagePermissionsProxy
|
||||
|
||||
from wagtail.wagtaildocs.models import Document
|
||||
|
||||
from wagtail.wagtailimages.models import get_image_model
|
||||
from wagtail.wagtailadmin.site_summary import SiteSummaryPanel
|
||||
|
||||
|
||||
# Panels for the homepage
|
||||
class SiteSummaryPanel(object):
|
||||
name = 'site_summary'
|
||||
order = 100
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def render(self):
|
||||
return render_to_string('wagtailadmin/home/site_summary.html', {
|
||||
'total_pages': Page.objects.count() - 1, # subtract 1 because the root node is not a real page
|
||||
'total_images': get_image_model().objects.count(),
|
||||
'total_docs': Document.objects.count(),
|
||||
'search_form': SearchForm(),
|
||||
}, RequestContext(self.request))
|
||||
|
||||
|
||||
class PagesForModerationPanel(object):
|
||||
name = 'pages_for_moderation'
|
||||
order = 200
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
__version__ = '0.9.dev0'
|
||||
__version__ = '1.0b1'
|
||||
default_app_config = 'wagtail.wagtailcore.apps.WagtailCoreAppConfig'
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ class Command(BaseCommand):
|
|||
)
|
||||
option_list = BaseCommand.option_list + base_options
|
||||
|
||||
def numberlist_to_string(self, numberlist):
|
||||
# Converts a list of numbers into a string
|
||||
# Doesn't put "L" after longs
|
||||
return '[' + ', '.join(map(str, numberlist)) + ']'
|
||||
|
||||
def handle(self, **options):
|
||||
any_problems_fixed = False
|
||||
|
||||
|
|
@ -34,9 +39,9 @@ class Command(BaseCommand):
|
|||
(bad_alpha, bad_path, orphans, bad_depth, bad_numchild) = Page.find_problems()
|
||||
|
||||
if bad_depth:
|
||||
self.stdout.write("Incorrect depth value found for pages: %r" % bad_depth)
|
||||
self.stdout.write("Incorrect depth value found for pages: %s" % self.numberlist_to_string(bad_depth))
|
||||
if bad_numchild:
|
||||
self.stdout.write("Incorrect numchild value found for pages: %r" % bad_numchild)
|
||||
self.stdout.write("Incorrect numchild value found for pages: %s" % self.numberlist_to_string(bad_numchild))
|
||||
|
||||
if bad_depth or bad_numchild:
|
||||
Page.fix_tree(destructive=False)
|
||||
|
|
@ -89,15 +94,15 @@ class Command(BaseCommand):
|
|||
if any((bad_alpha, bad_path, orphans, bad_depth, bad_numchild)):
|
||||
self.stdout.write("Remaining problems (cannot fix automatically):")
|
||||
if bad_alpha:
|
||||
self.stdout.write("Invalid characters found in path for pages: %r" % bad_alpha)
|
||||
self.stdout.write("Invalid characters found in path for pages: %s" % self.numberlist_to_string(bad_alpha))
|
||||
if bad_path:
|
||||
self.stdout.write("Invalid path length found for pages: %r" % bad_path)
|
||||
self.stdout.write("Invalid path length found for pages: %s" % self.numberlist_to_string(bad_path))
|
||||
if orphans:
|
||||
self.stdout.write("Orphaned pages found: %r" % orphans)
|
||||
self.stdout.write("Orphaned pages found: %s" % self.numberlist_to_string(orphans))
|
||||
if bad_depth:
|
||||
self.stdout.write("Incorrect depth value found for pages: %r" % bad_depth)
|
||||
self.stdout.write("Incorrect depth value found for pages: %s" % self.numberlist_to_string(bad_depth))
|
||||
if bad_numchild:
|
||||
self.stdout.write("Incorrect numchild value found for pages: %r" % bad_numchild)
|
||||
self.stdout.write("Incorrect numchild value found for pages: %s" % self.numberlist_to_string(bad_numchild))
|
||||
|
||||
elif any_problems_fixed:
|
||||
self.stdout.write("All problems fixed.")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
<li class="icon icon-doc-full-inverse">
|
||||
<a href="{% url 'wagtaildocs_index' %}">
|
||||
{% blocktrans count counter=total_docs with total_docs|intcomma as total %}
|
||||
<span>{{ total }}</span> Document
|
||||
{% plural %}
|
||||
<span>{{ total }}</span> Documents
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -8,8 +8,10 @@ from django.contrib.auth.models import Permission
|
|||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
from wagtail.wagtailadmin.site_summary import SummaryItem
|
||||
|
||||
from wagtail.wagtaildocs import admin_urls
|
||||
from wagtail.wagtaildocs.models import Document
|
||||
from wagtail.wagtaildocs.rich_text import DocumentLinkHandler
|
||||
|
||||
|
||||
|
|
@ -59,3 +61,17 @@ def register_permissions():
|
|||
@hooks.register('register_rich_text_link_handler')
|
||||
def register_document_link_handler():
|
||||
return ('document', DocumentLinkHandler)
|
||||
|
||||
|
||||
class DocumentsSummaryItem(SummaryItem):
|
||||
order = 300
|
||||
template = 'wagtaildocs/homepage/site_summary_documents.html'
|
||||
|
||||
def get_context(self):
|
||||
return {
|
||||
'total_docs': Document.objects.count(),
|
||||
}
|
||||
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
def add_documents_summary_item(request, items):
|
||||
items.append(DocumentsSummaryItem(request))
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from django import forms
|
|||
from django.forms.models import modelform_factory
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from wagtail.utils.deprecation import RemovedInWagtail11Warning
|
||||
from wagtail.utils.deprecation import RemovedInWagtail12Warning
|
||||
from wagtail.wagtailimages.formats import get_image_formats
|
||||
from wagtail.wagtailimages.fields import WagtailImageField
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ def get_image_form(model):
|
|||
"Custom image models without an 'admin_form_fields' attribute are now deprecated. "
|
||||
"Add admin_form_fields = (tuple of field names) to {classname}".format(
|
||||
classname=model.__name__
|
||||
), RemovedInWagtail11Warning, stacklevel=2)
|
||||
), RemovedInWagtail12Warning, stacklevel=2)
|
||||
|
||||
return modelform_factory(
|
||||
model,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
import os.path
|
||||
import hashlib
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
|
||||
from six import BytesIO, text_type
|
||||
|
||||
|
|
@ -84,6 +85,7 @@ class AbstractImage(models.Model, TagSearchable):
|
|||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@contextmanager
|
||||
def get_willow_image(self):
|
||||
try:
|
||||
image_file = self.file.file # triggers a call to self.storage.open, so IOErrors from missing files will be raised at this point
|
||||
|
|
@ -95,7 +97,9 @@ class AbstractImage(models.Model, TagSearchable):
|
|||
image_file.open('rb')
|
||||
image_file.seek(0)
|
||||
|
||||
return WillowImage.open(image_file)
|
||||
yield WillowImage.open(image_file)
|
||||
|
||||
image_file.close()
|
||||
|
||||
def get_rect(self):
|
||||
return Rect(0, 0, self.width, self.height)
|
||||
|
|
@ -128,27 +132,27 @@ class AbstractImage(models.Model, TagSearchable):
|
|||
self.focal_point_height = None
|
||||
|
||||
def get_suggested_focal_point(self):
|
||||
willow = self.get_willow_image()
|
||||
with self.get_willow_image() as willow:
|
||||
faces = willow.detect_faces()
|
||||
|
||||
faces = willow.detect_faces()
|
||||
if faces:
|
||||
# Create a bounding box around all faces
|
||||
left = min(face[0] for face in faces)
|
||||
top = min(face[1] for face in faces)
|
||||
right = max(face[2] for face in faces)
|
||||
bottom = max(face[3] for face in faces)
|
||||
focal_point = Rect(left, top, right, bottom)
|
||||
else:
|
||||
features = willow.detect_features()
|
||||
if features:
|
||||
# Create a bounding box around all features
|
||||
left = min(feature[0] for feature in features)
|
||||
top = min(feature[1] for feature in features)
|
||||
right = max(feature[0] for feature in features)
|
||||
bottom = max(feature[1] for feature in features)
|
||||
if faces:
|
||||
# Create a bounding box around all faces
|
||||
left = min(face[0] for face in faces)
|
||||
top = min(face[1] for face in faces)
|
||||
right = max(face[2] for face in faces)
|
||||
bottom = max(face[3] for face in faces)
|
||||
focal_point = Rect(left, top, right, bottom)
|
||||
else:
|
||||
return None
|
||||
features = willow.detect_features()
|
||||
if features:
|
||||
# Create a bounding box around all features
|
||||
left = min(feature[0] for feature in features)
|
||||
top = min(feature[1] for feature in features)
|
||||
right = max(feature[0] for feature in features)
|
||||
bottom = max(feature[1] for feature in features)
|
||||
focal_point = Rect(left, top, right, bottom)
|
||||
else:
|
||||
return None
|
||||
|
||||
# Add 20% to width and height and give it a minimum size
|
||||
x, y = focal_point.centroid
|
||||
|
|
@ -299,12 +303,11 @@ class Filter(models.Model):
|
|||
return operations
|
||||
|
||||
def run(self, image, output):
|
||||
willow = image.get_willow_image()
|
||||
with image.get_willow_image() as willow:
|
||||
for operation in self.operations:
|
||||
operation.run(willow, image)
|
||||
|
||||
for operation in self.operations:
|
||||
operation.run(willow, image)
|
||||
|
||||
willow.save_as_jpeg(output)
|
||||
willow.save_as_jpeg(output)
|
||||
|
||||
return output
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
<li class="icon icon-image">
|
||||
<a href="{% url 'wagtailimages_index' %}">
|
||||
{% blocktrans count counter=total_images with total_images|intcomma as total %}
|
||||
<span>{{ total }}</span> Image
|
||||
{% plural %}
|
||||
<span>{{ total }}</span> Images
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
from django import template
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from wagtail.wagtailimages.models import Filter, SourceImageIOError
|
||||
|
||||
register = template.Library()
|
||||
|
||||
# Local cache of filters, avoid hitting the DB
|
||||
filters = {}
|
||||
|
||||
|
||||
@register.tag(name="image")
|
||||
def image(parser, token):
|
||||
|
|
@ -37,10 +35,12 @@ class ImageNode(template.Node):
|
|||
self.image_var = template.Variable(image_var_name)
|
||||
self.output_var_name = output_var_name
|
||||
self.attrs = attrs
|
||||
self.filter_spec = filter_spec
|
||||
|
||||
if filter_spec not in filters:
|
||||
filters[filter_spec], _ = Filter.objects.get_or_create(spec=filter_spec)
|
||||
self.filter = filters[filter_spec]
|
||||
@cached_property
|
||||
def filter(self):
|
||||
_filter, _ = Filter.objects.get_or_create(spec=self.filter_spec)
|
||||
return _filter
|
||||
|
||||
def render(self, context):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -219,9 +219,8 @@ class TestGetWillowImage(TestCase):
|
|||
)
|
||||
|
||||
def test_willow_image_object_returned(self):
|
||||
willow_image = self.image.get_willow_image()
|
||||
|
||||
self.assertIsInstance(willow_image, WillowImage)
|
||||
with self.image.get_willow_image() as willow_image:
|
||||
self.assertIsInstance(willow_image, WillowImage)
|
||||
|
||||
def test_with_missing_image(self):
|
||||
# Image id=1 in test fixtures has a missing image file
|
||||
|
|
@ -229,7 +228,9 @@ class TestGetWillowImage(TestCase):
|
|||
|
||||
# Attempting to get the Willow image for images without files
|
||||
# should raise a SourceImageIOError
|
||||
self.assertRaises(SourceImageIOError, bad_image.get_willow_image)
|
||||
with self.assertRaises(SourceImageIOError):
|
||||
with bad_image.get_willow_image() as willow_image:
|
||||
self.fail() # Shouldn't get here
|
||||
|
||||
|
||||
class TestIssue573(TestCase):
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from django.db import models
|
|||
|
||||
from taggit.forms import TagField, TagWidget
|
||||
|
||||
from wagtail.utils.deprecation import RemovedInWagtail11Warning
|
||||
from wagtail.utils.deprecation import RemovedInWagtail12Warning
|
||||
from wagtail.tests.models import CustomImageWithAdminFormFields, CustomImageWithoutAdminFormFields
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
from wagtail.wagtailimages.utils import generate_signature, verify_signature
|
||||
|
|
@ -283,9 +283,9 @@ class TestGetImageForm(TestCase, WagtailTestUtils):
|
|||
with warnings.catch_warnings(record=True) as w:
|
||||
form = get_image_form(CustomImageWithoutAdminFormFields)
|
||||
|
||||
# Check that a RemovedInWagtail11Warning has been triggered
|
||||
# Check that a RemovedInWagtail12Warning has been triggered
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertTrue(issubclass(w[-1].category, RemovedInWagtail11Warning))
|
||||
self.assertTrue(issubclass(w[-1].category, RemovedInWagtail12Warning))
|
||||
self.assertTrue("Add admin_form_fields = (tuple of field names) to CustomImageWithoutAdminFormFields" in str(w[-1].message))
|
||||
|
||||
# All fields, including the not editable one should be on the form
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ from django.contrib.contenttypes.models import ContentType
|
|||
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
from wagtail.wagtailadmin.site_summary import SummaryItem
|
||||
|
||||
from wagtail.wagtailimages import admin_urls, image_operations
|
||||
from wagtail.wagtailimages.models import get_image_model
|
||||
from wagtail.wagtailimages.rich_text import ImageEmbedHandler
|
||||
|
||||
|
||||
|
|
@ -114,3 +116,17 @@ def register_image_operations():
|
|||
@hooks.register('register_rich_text_embed_handler')
|
||||
def register_image_embed_handler():
|
||||
return ('image', ImageEmbedHandler)
|
||||
|
||||
|
||||
class ImagesSummaryItem(SummaryItem):
|
||||
order = 200
|
||||
template = 'wagtailimages/homepage/site_summary_images.html'
|
||||
|
||||
def get_context(self):
|
||||
return {
|
||||
'total_images': get_image_model().objects.count(),
|
||||
}
|
||||
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
def add_images_summary_item(request, items):
|
||||
items.append(ImagesSummaryItem(request))
|
||||
|
|
|
|||
|
|
@ -21,4 +21,4 @@ class SitesMenuItem(MenuItem):
|
|||
|
||||
@hooks.register('register_settings_menu_item')
|
||||
def register_sites_menu_item():
|
||||
return MenuItem(_('Sites'), urlresolvers.reverse('wagtailsites_index'), classnames='icon icon-site', order=602)
|
||||
return SitesMenuItem(_('Sites'), urlresolvers.reverse('wagtailsites_index'), classnames='icon icon-site', order=602)
|
||||
|
|
|
|||
Loading…
Reference in a new issue