Merge branch 'master' of github.com:torchbox/wagtail into feature/streamfield

Conflicts:
	wagtail/wagtailadmin/templates/wagtailadmin/shared/field.html
This commit is contained in:
Matt Westcott 2015-01-16 14:51:09 +00:00
commit d536fc8315
37 changed files with 294 additions and 121 deletions

View file

@ -9,12 +9,14 @@ Changelog
* Added thousands separator for counters on dashboard
* Added contextual links to admin notification messages
* When copying pages, it is now possible to specify a place to copy to (Timo Rieber)
* FieldPanel now accepts an optional 'widget' parameter to override the field's default form widget (Alejandro Giacometti)
0.8.5 (xx.xx.20xx)
~~~~~~~~~~~~~~~~~~
* Fix: On adding a new page, the available page types are ordered by the displayed verbose name
* Fix: Active admin submenus were not properly closed when activating another
* Fix: get_sitemap_urls is now called on the specific page class so it can now be overridden (Jerel Unruh)
0.8.4 (04.12.2014)

View file

@ -38,6 +38,7 @@ Contributors
* Robert Rollins
* linibou
* Timo Rieber
* Jerel Unruh
Translators
===========

View file

@ -23,8 +23,10 @@ A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick
There are four basic types of panels:
``FieldPanel( field_name, classname=None )``
This is the panel used for basic Django field types. ``field_name`` is the name of the class property used in your model definition. ``classname`` is a string of optional CSS classes given to the panel which are used in formatting and scripted interactivity. By default, panels are formatted as inset fields. The CSS class ``full`` can be used to format the panel so it covers the full width of the Wagtail page editor. The CSS class ``title`` can be used to mark a field as the source for auto-generated slug strings.
``FieldPanel( field_name, classname=None, widget=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. The optional ``widget`` parameter allows you to specify a `django form widget`_ to use instead of the default widget for this field type.
.. _django form widget: https://docs.djangoproject.com/en/dev/ref/forms/widgets/
``MultiFieldPanel( children, heading="", classname=None )``
This panel condenses several ``FieldPanel`` s or choosers, from a list or tuple, under a single ``heading`` string.

View file

@ -15,3 +15,4 @@ Bug fixes
* On adding a new page, the available page types are ordered by the displayed verbose name
* Active admin submenus were not properly closed when activating another
* ``get_sitemap_urls`` is now called on the specific page class so it can now be overridden

View file

@ -18,6 +18,7 @@ Minor features
* Added thousands separator for counters on dashboard
* Added contextual links to admin notification messages
* When copying pages, it is now possible to specify a place to copy to
* ``FieldPanel`` now accepts an optional ``widget`` parameter to override the field's default form widget
Bug fixes

View file

@ -40,6 +40,7 @@ install_requires = [
"Unidecode>=0.04.14",
"six>=1.7.0",
'requests>=2.0.0',
"Willow==0.1",
]

View file

@ -17,6 +17,7 @@ base =
python-dateutil==2.2
pytz==2014.7
Embedly
Willow==0.1
coverage
dj17 =

View file

@ -12,7 +12,7 @@ class Sitemap(object):
def get_urls(self):
for page in self.get_pages():
for url in page.get_sitemap_urls():
for url in page.specific.get_sitemap_urls():
yield url
def render(self):

View file

@ -2,7 +2,7 @@ from django.test import TestCase
from django.core.cache import cache
from wagtail.wagtailcore.models import Page, PageViewRestriction, Site
from wagtail.tests.models import SimplePage
from wagtail.tests.models import SimplePage, EventIndex
from .sitemap_generator import Sitemap
@ -47,6 +47,20 @@ class TestSitemapGenerator(TestCase):
self.assertIn('http://localhost/', urls) # Homepage
self.assertIn('http://localhost/hello-world/', urls) # Child page
def test_get_urls_uses_specific(self):
# Add an event page which has an extra url in the sitemap
events_page = self.home_page.add_child(instance=EventIndex(
title="Events",
slug='events',
live=True,
))
sitemap = Sitemap(self.site)
urls = [url['location'] for url in sitemap.get_urls()]
self.assertIn('http://localhost/events/', urls) # Main view
self.assertIn('http://localhost/events/past/', urls) # Sub view
def test_render(self):
sitemap = Sitemap(self.site)
xml = sitemap.render()

View file

@ -306,6 +306,15 @@ class EventIndex(Page):
for path in super(EventIndex, self).get_static_site_paths():
yield path
def get_sitemap_urls(self):
# Add past events url to sitemap
return super(EventIndex, self).get_sitemap_urls() + [
{
'location': self.full_url + 'past/',
'lastmod': self.latest_revision_created_at
}
]
EventIndex.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('intro', classname="full"),

View file

@ -362,6 +362,15 @@ def MultiFieldPanel(children, heading="", classname=""):
class BaseFieldPanel(EditHandler):
@classmethod
def widget_overrides(cls):
"""check if a specific widget has been defined for this field"""
if hasattr(cls, 'widget'):
return {cls.field_name: cls.widget}
else:
return {}
def __init__(self, instance=None, form=None):
super(BaseFieldPanel, self).__init__(instance=instance, form=form)
self.bound_field = self.form[self.field_name]
@ -408,11 +417,16 @@ class BaseFieldPanel(EditHandler):
return [self.field_name]
def FieldPanel(field_name, classname=""):
return type(str('_FieldPanel'), (BaseFieldPanel,), {
def FieldPanel(field_name, classname="", widget=None):
base = {
'field_name': field_name,
'classname': classname,
})
}
if widget:
base['widget'] = widget
return type(str('_FieldPanel'), (BaseFieldPanel,), base)
class BaseRichTextFieldPanel(BaseFieldPanel):

View file

@ -225,13 +225,16 @@ a.tag:hover{
}
}
hr{
border:1px solid $color-grey-4;
border-width:1px 0 0;
margin:1.5em 0;
}
/* general image style */
img{
max-width:100%;
height:auto;
border: 3px solid $color-grey-4;
}
/* make a block-level element inline */

