merged master

This commit is contained in:
Dave Cranwell 2014-05-23 11:59:11 +01:00
commit e51dfca8a5
88 changed files with 3115 additions and 558 deletions

View file

@ -5,7 +5,9 @@ Changelog
~~~~~~~~~~~~~~~~
* Added toolbar to allow logged-in users to add and edit pages from the site front-end
* Support for alternative image processing backends such as Wand, via the WAGTAILIMAGES_BACKENDS setting
* Added support for generating static sites using django-medusa
* Added custom Query set for Pages with some handy methods for querying pages
* Added 'wagtailforms' module for creating form pages on a site, and handling form submissions
* Editor's guide documentation
* Editor interface now outputs form media CSS / JS, to support custom widgets with assets
* Migrations and user management now correctly handle custom AUTH_USER_MODEL settings
@ -24,6 +26,8 @@ Changelog
* Fix: Page slugs are now validated on page edit
* Fix: Filter objects are cached to avoid a database hit every time an {% image %} tag is compiled
* Fix: Moving or changing a site root page no longer causes URLs for subpages to change to 'None'
* Fix: Eliminated raw SQL queries from wagtailcore / wagtailadmin, to ensure cross-database compatibility
* Fix: Snippets menu item is hidden for administrators if no snippet types are defined
0.2 (11.03.2014)
~~~~~~~~~~~~~~~~

View file

@ -25,6 +25,7 @@ Contributors
* Miguel Vieira
* Ben Emery
* David Smith
* Ben Margolis
Translators
===========

17
docs/advanced_topics.rst Normal file
View file

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

View file

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

View file

@ -0,0 +1,184 @@
For Front End developers
========================
.. note::
This documentation is currently being written.
========================
Overview
========================
Wagtail uses Django's templating language. For developers new to Django, start with Django's own template documentation:
https://docs.djangoproject.com/en/dev/topics/templates/
Python programmers new to Django/Wagtail may prefer more technical documentation:
https://docs.djangoproject.com/en/dev/ref/templates/api/
==========================
Displaying Pages
==========================
Template Location
-----------------
For each of your ``Page``-derived models, Wagtail will look for a template in the following location, relative to your project root::
project/
app/
templates/
app/
blog_index_page.html
models.py
Class names are converted from camel case to underscores. For example, the template for model class ``BlogIndexPage`` would be assumed to be ``blog_index_page.html``. For more information, see the Django documentation for the `application directories template loader`_.
.. _application directories template loader: https://docs.djangoproject.com/en/dev/ref/templates/api/
Self
----
By default, the context passed to a model's template consists of two properties: ``self`` and ``request``. ``self`` is the model object being displayed. ``request`` is the normal Django request object. So, to include the title of a ``Page``, use ``{{ self.title }}``.
========================
Static files (css, js, images)
========================
Images
~~~~~~~~~~
Images uploaded to Wagtail go into the image library and from there are added to pages via the :doc:`page editor interface </editor_manual/new_pages/inserting_images>`.
Unlike other CMS, adding images to a page does not involve choosing a "version" of the image to use. Wagtail has no predefined image "formats" or "sizes". Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template.
Images from the library **must** be requested using this syntax, but images in your codebase can be added via conventional means e.g ``img`` tags. Only images from the library can be manipulated on the fly.
Read more about the image manipulation syntax here :ref:`Images tag <image-tag>`.
========================
Template tags & filters
========================
In addition to Django's standard tags and filters, Wagtail provides some of it's own, which can be ``load``-ed `as you would any other <https://docs.djangoproject.com/en/dev/topics/templates/#custom-tag-and-filter-libraries>`_
.. _image-tag:
Images (tag)
~~~~~~~~~~~~
The syntax for displaying/manipulating an image is thus::
{% image [image] [method]-[dimension(s)] %}
For example::
{% image self.photo width-400 %}
<!-- or a square thumbnail: -->
{% image self.photo fill-80x80 %}
The ``image`` is the Django object refering to the image. If your page model defined a field called "photo" then ``image`` would probably be ``self.photo``. The ``method`` defines which resizing algorithm to use and ``dimension(s)`` provides height and/or width values (as ``[width|height]`` or ``[width]x[height]``) to refine that algorithm.
Note that a space separates ``image`` and ``method``, but not ``method`` and ``dimensions``: a hyphen between ``method`` and ``dimensions`` is mandatory. Multiple dimensions must be separated by an ``x``.
The available ``method`` s are:
.. glossary::
``max``
(takes two dimensions)
Fit **within** the given dimensions.
The longest edge will be reduced to the equivalent dimension size defined. e.g A portrait image of width 1000, height 2000, treated with the ``max`` dimensions ``1000x500`` (landscape) would result in the image shrunk so the *height* was 500 pixels and the width 250.
``min``
(takes two dimensions)
**Cover** the given dimensions.
This may result in an image slightly **larger** than the dimensions you specify. e.g A square image of width 2000, height 2000, treated with the ``min`` dimensions ``500x200`` (landscape) would have it's height and width changed to 500, i.e matching the width required, but greater than the height.
``width``
(takes one dimension)
Reduces the width of the image to the dimension specified.
``height``
(takes one dimension)
Resize the height of the image to the dimension specified..
``fill``
(takes two dimensions)
Resize and **crop** to fill the **exact** dimensions.
This can be particularly useful for websites requiring square thumbnails of arbitrary images. e.g A landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have it's height reduced to 200, then it's width (ordinarily 400) cropped to 200.
**The crop always aligns on the centre of the image.**
.. Note::
Wagtail *does not allow deforming or stretching images*. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions.
To request the "original" version of an image, it is suggested you rely on the lack of upscalling support by requesting an image much larger than it's maximum dimensions. e.g to insert an image who's dimensions are uncertain/unknown, at it's maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide.
Rich text (filter)
~~~~~~~~~~~~~~~~~~
This filter is required for use with any ``RichTextField``. It will expand internal shorthand references to embeds and links made in the Wagtail editor into fully-baked HTML ready for display. **Note that the template tag loaded differs from the name of the filter.**
.. code-block:: django
{% load rich_text %}
...
{{ body|richtext }}
Internal links (tag)
~~~~~~~~~~~~~~~~~~~~
**pageurl**
Takes a ``Page``-derived object and returns its URL as relative (``/foo/bar/``) if it's within the same site as the current page, or absolute (``http://example.com/foo/bar/``) if not.
.. code-block:: django
{% load pageurl %}
...
<a href="{% pageurl blog %}">
**slugurl**
Takes a ``slug`` string and returns the URL for the ``Page``-derived object with that slug. Like ``pageurl``, will try to provide a relative link if possible, but will default to an absolute link if on a different site.
.. code-block:: django
{% load slugurl %}
...
<a href="{% slugurl blogslug %}">
Static files (tag)
~~~~~~~~~~~~~~
Misc
~~~~~~~~~~
========================
Wagtail User Bar
========================
This tag provides a Wagtail icon and flyout menu on the top-right of a page for a logged-in user with editing capabilities, with the option of editing the current Page-derived object or adding a new sibling object.
.. code-block:: django
{% load wagtailuserbar %}
...
{% wagtailuserbar %}

View file

@ -1,6 +1,15 @@
Building your site
==================
.. note::
Documentation currently incomplete and in draft status
Serafeim Papastefanos has written a comprehensive tutorial on creating a site from scratch in Wagtail; for the time being, this is our recommended resource:
`spapas.github.io/2014/02/13/wagtail-tutorial/ <http://spapas.github.io/2014/02/13/wagtail-tutorial/>`_
`spapas.github.io/2014/02/13/wagtail-tutorial/ <http://spapas.github.io/2014/02/13/wagtail-tutorial/>`_
.. toctree::
:maxdepth: 3
djangodevelopers
frontenddevelopers

202
docs/editing_api.rst Normal file
View file

@ -0,0 +1,202 @@
Editing API
===========
.. note::
This documentation is currently being written.
Wagtail provides a highly-customizable editing interface consisting of several components:
* **Fields** — built-in content types to augment the basic types provided by Django.
* **Panels** — the basic editing blocks for fields, groups of fields, and related object clusters
* **Choosers** — interfaces for finding related objects in a ForeignKey relationship
Configuring your models to use these components will shape the Wagtail editor to your needs. Wagtail also provides an API for injecting custom CSS and Javascript for further customization, including extending the hallo.js rich text editor.
There is also an Edit Handler API for creating your own Wagtail editor components.
Defining Panels
~~~~~~~~~~~~~~~
A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types, you just need to add a panel for each field you want to show in the Wagtail page editor, in the order you want them to appear.
There are three types of panels:
``FieldPanel( field_name, classname=None )``
This is the panel used for basic Django field types. ``field_name`` is the name of the class property used in your model definition. ``classname`` is a string of optional CSS classes given to the panel which are used in formatting and scripted interactivity. By default, panels are formatted as inset fields. The CSS class ``full`` can be used to format the panel so it covers the full width of the Wagtail page editor. The CSS class ``title`` can be used to mark a field as the source for auto-generated slug strings.
``MultiFieldPanel( panel_list, heading )``
This panel condenses several ``FieldPanel`` s or choosers, from a list or tuple, under a single ``heading`` string.
``InlinePanel( base_model, relation_name, panels=None, label='', help_text='' )``
This panel allows for the creation of a "cluster" of related objects over a join to a separate model, such as a list of related links or slides to an image carousel. This is a very powerful, but tricky feature which will take some space to cover, so we'll skip over it for now. For a full explaination on the usage of ``InlinePanel``, see :ref:`inline_panels`.
Wagtail provides a tabbed interface to help organize panels. ``content_panels`` is the main tab, used for the meat of your model content. The other, ``promote_panels``, is suggested for organizing metadata about the content, such as SEO information and other machine-readable information. Since you're writing the panel definitions, you can organize them however you want.
Let's look at an example of a panel definition:
.. code-block:: python
COMMON_PANELS = (
FieldPanel('slug'),
FieldPanel('seo_title'),
FieldPanel('show_in_menus'),
FieldPanel('search_description'),
)
...
class ExamplePage( Page ):
# field definitions omitted
...
ExamplePage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('body', classname="full"),
FieldPanel('date'),
ImageChooserPanel('splash_image'),
DocumentChooserPanel('free_download'),
PageChooserPanel('related_page'),
]
ExamplePage.promote_panels = [
MultiFieldPanel(COMMON_PANELS, "Common page configuration"),
]
Built-in Fields and Choosers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Django's field types are automatically recognized and provided with an appropriate widget for input. Just define that field the normal Django way and pass the field name into ``FieldPanel()`` when defining your panels. Wagtail will take care of the rest.
Here are some Wagtail-specific types that you might include as fields in your models.
Rich Text (HTML)
----------------
Wagtail provides a general-purpose WYSIWYG editor for creating rich text content (HTML) and embedding media such as images, video, and documents. To include this in your models, use the ``RichTextField()`` function when defining a model field:
.. code-block:: python
from wagtail.wagtailcore.fields import RichTextField
...
class BookPage(Page):
book_text = RichTextField()
If you're interested in extending the capabilities of the Wagtail editor, See :ref:`extending_wysiwyg`.
Images
------
.. code-block:: python
from wagtail.wagtailimages.models import Image
feed_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
Documents
---------
.. code-block:: python
from wagtail.wagtaildocs.models import Document
link_document = models.ForeignKey(
'wagtaildocs.Document',
null=True,
blank=True,
related_name='+'
)
Pages and Page-derived Models
-----------------------------
.. code-block:: python
from wagtail.wagtailcore.models import Page
page = models.ForeignKey(
'wagtailcore.Page',
related_name='+',
null=True,
blank=True
)
Can also use more specific models.
Snippets (and Basic Django Models?)
--------
Snippets are not not subclasses, so you must include the model class directly. A chooser is provided which takes the snippet class.
.. code-block:: python
advert = models.ForeignKey(
'demo.Advert',
related_name='+'
)
PageChooserPanel
~~~~~~~~~~~~~~~~
ImageChooserPanel
~~~~~~~~~~~~~~~~~
DocumentChooserPanel
~~~~~~~~~~~~~~~~~~~~
SnippetChooserPanel
~~~~~~~~~~~~~~~~~~~
.. _inline_panels:
Inline Panels and Model Clusters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``django-modelcluster`` module allows for streamlined relation of extra models to a Wagtail page.
.. _extending_wysiwyg:
Extending the WYSIWYG Editor (hallo.js)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Edit Handler API
~~~~~~~~~~~~~~~~

View file

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

69
docs/form_builder.rst Normal file
View file

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

View file

@ -4,7 +4,7 @@ Getting Started
On Ubuntu
~~~~~~~~~
If you have a fresh instance of Ubuntu 13.04 or 13.10, you can install Wagtail,
If you have a fresh instance of Ubuntu 13.04 or later, you can install Wagtail,
along with a demonstration site containing a set of standard templates and page
types, in one step. As the root user::
@ -102,12 +102,17 @@ Required dependencies
=====================
- `pip <https://github.com/pypa/pip>`_
- `libjpeg <http://ijg.org/>`_
- `libxml2 <http://xmlsoft.org/>`_
- `libxslt <http://xmlsoft.org/XSLT/>`_
- `zlib <http://www.zlib.net/>`_
Optional dependencies
=====================
- `PostgreSQL`_
- `Elasticsearch`_
- `Redis`_
Installation
============
@ -137,6 +142,7 @@ with a regular Django project.
.. _the Wagtail codebase: https://github.com/torchbox/wagtail
.. _PostgreSQL: http://www.postgresql.org
.. _Elasticsearch: http://www.elasticsearch.org
.. _Redis: http://redis.io/
_`Remove the demo app`
~~~~~~~~~~~~~~~~~~~~~~

View file

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

176
docs/model_recipes.rst Normal file
View file

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

View file

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

140
docs/snippets.rst Normal file
View file

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

View file

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

View file

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

View file

@ -80,6 +80,7 @@ if not settings.configured:
'wagtail.wagtailembeds',
'wagtail.wagtailsearch',
'wagtail.wagtailredirects',
'wagtail.wagtailforms',
'wagtail.tests',
],

View file