View file

@ -803,8 +803,7 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
.boolean_field &,
.choice_field &,
.model_choice_field &,
.image_field &,
.file_field &{
.image_field &{
padding-top:0;
}
}

View file

@ -353,6 +353,11 @@ ul.listing{
font-size:1em;
opacity:0.7;
}
&.images img{
@include transition(border-color 0.2s ease);
border: 3px solid transparent;
}
}
ul.listing{
border-top:1px dashed $color-input-border;

View file

@ -66,6 +66,9 @@
content:"w";
margin-right:0.5em;
font-size:1.2em;
font-weight:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}

View file

@ -58,6 +58,22 @@ kbd{
padding:0.3em 0.5em;
}
dl, dt, dd{
padding:0;
margin:0;
}
dl{
margin-top:1em;
}
dt{
color:darken($color-grey-3, 5%);
text-transform:uppercase;
font-size:0.9em;
}
dd{
margin-bottom:1em;
}
/* Help text formatters */
.help-block{

View file

@ -314,6 +314,13 @@ footer, .logo{
@include column(12);
}
.divider-before{
border-left:1px solid $color-grey-4;
}
.divider-after{
border-right:1px solid $color-grey-4;
}
.row{
@include row();
}

View file

@ -1,7 +1,7 @@
{
"metadata": {
"name": "Wagtail 1",
"lastOpened": 1410881728324,
"name": "Wagtail",
"lastOpened": 1420801397108,
"created": 1405597423787
},
"iconSets": [
@ -8564,20 +8564,23 @@
"minorVersion": 0
},
"metrics": {
"emSize": 512,
"emSize": 1024,
"baseline": 6.25,
"whitespace": 50
},
"showMetrics": false,
"showMetrics": true,
"showMetadata": false,
"showVersion": false
"showVersion": false,
"includeMetadata": false
},
"imagePref": {},
"historySize": 100,
"showCodes": true,
"search": "",
"gridSize": 16,
"showGrid": true
"showGrid": true,
"showQuickUse2": true,
"showSVGs": true
},
"externalSets": []
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -110,6 +110,8 @@
margin:0;
cursor:pointer;
background-color:$color-salmon;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}