@ -1,5 +1,5 @@
# Production-configured Wagtail installation
# (secure services/account for full production use).
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# Tested on Debian 7.0.
# Tom Dyson and Neal Todd
@ -42,6 +42,7 @@ aptitude -y install openjdk-7-jre-headless
curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb
dpkg -i elasticsearch-1.0.0.deb
rm elasticsearch-1.0.0.deb
perl -pi -e"s/# network.host: 192.168.0.1/network.host: 127.0.0.1/" /etc/elasticsearch/elasticsearch.yml
update-rc.d elasticsearch defaults 95 10
service elasticsearch start

View file

@ -1,5 +1,5 @@
# Production-configured Wagtail installation
# (secure services/account for full production use).
# Production-configured Wagtail installation.
# BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE!
# Tested on Ubuntu 13.04 and 13.10.
# Tom Dyson and Neal Todd
@ -40,6 +40,7 @@ aptitude -y install openjdk-7-jre-headless
curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb
dpkg -i elasticsearch-1.0.0.deb
rm elasticsearch-1.0.0.deb
perl -pi -e"s/# network.host: 192.168.0.1/network.host: 127.0.0.1/" /etc/elasticsearch/elasticsearch.yml
update-rc.d elasticsearch defaults 95 10
service elasticsearch start

View file

@ -51,6 +51,7 @@ setup(
"Pillow>=2.3.0",
"beautifulsoup4>=4.3.2",
"lxml>=3.3.0",
'unicodecsv>=0.9.4',
'Unidecode>=0.04.14',
"BeautifulSoup==3.2.1", # django-compressor gets confused if we have lxml but not BS3 installed
],

View file

@ -3,7 +3,6 @@ dj16=
Django>=1.6,<1.7
pyelasticsearch==0.6.1
elasticutils==0.8.2
unittest2
[tox]
envlist =

View file

View file

View file

@ -0,0 +1,24 @@
from django_medusa.renderers import StaticSiteRenderer
from wagtail.wagtailcore.models import Site
from wagtail.wagtaildocs.models import Document
class PageRenderer(StaticSiteRenderer):
def get_paths(self):
# Get site
# TODO: Find way to get this to work with other sites
site = Site.objects.filter(is_default_site=True).first()
if site is None:
return []
# Return list of paths
return site.root_page.get_static_site_paths()
class DocumentRenderer(StaticSiteRenderer):
def get_paths(self):
# Return list of paths to documents
return (doc.url for doc in Document.objects.all())
renderers = [PageRenderer, DocumentRenderer]

View file

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

View file

@ -1,10 +1,12 @@
from django.db import models
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from modelcluster.fields import ParentalKey
from wagtail.wagtailcore.models import Page, Orderable
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel, InlinePanel, PageChooserPanel
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
EVENT_AUDIENCE_CHOICES = (
@ -187,12 +189,66 @@ class EventIndex(Page):
intro = RichTextField(blank=True)
ajax_template = 'tests/includes/event_listing.html'
def get_context(self, request):
def get_events(self):
return self.get_children().live().type(EventPage)
def get_paginator(self):
return Paginator(self.get_events(), 4)
def get_context(self, request, page=1):
# Pagination
paginator = self.get_paginator()
try:
events = paginator.page(page)
except PageNotAnInteger:
events = paginator.page(1)
except EmptyPage:
events = paginator.page(paginator.num_pages)
# Update context
context = super(EventIndex, self).get_context(request)
context['events'] = EventPage.objects.filter(live=True)
context['events'] = events
return context
def route(self, request, path_components):
if self.live and len(path_components) == 1:
try:
return self.serve(request, page=int(path_components[0]))
except (TypeError, ValueError):
pass
return super(EventIndex, self).route(request, path_components)
def get_static_site_paths(self):
# Get page count
page_count = self.get_paginator().num_pages
# Yield a path for each page
for page in range(page_count):
yield '/%d/' % (page + 1)
# Yield from superclass
for path in super(EventIndex, self).get_static_site_paths():
yield path
EventIndex.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('intro', classname="full"),
]
class FormField(AbstractFormField):
page = ParentalKey('FormPage', related_name='form_fields')
class FormPage(AbstractEmailForm):
pass
FormPage.content_panels = [
FieldPanel('title', classname="full title"),
InlinePanel(FormPage, 'form_fields', label="Form fields"),
MultiFieldPanel([
FieldPanel('to_address', classname="full"),
FieldPanel('from_address', classname="full"),
FieldPanel('subject', classname="full"),
], "Email")
]

View file

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

View file

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

View file

@ -1,9 +1,20 @@
from django.contrib.auth.models import User
# We need to make sure that we're using the same unittest library that Django uses internally
# Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors
try:
# Firstly, try to import unittest from Django
from django.utils import unittest
except ImportError:
# Django doesn't include unittest
# We must be running on Django 1.7+ which doesn't support Python 2.6 so
# the standard unittest library should be unittest2
import unittest
def login(client):
# Create a user
User.objects.create_superuser(username='test', email='test@email.com', password='password')
# Login
client.login(username='test', password='password')
client.login(username='test', password='password')

View file

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

View file

@ -1,3 +1,15 @@
var halloPlugins = {
'halloformat': {},
'halloheadings': {formatBlocks: ["p", "h2", "h3", "h4", "h5"]},
'hallolists': {},
'hallohr': {},
'halloreundo': {},
'hallowagtaillink': {},
};
function registerHalloPlugin(name, opts) {
halloPlugins[name] = (opts || {});
}
function makeRichTextEditable(id) {
var input = $('#' + id);
var richText = $('<div class="richtext"></div>').html(input.val());
@ -19,17 +31,7 @@ function makeRichTextEditable(id) {
richText.hallo({
toolbar: 'halloToolbarFixed',
toolbarcssClass: 'testy',
plugins: {
'halloformat': {},
'halloheadings': {formatBlocks: ["p", "h2", "h3", "h4", "h5"]},
'hallolists': {},
'hallohr': {},
'halloreundo': {},
'hallowagtailimage': {},
'hallowagtailembeds': {},
'hallowagtaillink': {},
'hallowagtaildoclink': {}
}
plugins: halloPlugins
}).bind('hallomodified', function(event, data) {
input.val(data.content);
if (!removeStylingPending) {

View file

@ -181,4 +181,9 @@ a.tag:hover{
.unlist{
@include unlist();
}
/* utility class to allow things to be scrollable if their contents can't wrap more nicely */
.overflow{
overflow:auto;
}

View file

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

View file

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

View file

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

View file

@ -11,6 +11,7 @@
@import "components/listing.scss";
@import "components/messages.scss";
@import "components/formatters.scss";
@import "components/header.scss";
@import "fonts.scss";
@ -115,7 +116,7 @@ img{
}
.nav-wrapper{
@include box-shadow(inset -2px 0px 10px 0px rgba(0, 0, 0, 0.5));
@include box-shadow(inset -5px 0px 5px -3px rgba(0, 0, 0, 0.3));
position:relative;
background: $color-grey-1;
margin-left: -100%;
@ -385,88 +386,6 @@ body.explorer-open {
}
}
header{
padding-top:1em;
padding-bottom:1em;
background-color: $color-header-bg;
margin-bottom:2em;
color:white;
h1, h2{
margin:0;
color:white;
}
h1{
padding:0.2em 0;
&.icon:before{
width:1em;
display:none;
margin-right:0.4em;
font-size:1.5em;
}
}
.col{
float:left;
margin-right:2em;
}
.left{
float:left;
.hasform &:first-child{
padding-bottom:0.5em;
float:none;
}
}
.search-bar input{
@include border-radius(3px);
width:auto;
border-width:0;
}
.right{
text-align:right;
float:right;
}
/* For case where content below header should merge with it */
&.merged{
margin-bottom:0;
}
&.tab-merged, &.no-border{
border:0;
}
&.merged.no-border{
padding-bottom:0;
}
&.no-v-padding{
padding-top:0;
padding-bottom:0;
}
/*
&.hasform h1{
margin-top:0.2em;
}
*/
.button{
background-color:$color-teal-darker;
&:hover{
background-color:$color-teal-dark;
}
}
/* necessary on mobile only to make way for hamburger menu */
&.nice-padding{
padding-left:4em;
}
}
.page-explorer header{
margin-bottom:0;
padding-bottom:0em;
}
footer{
@include row();
@include border-radius(3px 3px 0 0);
@ -832,48 +751,6 @@ footer, .logo{
}
}
header{
padding-top:1.5em;
padding-bottom:1.5em;
.left{
float:left;
margin-right:0;
&:first-child{
padding-bottom:0;
float:left;
}
}
.second{
clear:none;
.right, .left{
float:right;
}
}
h1.icon:before{
display:inline-block;
}
.col3{
@include column(3);
}
.col3.addbutton{
width:auto;
}
.col6{
@include column(6);
}
.col9{
@include column(9);
}
.breadcrumb{
margin-left:-($desktop-nice-padding);
margin-right:-($desktop-nice-padding);
}
}
footer{
width:80%;
margin-left:50px;
@ -900,13 +777,8 @@ footer, .logo{
.wrapper{
max-width:$breakpoint-desktop-larger;
}
.nav-wrapper{
@include box-shadow(inset -6px 0px 4px 0px rgba(0, 0, 0, 0.2));
.inner{
background:$color-grey-1;
@include box-shadow(inset -6px 0px 4px 0px rgba(0, 0, 0, 0.2));
}
.nav-wrapper .inner{
background:$color-grey-1;
}
footer{

View file

@ -6,6 +6,7 @@
<font id="wagtail" horiz-adv-x="512">
<font-face units-per-em="512" ascent="480" descent="-32" />
<missing-glyph horiz-adv-x="512" />
<<<<<<< HEAD
<glyph unicode="&#x20;" d="" horiz-adv-x="256" />
<glyph unicode="&#x21;" d="M499 33c4-6 4-12 0-18-3-5-8-8-15-8 0 0-457 0-457 0-6 0-11 3-14 8-4 6-4 12-1 18 0 0 228 400 228 400 3 6 8 9 16 9 7 0 12-3 15-9 0 0 228-400 228-400zM284 58c0 0 0 51 0 51s-56 0-56 0c0 0 0-51 0-51 0 0 56 0 56 0zM284 147c0 0 0 154 0 154s-56 0-56 0c0 0 0-154 0-154 0 0 56 0 56 0z" />
<glyph unicode="&#x24;" d="M51.015 429.196v-409.778h409.777v409.777h-409.777zM176.358 84.156h-82.644v42.699h150.825v-42.699zM396.055 299.719h-302.339v64.050h301.651v-64.050zM395.365 184.017h-301.651v64.049h301.651v-64.049z" />
@ -71,4 +72,72 @@
<glyph unicode="&#x78;" d="M477 318c0-7-2-14-8-19l-245-246c-6-5-12-8-20-8-7 0-14 3-19 8l-142 142c-6 6-8 12-8 20 0 7 2 14 8 19l38 39c6 5 12 8 20 8 7 0 14-3 19-8l84-84 188 188c5 5 12 8 19 8 8 0 14-3 20-8l38-39c6-6 8-12 8-20z" />
<glyph unicode="&#x79;" d="M392 110c70-24 105-45 105-62 0 0 0-54 0-54 0 0-241 0-241 0 0 0-241 0-241 0 0 0 0 54 0 54 0 17 35 38 105 62 32 12 54 24 65 36 12 12 18 28 18 48 0 8-4 16-12 25-7 10-12 22-16 38-1 4-2 7-5 9-2 2-4 3-7 4-2 1-4 4-7 9-2 5-4 12-4 22 0 5 0 10 2 13 2 4 3 6 5 6 0 0 2 2 2 2-3 17-5 32-6 45-2 19 5 38 21 58 15 19 42 29 80 29 38 0 65-10 81-29 16-20 22-39 20-58 0 0-6-45-6-45 6-2 9-10 9-21 0-10-2-17-4-22-3-5-5-8-7-9-3-1-5-2-7-4-3-2-4-5-5-9-3-17-8-29-16-38-8-9-12-17-12-25 0-20 6-36 18-48 12-12 34-24 65-36z" />
<glyph unicode="&#x7a;" d="M344 325c0-3-1-5-3-7l-112-112 112-113c2-1 3-4 3-6 0-3-1-5-3-7l-14-14c-2-2-5-3-7-3s-5 1-7 3l-133 133c-2 2-3 4-3 7 0 2 1 4 3 6l133 133c2 2 5 3 7 3s5-1 7-3l14-14c2-2 3-4 3-6z" />
</font></defs></svg>
</font></defs></svg>
=======
<glyph unicode="&#97;" d="M239 256c0 19-7 35-20 48-13 14-29 20-48 20-19 0-35-6-49-20-13-13-20-29-20-48 0-19 7-35 20-48 14-14 30-20 49-20 19 0 35 6 48 20 13 13 20 29 20 48z m205-137c0 10-4 18-10 24-7 7-15 11-24 11-10 0-18-4-24-11-7-6-11-14-11-24 0-9 4-17 10-24 7-6 15-10 25-10 9 0 17 4 24 10 6 7 10 15 10 24z m0 274c0 9-4 17-10 24-7 6-15 10-24 10-10 0-18-4-24-10-7-7-11-15-11-24 0-10 4-18 10-25 7-6 15-10 25-10 9 0 17 4 24 10 6 7 10 15 10 25z m-103-113l0-49c0-2 0-4-2-5-1-2-2-3-4-3l-41-6c-2-7-5-13-9-21 6-8 14-18 24-30 2-2 2-4 2-6 0-2 0-4-2-5-4-5-11-13-22-24-10-10-17-16-21-16-2 0-3 1-5 2l-31 24c-6-3-13-6-20-8-2-19-4-33-7-41-1-5-3-7-8-7l-49 0c-2 0-4 1-5 2-2 2-3 3-3 5l-6 41c-6 2-13 4-20 8l-32-24c-1-1-3-2-5-2-2 0-4 1-6 3-25 23-38 37-38 42 0 2 1 4 2 5 2 3 5 8 11 14 5 7 9 13 12 17-4 8-7 15-9 22l-41 6c-1 0-3 1-4 3-1 1-2 3-2 5l0 49c0 2 1 4 2 5 1 2 3 3 4 3l41 6c2 7 5 13 9 21-6 8-14 18-24 30-1 2-2 4-2 6 0 2 1 4 2 5 4 5 11 13 22 24 11 10 18 16 21 16 2 0 4-1 6-2l30-24c6 3 13 6 21 8 2 19 4 33 6 41 1 5 4 7 8 7l49 0c2 0 4-1 6-2 1-2 2-3 2-5l7-41c6-2 12-4 20-8l31 24c1 1 3 2 5 2 2 0 4-1 6-3 26-23 38-37 38-42 0-2 0-4-1-5-3-3-6-8-12-15-5-6-9-12-12-16 4-8 7-16 9-22l41-6c2 0 3-1 4-3 2-1 2-3 2-5z m171-142l0-37c0-3-13-6-40-8-2-5-5-10-8-14 9-20 14-33 14-37 0-1 0-1-1-2-22-13-33-19-33-19-2 0-6 4-13 13-6 8-11 14-13 18-4-1-7-1-8-1-2 0-5 0-8 1-3-4-8-10-14-18-7-9-11-13-13-13 0 0-11 6-33 19 0 1-1 1-1 2 0 4 5 17 14 37-3 4-6 9-8 14-27 2-40 5-40 8l0 37c0 3 13 6 40 8 2 6 5 10 8 14-9 20-14 33-14 37 0 1 1 1 1 2 1 0 4 2 10 5 5 3 10 6 15 9 5 3 8 5 8 5 2 0 6-5 13-13 6-8 11-14 14-18 3 1 6 1 8 1 1 0 4 0 8-1 9 13 17 23 24 30l2 1c0 0 11-7 33-19 1-1 1-1 1-2 0-4-5-17-14-37 3-4 6-8 8-14 27-2 40-5 40-8z m0 273l0-37c0-3-13-6-40-8-2-5-5-10-8-14 9-20 14-33 14-37 0-1 0-1-1-2-22-13-33-19-33-19-2 0-6 4-13 13-6 8-11 14-13 18-4-1-7-1-8-1-2 0-5 0-8 1-3-4-8-10-14-18-7-9-11-13-13-13 0 0-11 6-33 19 0 1-1 1-1 2 0 4 5 17 14 37-3 4-6 9-8 14-27 2-40 5-40 8l0 37c0 3 13 6 40 8 2 6 5 10 8 14-9 20-14 33-14 37 0 1 1 1 1 2 1 0 4 2 10 5 5 4 10 7 15 9 5 3 8 5 8 5 2 0 6-4 13-13 6-8 11-14 14-18 3 1 6 1 8 1 1 0 4 0 8-1 9 13 17 23 24 30l2 1c0 0 11-7 33-19 1-1 1-1 1-2 0-4-5-17-14-37 3-4 6-8 8-14 27-2 40-5 40-8z"/>
<glyph unicode="&#98;" d="M439 293l0-229c0-8-3-14-8-19-5-6-12-8-20-8l-310 0c-8 0-15 2-20 8-5 5-8 11-8 19l0 384c0 8 3 14 8 19 5 6 12 8 20 8l155 0 0-155c0-8 3-14 8-19 5-6 12-8 19-8z m-1 36l-145 0 0 146c15-3 28-9 37-19l89-89c10-10 16-22 19-38z"/>
<glyph unicode="&#99;" d="M110 73l292 0 0 220-119 0c-7 0-14 2-19 8-5 5-8 11-8 19l0 119-146 0z m183 256l107 0c-2 6-4 10-6 12l-90 89c-2 3-6 5-11 7z m146-9l0-256c0-8-3-14-8-19-5-6-12-8-20-8l-310 0c-8 0-15 2-20 8-5 5-8 11-8 19l0 384c0 8 3 14 8 19 5 6 12 8 20 8l182 0c8 0 16-1 26-5 9-4 16-9 21-14l89-89c6-5 10-13 14-22 4-9 6-17 6-25z"/>
<glyph unicode="&#100;" d="M140 73l26 26-67 67-26-26 0-30 37 0 0-37z m150 265c0 4-2 7-7 7-1 0-3-1-4-2l-155-155c-2-2-2-3-2-5 0-4 2-6 6-6 2 0 4 0 5 2l155 154c1 2 2 3 2 5z m-16 55l119-119-238-237-118 0 0 118z m195-27c0-10-3-19-10-26l-48-47-118 118 47 48c7 7 15 10 26 10 10 0 18-3 26-10l67-67c7-8 10-16 10-26z"/>
<glyph unicode="&#101;" d="M399 174c0-3-1-5-3-7l-15-14c-1-2-4-3-6-3-3 0-5 1-7 3l-112 112-112-112c-2-2-4-3-7-3-2 0-5 1-6 3l-15 14c-2 2-3 4-3 7 0 2 1 4 3 6l133 133c2 2 5 3 7 3 2 0 5-1 7-3l133-133c2-2 3-4 3-6z"/>
<glyph unicode="&#102;" d="M347 274c0 36-12 66-37 91-25 25-55 37-91 37-35 0-65-12-90-37-25-25-38-55-38-91 0-35 13-65 38-90 25-25 55-38 90-38 36 0 66 13 91 38 25 25 37 55 37 90z m147-237c0-10-4-19-11-26-7-7-16-11-26-11-10 0-19 4-26 11l-98 98c-34-24-72-36-114-36-27 0-53 5-78 16-25 11-46 25-64 43-18 18-32 39-43 64-10 25-16 51-16 78 0 28 6 54 16 78 11 25 25 47 43 65 18 18 39 32 64 43 25 10 51 15 78 15 28 0 54-5 79-15 24-11 46-25 64-43 18-18 32-40 43-65 10-24 16-50 16-78 0-42-12-80-36-114l98-98c7-7 11-15 11-25z"/>
<glyph unicode="&#103;" d="M426 134c0-7-3-14-8-19l-39-39c-5-5-12-8-20-8-7 0-14 3-19 8l-84 84-84-84c-5-5-12-8-19-8-8 0-15 3-20 8l-39 39c-5 5-8 12-8 19 0 8 3 14 8 20l84 84-84 84c-5 5-8 12-8 19 0 8 3 14 8 20l39 38c5 6 12 8 20 8 7 0 14-2 19-8l84-84 84 84c5 6 12 8 19 8 8 0 15-2 20-8l39-38c5-6 8-12 8-20 0-7-3-14-8-19l-84-84 84-84c5-6 8-12 8-20z"/>
<glyph unicode="&#105;" d="M475 247c0 6-5 9-14 9l-290 0c-7 0-15-2-23-6-8-4-15-8-19-14l-79-97c-3-4-4-7-4-10 0-6 4-10 14-10l290 0c7 0 15 2 23 6 8 4 14 9 19 14l78 97c3 4 5 8 5 11z m-304 43l204 0 0 43c0 7-2 13-7 18-5 5-11 7-18 7l-154 0c-7 0-13 3-18 8-5 5-7 11-7 18l0 17c0 7-3 13-8 18-5 5-11 8-18 8l-85 0c-7 0-13-3-18-8-5-5-8-11-8-18l0-227 68 84c8 9 19 17 31 23 13 6 26 9 38 9z m338-43c0-11-4-22-12-32l-79-97c-8-10-18-17-31-23-13-7-25-10-37-10l-290 0c-17 0-31 6-42 18-12 12-18 26-18 42l0 256c0 16 6 30 18 42 11 12 25 18 42 18l85 0c16 0 30-6 42-18 12-12 18-26 18-42l0-8 145 0c16 0 30-6 42-18 12-12 18-26 18-42l0-43 51 0c9 0 18-2 26-6 8-5 14-11 18-19 3-6 4-12 4-18z"/>
<glyph unicode="&#106;" d="M494 338l0-201c0-17-7-32-19-45-13-13-28-19-45-19l-348 0c-17 0-32 6-45 19-12 13-19 28-19 45l0 274c0 18 7 33 19 46 13 12 28 18 45 18l92 0c17 0 32-6 45-18 12-13 19-28 19-46l0-9 192 0c17 0 32-6 45-19 12-12 19-27 19-45z"/>
<glyph unicode="&#107;" d="M512 309l0-227c0-12-4-23-13-32-9-9-20-13-33-13l-420 0c-13 0-24 4-33 13-9 9-13 20-13 32l0 227c8-9 18-17 29-25 69-47 116-79 142-98 11-8 20-15 26-19 7-4 16-9 27-14 12-4 22-7 32-7l0 0c10 0 20 3 32 7 11 5 20 10 27 14 6 4 15 11 26 19 33 23 80 56 142 98 11 8 21 16 29 25z m0 84c0-15-5-29-14-43-9-14-21-25-35-35-71-50-116-81-134-93-1-1-6-4-12-9-6-4-11-8-15-11-4-2-9-5-15-9-6-3-11-6-16-8-6-1-10-2-15-2l0 0c-5 0-9 1-15 2-5 2-10 5-16 8-6 4-11 7-15 9-4 3-9 7-15 11-6 5-11 8-12 9-18 12-43 30-75 52-33 23-52 36-59 41-12 8-23 19-33 33-11 14-16 27-16 39 0 15 4 27 12 37 8 10 19 15 34 15l420 0c13 0 23-5 32-14 9-9 14-19 14-32z"/>
<glyph unicode="&#108;" d="M347 421c0-5-1-10-5-13-4-4-8-6-13-6l-36 0 0-292 36 0c5 0 9-2 13-6 4-3 5-8 5-13 0-5-1-9-5-12l-73-74c-4-3-8-5-13-5-5 0-9 2-13 5l-73 74c-4 3-5 7-5 12 0 5 1 10 5 13 4 4 8 6 13 6l36 0 0 292-36 0c-5 0-9 2-13 6-4 3-5 8-5 13 0 5 1 9 5 12l73 74c4 3 8 5 13 5 5 0 9-2 13-5l73-74c4-3 5-7 5-12z"/>
<glyph unicode="&#109;" d="M183 293l146 0 0 54c0 21-7 38-21 52-15 14-32 22-52 22-20 0-37-8-52-22-14-14-21-31-21-52z m238-28l0-164c0-8-3-15-8-20-6-5-12-8-20-8l-274 0c-8 0-14 3-20 8-5 5-8 12-8 20l0 164c0 8 3 14 8 20 6 5 12 8 20 8l9 0 0 54c0 35 13 66 38 91 25 25 55 37 90 37 35 0 65-12 90-37 25-25 38-56 38-91l0-54 9 0c8 0 14-3 20-8 5-6 8-12 8-20z"/>
<glyph unicode="&#110;" d="M335 238c0-3-1-5-3-7l-133-133c-2-2-5-3-7-3-2 0-5 1-7 3l-14 14c-2 2-3 4-3 7 0 2 1 5 3 6l112 113-112 112c-2 2-3 4-3 7 0 2 1 4 3 6l14 14c2 2 5 3 7 3 2 0 5-1 7-3l133-133c2-2 3-4 3-6z"/>
<glyph unicode="&#104;" d="M366 174l0-19c0-2-1-4-3-6-2-2-4-3-6-3l-202 0c-2 0-4 1-6 3-2 2-3 4-3 6l0 19c0 2 1 5 3 6 2 2 4 3 6 3l202 0c2 0 4-1 6-3 2-1 3-4 3-6z m0 73l0-18c0-3-1-5-3-7-2-2-4-3-6-3l-202 0c-2 0-4 1-6 3-2 2-3 4-3 7l0 18c0 3 1 5 3 6 2 2 4 3 6 3l202 0c2 0 4-1 6-3 2-1 3-3 3-6z m-256-174l292 0 0 220-119 0c-7 0-14 2-19 8-5 5-8 11-8 19l0 119-146 0z m183 256l107 0c-2 6-4 10-6 12l-90 89c-2 3-6 5-11 7z m146-9l0-256c0-8-3-14-8-19-5-6-12-8-20-8l-310 0c-8 0-15 2-20 8-5 5-8 11-8 19l0 384c0 8 3 14 8 19 5 6 12 8 20 8l182 0c8 0 16-1 26-5 9-4 16-9 21-14l89-89c6-5 10-13 14-22 4-9 6-17 6-25z"/>
<glyph unicode="&#111;" d="M171 341c0-14-5-26-15-36-10-10-22-15-37-15-14 0-26 5-36 15-10 10-15 22-15 36 0 15 5 27 15 37 10 10 22 15 36 15 15 0 27-5 37-15 10-10 15-22 15-37z m273-102l0-120-376 0 0 52 86 85 42-43 137 137z m25 188l-426 0c-3 0-5-1-6-3-2-2-3-4-3-6l0-324c0-2 1-4 3-6 1-2 3-3 6-3l426 0c3 0 5 1 6 3 2 2 3 4 3 6l0 324c0 2-1 4-3 6-1 2-3 3-6 3z m43-9l0-324c0-12-4-22-13-30-8-9-18-13-30-13l-426 0c-12 0-22 4-30 13-9 8-13 18-13 30l0 324c0 12 4 22 13 30 8 9 18 13 30 13l426 0c12 0 22-4 30-13 9-8 13-18 13-30z"/>
<glyph unicode="&#112;" d="M494 347l0-73c0-5-2-9-6-13-3-3-8-5-13-5l-18 0c-5 0-9 2-13 5-3 4-5 8-5 13l0 73c0 21-7 38-22 52-14 14-31 22-51 22-20 0-38-8-52-22-14-14-21-31-21-52l0-54 27 0c8 0 14-3 19-8 6-6 8-12 8-20l0-164c0-8-2-15-8-20-5-5-11-8-19-8l-274 0c-8 0-14 3-20 8-5 5-8 12-8 20l0 164c0 8 3 14 8 20 6 5 12 8 20 8l192 0 0 54c0 36 12 66 37 91 25 25 55 37 91 37 35 0 65-12 90-37 25-25 38-55 38-91z"/>
<glyph unicode="&#113;" d="M399 302c0-3-1-5-3-7l-133-133c-2-2-5-3-7-3-2 0-5 1-7 3l-133 133c-2 2-3 4-3 7 0 2 1 4 3 6l15 15c1 1 4 2 6 2 3 0 5-1 7-2l112-113 112 113c2 1 4 2 7 2 2 0 5-1 6-2l15-15c2-2 3-4 3-6z"/>
<glyph unicode="&#114;" d="M366 119l0 18c0 3-1 5-3 7-2 1-4 2-6 2l-202 0c-2 0-4-1-6-2-2-2-3-4-3-7l0-18c0-3 1-5 3-7 2-1 4-2 6-2l202 0c2 0 4 1 6 2 2 2 3 4 3 7z m0 73l0 18c0 3-1 5-3 7-2 2-4 2-6 2l-202 0c-2 0-4 0-6-2-2-2-3-4-3-7l0-18c0-3 1-5 3-7 2-1 4-2 6-2l202 0c2 0 4 1 6 2 2 2 3 4 3 7z m73 101l0-229c0-8-3-14-8-19-5-6-12-8-20-8l-310 0c-8 0-15 2-20 8-5 5-8 11-8 19l0 384c0 8 3 14 8 19 5 6 12 8 20 8l155 0 0-155c0-8 3-14 8-19 5-6 12-8 19-8z m-1 36l-145 0 0 146c15-3 28-9 37-19l89-89c10-10 16-22 19-38z"/>
<glyph unicode="&#115;" d="M457 137l0 201c0 8-3 14-8 20-5 5-12 8-19 8l-201 0c-8 0-15 2-20 8-5 5-8 12-8 19l0 18c0 8-3 15-8 20-5 5-12 8-19 8l-92 0c-7 0-14-3-19-8-5-5-8-12-8-20l0-274c0-7 3-14 8-19 5-6 12-8 19-8l348 0c7 0 14 2 19 8 5 5 8 12 8 19z m37 201l0-201c0-17-7-32-19-45-13-13-28-19-45-19l-348 0c-17 0-32 6-45 19-12 13-19 28-19 45l0 274c0 18 7 33 19 46 13 12 28 18 45 18l92 0c17 0 32-6 45-18 12-13 19-28 19-46l0-9 192 0c17 0 32-6 45-19 12-12 19-27 19-45z"/>
<glyph unicode="&#116;" d="M457 302l0-55c0-8-3-14-8-20-5-5-12-8-19-8l-119 0 0-118c0-8-3-15-8-20-5-5-12-8-20-8l-54 0c-8 0-15 3-20 8-5 5-8 12-8 20l0 118-119 0c-7 0-14 3-19 8-5 6-8 12-8 20l0 55c0 7 3 14 8 19 5 5 12 8 19 8l119 0 0 119c0 8 3 14 8 19 5 6 12 8 20 8l54 0c8 0 15-2 20-8 5-5 8-11 8-19l0-119 119 0c7 0 14-3 19-8 5-5 8-12 8-19z"/>
<glyph unicode="&#117;" d="M165 384c0 10-4 19-11 26-7 7-16 11-26 11-10 0-19-4-26-11-7-7-11-16-11-26 0-10 4-19 11-26 7-7 16-11 26-11 10 0 19 4 26 11 7 7 11 16 11 26z m304-165c0-10-3-18-10-25l-140-141c-8-7-17-10-26-10-11 0-19 3-26 10l-204 205c-8 7-14 16-19 29-5 12-7 23-7 33l0 119c0 10 3 18 10 26 8 7 16 10 26 10l119 0c10 0 21-2 33-7 13-5 22-11 30-19l204-204c7-7 10-16 10-26z"/>
<glyph unicode="&#118;" d="M501 241c0-5-3-11-8-18l-90-105c-7-9-18-17-32-23-14-7-26-10-38-10l-290 0c-6 0-12 1-16 4-5 2-8 6-8 11 0 6 3 12 9 18l89 105c8 10 19 17 32 24 14 6 27 9 39 9l290 0c6 0 11-1 16-3 5-3 7-7 7-12z m-91 92l0-43-222 0c-17 0-34-4-53-13-18-8-33-19-44-31l-89-106-2-2c0 1 0 2 0 4 0 1 0 2 0 3l0 256c0 16 6 30 18 42 11 12 25 18 42 18l85 0c16 0 30-6 42-18 12-12 18-26 18-42l0-8 145 0c16 0 30-6 42-18 12-12 18-26 18-42z"/>
<glyph unicode="&#119;" d="M329 256c0 20-7 37-21 52-15 14-32 21-52 21-20 0-37-7-52-21-14-15-21-32-21-52 0-20 7-37 21-52 15-14 32-21 52-21 20 0 37 7 52 21 14 15 21 32 21 52z m146 31l0-63c0-3 0-5-2-7-1-2-3-3-6-4l-52-8c-4-10-8-19-12-26 7-9 17-22 31-39 2-2 3-5 3-7 0-3-1-5-3-7-5-7-14-17-28-31-14-13-23-20-27-20-2 0-5 1-7 3l-40 31c-8-5-17-8-26-11-3-26-6-44-8-53-1-6-5-8-10-8l-64 0c-2 0-5 0-7 2-2 2-3 4-3 6l-8 53c-9 3-18 6-26 10l-40-30c-2-2-4-3-7-3-3 0-5 1-7 3-24 22-40 38-47 48-2 2-2 4-2 7 0 2 0 4 2 6 3 4 8 11 14 19 7 9 12 16 16 21-5 9-9 19-12 28l-52 8c-3 0-5 1-6 3-2 2-2 4-2 7l0 63c0 3 0 5 2 7 1 2 3 3 5 4l53 8c3 8 7 17 12 26-8 11-18 24-31 39-2 3-3 5-3 7 0 2 1 4 3 7 5 7 14 17 28 30 14 14 23 21 27 21 2 0 5-1 7-3l40-31c8 5 17 8 26 11 3 26 6 44 8 53 1 6 5 8 10 8l64 0c2 0 5 0 7-2 2-2 3-4 3-6l8-53c9-3 18-6 26-10l40 30c2 2 4 3 7 3 3 0 5-1 7-3 25-23 41-39 47-49 2-1 2-3 2-6 0-2 0-4-2-6-3-4-8-11-14-19-7-9-12-16-16-21 5-9 9-18 12-28l52-8c3 0 5-1 6-3 2-2 2-4 2-7z"/>
<glyph unicode="&#120;" d="M477 350c0-7-2-14-8-19l-206-207-39-39c-6-5-12-8-20-8-7 0-14 3-19 8l-142 142c-6 6-8 12-8 20 0 7 2 14 8 19l38 39c6 5 12 8 20 8 7 0 14-3 19-8l84-84 188 188c5 5 12 8 19 8 8 0 14-3 20-8l38-39c6-6 8-12 8-20z"/>
<glyph unicode="&#122;" d="M344 357c0-3-1-5-3-7l-112-112 112-113c2-1 3-4 3-6 0-3-1-5-3-7l-14-14c-2-2-5-3-7-3-2 0-5 1-7 3l-133 133c-2 2-3 4-3 7 0 2 1 4 3 6l133 133c2 2 5 3 7 3 2 0 5-1 7-3l14-14c2-2 3-4 3-6z"/>
<glyph unicode="&#65;" d="M232 141l176 175c3 4 5 8 5 13 0 5-2 9-5 13l-29 29c-4 4-8 6-13 6-5 0-10-2-13-6l-134-133-60 60c-3 4-8 5-13 5-5 0-9-1-13-5l-29-29c-3-4-5-8-5-13 0-5 2-9 5-13l103-102c3-4 7-6 12-6 5 0 10 2 13 6z m243 252l0-274c0-23-8-42-24-58-16-16-35-24-58-24l-274 0c-23 0-42 8-58 24-16 16-24 35-24 58l0 274c0 23 8 42 24 58 16 16 35 24 58 24l274 0c23 0 42-8 58-24 16-16 24-35 24-58z"/>
<glyph unicode="&#66;" d="M384 238l0 36c0 5-2 10-5 13-4 4-8 6-13 6l-73 0 0 73c0 5-2 9-6 13-3 3-8 5-13 5l-36 0c-5 0-10-2-13-5-4-4-6-8-6-13l0-73-73 0c-5 0-9-2-13-6-3-3-5-8-5-13l0-36c0-5 2-10 5-13 4-4 8-6 13-6l73 0 0-73c0-5 2-9 6-13 3-3 8-5 13-5l36 0c5 0 10 2 13 5 4 4 6 8 6 13l0 73 73 0c5 0 9 2 13 6 3 3 5 8 5 13z m91 18c0-40-9-77-29-110-20-34-46-60-80-80-33-20-70-29-110-29-40 0-77 9-110 29-34 20-60 46-80 80-20 33-29 70-29 110 0 40 9 77 29 110 20 34 46 60 80 80 33 20 70 29 110 29 40 0 77-9 110-29 34-20 60-46 80-80 20-33 29-70 29-110z"/>
<glyph unicode="&#68;" d="M173 54c-80 23-309 124-49 458 140-95 194-179 204-248-67-24-113-71-107-156-37 87-66 208-78 283-6-88 7-244 30-337m89-21c-19 53-59 228 241 219 9-275-137-265-202-246 27 53 62 101 106 141-54-30-103-68-145-114"/>
<glyph unicode="&#67;" d="M213 77c15-6 28-9 40-9 25 0 46 4 62 12 16 8 28 19 35 32 7 14 11 31 11 52 0 22-4 39-12 51-11 18-24 30-40 36-15 7-39 10-71 10-14 0-23-1-29-3l0-41 0-50 1-77c0-3 1-7 3-13z m-4 214c9-2 19-2 32-2 33 0 58 6 75 18 17 13 25 34 25 64 0 21-8 39-24 54-16 14-40 21-73 21-10 0-22-1-37-4 0-8 0-15 1-22 1-23 2-50 1-79l0-28c0-9 0-16 0-22z m-154-254l0 26c9 2 15 3 20 4 15 2 26 5 35 9 3 5 5 10 6 14 2 13 3 31 3 56l-1 142c-1 48-2 87-3 115 0 17-1 27-3 31 0 1-1 2-3 4-4 2-10 3-20 4-6 0-16 2-32 4l-2 23 75 2 108 4 13 0c1 0 2 0 4 0 2 0 3 0 4 0 0 0 2 0 6 0 4 0 8 0 12 0l21 0c17 0 35-2 55-8 8-2 17-6 27-11 11-5 21-12 29-21 9-9 15-19 19-30 4-11 6-23 6-35 0-13-3-26-9-37-7-11-16-21-28-30-5-3-19-11-42-22 33-7 59-21 76-41 17-20 26-43 26-68 0-14-3-30-8-46-4-12-11-23-20-33-13-14-26-24-40-31-14-7-34-13-58-17-16-3-35-4-57-3l-56 1c-16 0-45-1-85-3-7-1-33-2-78-3z"/>
<glyph unicode="&#69;" d="M110 37l5 24c0 1 8 3 22 6 14 4 25 7 33 11 5 7 9 17 11 29l8 40 16 76 4 19c1 8 3 16 4 24 2 8 4 14 5 19 1 5 3 9 4 13 1 4 1 7 2 9 1 2 1 3 1 3l8 45 5 18 6 39 3 14 0 11c-8 4-22 7-42 8-5 0-9 1-10 1l5 29 91-4c7 0 14 0 20 0 13 0 33 1 62 2 6 1 12 1 19 2 7 0 10 0 10 0 0-3-1-7-1-10-2-6-3-11-4-15-11-4-21-7-31-9-12-3-22-6-29-9-2-6-5-14-7-25-2-8-3-16-4-23-8-38-14-67-19-88l-17-89-11-45-12-67-4-13c0-1 0-4 1-7 12-3 23-5 34-6 7-1 13-2 19-3-1-6-1-11-2-17-2-6-3-10-3-12-3 0-6 0-7 0-4 0-8 0-12 0-1 0-4 0-8 0-3 1-17 3-41 5l-57 1c-7 0-24-1-49-3-14-2-24-2-28-3z"/>
<glyph unicode="&#70;" d="M110 110c0-16-6-28-16-39-11-11-24-16-39-16-15 0-28 5-39 16-11 11-16 23-16 39 0 15 5 28 16 39 11 10 24 16 39 16 15 0 28-6 39-16 10-11 16-24 16-39z m0 146c0-15-6-28-16-39-11-11-24-16-39-16-15 0-28 5-39 16-11 11-16 24-16 39 0 15 5 28 16 39 11 11 24 16 39 16 15 0 28-5 39-16 10-11 16-24 16-39z m402-119l0-55c0-2-1-4-3-6-2-2-4-3-6-3l-348 0c-2 0-4 1-6 3-2 2-3 4-3 6l0 55c0 3 1 5 3 7 2 1 4 2 6 2l348 0c2 0 4-1 6-2 2-2 3-4 3-7z m-402 265c0-15-6-28-16-39-11-10-24-16-39-16-15 0-28 6-39 16-11 11-16 24-16 39 0 16 5 28 16 39 11 11 24 16 39 16 15 0 28-5 39-16 10-11 16-23 16-39z m402-119l0-54c0-3-1-5-3-7-2-2-4-3-6-3l-348 0c-2 0-4 1-6 3-2 2-3 4-3 7l0 54c0 3 1 5 3 7 2 2 4 3 6 3l348 0c2 0 4-1 6-3 2-2 3-4 3-7z m0 147l0-55c0-3-1-5-3-7-2-1-4-2-6-2l-348 0c-2 0-4 1-6 2-2 2-3 4-3 7l0 55c0 2 1 4 3 6 2 2 4 3 6 3l348 0c2 0 4-1 6-3 2-2 3-4 3-6z"/>
<glyph unicode="&#71;" d="M109 49c0-15-5-27-16-36-10-9-23-13-38-13-21 0-37 6-50 19l17 25c9-9 19-13 30-13 6 0 10 2 14 4 5 3 7 7 7 12 0 13-10 18-30 16l-8 16c2 2 5 6 9 13 5 6 9 11 13 15 3 4 7 8 10 11l0 1c-3 0-8-1-14-1-6 0-11 0-14 0l0-15-30 0 0 43 95 0 0-25-27-33c10-2 18-7 23-14 6-7 9-15 9-25z m0 179l0-45-103 0c-1 7-2 12-2 15 0 10 3 19 7 27 4 8 10 14 16 19 6 5 13 10 19 14 6 4 12 8 16 12 5 4 7 9 7 13 0 5-2 9-4 11-3 3-7 4-12 4-8 0-16-6-23-17l-24 17c5 10 11 18 20 23 9 5 20 8 31 8 13 0 25-4 35-12 9-8 14-18 14-32 0-9-3-18-10-26-6-8-13-14-21-18-8-5-15-10-22-15-6-5-10-10-10-15l36 0 0 17z m403-91l0-55c0-2-1-4-3-6-2-2-4-3-6-3l-348 0c-2 0-4 1-6 3-2 2-3 4-3 6l0 55c0 3 1 5 3 7 2 1 4 2 6 2l348 0c2 0 4-1 6-2 2-2 3-4 3-7z m-402 257l0-28-96 0 0 28 31 0c0 8 0 19 0 35 0 15 0 27 0 34l0 4-1 0c-1-3-6-9-14-16l-20 22 39 36 30 0 0-115z m402-111l0-54c0-3-1-5-3-7-2-2-4-3-6-3l-348 0c-2 0-4 1-6 3-2 2-3 4-3 7l0 54c0 3 1 5 3 7 2 2 4 3 6 3l348 0c2 0 4-1 6-3 2-2 3-4 3-7z m0 147l0-55c0-3-1-5-3-7-2-1-4-2-6-2l-348 0c-2 0-4 1-6 2-2 2-3 4-3 7l0 55c0 2 1 4 3 6 2 2 4 3 6 3l348 0c2 0 4-1 6-3 2-2 3-4 3-6z"/>
<glyph unicode="&#72;" d="M475 256c0-30-5-58-17-85-12-27-27-51-47-70-19-20-43-35-70-47-27-12-55-17-85-17-33 0-64 6-93 20-30 14-55 34-76 59-1 2-2 4-2 6 0 3 1 4 3 6l39 39c2 2 4 3 7 3 3 0 5-2 7-3 13-19 31-33 51-42 20-10 41-15 64-15 20 0 39 4 57 11 18 8 33 18 46 32 14 13 24 28 32 46 7 18 11 37 11 57 0 20-4 39-11 57-8 18-18 33-32 46-13 14-28 24-46 32-18 7-37 11-57 11-19 0-37-3-54-10-17-7-32-16-45-29l39-39c6-6 7-13 4-20-4-8-9-11-17-11l-128 0c-5 0-9 1-13 5-4 4-5 8-5 13l0 128c0 8 3 13 11 17 7 3 14 2 20-4l37-37c20 19 44 34 70 45 26 10 53 15 81 15 30 0 58-5 85-17 27-12 51-27 70-47 20-19 35-43 47-70 12-27 17-55 17-85z"/>
<glyph unicode="&#73;" d="M475 439l0-128c0-5-1-9-5-13-4-4-8-5-13-5l-128 0c-8 0-13 3-17 11-3 7-2 14 4 20l40 39c-28 26-62 39-100 39-20 0-39-4-57-11-18-8-33-18-46-32-14-13-24-28-32-46-7-18-11-37-11-57 0-20 4-39 11-57 8-18 18-33 32-46 13-14 28-24 46-32 18-7 37-11 57-11 23 0 44 5 64 15 20 9 38 23 51 42 2 1 4 3 7 3 3 0 5-1 7-3l39-39c2-2 3-3 3-6 0-2-1-4-2-6-21-25-46-45-76-59-29-14-60-20-93-20-30 0-58 5-85 17-27 12-51 27-70 47-20 19-35 43-47 70-12 27-17 55-17 85 0 30 5 58 17 85 12 27 27 51 47 70 19 20 43 35 70 47 27 12 55 17 85 17 28 0 55-5 81-15 26-11 50-26 70-45l37 37c6 6 12 7 20 4 8-4 11-9 11-17z"/>
<glyph unicode="&#74;" d="M434 165c0 7-2 14-8 19l-59 59c-5 6-12 8-20 8-8 0-14-3-20-9 0 0 2-2 5-5 3-3 5-5 6-6 1-1 3-3 5-6 2-2 3-4 3-7 1-2 1-5 1-8 0-7-2-14-8-19-5-5-11-8-19-8-3 0-5 0-8 1-2 1-5 2-7 4-3 1-4 3-6 4-1 1-3 3-6 6-3 3-4 5-5 5-6-5-9-12-9-20 0-8 2-15 8-20l58-59c6-5 12-8 20-8 7 0 14 3 19 8l42 41c6 6 8 12 8 20z m-201 201c0 8-2 14-8 19l-58 60c-6 5-12 8-20 8-7 0-14-3-19-8l-42-42c-6-5-8-12-8-19 0-8 2-14 8-19l59-60c5-5 12-8 20-8 8 0 14 3 20 9 0 1-2 3-5 6-3 3-5 5-6 6-1 1-3 3-5 5-2 3-3 5-3 7-1 3-1 5-1 8 0 8 2 14 8 20 5 5 11 8 19 8 3 0 5-1 8-1 2-1 5-2 7-4 3-2 4-3 6-4 1-1 3-3 6-6 3-3 4-5 5-6 6 6 9 13 9 21z m256-201c0-23-8-43-24-58l-42-42c-16-16-35-24-58-24-23 0-43 8-58 24l-59 60c-16 15-24 35-24 58 0 23 8 43 25 59l-25 25c-16-16-36-25-59-25-23 0-43 8-59 24l-59 60c-16 16-24 35-24 58 0 23 8 42 24 58l42 42c16 16 35 23 58 23 23 0 43-8 58-24l59-59c16-16 24-35 24-58 0-23-8-43-25-60l25-25c16 17 36 25 59 25 23 0 43-8 59-24l59-59c16-16 24-36 24-58z"/>
<glyph unicode="&#75;" d="M475 256c0-40-9-77-29-110-20-34-46-60-80-80-33-20-70-29-110-29-40 0-77 9-110 29-34 20-60 46-80 80-20 33-29 70-29 110 0 40 9 77 29 110 20 34 46 60 80 80 33 20 70 29 110 29 40 0 77-9 110-29 34-20 60-46 80-80 20-33 29-70 29-110z"/>
<glyph unicode="&#76;" d="M256 411c-28 0-54-7-78-20-24-14-43-33-57-57-13-24-20-50-20-78 0-28 7-54 20-78 14-24 33-43 57-57 24-13 50-20 78-20 28 0 54 7 78 20 24 14 43 33 57 57 13 24 20 50 20 78 0 28-7 54-20 78-14 24-33 43-57 57-24 13-50 20-78 20z m219-155c0-40-9-77-29-110-20-34-46-60-80-80-33-20-70-29-110-29-40 0-77 9-110 29-34 20-60 46-80 80-20 33-29 70-29 110 0 40 9 77 29 110 20 34 46 60 80 80 33 20 70 29 110 29 40 0 77-9 110-29 34-20 60-46 80-80 20-33 29-70 29-110z"/>
<glyph unicode="&#77;" d="M479 165c0-11-4-19-11-26l-21-22c-8-7-16-10-26-10-11 0-19 3-26 10l-139 139-139-139c-7-7-15-10-26-10-10 0-18 3-25 10l-22 22c-7 7-11 15-11 26 0 10 4 18 11 26l186 186c7 7 16 10 26 10 10 0 19-3 26-10l186-186c7-8 11-16 11-26z"/>
<glyph unicode="&#78;" d="M479 311c0-10-4-19-11-26l-186-186c-7-7-16-11-26-11-10 0-19 4-26 11l-186 186c-7 7-11 16-11 26 0 10 4 19 11 26l21 21c8 7 17 11 26 11 11 0 19-4 26-11l139-139 139 139c7 7 15 11 26 11 9 0 18-4 26-11l21-21c7-8 11-16 11-26z"/>
<glyph unicode="&#79;" d="M158 256c-29-1-52-12-71-34l-35 0c-15 0-27 3-37 11-10 7-15 17-15 31 0 63 11 94 33 94 1 0 5-1 12-5 6-4 15-8 26-12 10-3 21-5 31-5 12 0 24 2 36 6-1-7-1-13-1-18 0-24 7-47 21-68z m286-170c0-21-7-38-20-50-13-13-30-19-51-19l-234 0c-21 0-38 6-51 19-13 12-20 29-20 50 0 10 1 19 1 28 1 9 2 18 4 29 2 10 4 20 7 29 3 9 7 17 11 26 5 8 11 15 17 21 6 6 14 11 23 15 9 3 19 5 30 5 1 0 5-2 11-6 6-4 12-8 19-13 8-4 17-9 29-12 12-4 24-6 36-6 12 0 24 2 36 6 12 3 21 8 29 12 7 5 13 9 19 13 6 4 10 6 11 6 11 0 21-2 30-5 9-4 17-9 23-15 6-6 12-13 17-21 4-9 8-17 11-26 3-9 5-19 7-29 2-11 3-20 4-29 0-9 1-18 1-28z m-273 341c0-19-7-35-20-49-14-13-30-20-49-20-18 0-35 7-48 20-13 14-20 30-20 49 0 19 7 35 20 48 13 13 30 20 48 20 19 0 35-7 49-20 13-13 20-29 20-48z m187-103c0-28-10-52-30-72-20-20-44-30-72-30-28 0-52 10-72 30-20 20-30 44-30 72 0 29 10 53 30 73 20 20 44 30 72 30 28 0 52-10 72-30 20-20 30-44 30-73z m154-60c0-14-5-24-15-31-10-8-22-11-37-11l-35 0c-19 22-42 33-71 34 14 21 21 44 21 68 0 5 0 11-1 18 12-4 24-6 36-6 10 0 21 2 31 5 11 4 20 8 26 12 7 4 11 5 12 5 22 0 33-31 33-94z m-34 163c0-19-7-35-20-49-13-13-30-20-48-20-19 0-35 7-49 20-13 14-20 30-20 49 0 19 7 35 20 48 14 13 30 20 49 20 18 0 35-7 48-20 13-13 20-29 20-48z"/>
<glyph unicode="&#80;" d="M375 256c0-7-3-12-9-16l-156-91c-3-2-6-3-9-3-3 0-6 1-9 3-6 3-9 9-9 16l0 182c0 7 3 13 9 16 6 4 12 4 18 0l156-91c6-4 9-9 9-16z m36 0c0 28-7 54-20 78-14 24-33 43-57 57-24 13-50 20-78 20-28 0-54-7-78-20-24-14-43-33-57-57-13-24-20-50-20-78 0-28 7-54 20-78 14-24 33-43 57-57 24-13 50-20 78-20 28 0 54 7 78 20 24 14 43 33 57 57 13 24 20 50 20 78z m64 0c0-40-9-77-29-110-20-34-46-60-80-80-33-20-70-29-110-29-40 0-77 9-110 29-34 20-60 46-80 80-20 33-29 70-29 110 0 40 9 77 29 110 20 34 46 60 80 80 33 20 70 29 110 29 40 0 77-9 110-29 34-20 60-46 80-80 20-33 29-70 29-110z"/>
<glyph unicode="&#81;" d="M238 366c0 15-6 28-16 39-11 10-24 16-39 16-15 0-28-6-39-16-11-11-16-24-16-39 0-8 2-16 5-24-7 4-15 5-23 5-16 0-28-5-39-16-11-10-16-23-16-38 0-16 5-29 16-39 11-11 23-16 39-16 15 0 28 5 39 16 10 10 16 23 16 39 0 8-2 15-6 23 8-3 16-5 24-5 15 0 28 5 39 16 10 11 16 23 16 39z m243-201c0-4-5-10-14-19-9-10-16-14-19-14-2 0-4 1-8 4-4 3-7 6-11 10-3 3-6 7-11 11-4 5-6 7-7 8l-27-28 63-63c5-5 8-12 8-19 0-8-4-16-11-23-8-8-15-11-23-11-8 0-15 2-20 8l-192 191c-33-25-68-37-104-37-31 0-56 10-76 29-19 20-29 45-29 76 0 30 9 60 27 89 18 30 42 53 71 71 29 18 59 27 89 27 31 0 57-9 76-29 20-19 30-45 30-76 0-36-13-70-38-104l102-101 27 27c-1 1-3 3-7 7-5 4-9 8-12 11-3 3-6 7-9 10-3 4-5 7-5 9 0 3 5 9 14 18 9 10 16 14 19 14 2 0 5-1 7-2 1-2 5-6 13-13 7-7 15-15 23-23 8-8 16-16 25-24 8-9 15-16 21-23 5-6 8-10 8-11z"/>
<glyph unicode="&#121;" d="M392 142c70-24 105-45 105-62 0 0 0-54 0-54 0 0-241 0-241 0 0 0-241 0-241 0 0 0 0 54 0 54 0 17 35 38 105 62 32 12 54 24 65 36 12 12 18 28 18 48 0 8-4 16-12 25-7 10-12 22-16 38-1 4-2 7-5 9-2 2-4 3-7 4-2 1-4 4-7 9-2 5-4 12-4 22 0 5 0 10 2 13 2 4 3 6 5 6 0 0 2 2 2 2-3 17-5 32-6 45-2 19 5 38 21 58 15 19 42 29 80 29 38 0 65-10 81-29 16-20 22-39 20-58 0 0-6-45-6-45 6-2 9-10 9-21 0-10-2-17-4-22-3-5-5-8-7-9-3-1-5-2-7-4-3-2-4-5-5-9-3-17-8-29-16-38-8-9-12-17-12-25 0-20 6-36 18-48 12-12 34-24 65-36"/>
<glyph unicode="&#82;" d="M440 389l-188 0c-10 12-20 25-23 28-2 6-8 10-14 10l-75 0c-5 0-9-3-13-6l-25-32-30 0c-16 0-28-13-28-28l0-248c0-15 12-28 28-28l368 0c16 0 28 13 28 28l0 248c0 15-12 28-28 28z"/>
<glyph unicode="&#84;" d="M421 137c0 12-4 23-12 33-9 10-19 15-30 15-10 0-18-3-24-10-5-7-8-16-8-27 0-11 3-20 10-27 7-8 17-11 30-11 9 0 17 2 24 7 7 6 10 12 10 20z m-174-36c0-3-1-5-3-7l-91-91c-2-2-4-3-7-3-2 0-4 1-6 3l-92 91c-3 3-3 6-2 10 2 4 5 6 9 6l55 0 0 393c0 3 1 5 2 6 2 2 4 3 7 3l55 0c2 0 5-1 6-3 2-1 3-3 3-6l0-393 55 0c2 0 5-1 6-3 2-2 3-4 3-6z m214 19c0-12-1-23-4-34-2-12-6-23-11-33-6-10-12-19-20-27-7-8-17-14-28-19-11-5-23-7-36-7-12 0-22 2-31 5-5 1-9 3-12 4l11 32c3-1 6-2 9-3 7-2 14-4 21-4 16 0 29 6 39 17 9 11 16 25 19 42l-1 0c-4-5-10-8-17-11-8-3-16-4-25-4-20 0-36 7-49 20-13 14-19 30-19 50 0 20 7 37 20 51 14 13 31 20 52 20 24 0 43-9 59-27 15-18 23-42 23-72z m-8 205l0-32-134 0 0 32 47 0 0 124c0 1 0 3 0 5 1 2 1 4 1 5l0 4-1 0-2-3c-2-2-4-5-7-9l-18-16-24 24 55 53 35 0 0-187z"/>
<glyph unicode="&#87;" d="M421 229l0-138c0-5-2-9-6-12-3-4-8-6-13-6l-109 0 0 110-74 0 0-110-109 0c-5 0-10 2-13 6-4 3-6 7-6 12l0 138c0 0 0 0 1 0 0 1 0 1 0 1l164 136 164-136c0 0 1-1 1-1z m63 19l-17-21c-2-2-4-3-6-3l-1 0c-3 0-5 1-6 2l-198 165-198-165c-2-2-4-2-7-2-2 0-4 1-6 3l-17 21c-2 2-2 4-2 7 0 3 1 5 3 6l205 171c6 5 14 8 22 8 8 0 16-3 22-8l69-58 0 56c0 2 1 5 3 6 2 2 4 3 7 3l54 0c3 0 5-1 7-3 2-1 3-4 3-6l0-117 62-52c2-1 3-3 3-6 0-3 0-5-2-7z"/>
<glyph unicode="&#88;" d="M365 141c1-4 1-7-2-10l-100-110c-1-2-4-3-6-3-3 0-5 1-7 3l-101 110c-3 3-3 6-2 10 2 3 5 5 8 5l64 0 0 357c0 3 1 5 3 6 2 2 4 3 7 3l54 0c3 0 5-1 7-3 2-1 3-3 3-6l0-357 64 0c4 0 6-2 8-5z"/>
<glyph unicode="&#89;" d="M365 371c-2-3-5-5-8-5l-64 0 0-357c0-3-1-5-3-6-2-2-4-3-7-3l-54 0c-3 0-5 1-7 3-2 1-3 3-3 6l0 357-64 0c-4 0-6 2-8 5-1 4-1 7 2 10l100 110c1 2 4 3 6 3 3 0 5-1 7-3l101-110c3-3 3-6 2-10z"/>
<glyph unicode="&#90;" d="M201 302l0-165c0-3-1-5-2-6-2-2-4-3-7-3l-18 0c-3 0-5 1-7 3-2 1-2 3-2 6l0 165c0 2 0 5 2 6 2 2 4 3 7 3l18 0c3 0 5-1 7-3 1-1 2-4 2-6z m73 0l0-165c0-3-1-5-2-6-2-2-4-3-7-3l-18 0c-3 0-5 1-7 3-1 1-2 3-2 6l0 165c0 2 1 5 2 6 2 2 4 3 7 3l18 0c3 0 5-1 7-3 1-1 2-4 2-6z m73 0l0-165c0-3 0-5-2-6-2-2-4-3-7-3l-18 0c-3 0-5 1-7 3-1 1-2 3-2 6l0 165c0 2 1 5 2 6 2 2 4 3 7 3l18 0c3 0 5-1 7-3 2-1 2-4 2-6z m37-207l0 271-256 0 0-271c0-4 1-8 2-12 1-3 3-6 4-7 2-2 3-3 3-3l238 0c0 0 1 1 3 3 1 1 3 4 4 7 1 4 2 8 2 12z m-192 307l128 0-14 34c-1 1-3 2-5 3l-90 0c-2-1-4-2-5-3z m265-9l0-18c0-3-1-5-2-7-2-1-4-2-7-2l-27 0 0-271c0-16-5-30-14-41-9-12-20-17-32-17l-238 0c-12 0-23 5-32 16-9 11-14 25-14 41l0 272-27 0c-3 0-5 1-7 2-1 2-2 4-2 7l0 18c0 3 1 5 2 7 2 1 4 2 7 2l88 0 20 48c3 7 8 13 16 18 7 5 15 7 22 7l92 0c7 0 15-2 22-7 8-5 13-11 16-18l20-48 88 0c3 0 5-1 7-2 1-2 2-4 2-7z"/>
<glyph unicode="&#86;" d="M370 400c0-4-3-8-8-8-4 0-8 4-8 8 0 5 4 8 8 8 5 0 8-3 8-8m3-76c0 0-11 50-77 37-6 23-5 40 9 55 20 21 52 10 52 10l0 17c-11 5-22 6-33 6-43 0-67-32-77-54l-119-219 34 6-62-119 43 8 33 93c93 0 213 34 197 160m40 42l-26 26-21-26z m-222-175c0 0 4 1 9 2 5 2 13 3 22 6 5 1 10 3 15 4 5 2 11 4 16 6 6 3 11 5 16 8 6 3 11 7 16 10 1 1 3 2 4 3l3 3c3 3 5 5 7 7 2 2 4 5 6 7 1 1 2 2 3 4l1 2 1 1c1 2 2 3 2 4 1 1 2 3 2 4 1 1 1 1 1 2 1 0 1 1 1 2 1 1 1 2 2 3 1 3 2 5 3 8 0 2 1 4 2 7 0 2 1 4 1 6 0 2 1 4 1 6 0 2 1 4 1 6 0 1 0 3 0 4 0 5 1 9 1 9l5-1c0 0-1-3-1-8 0-2-1-3-1-5 0-2 0-4-1-5 0-2 0-4-1-7 0-2-1-4-1-6-1-3-2-5-3-8-1-2-2-5-3-7 0-1-1-3-2-4 0-1 0-1-1-2 0-1 0-1-1-2 0-1-1-2-2-4-1-1-1-2-2-3-1-1-1-2-1-2l-2-2c-1-1-2-3-3-4-2-2-4-5-6-7-2-2-4-4-7-7l-3-3c-2-1-3-1-4-2-6-4-11-8-17-11-5-2-11-5-16-7-6-2-11-4-17-6-5-1-10-3-15-4-9-2-17-4-22-5-5-1-9-2-9-2"/>
<glyph unicode="&#49;" d="M75 75c-48 48-75 113-75 181 0 141 115 256 256 256l32-32-32-32c-106 0-192-86-192-192 0-51 20-100 56-136l-39-5z m181-75l-32 32 32 32c106 0 192 86 192 192 0 51-20 100-56 136l39 5 6 40c48-48 75-113 75-181 0-141-115-256-256-256z"/>
<glyph unicode="&#85;" d="M218 63c0-35-29-63-64-63-35 0-64 28-64 63 0 36 29 64 64 64 35 0 64-28 64-64z m0 193c0-35-29-64-64-64-35 0-64 29-64 64 0 35 29 64 64 64 35 0 64-29 64-64z m0 193c0-36-29-64-64-64-35 0-64 28-64 64 0 35 29 63 64 63 35 0 64-28 64-63z m204-386c0-35-29-63-64-63-35 0-64 28-64 63 0 36 29 64 64 64 35 0 64-28 64-64z m0 193c0-35-29-64-64-64-35 0-64 29-64 64 0 35 29 64 64 64 35 0 64-29 64-64z m0 193c0-36-29-64-64-64-35 0-64 28-64 64 0 35 29 63 64 63 35 0 64-28 64-63z"/>
<glyph unicode="&#51;" d="M332 426l0-94 49 0 0-98-192 0 0 55-152-102 152-101 0 54 286 0 0 286z"/>
<glyph unicode="&#50;" d="M192 320l-192 0 149-109-54-179 153 107 168-107-69 179 165 109-192 0-72 184z"/>
<glyph unicode="&#53;" d="M135 456l241 0 0-23-241 0z m270-177l-127-124 0 222-45 0 0-220-125 122-33-32 181-181 181 181z"/>
<glyph unicode="&#54;" d="M136 456l241 0 0-23-241 0z m-28-302l126 124 0-222 45 0 0 220 126-122 32 32-181 181-181-181z"/>
<glyph unicode="&#48;" d="M346 321c0-2 0-4 0-6 0-2-1-4-1-7 0-2-1-5-1-7-1-3-2-6-2-9-1-3-2-6-3-9-1-3-2-6-3-10-1-1-2-3-2-4-1-1-1-2-1-3-1-1-1-1-2-2-1-2-1-3-2-5-1-2-2-3-3-5l-2-2-1-3c-1-1-2-3-4-4-2-3-4-7-7-9-3-3-6-6-9-9l-4-4c-2-1-3-2-5-4-6-5-13-9-20-13-7-4-14-7-21-10-7-3-14-6-21-8-6-2-12-4-18-6-12-3-22-5-29-7-7-2-11-2-11-2 0 0 4 0 11 2 7 1 17 3 29 6 6 2 12 3 19 5 7 2 14 5 21 8 7 3 14 6 21 10 7 3 14 8 21 13 2 1 3 2 5 3l5 4c3 3 6 6 8 9 3 3 6 6 8 9 2 2 3 3 4 5l2 2c0 1 1 2 1 3 1 1 2 3 4 5 0 1 1 3 2 5 1 0 1 1 2 2 0 1 0 2 1 3 1 1 1 3 2 5 2 3 3 6 4 9 1 4 2 7 3 10 1 3 1 6 2 8 1 3 1 6 2 8 0 3 0 5 1 7 0 3 0 5 0 6 1 8 1 12 1 12l-6 0c0 0 0-4-1-11m112 80l-34 33c-1 33-22 55-32 61-10 9-33 16-59 13-25-4-61-17-86-64-49-92-154-289-155-291-1 0-1-1 0-1 0-1 1-1 1-1l40 7-78-151c0-1 0-1 1-2 0 0 0 0 1 0 0 0 0 0 0 0l56 10c0 0 1 0 1 1l41 120c105 1 178 26 218 77 42 54 36 122 29 154l-5 31 60 0c0 0 1 0 1 1 1 0 0 1 0 2m-55 42c0-5-5-10-10-10-6 0-10 5-10 10 0 6 4 11 10 11 5 0 10-5 10-11m-33-228c-40-50-112-75-216-75-1 0-1-1-1-2l-42-120-51-9 78 151c0 1 0 1 0 2-1 0-1 1-2 1l-40-8c14 26 109 202 154 287 25 46 59 59 83 62 25 3 46-3 56-10l0-22c-2 0-16 5-29 5-13 0-28-5-37-15-16-18-20-36-12-66 54 10 78-13 87-27 7-30 15-99-28-154m29 186l23 30 30-30z"/>
<glyph unicode="&#63;" d="M253 492c65 0 120-22 167-67 46-45 70-100 72-165 0-65-22-121-68-167-45-47-100-71-165-73-65 0-121 22-167 68-47 45-71 100-72 165-1 65 21 121 67 167 46 47 101 71 166 72m-1-379c10 0 19 3 25 9 6 7 10 15 10 24 0 11-2 19-9 26-6 6-15 9-25 9 0 0-1 0-1 0-10 0-18-3-24-9-7-6-10-14-11-24 0-10 3-18 10-25 6-6 14-10 24-10 0 0 1 0 1 0m85 168c9 11 13 24 13 40 0 26-9 46-27 59-18 13-41 19-69 19-22 0-40-4-53-13-24-14-36-39-37-75 0 0 0-2 0-2 0 0 56 0 56 0 0 0 0 2 0 2 0 9 3 18 8 28 6 8 15 12 28 12 14 0 23-3 27-10 5-7 8-14 8-23 0-6-3-13-8-20-3-4-7-8-11-10 0 0-3-2-3-2-2-2-4-4-8-6-3-2-7-5-10-8-4-2-7-5-11-8-4-3-7-6-9-9-4-7-7-20-9-40 0 0 0-4 0-4 0 0 56 0 56 0 0 0 0 2 0 2 0 4 0 9 2 14 2 7 6 13 14 19 0 0 14 9 14 9 16 12 25 20 29 26"/>
<glyph unicode="&#33;" d="M499 65c4-6 4-12 0-18-3-5-8-8-15-8 0 0-457 0-457 0-6 0-11 3-14 8-4 6-4 12-1 18 0 0 228 400 228 400 3 6 8 9 16 9 7 0 12-3 15-9 0 0 228-400 228-400m-215 25c0 0 0 51 0 51 0 0-56 0-56 0 0 0 0-51 0-51 0 0 56 0 56 0m0 89c0 0 0 154 0 154 0 0-56 0-56 0 0 0 0-154 0-154 0 0 56 0 56 0"/>
<glyph unicode="&#57;" d="M256 512c-141 0-256-115-256-256 0-141 115-256 256-256 141 0 256 115 256 256 0 141-115 256-256 256z m-40-374l-117 118 45 45 72-73 154 154 45-45z"/>
<glyph unicode="&#83;" d="M435 186l0-117-358 0 0 117-68 0 0-151c0-19 15-35 34-35l426 0c19 0 34 16 34 35l0 151z m-185 12l-99 118c0 0-15 15 2 15 16 0 55 0 55 0 0 0 0 9 0 24 0 41 0 118 0 149 0 0-2 8 11 8 12 0 68 0 78 0 9 0 8-7 8-7 0-30 0-109 0-149 0-13 0-22 0-22 0 0 32 0 52 0 19 0 5-15 5-15 0 0-84-111-96-122-8-9-16 1-16 1z"/>
<glyph unicode="&#52;" d="M256 309c8 0 15-1 22-4 7-3 13-6 18-11 5-5 9-11 11-17 3-7 5-14 5-21 0-7-2-14-5-21-2-6-6-12-11-17-5-5-11-8-18-11-7-3-14-4-22-4-8 0-15 1-22 4-7 3-13 6-18 11-5 5-9 11-11 17-3 7-5 14-5 21 0 7 2 14 5 21 2 6 6 12 11 17 5 5 11 8 18 11 7 3 14 4 22 4z m0-136c12 0 23 2 34 6 11 4 20 10 28 18 8 7 14 16 19 26 4 11 6 21 6 33 0 12-2 22-6 33-5 10-11 19-19 26-8 8-17 14-28 18-11 4-22 6-34 6-12 0-23-2-34-6-11-4-20-10-28-18-8-7-14-16-19-26-4-11-6-21-6-33 0-12 2-22 6-33 5-10 11-19 19-26 8-8 17-14 28-18 11-4 22-6 34-6z m0 194c18 0 35-2 51-6 16-4 31-10 45-17 14-6 26-14 37-22 11-9 21-17 29-25 7-8 13-16 17-23 5-8 7-14 7-18 0-4-2-10-7-18-4-7-10-15-17-23-8-8-18-16-29-25-11-8-23-16-37-22-14-7-29-13-45-17-16-4-33-6-51-6-18 0-35 2-51 6-17 4-31 10-45 17-14 6-26 14-37 22-11 9-21 17-29 25-7 8-13 16-17 23-5 8-7 14-7 18 0 4 2 10 7 18 4 7 10 15 17 23 8 8 18 16 29 25 11 8 23 16 37 22 14 7 28 13 45 17 16 4 33 6 51 6z"/>
<glyph unicode="&#55;" d="M416 0l-320 0c-53 0-96 43-96 96l0 320c0 42 27 77 64 90l0-42c0-27 22-48 48-48 26 0 48 21 48 48l0 48 192 0 0-48c0-27 22-48 48-48 26 0 48 21 48 48l0 42c37-13 64-48 64-90l0-320c0-53-43-96-96-96z m32 352l-384 0 0-256c0-18 14-32 32-32l320 0c18 0 32 14 32 32z m-128-160l64 0 0-64-64 0z m0 96l64 0 0-64-64 0z m-96-96l64 0 0-64-64 0z m0 96l64 0 0-64-64 0z m-96-96l64 0 0-64-64 0z m0 96l64 0 0-64-64 0z m272 160c-9 0-16 7-16 16l0 48 32 0 0-48c0-9-7-16-16-16z m-288 0c-9 0-16 7-16 16l0 48 32 0 0-48c0-9-7-16-16-16z"/>
<glyph unicode="&#36;" d="M0 512l0-512 512 0 0 512z m157-431l-104 0 0 53 189 0 0-53z m274 269l-378 0 0 81 377 0 0-81z m-1-144l-377 0 0 80 377 0z"/>
</font></defs></svg>
>>>>>>> master

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,11 +4,6 @@
CSS declarations to be included on the 'create page' and 'edit page' views
{% endcomment %}
{% comment %}
TODO: have a mechanism for sub-apps to specify their own declarations -
ideally wagtailadmin shouldn't have to know anything at all about wagtailimages and friends
{% endcomment %}
{% compress css %}
<link rel="stylesheet" href="{{ STATIC_URL }}wagtailadmin/scss/layouts/page-editor.scss" type="text/x-scss" />
<link rel="stylesheet" href="{{ STATIC_URL }}wagtailadmin/scss/panels/rich-text.scss" type="text/x-scss" />

View file

@ -4,6 +4,11 @@
Javascript declarations to be included on the 'create page' and 'edit page' views
{% endcomment %}
<script>
window.chooserUrls = {
'pageChooser': '{% url "wagtailadmin_choose_page" %}'
};
</script>
{% compress js %}
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/rangy-core.js"></script>
@ -14,21 +19,9 @@
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-wagtail-toolbar.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-hr.js"></script>
<script src="{{ STATIC_URL }}wagtailimages/js/hallo-plugins/hallo-wagtailimage.js"></script>
<script src="{{ STATIC_URL }}wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js"></script>
<script src="{{ STATIC_URL }}wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/page-editor.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/page-chooser.js"></script>
{% comment %}
TODO: use the insert_editor_js hook to inject things like image-chooser.js and hallo-wagtailimage.js
from their respective apps such as wagtailimages -
ideally wagtailadmin shouldn't have to know anything at all about wagtailimages.
TODO: a method of injecting these sorts of things on demand when the modal is spawned.
{% endcomment %}
<script src="{{ STATIC_URL }}wagtailimages/js/image-chooser.js"></script>
<script src="{{ STATIC_URL }}wagtaildocs/js/document-chooser.js"></script>
<script src="{{ STATIC_URL }}wagtailsnippets/js/snippet-chooser.js"></script>
<script src="{{ STATIC_URL }}admin/js/urlify.js"></script>
{% hook_output 'insert_editor_js' %}
@ -41,19 +34,10 @@
{% comment %}
Additional js from widgets media. Allows for custom widgets in admin panel.
Can be used for TODO above (including image-choser.js at wagtailimages)
{% endcomment %}
{{ edit_handler.form.media.js }}
<script>
window.chooserUrls = {
'documentChooser': '{% url "wagtaildocs_chooser" %}',
'imageChooser': '{% url "wagtailimages_chooser" %}',
'embedsChooser': '{% url "wagtailembeds_chooser" %}',
'pageChooser': '{% url "wagtailadmin_choose_page" %}',
'snippetChooser': '{% url "wagtailsnippets_choose_generic" %}'
};
{% get_date_format_override as format_override %}
window.overrideDateInputFormat ='{{ format_override }}';

View file

@ -1,13 +0,0 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% blocktrans with page_type=content_type.model_class.get_verbose_name %}Where do you want to create a {{ page_type }}{% endblocktrans %}{% endblock %}
{% block content %}
{% trans "Where do you want to create this" as where_str %}
{% include "wagtailadmin/shared/header.html" with title=where_str subtitle=content_type.model_class.get_verbose_name icon="doc-empty-inverse" %}
<ul>
{% for page in parent_pages %}
<li><a href="{% url 'wagtailadmin_pages_create' content_type.app_label content_type.model page.id %}">{{ page.title }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -1,20 +0,0 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n %}
{% block titletag %}{% trans "Create a new page" %}{% endblock %}
{% block bodyclass %}menu-explorer{% endblock %}
{% block content %}
{% trans "Create a new page" as create_str %}
{% include "wagtailadmin/shared/header.html" with title=create_str icon="doc-empty-inverse" %}
<div class="nice-padding">
<p>{% trans "Your new page will be saved in the <em>top level</em> of your website. You can move it after saving." %}</p>
{% if all_page_types %}
<ul class="listing">
{% for content_type in all_page_types %}
<li><a href="{% url 'wagtailadmin_pages_select_location' content_type.app_label content_type.model %}" class="icon icon-plus-inverse">{{ content_type.model_class.get_verbose_name }}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}

View file

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

View file

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

View file

@ -6,10 +6,6 @@
{{ menu_item.render_html }}
{% endfor %}
{% comment %}
{# TODO: make this work #}
<li><a href="{% url 'wagtailadmin_pages_select_type' %}" class="icon teal icon-plus-inverse">{% trans 'New page' %}</a></li>
{% endcomment %}
<li class="footer">
<div class="avatar icon icon-user"><a href="{% url 'wagtailadmin_account' %}" title="{% trans 'Account settings' %}">{% if request.user.email %}<img src="{% gravatar_url request.user.email %}" />{% endif %}</a></div>
<a href="{% url 'wagtailadmin_logout' %}">{% trans "Log out" %}</a>

View file

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

View file

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

View file

@ -8,6 +8,7 @@ from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailcore.models import get_navigation_menu_items, UserPagePermissionsProxy
from wagtail.wagtailcore.util import camelcase_to_underscore
register = template.Library()

View file

@ -1,7 +1,6 @@
from django.test import TestCase
import unittest2 as unittest
from wagtail.tests.models import SimplePage, EventPage
from wagtail.tests.utils import login
from wagtail.tests.utils import login, unittest
from wagtail.wagtailcore.models import Page
from django.core.urlresolvers import reverse
@ -37,32 +36,6 @@ class TestPageExplorer(TestCase):
self.assertTrue(response.context['pages'].filter(id=self.child_page.id).exists())
class TestPageSelectTypeLocation(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
# Login
login(self.client)
def test_select_type(self):
response = self.client.get(reverse('wagtailadmin_pages_select_type'))
self.assertEqual(response.status_code, 200)
@unittest.expectedFailure # For some reason, this returns a 302...
def test_select_location_testpage(self):
response = self.client.get(reverse('wagtailadmin_pages_select_location', args=('tests', 'eventpage')))
self.assertEqual(response.status_code, 200)
def test_select_location_nonexistanttype(self):
response = self.client.get(reverse('wagtailadmin_pages_select_location', args=('notanapp', 'notamodel')))
self.assertEqual(response.status_code, 404)
def test_select_location_nonpagetype(self):
response = self.client.get(reverse('wagtailadmin_pages_select_location', args=('wagtailimages', 'image')))
self.assertEqual(response.status_code, 404)
class TestPageCreation(TestCase):
def setUp(self):
# Find root page

View file

@ -49,8 +49,6 @@ urlpatterns += [
url(r'^pages/$', pages.index, name='wagtailadmin_explore_root'),
url(r'^pages/(\d+)/$', pages.index, name='wagtailadmin_explore'),
url(r'^pages/new/$', pages.select_type, name='wagtailadmin_pages_select_type'),
url(r'^pages/new/(\w+)/(\w+)/$', pages.select_location, name='wagtailadmin_pages_select_location'),
url(r'^pages/new/(\w+)/(\w+)/(\d+)/$', pages.create, name='wagtailadmin_pages_create'),
url(r'^pages/new/(\w+)/(\w+)/(\d+)/preview/$', pages.preview_on_create, name='wagtailadmin_pages_preview_on_create'),
url(r'^pages/usage/(\w+)/(\w+)/$', pages.content_type_use, name='wagtailadmin_pages_type_use'),

View file

@ -40,28 +40,6 @@ def index(request, parent_page_id=None):
})
@permission_required('wagtailadmin.access_admin')
def select_type(request):
# Get the list of page types that can be created within the pages that currently exist
existing_page_types = ContentType.objects.raw("""
SELECT DISTINCT content_type_id AS id FROM wagtailcore_page
""")
all_page_types = sorted(get_page_types(), key=lambda pagetype: pagetype.name.lower())
page_types = set()
for content_type in existing_page_types:
allowed_subpage_types = content_type.model_class().clean_subpage_types()
for subpage_type in allowed_subpage_types:
subpage_content_type = ContentType.objects.get_for_model(subpage_type)
page_types.add(subpage_content_type)
return render(request, 'wagtailadmin/pages/select_type.html', {
'page_types': page_types,
'all_page_types': all_page_types
})
@permission_required('wagtailadmin.access_admin')
def add_subpage(request, parent_page_id):
parent_page = get_object_or_404(Page, id=parent_page_id).specific
@ -78,38 +56,6 @@ def add_subpage(request, parent_page_id):
})
@permission_required('wagtailadmin.access_admin')
def select_location(request, content_type_app_name, content_type_model_name):
try:
content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
except ContentType.DoesNotExist:
raise Http404
page_class = content_type.model_class()
# page_class must be a Page type and not some other random model
if not issubclass(page_class, Page):
raise Http404
# find all the valid locations (parent pages) where a page of the chosen type can be added
parent_pages = page_class.allowed_parent_pages()
if len(parent_pages) == 0:
# user cannot create a page of this type anywhere - fail with an error
messages.error(request, _("Sorry, you do not have access to create a page of type <em>'{0}'</em>.").format(content_type.name))
return redirect('wagtailadmin_pages_select_type')
elif len(parent_pages) == 1:
# only one possible location - redirect them straight there
messages.warning(request, _("Pages of this type can only be created as children of <em>'{0}'</em>. This new page will be saved there.").format(parent_pages[0].title))
return redirect('wagtailadmin_pages_create', content_type_app_name, content_type_model_name, parent_pages[0].id)
else:
# prompt them to select a location
return render(request, 'wagtailadmin/pages/select_location.html', {
'content_type': content_type,
'page_class': page_class,
'parent_pages': parent_pages,
})
@permission_required('wagtailadmin.access_admin')
def content_type_use(request, content_type_app_name, content_type_model_name):
try:
@ -136,8 +82,6 @@ def content_type_use(request, content_type_app_name, content_type_model_name):
except EmptyPage:
pages = paginator.page(paginator.num_pages)
print page_class
return render(request, 'wagtailadmin/pages/content_type_use.html', {
'pages': pages,
'app_name': content_type_app_name,

View file

@ -622,6 +622,19 @@ class Page(MP_Node, ClusterableModel, Indexed):
"""
return self.serve(self.dummy_request())
def get_static_site_paths(self):
"""
This is a generator of URL paths to feed into a static site generator
Override this if you would like to create static versions of subpages
"""
# Yield paths for this page
yield '/'
# Yield paths for child pages
for child in self.get_children().live():
for path in child.specific.get_static_site_paths():
yield '/' + child.slug + path
def get_ancestors(self, inclusive=False):
return Page.objects.ancestor_of(self, inclusive)
@ -638,17 +651,9 @@ def get_navigation_menu_items():
# or are at the top-level (this rule required so that an empty site out-of-the-box has a working menu)
navigable_content_type_ids = get_navigable_page_content_type_ids()
if navigable_content_type_ids:
pages = Page.objects.raw("""
SELECT * FROM wagtailcore_page
WHERE numchild > 0 OR content_type_id IN %s OR depth = 2
ORDER BY path
""", [tuple(navigable_content_type_ids)])
pages = Page.objects.filter(Q(content_type__in=navigable_content_type_ids)|Q(depth=2)|Q(numchild__gt=0)).order_by('path')
else:
pages = Page.objects.raw("""
SELECT * FROM wagtailcore_page
WHERE numchild > 0 OR depth = 2
ORDER BY path
""")
pages = Page.objects.filter(Q(depth=2)|Q(numchild__gt=0)).order_by('path')
# Turn this into a tree structure:
# tree_node = (page, children)
@ -794,6 +799,37 @@ class UserPagePermissionsProxy(object):
permission to perform specific tasks on the given page"""
return PagePermissionTester(self, page)
def editable_pages(self):
"""Return a queryset of the pages that this user has permission to edit"""
# Deal with the trivial cases first...
if not self.user.is_active:
return Page.objects.none()
if self.user.is_superuser:
return Page.objects.all()
# Translate each of the user's permission rules into a Q-expression
q_expressions = []
for perm in self.permissions:
if perm.permission_type == 'add':
# user has edit permission on any subpage of perm.page
# (including perm.page itself) that is owned by them
q_expressions.append(
Q(path__startswith=perm.page.path, owner=self.user)
)
elif perm.permission_type == 'edit':
# user has edit permission on any subpage of perm.page
# (including perm.page itself) regardless of owner
q_expressions.append(
Q(path__startswith=perm.page.path)
)
if q_expressions:
all_rules = q_expressions[0]
for expr in q_expressions[1:]:
all_rules = all_rules | expr
return Page.objects.filter(all_rules)
else:
return Page.objects.none()
class PagePermissionTester(object):
def __init__(self, user_perms, page):

View file

@ -3,7 +3,7 @@ from django.http import HttpRequest, Http404
from django.contrib.auth.models import User
from wagtail.wagtailcore.models import Page, Site
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
@ -162,6 +162,47 @@ class TestServeView(TestCase):
self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')
class TestStaticSitePaths(TestCase):
def setUp(self):
self.root_page = Page.objects.get(id=1)
# For simple tests
self.home_page = self.root_page.add_child(instance=SimplePage(title="Homepage", slug="home"))
self.about_page = self.home_page.add_child(instance=SimplePage(title="About us", slug="about"))
self.contact_page = self.home_page.add_child(instance=SimplePage(title="Contact", slug="contact"))
# For custom tests
self.event_index = self.root_page.add_child(instance=EventIndex(title="Events", slug="events"))
for i in range(20):
self.event_index.add_child(instance=EventPage(title="Event " + str(i), slug="event" + str(i)))
def test_local_static_site_paths(self):
paths = list(self.about_page.get_static_site_paths())
self.assertEqual(paths, ['/'])
def test_child_static_site_paths(self):
paths = list(self.home_page.get_static_site_paths())
self.assertEqual(paths, ['/', '/about/', '/contact/'])
def test_custom_static_site_paths(self):
paths = list(self.event_index.get_static_site_paths())
# Event index path
expected_paths = ['/']
# One path for each page of results
expected_paths.extend(['/' + str(i + 1) + '/' for i in range(5)])
# One path for each event page
expected_paths.extend(['/event' + str(i) + '/' for i in range(20)])
paths.sort()
expected_paths.sort()
self.assertEqual(paths, expected_paths)
class TestPageUrlTags(TestCase):
fixtures = ['test.json']
@ -336,6 +377,62 @@ class TestPagePermission(TestCase):
self.assertTrue(homepage_perms.can_move_to(root))
self.assertFalse(homepage_perms.can_move_to(unpublished_event_page))
def test_editable_pages_for_user_with_add_permission(self):
event_editor = User.objects.get(username='eventeditor')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(event_editor).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_user_with_edit_permission(self):
event_moderator = User.objects.get(username='eventmoderator')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(event_moderator).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_inactive_user(self):
user = User.objects.get(username='inactiveuser')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(user).editable_pages()
self.assertFalse(editable_pages.filter(id=homepage.id).exists())
self.assertFalse(editable_pages.filter(id=christmas_page.id).exists())
self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
def test_editable_pages_for_superuser(self):
user = User.objects.get(username='superuser')
homepage = Page.objects.get(url_path='/home/')
christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
editable_pages = UserPagePermissionsProxy(user).editable_pages()
self.assertTrue(editable_pages.filter(id=homepage.id).exists())
self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
class TestPageQuerySet(TestCase):
fixtures = ['test.json']

View file

@ -1,5 +1,7 @@
from django.conf import settings
from django.conf.urls import include, url
from django.core import urlresolvers
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
@ -21,3 +23,23 @@ def construct_main_menu(request, menu_items):
MenuItem(_('Documents'), urlresolvers.reverse('wagtaildocs_index'), classnames='icon icon-doc-full-inverse', order=400)
)
hooks.register('construct_main_menu', construct_main_menu)
def editor_js():
js_files = [
'wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js',
'wagtaildocs/js/document-chooser.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>
window.chooserUrls.documentChooser = '{0}';
registerHalloPlugin('hallowagtaildoclink');
</script>
""",
urlresolvers.reverse('wagtaildocs_chooser')
)
hooks.register('insert_editor_js', editor_js)

View file

@ -152,6 +152,17 @@ def get_embed(url, max_width=None, finder=None):
finder = get_default_finder()
embed_dict = finder(url, max_width)
# Make sure width and height are valid integers before inserting into database
try:
embed_dict['width'] = int(embed_dict['width'])
except (TypeError, ValueError):
embed_dict['width'] = None
try:
embed_dict['height'] = int(embed_dict['height'])
except (TypeError, ValueError):
embed_dict['height'] = None
# Create database record
embed, created = Embed.objects.get_or_create(
url=url,

View file

@ -8,6 +8,20 @@ class TestEmbeds(TestCase):
def setUp(self):
self.hit_count = 0
def dummy_finder(self, url, max_width=None):
# Up hit count
self.hit_count += 1
# Return a pretend record
return {
'title': "Test: " + url,
'type': 'video',
'thumbnail_url': '',
'width': max_width if max_width else 640,
'height': 480,
'html': "<p>Blah blah blah</p>",
}
def test_get_embed(self):
embed = get_embed('www.test.com/1234', max_width=400, finder=self.dummy_finder)
@ -31,20 +45,23 @@ class TestEmbeds(TestCase):
embed = get_embed('www.test.com/4321', finder=self.dummy_finder)
self.assertEqual(self.hit_count, 3)
def dummy_finder(self, url, max_width=None):
# Up hit count
self.hit_count += 1
# Return a pretend record
def dummy_finder_invalid_width(self, url, max_width=None):
# Return a record with an invalid width
return {
'title': "Test: " + url,
'type': 'video',
'thumbnail_url': '',
'width': max_width if max_width else 640,
'width': '100%',
'height': 480,
'html': "<p>Blah blah blah</p>",
}
def test_invalid_width(self):
embed = get_embed('www.test.com/1234', max_width=400, finder=self.dummy_finder_invalid_width)
# Width must be set to None
self.assertEqual(embed.width, None)
class TestChooser(TestCase):
def setUp(self):

View file

@ -1,4 +1,7 @@
from django.conf import settings
from django.conf.urls import include, url
from django.core import urlresolvers
from django.utils.html import format_html
from wagtail.wagtailadmin import hooks
from wagtail.wagtailembeds import urls
@ -9,3 +12,18 @@ def register_admin_urls():
url(r'^embeds/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
def editor_js():
return format_html("""
<script src="{0}{1}"></script>
<script>
window.chooserUrls.embedsChooser = '{2}';
registerHalloPlugin('hallowagtailembeds');
</script>
""",
settings.STATIC_URL,
'wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js',
urlresolvers.reverse('wagtailembeds_chooser')
)
hooks.register('insert_editor_js', editor_js)

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,104 @@
import datetime
import unicodecsv
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from django.contrib.auth.decorators import permission_required
from wagtail.wagtailcore.models import Page
from wagtail.wagtailforms.models import FormSubmission, get_forms_for_user
from wagtail.wagtailforms.forms import SelectDateForm
@permission_required('wagtailadmin.access_admin')
def index(request):
p = request.GET.get("p", 1)
form_pages = get_forms_for_user(request.user)
paginator = Paginator(form_pages, 20)
try:
form_pages = paginator.page(p)
except PageNotAnInteger:
form_pages = paginator.page(1)
except EmptyPage:
form_pages = paginator.page(paginator.num_pages)
return render(request, 'wagtailforms/index.html', {
'form_pages': form_pages,
})
@permission_required('wagtailadmin.access_admin')
def list_submissions(request, page_id):
form_page = get_object_or_404(Page, id=page_id).specific
if not get_forms_for_user(request.user).filter(id=page_id).exists():
raise PermissionDenied
data_fields = [
(field.clean_name, field.label)
for field in form_page.form_fields.all()
]
submissions = FormSubmission.objects.filter(page=form_page)
select_date_form = SelectDateForm(request.GET)
if select_date_form.is_valid():
date_from = select_date_form.cleaned_data.get('date_from')
date_to = select_date_form.cleaned_data.get('date_to')
# careful: date_to should be increased by 1 day since the submit_time
# is a time so it will always be greater
if date_to:
date_to += datetime.timedelta(days=1)
if date_from and date_to:
submissions = submissions.filter(submit_time__range=[date_from, date_to])
elif date_from and not date_to:
submissions = submissions.filter(submit_time__gte=date_from)
elif not date_from and date_to:
submissions = submissions.filter(submit_time__lte=date_to)
if request.GET.get('action') == 'CSV':
# return a CSV instead
response = HttpResponse(content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = 'attachment;filename=export.csv'
writer = unicodecsv.writer(response, encoding='utf-8')
header_row = ['Submission date'] + [label for name, label in data_fields]
writer.writerow(header_row)
for s in submissions:
data_row = [s.submit_time]
form_data = s.get_data()
for name, label in data_fields:
data_row.append(form_data.get(name))
writer.writerow(data_row)
return response
p = request.GET.get('p', 1)
paginator = Paginator(submissions, 20)
try:
submissions = paginator.page(p)
except PageNotAnInteger:
submissions = paginator.page(1)
except EmptyPage:
submissions = paginator.page(paginator.num_pages)
data_headings = [label for name, label in data_fields]
data_rows = []
for s in submissions:
form_data = s.get_data()
data_row = [s.submit_time] + [form_data.get(name) for name, label in data_fields]
data_rows.append(data_row)
return render(request, 'wagtailforms/index_submissions.html', {
'form_page': form_page,
'select_date_form': select_date_form,
'submissions': submissions,
'data_headings': data_headings,
'data_rows': data_rows
})

View file

@ -0,0 +1,28 @@
from django.core import urlresolvers
from django.conf import settings
from django.conf.urls import include, url
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailforms import urls
from wagtail.wagtailforms.models import get_forms_for_user
def register_admin_urls():
return [
url(r'^forms/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
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)
def editor_js():
return """<script src="%swagtailforms/js/page-editor.js"></script>""" % settings.STATIC_URL
hooks.register('insert_editor_js', editor_js)

View file

@ -4,7 +4,7 @@ import os.path
from taggit.managers import TaggableManager
from django.core.files import File
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, ValidationError
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.db import models
from django.db.models.signals import pre_delete
from django.dispatch.dispatcher import receiver
@ -17,6 +17,8 @@ from unidecode import unidecode
from wagtail.wagtailadmin.taggable import TagSearchable
from wagtail.wagtailimages.backends import get_image_backend
from .utils import validate_image_format
class AbstractImage(models.Model, TagSearchable):
title = models.CharField(max_length=255, verbose_name=_('Title') )
@ -34,12 +36,7 @@ class AbstractImage(models.Model, TagSearchable):
filename = prefix[:-1] + dot + extension
return os.path.join(folder_name, filename)
def file_extension_validator(ffile):
extension = ffile.name.split(".")[-1].lower()
if extension not in ["gif", "jpg", "jpeg", "png"]:
raise ValidationError(_("Not a valid image format. Please use a gif, jpeg or png file instead."))
file = models.ImageField(verbose_name=_('File'), upload_to=get_upload_to, width_field='width', height_field='height', validators=[file_extension_validator])
file = models.ImageField(verbose_name=_('File'), upload_to=get_upload_to, width_field='width', height_field='height', validators=[validate_image_format])
width = models.IntegerField(editable=False)
height = models.IntegerField(editable=False)
created_at = models.DateTimeField(auto_now_add=True)

View file

@ -4,9 +4,7 @@ from django.contrib.auth.models import User, Group, Permission
from django.core.urlresolvers import reverse
from django.core.files.uploadedfile import SimpleUploadedFile
import unittest2 as unittest
from wagtail.tests.utils import login
from wagtail.tests.utils import login, unittest
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.templatetags import image_tags

View file

@ -0,0 +1,28 @@
import os
from PIL import Image
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
def validate_image_format(f):
# Check file extension
extension = os.path.splitext(f.name)[1].lower()[1:]
if extension == 'jpg':
extension = 'jpeg'
if extension not in ['gif', 'jpeg', 'png']:
raise ValidationError(_("Not a valid image. Please use a gif, jpeg or png file with the correct file extension."))
if not f.closed:
# Open image file
file_position = f.tell()
f.seek(0)
image = Image.open(f)
f.seek(file_position)
# Check that the internal format matches the extension
if image.format.upper() != extension.upper():
raise ValidationError(_("Not a valid %s image. Please use a gif, jpeg or png file with the correct file extension.") % (extension.upper()))

View file

@ -1,5 +1,7 @@
from django.conf import settings
from django.conf.urls import include, url
from django.core import urlresolvers
from django.utils.html import format_html, format_html_join
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
@ -21,3 +23,23 @@ def construct_main_menu(request, menu_items):
MenuItem(_('Images'), urlresolvers.reverse('wagtailimages_index'), classnames='icon icon-image', order=300)
)
hooks.register('construct_main_menu', construct_main_menu)
def editor_js():
js_files = [
'wagtailimages/js/hallo-plugins/hallo-wagtailimage.js',
'wagtailimages/js/image-chooser.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>
window.chooserUrls.imageChooser = '{0}';
registerHalloPlugin('hallowagtailimage');
</script>
""",
urlresolvers.reverse('wagtailimages_chooser')
)
hooks.register('insert_editor_js', editor_js)

View file

@ -2,7 +2,7 @@ from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
from django.core import management
import unittest2 as unittest
from wagtail.tests.utils import unittest
from wagtail.wagtailsearch import models, get_search_backend
from wagtail.wagtailsearch.backends.db import DBSearch
from wagtail.wagtailsearch.backends import InvalidSearchBackendError

View file

@ -1,9 +1,8 @@
from django.test import TestCase
from django.core import management
from wagtail.wagtailsearch import models
from wagtail.tests.utils import login
from wagtail.tests.utils import login, unittest
from StringIO import StringIO
import unittest2 as unittest
class TestHitCounter(TestCase):

View file

@ -19,10 +19,12 @@ def user_can_edit_snippet_type(user, content_type):
def user_can_edit_snippets(user):
""" true if user has any permission related to any content type registered as a snippet type """
snippet_content_types = get_snippet_content_types()
if user.is_active and user.is_superuser:
return True
# admin can edit snippets iff any snippet types exist
return bool(snippet_content_types)
permissions = Permission.objects.filter(content_type__in=get_snippet_content_types()).select_related('content_type')
permissions = Permission.objects.filter(content_type__in=snippet_content_types).select_related('content_type')
for perm in permissions:
permission_name = "%s.%s" % (perm.content_type.app_label, perm.codename)
if user.has_perm(permission_name):

View file

@ -5,7 +5,7 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
import unittest2 as unittest
from wagtail.tests.utils import unittest
from django.test import TestCase

View file

@ -1,5 +1,7 @@
from django.conf import settings
from django.conf.urls import include, url
from django.core import urlresolvers
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
@ -22,3 +24,15 @@ def construct_main_menu(request, menu_items):
MenuItem(_('Snippets'), urlresolvers.reverse('wagtailsnippets_index'), classnames='icon icon-snippet', order=500)
)
hooks.register('construct_main_menu', construct_main_menu)
def editor_js():
return format_html("""
<script src="{0}{1}"></script>
<script>window.chooserUrls.snippetChooser = '{2}';</script>
""",
settings.STATIC_URL,
'wagtailsnippets/js/snippet-chooser.js',
urlresolvers.reverse('wagtailsnippets_choose_generic')
)
hooks.register('insert_editor_js', editor_js)