View file

@ -2,10 +2,10 @@
<li id="inline_child_{{ child.form.prefix }}">
<ul class="controls">
{% if can_order %}
<li class="icon text-replace white icon-order-up inline-child-move-up" id="{{ child.form.prefix }}-move-up">{% trans "Move up" %}</li>
<li class="icon text-replace white icon-order-down inline-child-move-down" id="{{ child.form.prefix }}-move-down">{% trans "Move down" %}</li>
<li class="icon text-replace white icon-order-up inline-child-move-up" id="{{ child.form.prefix }}-move-up" title="{% trans 'Move up' %}">{% trans "Move up" %}</li>
<li class="icon text-replace white icon-order-down inline-child-move-down" id="{{ child.form.prefix }}-move-down" title="{% trans 'Move down' %}">{% trans "Move down" %}</li>
{% endif %}
<li class="icon text-replace white icon-bin" id="{{ child.form.DELETE.id_for_label }}-button">{% trans "Delete" %}</li>
<li class="icon text-replace white icon-bin" id="{{ child.form.DELETE.id_for_label }}-button" title="{% trans 'Delete' %}">{% trans "Delete" %}</li>
</ul>
{{ child.render_form_content }}
</li>

View file

@ -3,7 +3,7 @@
{% load i18n %}
{% block titletag %}{% blocktrans with page_type=content_type.model_class.get_verbose_name %}New {{ page_type }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-explorer page-editor create{% endblock %}
{% block bodyclass %}menu-explorer page-editor create model-{{ content_type.model }}{% endblock %}
{% block content %}
@ -66,4 +66,10 @@
{% endblock %}
{% block extra_js %}
{% include "wagtailadmin/pages/_editor_js.html" %}
<script>
$(function(){
$('#page-edit-form .tab-content section.active input').first().focus();
});
</script>
{% endblock %}

View file

@ -2,8 +2,8 @@
{% load wagtailadmin_tags %}
{% load gravatar %}
{% load i18n %}
{% block titletag %}{% blocktrans with title=page.title %}Editing {{ title }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-explorer page-editor{% endblock %}
{% block titletag %}{% blocktrans with title=page.title page_type=content_type.model_class.get_verbose_name %}Editing {{ page_type }}: {{ title }}{% endblocktrans %}{% endblock %}
{% block bodyclass %}menu-explorer page-editor model-{{ content_type.model }}{% endblock %}
{% block content %}
{% page_permissions page as page_perms %}
@ -12,7 +12,7 @@
<div class="row row-flush">
<div class="left col9">
<h1 class="icon icon-doc-empty-inverse">{% blocktrans with title=page.title %}Editing <span>{{ title }}</span>{% endblocktrans %}</h1>
<h1 class="icon icon-doc-empty-inverse">{% blocktrans with title=page.title page_type=content_type.model_class.get_verbose_name %}Editing {{ page_type }} <span>{{ title }}</span>{% endblocktrans %}</h1>
</div>
<div class="right col3">
{% trans "Status" %}

View file

@ -1,5 +1,5 @@
{% load wagtailadmin_tags %}
<div class="field {{ field|fieldtype }} {{ field_classes }}">
<div class="field {{ field|fieldtype }} {{ field|widgettype }} {{ field_classes }}">
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ input_classes }} ">

View file

@ -65,6 +65,14 @@ def fieldtype(bound_field):
return ""
@register.filter
def widgettype(bound_field):
try:
return camelcase_to_underscore(bound_field.field.widget.__class__.__name__)
except AttributeError:
return ""
@register.filter
def meta_description(model):
try:

View file

@ -255,6 +255,8 @@ def edit(request, page_id):
page = get_object_or_404(Page, id=page_id).get_latest_revision_as_page()
parent = page.get_parent()
content_type = ContentType.objects.get_for_model(page)
page_perms = page.permissions_for_user(request.user)
if not page_perms.can_edit():
raise PermissionDenied
@ -373,6 +375,7 @@ def edit(request, page_id):
return render(request, 'wagtailadmin/pages/edit.html', {
'page': page,
'content_type': content_type,
'edit_handler': edit_handler,
'errors_debug': errors_debug,
'preview_modes': page.preview_modes,

View file

@ -1,7 +1,7 @@
{% extends "wagtailadmin/shared/field_as_li.html" %}
{% extends "wagtailadmin/shared/field.html" %}
{% load i18n %}
{% block form_field %}
<a href="{{ document.url }}" class="icon icon-doc-full-inverse">{{ document.file }}</a><br /><br />
<a href="{{ document.url }}" class="icon icon-doc-full-inverse">{{ document.filename }}</a><br /><br />
{% trans "Change document:" %}
{{ field }}
{% endblock %}

View file

@ -0,0 +1,4 @@
{% load wagtailadmin_tags %}
<li class="{% if field.field.required %}required{% endif %} {{ wrapper_classes }} {{ li_classes }} {% if field.errors %}error{% endif %}">
{% include "wagtaildocs/documents/_file_field.html" %}
</li>

View file

@ -15,20 +15,32 @@
{% trans "Editing" as editing_str %}
{% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=document.title icon="doc-full-inverse" usage_object=document %}
<div class="nice-padding">
<form action="{% url 'wagtaildocs_edit_document' document.id %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<ul class="fields">
{% for field in form %}
{% if field.name == 'file' %}
{% include "wagtaildocs/documents/_file_field.html" %}
{% else %}
{% include "wagtailadmin/shared/field_as_li.html" %}
{% endif %}
{% endfor %}
<li><input type="submit" value="{% trans 'Save' %}" /> <a href="{% url 'wagtaildocs_delete_document' document.id %}" class="button button-secondary no">{% trans "Delete document" %}</a></li>
</ul>
</form>
<div class="row row-flush nice-padding">
<div class="col10 divider-after">
<form action="{% url 'wagtaildocs_edit_document' document.id %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<ul class="fields">
{% for field in form %}
{% if field.name == 'file' %}
{% include "wagtaildocs/documents/_file_field_as_li.html" %}
{% else %}
{% include "wagtailadmin/shared/field_as_li.html" %}
{% endif %}
{% endfor %}
<li><input type="submit" value="{% trans 'Save' %}" /> <a href="{% url 'wagtaildocs_delete_document' document.id %}" class="button button-secondary no">{% trans "Delete document" %}</a></li>
</ul>
</form>
</div>
<div class="col2">
<dl>
{% if document.file %}
<dt>{% trans "Filesize" %}</dt>
<dd>{{ document.file.size|filesizeformat }}</dd>
{% endif %}
</dl>
</div>
</div>
</div>

View file

@ -4,6 +4,7 @@ import re
from six import BytesIO, text_type
from taggit.managers import TaggableManager
from willow.image import Image as WillowImage
from django.core.files import File
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
@ -80,6 +81,19 @@ class AbstractImage(models.Model, TagSearchable):
def __str__(self):
return self.title
def get_willow_image(self):
try:
image_file = self.file.file # triggers a call to self.storage.open, so IOErrors from missing files will be raised at this point
except IOError as e:
# re-throw this as a SourceImageIOError so that calling code can distinguish
# these from IOErrors elsewhere in the process
raise SourceImageIOError(text_type(e))
image_file.open('rb')
image_file.seek(0)
return WillowImage.open(image_file)
def get_rect(self):
return Rect(0, 0, self.width, self.height)

View file

@ -1,8 +1,10 @@
{% extends "wagtailadmin/shared/field_as_li.html" %}
{% load i18n %}
{% extends "wagtailadmin/shared/field.html" %}
{% load i18n wagtailimages_tags %}
{% block form_field %}
<span class="icon icon-image">{{ image.filename }}</span><br /><br />
{% image image original as original_image %}
{% trans "Change image:" %}
<a href="{{ original_image.url }}" class="icon icon-image">{{ image.filename }}</a> ({{ original_image.width }}x{{ original_image.height}})<br /><br />
{% trans "Change image file:" %}
{{ field }}
{% endblock %}

View file

@ -0,0 +1,4 @@
{% load wagtailadmin_tags %}
<li class="{% if field.field.required %}required{% endif %} {{ wrapper_classes }} {{ li_classes }} {% if field.errors %}error{% endif %}">
{% include "wagtailimages/images/_file_field.html" %}
</li>

View file

@ -28,14 +28,14 @@
<div class="row row-flush nice-padding">
<div class="col7">
<div class="col5">
<form action="{% url 'wagtailimages_edit_image' image.id %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<ul class="fields">
{% for field in form %}
{% if field.name == 'file' %}
{% include "wagtailimages/images/_file_field.html" %}
{% include "wagtailimages/images/_file_field_as_li.html" %}
{% elif field.is_hidden %}
{{ field }}
{% else %}
@ -47,7 +47,7 @@
</ul>
</form>
</div>
<div class="col5">
<div class="col5 divider-after">
<h2 class="label">{% trans "Focal point (optional)" %}</h2>
<p>{% trans "To define this image's most important region, drag a box over the image below." %} {% if image.focal_point %}({% trans "Current focal point shown" %}){% endif %}</p>
@ -64,10 +64,22 @@
<div class="current-focal-point-indicator{% if not image.focal_point %} hidden{% endif %}"></div>
</div>
</div>
<div class="col2 ">
{% if url_generator_enabled %}
<a href="{% url 'wagtailimages_url_generator' image.id %}" class="button bicolor icon icon-link">{% trans "URL Generator" %}</a>
<hr />
{% endif %}
{% image image original as original_image %}
<dl>
<dt>{% trans "Max dimensions" %}</dt>
<dd>{{ original_image.width }}x{{ original_image.height }}</dd>
<dt>{% trans "Filesize" %}</dt>
<dd>{{ image.file.size|filesizeformat }}</dd>
</dl>
</div>
</div>
{% endblock %}

View file

@ -1,4 +1,5 @@
import unittest
from willow.image import Image as WillowImage
from django.test import TestCase
from django.core.urlresolvers import reverse
@ -13,7 +14,7 @@ from django.db import connection
from wagtail.tests.utils import WagtailTestUtils, test_concurrently
from wagtail.wagtailcore.models import Page
from wagtail.tests.models import EventPage, EventPageCarouselItem
from wagtail.wagtailimages.models import Rendition, Filter
from wagtail.wagtailimages.models import Rendition, Filter, SourceImageIOError
from wagtail.wagtailimages.backends import get_image_backend
from wagtail.wagtailimages.backends.pillow import PillowBackend
from wagtail.wagtailimages.rect import Rect
@ -268,6 +269,29 @@ class TestGetUsage(TestCase):
self.assertTrue(issubclass(Page, type(self.image.get_usage()[0])))
class TestGetWillowImage(TestCase):
fixtures = ['test.json']
def setUp(self):
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
def test_willow_image_object_returned(self):
willow_image = self.image.get_willow_image()
self.assertIsInstance(willow_image, WillowImage)
def test_with_missing_image(self):
# Image id=1 in test fixtures has a missing image file
bad_image = Image.objects.get(id=1)
# Attempting to get the Willow image for images without files
# should raise a SourceImageIOError
self.assertRaises(SourceImageIOError, bad_image.get_willow_image)
class TestIssue573(TestCase):
"""
This tests for a bug which causes filename limit on Renditions to be reached